【西瓜播放器+Vue】前端实现网页短视频:上下滑动、自动播放、显示视频信息等
文章目录
- 目标
- 依赖
- 逻辑
- 上下滑动:swipe
- 播放器实例
- 事件
- 配置
- 隐藏控制栏
- 暂停全部
- 播放完后自动播放下一个
- 滑动后自动播放
- 页面不可见时暂停播放
- 播放器显示视频信息
- 代码
- 播放器组件
- 外部调用
目标
前端实现像抖音短视频那样上下滑动播放视频等效果。
使用:西瓜播放器 | 快速上手
大致效果:
依赖
上下滑动使用走马灯组件。这里使用vant
的走马灯。
Swipe 轮播 - Vant 4
播放器使用西瓜播放器:
西瓜播放器 | 快速上手
需要注意的是,在pc端,swipe的上下滑动效果无响应,需要这样处理:进阶用法 - Vant 4
# 安装模块
npm i @vant/touch-emulator -S
// 引入模块后自动生效
import '@vant/touch-emulator';
逻辑
提示:本部分边写代码边写文档,因此代码片段仅是当前功能片段,非完整代码。
上下滑动:swipe
使用走马灯swipe
组件实现上下滑动,需要设置:
- 竖直滑动
- 隐藏指示器
- 循环轮播
<van-swiperef="swipeRef"vertical:show-indicators="false"style="height: 700px; width: 490px":stop-propagation="false":loop="true"@change="handleChange"><van-swipe-item v-for="(item, index) in playList" :key="index"><playerDemo:id="item.id":url="item.url":poster="item.poster":on-ended="onEnded":bind-player="bindPlayer"></playerDemo></van-swipe-item></van-swipe>
封装一个播放器组件:playerDemo
,传入视频相关参数:
const playList = ref([{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '1',poster: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '2',poster: 'https://vcg02.cfp.cn/creative/vcg/800/new/VCG211555535113-MDJ.jpg',},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '3',poster: 'https://vcg01.cfp.cn/creative/vcg/800/new/VCG211513792984.jpg',},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '4',poster: 'https://vcg03.cfp.cn/creative/vcg/800/new/VCG211563273611.jpg',},
])
播放器实例
<div class="main"><div ref="videoRef"></div></div>
import { defineProps } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import Player from 'xgplayer'
import 'xgplayer/dist/index.min.css'
import { Events } from 'xgplayer'
import XgPlayer from 'xgplayer'interface PlayerProps {url: string // 视频链接id: string // 视频idposter: string // 视频封面onEnded: () => voidbindPlayer: (id: string, player: XgPlayer) => void
}
const props = defineProps<PlayerProps>()
const videoRef = ref<HTMLElement>()let player: Player | nullonMounted(() => {if (!videoRef.value) return// https://v3.h5player.bytedance.com/config/player = new Player({el: videoRef.value,url: props.url,width: '390px',height: '700px',videoInit: true,autoplay: true,poster: props.poster,videoFillMode: 'auto',marginControls: false,closeVideoDblclick: true, // 禁止双击全屏commonStyle: {progressColor: 'rgba(255, 255, 255, 0.2)',playedColor: '#fff',cachedColor: 'rgba(255, 255, 255, 0.6)',},// https://v3.h5player.bytedance.com/plugins/icons.html// icons: {},start: {isShowPause: true,disableAnimate: true,},// https://v3.h5player.bytedance.com/pluginsplugins: [],})// 绑定播放器事件,事件从组件外传入player.on(Events.ENDED, handleEnded)props.bindPlayer(props.id, player)
})onUnmounted(() => {player?.off(Events.ENDED, handleEnded)player?.destroy()
})
事件
要给组件定义事件方法,如:
onEnded: () => voidbindPlayer: (id: string, player: XgPlayer) => void
西瓜播放器 | 事件
从组件外传入一个事件的回调函数,在组件内绑定此事件。
如onEnded
,在组件外传入onEnded事件:
const onEnded = () => {// ...
}
<playerDemo:id="item.id":url="item.url":poster="item.poster":on-ended="onEnded":bind-player="bindPlayer"></playerDemo>
在组件内绑定onEnded
为视频播放结束的事件:
// 处理传入的播放器事件
const handleEnded = () => {props.onEnded()
}
onMounted(() => {// ...player.on(Events.ENDED, handleEnded)
})
销毁:
onUnmounted(() => {player?.off(Events.ENDED, handleEnded)player?.destroy()
})
这样一来,播放器内视频结束后,就会触发传入的onEnded
了。
我们希望播放器组件只处理播放器相关的逻辑,具体对应事件的业务逻辑都从组件外传入。
配置
指的是创建播放器实例的配置:
西瓜播放器 | 配置
player = new Player({el: videoRef.value,url: props.url,width: '390px',height: '700px',videoInit: true,autoplay: true,poster: props.poster,videoFillMode: 'auto',marginControls: false,closeVideoDblclick: true, // 禁止双击全屏commonStyle: {progressColor: 'rgba(255, 255, 255, 0.2)',playedColor: '#fff',cachedColor: 'rgba(255, 255, 255, 0.6)',},// https://v3.h5player.bytedance.com/plugins/icons.html// icons: {},start: {isShowPause: true,disableAnimate: true,},// https://v3.h5player.bytedance.com/pluginsplugins: [],})
主要是看文档写即可。此文档很清晰,需要啥功能直接搜配置。
隐藏控制栏
我们想要做短视频的效果,需要保留进度条,隐藏控制栏,因此不能设置:
controls:false
我们隐藏除了进度条外的其他类即可。
<!-- 全局样式,see:https://cn.vuejs.org/api/sfc-css-features.html#global-selectors -->
<style>
.xg-left-grid,
.xg-right-grid {display: none !important;
}
</style>
暂停全部
场景:滑动后,发现被划走的视频(不可见)依旧在播放。
需求:不可见的视频暂停播放。
播放器组件只知道当前播放器的情况,因此此逻辑需要在组件外实现。
定义一个对外暴露、绑定播放器的方法:
bindPlayer: (id: string, player: XgPlayer) => void
在实例化完播放器后:
onMounted(() => {if (!videoRef.value) return// https://v3.h5player.bytedance.com/config/player = new Player({// ...})props.bindPlayer(props.id, player)
})
组件外绑定:
import XgPlayer from 'xgplayer'
const playersRef = ref<XgPlayer[]>([])// 绑定对应视频与播放器实例
const bindPlayer = (id: string, player: XgPlayer) => {const index = playList.value.findIndex((item) => item.id === id)if (index != -1) {playersRef.value[index] = player}
}const pauseAll = () => {playersRef.value.forEach((player) => {player.pause()})
}
在滑动时暂停全部pauseAll()
即可。
播放完后自动播放下一个
这里有一个问题。
播放完后自动播放下一个,此时往上滑,回到上一个播放器,我们的预期是重播此视频,然而实际效果是:触发完播自动播下一个。
一开始处理为,滑动播放器时,若视频播完,则重播:
const handleChange = (index: number) => {pauseAll()// 如果当前视频已结束,重播const currentPlayer = playersRef.value[index]if (currentPlayer.ended) {currentPlayer.retry()}console.log(index, playList.value[index], '当前', currentPlayer)
}
但是无效。
场景:视频A播完后,自动跳到视频B。此时回到视频A,currentPlayer.ended
为false
。
无法准确判断视频是否播完。因此处理方法为:视频播完时,直接调用重播,然后暂停,切到下一个视频。这样就不需要判断currentPlayer.ended
。
组件内绑定的handleEnded
// 处理传入的播放器事件
const handleEnded = () => {// 需求:视频结束时,自动跳到下一个。此时若回到当前视频,需要重播// 但是,此时ended状态为false,无法判断是否是结束后重播。因此需要在视频结束时直接重播,然后全部暂停,跳到下一集// 此时上滑回到本集,即继续播放,效果与重播一致player?.retry()props.onEnded()
}
组件外,触发滑动时:
const handleChange = (index: number) => {pauseAll()
}// 传入播放器事件到组件
const onEnded = () => {pauseAll()swipeRef?.value?.next()
}
效果:
滑动后自动播放
const handleChange = (index: number) => {pauseAll()const currentPlayer = playersRef.value[index]currentPlayer.play()
}
页面不可见时暂停播放
需求:页面不可见时暂停播放,回到页面时继续播放当前视频。
const currentPlayer = ref<XgPlayer | null>(null)
const activeIndexRef = ref(0)onMounted(() => {document.addEventListener('visibilitychange', handleVisibilitychange)
})onUnmounted(() => {document.removeEventListener('visibilitychange', handleVisibilitychange)
})const handleVisibilitychange = () => {if (document.hidden) {pauseAll()} else {playCurrent()}
}// 播放当前视频
const playCurrent = () => {currentPlayer.value = playersRef.value[activeIndexRef.value]currentPlayer.value?.play()
}const handleChange = (index: number) => {pauseAll()activeIndexRef.value = indexplayCurrent()
}
播放器显示视频信息
传入信息,新写样式即可。
<playerDemo:id="item.id":url="item.url":poster="item.poster":info="item.info":on-ended="onEnded":bind-player="bindPlayer"
></playerDemo>
const playList = ref([{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '1',poster: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',info: {cover: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',name: '不重要的名字1',},},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '2',poster: 'https://vcg02.cfp.cn/creative/vcg/800/new/VCG211555535113-MDJ.jpg',info: {cover: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',name: '不重要的名字2',},},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '3',poster: 'https://vcg01.cfp.cn/creative/vcg/800/new/VCG211513792984.jpg',},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '4',poster: 'https://vcg03.cfp.cn/creative/vcg/800/new/VCG211563273611.jpg',},
])
组件内:
<div v-if="props.info" class="info"><img class="cover" :src="props.info.cover" alt="" /><div class="name">{{ props.info.name }}</div>
</div>
.main {position: relative;
}.info {position: absolute;bottom: 65px;left: 15px;display: flex;align-items: center;justify-content: center;.cover {width: 30px;height: 55px;margin-right: 10px;border-radius: 8px;}.name {color: #fff;}
}
效果:
代码
播放器组件
player.vue
<template><div class="main"><div ref="videoRef"></div><div v-if="props.info" class="info"><img class="cover" :src="props.info.cover" alt="" /><div class="name">{{ props.info.name }}</div></div></div>
</template><script setup lang="ts">
import { defineProps } from 'vue'
import { ref, onMounted, onUnmounted } from 'vue'
import Player from 'xgplayer'
import 'xgplayer/dist/index.min.css'
import { Events } from 'xgplayer'
import XgPlayer from 'xgplayer'interface PlayerInfo {name: stringcover: string
}interface PlayerProps {url: string // 视频链接id: string // 视频idposter: string // 视频封面info?: PlayerInfoonEnded: () => voidbindPlayer: (id: string, player: XgPlayer) => void
}
const props = defineProps<PlayerProps>()
const videoRef = ref<HTMLElement>()let player: Player | nullonMounted(() => {if (!videoRef.value) return// https://v3.h5player.bytedance.com/config/player = new Player({el: videoRef.value,url: props.url,width: '390px',height: '700px',videoInit: true,autoplay: true,poster: props.poster,videoFillMode: 'auto',marginControls: false,closeVideoDblclick: true, // 禁止双击全屏commonStyle: {progressColor: 'rgba(255, 255, 255, 0.2)',playedColor: '#fff',cachedColor: 'rgba(255, 255, 255, 0.6)',},// https://v3.h5player.bytedance.com/plugins/icons.html// icons: {},start: {isShowPause: true,disableAnimate: true,},// https://v3.h5player.bytedance.com/pluginsplugins: [],})// 绑定播放器事件,事件从组件外传入player.on(Events.ENDED, handleEnded)props.bindPlayer(props.id, player)
})onUnmounted(() => {player?.off(Events.ENDED, handleEnded)player?.destroy()
})// 处理传入的播放器事件
const handleEnded = () => {// 需求:视频结束时,自动跳到下一个。此时若回到当前视频,需要重播// 但是,此时ended状态为false,无法判断是否是结束后重播。因此需要在视频结束时直接重播,然后全部暂停,跳到下一集// 此时上滑回到本集,即继续播放,效果与重播一致player?.retry()props.onEnded()
}
</script><!-- 全局样式,see:https://cn.vuejs.org/api/sfc-css-features.html#global-selectors -->
<style>
.xg-left-grid,
.xg-right-grid {display: none !important;
}
</style><style lang="less" scoped>
.main {position: relative;
}.info {position: absolute;bottom: 65px;left: 15px;display: flex;align-items: center;justify-content: center;.cover {width: 30px;height: 55px;margin-right: 10px;border-radius: 8px;}.name {color: #fff;}
}
</style>
外部调用
<template><h3>封装西瓜播放器,上下滑动视频</h3><p><a href="https://v3.h5player.bytedance.com/guide/#%E5%AE%89%E8%A3%85">https://v3.h5player.bytedance.com/guide/#%E5%AE%89%E8%A3%85</a></p><van-swiperef="swipeRef"vertical:show-indicators="false"style="height: 700px; width: 490px":stop-propagation="false":loop="true"@change="handleChange"><van-swipe-item v-for="(item, index) in playList" :key="index"><playerDemo:id="item.id":url="item.url":poster="item.poster":info="item.info":on-ended="onEnded":bind-player="bindPlayer"></playerDemo></van-swipe-item></van-swipe>
</template><script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import playerDemo from '@/components/player.vue'
// 适配pc端-vant-Swipe,see : https://vant-ui.github.io/vant/#/zh-CN/advanced-usage#zhuo-mian-duan-gua-pei
import '@vant/touch-emulator'
import type { SwipeInstance } from 'vant'
import XgPlayer from 'xgplayer'
const swipeRef = ref<SwipeInstance>()
const playersRef = ref<XgPlayer[]>([])
const currentPlayer = ref<XgPlayer | null>(null)
const activeIndexRef = ref(0)const playList = ref([{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '1',poster: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',info: {cover: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',name: '不重要的名字1',},},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '2',poster: 'https://vcg02.cfp.cn/creative/vcg/800/new/VCG211555535113-MDJ.jpg',info: {cover: 'https://vcg00.cfp.cn/creative/vcg/800/new/VCG211432661212.jpg',name: '不重要的名字2',},},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '3',poster: 'https://vcg01.cfp.cn/creative/vcg/800/new/VCG211513792984.jpg',},{url: '//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/mp4/xgplayer-demo-360p.mp4',id: '4',poster: 'https://vcg03.cfp.cn/creative/vcg/800/new/VCG211563273611.jpg',},
])onMounted(() => {document.addEventListener('visibilitychange', handleVisibilitychange)
})onUnmounted(() => {document.removeEventListener('visibilitychange', handleVisibilitychange)
})const handleVisibilitychange = () => {if (document.hidden) {pauseAll()} else {playCurrent()}
}// 播放当前视频
const playCurrent = () => {currentPlayer.value = playersRef.value[activeIndexRef.value]currentPlayer.value?.play()
}const handleChange = (index: number) => {pauseAll()activeIndexRef.value = indexplayCurrent()
}// 绑定对应视频与播放器实例
const bindPlayer = (id: string, player: XgPlayer) => {const index = playList.value.findIndex((item) => item.id === id)if (index != -1) {playersRef.value[index] = player}
}const pauseAll = () => {playersRef.value.forEach((player) => {player.pause()})
}// 传入播放器事件到组件
const onEnded = () => {pauseAll()swipeRef?.value?.next()
}
</script><style scoped lang="less"></style>