目标
前端实现像抖音短视频那样上下滑动播放视频等效果。
使用:西瓜播放器 | 快速上手
大致效果:

依赖
上下滑动使用走马灯组件。这里使用vant的走马灯。
Swipe 轮播 - Vant 4
播放器使用西瓜播放器:
西瓜播放器 | 快速上手
需要注意的是,在pc端,swipe的上下滑动效果无响应,需要这样处理:进阶用法 - Vant 4
# 安装模块
npm i @vant/touch-emulator -S
// 引入模块后自动生效
import '@vant/touch-emulator';
逻辑
提示:本部分边写代码边写文档,因此代码片段仅是当前功能片段,非完整代码。
上下滑动:swipe
使用走马灯swipe组件实现上下滑动,需要设置:
- 竖直滑动
- 隐藏指示器
- 循环轮播
<van-swipe
ref="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 // 视频id
poster: string // 视频封面
onEnded: () => void
bindPlayer: (id: string, player: XgPlayer) => void
}
const props = defineProps<PlayerProps>()
const videoRef = ref<HTMLElement>()
let player: Player | null
onMounted(() => {
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/plugins
plugins: [],
})
// 绑定播放器事件,事件从组件外传入
player.on(Events.ENDED, handleEnded)
props.bindPlayer(props.id, player)
})
onUnmounted(() => {
player?.off(Events.ENDED, handleEnded)
player?.destroy()
})
事件
要给组件定义事件方法,如:
onEnded: () => void
bindPlayer: (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/plugins
plugins: [],
})
主要是看文档写即可。此文档很清晰,需要啥功能直接搜配置。
隐藏控制栏
我们想要做短视频的效果,需要保留进度条,隐藏控制栏,因此不能设置:
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 = index
playCurrent()
}

播放器显示视频信息
传入信息,新写样式即可。
<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: string
cover: string
}
interface PlayerProps {
url: string // 视频链接
id: string // 视频id
poster: string // 视频封面
info?: PlayerInfo
onEnded: () => void
bindPlayer: (id: string, player: XgPlayer) => void
}
const props = defineProps<PlayerProps>()
const videoRef = ref<HTMLElement>()
let player: Player | null
onMounted(() => {
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/plugins
plugins: [],
})
// 绑定播放器事件,事件从组件外传入
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-swipe
ref="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 = index
playCurrent()
}
// 绑定对应视频与播放器实例
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>
Vue实现短视频滑动播放功能
6722

被折叠的 条评论
为什么被折叠?



