【西瓜播放器+Vue】前端实现网页短视频:上下滑动、自动播放、显示视频信息等

Vue实现短视频滑动播放功能

目标

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

大致效果:
在这里插入图片描述

依赖

上下滑动使用走马灯组件。这里使用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.endedfalse
无法准确判断视频是否播完。因此处理方法为:视频播完时,直接调用重播,然后暂停,切到下一个视频。这样就不需要判断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>

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

karshey

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值