likes
comments
collection
share

实战分享:上下滑动刷视频、性能优化、踩坑及总结

作者站长头像
站长
· 阅读数 2

本文: 从0到搭建一个视频流页面。 功能:m3u8视频解析、基本播放、上下滑动切换、性能优化等 框架:SSR(nestjs) + Vue3 + Vant3 环境:node版本:v16.20.0

线上体验地址:😍视频流😍 😍视频流😍

背景

  1. 视频解析:因为视频格式基于m3u8,本文选择的是videojs,可以考虑xgplay、hls.js 等
  2. 视频流的实现:上下滑动、基本播放、切后台暂停
  3. 考虑性能问题:首屏加载、懒加载、预加载、动态销毁dom
    • 首屏加载:初始化接口需要注册、拉取特别多信息,所以选择了SSR首屏更快一些
    • 懒加载、预加载: 预热两个视频,分页渲染、拉取视频
    • 动态销毁dom: 切换视频时,动态销毁前第n个视频的dom、再动态渲染后面的

一、实现步骤

基于srr脚手架快速搭建项目 SSR

npm init ssr-app xxx-project
cd ssr-app xxx-project
yarn
yarn start

实战分享:上下滑动刷视频、性能优化、踩坑及总结

swiper实现上下切换

  1. 监听滚动事件,来切换(不好)
  2. css吸附属性 (需要配合IntersectionObserver使用,才能自动播放下一个视频)
.videos {
  font-size: 0.14rem;
  height: 100%;
  overflow-y: auto;
  scroll-snap-type: y mandatory;

  &-item {
    scroll-snap-align: start;
    scroll-snap-stop: always;
  }
}
  1. swipe轮播图(通过change事件来控制播放,好实现👍)
<template>
  <van-swipe ref="swiperRef" style="width: 100%;height: 100%; background-color: black;" :initial-swipe="0"
    @change="changeVideo" :loop="false" vertical :show-indicators="false" :lazy-render="false">
    <van-swipe-item v-for="(item, index) in videoList" :key="index">
      <img :src="item.cover" >
    </van-swipe-item>
  </van-swipe>
</template>

看下效果=> 这样就实现了一个竖向的轮播图 实战分享:上下滑动刷视频、性能优化、踩坑及总结

videojs解析m3u8视频播放

  1. 安装npm包 videojs
yarn add videojs@8.11.0
  1. vue中具体使用
<template>
  <div style="width: 100vw;height: 100vh;">
    <video :class="`video-item video-js vjs-big-play-centered vjs-fluid`" style="width: 100%;height: 100vh;" ref="videRef" :poster="item.videoBaseInfo.cover">
      <p>Your browser does not support the video tag.</p>
    </video>
  </div>
</template>
<script setup lang="ts">
import { ref,  reactive, defineProps } from "vue";
import 'video.js/dist/video-js.css'
import videojs from 'video.js'

const props = defineProps<{
  index: number,
  cureentIndex: number,
  item: {
    "videoAuthorInfo": {
      "authorUid": 0,
      "avatar": "",
      followFlag: false
    },
    "videoBaseInfo": {
      "contentId": 0,
      "cover": "",
      "description": "",
      "title": "",
      "videoUrl": ""
    },
    "videoStatisticsInfo": {
      "comments": 0,
      "likes": 0
    }
  },
  isMuted: Boolean
}>()

const player: any = reactive({
  instance: null,
  status: 'pause',
  totalTime: 0,
  cureentTime: 0,
})
const videRef = ref(null)

const videoOptions = {
    controls: false, // 是否显示控制条
    autoplay: false,
    // preload: 'auto',
    // muted: props.isMuted,
    muted: false,
    loop: true,
    fluid: true,
    controlBar: { 
      children: [
      ],
    },
    sources: [{
      // src: 'http://recordcdn.quklive.com/upload/vod/user1462960877450854/1550739580345725/1/video.m3u8',
      src: props.item.videoBaseInfo.videoUrl,
      // type: 'application/x-mpegURL'
    }],
    bigPlayButton: false, // 播放按钮
    userActions: {
      click: false, // 禁止点击
      // download: false, // 禁止下载
      // hotkeys: false, // 禁止热键
      doubleClick: true // 禁止双击
    }
}

// videojs创建播放器会返回实例,用变量保存起来,然后在需要的地方调用。
player.instance = videojs(videRef.value, videoOptions,function(){
  // 对视频播放、暂停、异常等事件进行监听 处理
})
</script>
  1. 播放事件、双击点赞 因为后期会用到一个双击点赞的功能,所以这里使用一个空盒子来进行事件绑定
<template>
  <div class="video-box" @click="changePlay" @dblclick="changeCollect"></div>
  <video ref="videRef" ></video>
</template>

<script setup lang="ts">
let timer: number = 0;
const changePlay = (type: number) => {
  clearTimeout(timer)
  timer = setTimeout(() => {
    if (player?.instance?.paused()) {
      player.instance?.play()
    } else {
      player.instance?.pause()
    }
  }, 100)
}
</script>
<style>
  .video-box {
    position: absolute;
    top: 0;
    left: 0;
    height: 100%;
    width: 100%;
    z-index: 3;
  }
</style>

我们看下播放效果 实战分享:上下滑动刷视频、性能优化、踩坑及总结 从上图中看,实现了上下滑动,点击播放。 细心的小伙伴就会发现---我开始播放这个了,上一个还在播放... 是的,没错---这就是接下来要解决的问题

多视频播放问题

问题描述:

  • 上下滑动时,上一个播放中的视频没有暂停,开始播放当前视频。出现多视频播放
  • 希望自动播放当前视频
  • 解决:在swiper的change事件中进行控制,代码如下
  1. 在swiper的change事件中进行控制 => 播放当前视频 + 暂停上一个视频(可能是上滑、下滑==> 所以直接暂停上一个和下一个)
  2. 使用ref获取实例进行控制,在VideoItem组件中向外暴露控制视实例player
<!-- 子组件 -->
<script setup lang="ts">
import videojs from 'video.js'

const player = reactive({
  instance: null
})

player.instance = videojs(document.getElementById(id), {})
defineExpose({
  player, // 播放器实例
})
</script>

<!-- 父组件 -->
<template>
  <div class="videos">
    <van-swipe ref="swiperRef" @change="changeVideo" >
      <van-swipe-item v-for="(item, index) in videoList" :key="index">
        <VideoItem ref="videoItemRef" :index="index + 1" :item="list[index]" :isMuted="isMuted" :cureentIndex="cureentIndex" @handleNext="handleNext" />
      </van-swipe-item>
    </van-swipe>
  </div>
</template>
<script setup lang="ts">
import VideoItem from './components/videoItem.vue';
import { ref,getCurrentInstance } from 'vue';
const { proxy } = getCurrentInstance()

let cureentIndex = ref(0)

const changeVideo = (index: number) => {
  cureentIndex.value = index;
  const player = proxy.$refs.videoItemRef[cureentIndex.value]?.player?.instance
  const player1 = proxy.$refs.videoItemRef[cureentIndex.value - 1]?.player?.instance
  const player2 = proxy.$refs.videoItemRef[cureentIndex.value + 1]?.player?.instance
  player1?.pause() // 暂停上一个
  player2?.pause() // 暂停下一个
  player?.play() // 播放当前
}
</script>

看下最终效果---美滋滋 😍😍😍 实战分享:上下滑动刷视频、性能优化、踩坑及总结

二、优化方向

  • 视频尺寸问题(产品说就要撑满屏幕
  • 视频播放卡顿问题(产品说要丝滑一点
  • 视频播自动播放(静音)
  • 切后台暂停

1.视频尺寸问题

我们经常刷的抖音很多视频就是撑满屏幕的。

产品:人家怎么做到的呢?像抖音、像油管 卑微前端:根据视频原尺寸动态计算 服务端: 我能给你返视频原尺寸 卑微前端:唯唯诺诺,,那那那行吧!!!

解决思路:

  1. 获取视频原尺寸 (接口返回)
  2. 计算视频尺寸
    • 需要判断是否为竖屏视频 (宽高比 => 具体可以自己定)
    • 根据宽高比去通过屏幕高 => 动态计算视频宽度
    • 因为原先视频高度就不够,宽度刚好撑满,所以让视频撑满必然需要拉高,拉高好不能影响原视频比例
    • 视频标签需要一个父盒子来进行定位居中
let videoWidth = item.videoBaseInfo.width; // 原视频宽
let videoHeight = item.videoBaseInfo.height; // 原视频高
let videoRate = videoWidth/videoHeight; // 原视频宽高比

let screenHeight = window.innerHeight; // 设备高
let screenWidth = window.innerWidth; // 设备宽

if (videoRate < 1) { // 竖屏视频
  videoWidth = screenHeight * videoRate; // 屏幕高 * 视频宽高比 = 视频宽度(视频高度刚好撑满)
}
// 视频宽度 > 屏幕宽度,需要进行居中
let left = 0
if (videoWidth > screenWidth) {
  left = (videoWidth - screenWidth) / 2;
}
  1. 动态设置video标签的宽高及居中 我们先记住这个黑边 实战分享:上下滑动刷视频、性能优化、踩坑及总结 下面是关键代码
<template>
<div :style="videoStyle[1]">
  <video :style="videoStyle[0]":poster="item.videoBaseInfo.cover">
    <p>Your browser does not support the video tag.</p>
  </video>
</div>
</template>
<script setup lang="ts">
import { ref,  reactive, computed } from "vue";

const videoStyle = computed(() => {
  const { videoWidth, videoHeight } = props.item.videoBaseInfo;
  const deviceHeight = window.innerHeight;

  const videoAspectRatio = videoWidth / videoHeight;
  // 获取设备屏幕高度,用于计算视频的最终宽度
  
  // 宽视频不处理
  if(videoAspectRatio > 1){
    return [{},{height: '100vh', width: '100vw'}];
  }

  const finalWidth = deviceHeight * videoAspectRatio;
  // 获取设备屏幕宽度,用于后续判断是否需要进行水平平移
  const deviceWidth = window.innerWidth;
  // 计算视频元素的左边距,以实现居中
  const left = finalWidth > deviceWidth ? -(finalWidth - deviceWidth) / 2 : 0;

  // 返回应用于视频的样式对象
  return [
    {
      // 
      width: `${finalWidth}px`,
      height: '100vh',
    },
    {
      position: 'absolute',
      width: `${finalWidth}px`,
      height: '100vh',
      left: `${left}px`,
    }
  ];
});
</script>

下面是效果图 实战分享:上下滑动刷视频、性能优化、踩坑及总结

2.卡顿问题--动态渲染及销毁

  • 背景:因为这个视频流的需求,最终是嵌套在webview中使用。然后app分给webview的内存是有限的,所以video标签过多就会出现卡顿,再者就是video本身就很耗内存。
  • 现象:测试那边刷到50多个视频的时候,就会变得很卡顿。60多甚至会卡死app

当然,直接在chrome上,测试是100来个才会卡死。

解决思路

  1. 通过控制renderVideoList 来分批次渲染, allVideoList(接口拉取的所有视频)

    • allVideoList动态从分页接口拉取
    • renderVideoList动态从allVideoList添加
  2. 通过动态增加和减少video标签的个数来解决

  3. 参考下抖音和油管

    • 油管:只留了1个video
    • 抖音:只留了3个video
  4. 实践之后发现刷得快,3个有点不够,出现loading较多--还是保留四个吧

    • 维护renderVideoList和allVideoList
    • video只存在4个(当前、上一个、下一个、下下个)
    • 第一次只初始化2个video
    • 滑到第2个 => 加载第3个、第4个
    • 滑到第3个 => 加载第5个 + 销毁第1个 => 存在2,3,4,5
    • 滑到第4个 => 加载第6个 + 销毁第2个 => 存在3,4,5,6

关键代码

const list:any = computed(()=> VIDEO_LIST || []) // 接口拉的所有视频列表
let cureentIndex = ref(0) // 当前播放视频索引

let maxIdx = 0 // 播放到的最大索引
// 切换上、下视频, 播放当前,暂停 上、下
const changeVideo = (index: number) => {
  cureentIndex.value = index;
  maxIdx = Math.max(maxIdx, index)
  if (videoList.value.length - 3 === index) {
    onLoad()
  }
  loadNextVideo(maxIdx===index)
  const player = proxy.$refs.videoItemRef[cureentIndex.value]?.player?.instance
  const player1 = proxy.$refs.videoItemRef[cureentIndex.value - 1]?.player?.instance
  const player2 = proxy.$refs.videoItemRef[cureentIndex.value + 1]?.player?.instance
  player1?.pause()
  player2?.pause()
  player?.play()
}

/**
 * 进视频预热 + 播放器删除
 */
 const loadNextVideo = (isMax = false) => {
  let nextVideo = proxy.$refs.videoItemRef[cureentIndex.value +1]
  let preVideo = proxy.$refs.videoItemRef[cureentIndex.value -1]
  if (nextVideo && !nextVideo.player?.instance){
    console.log(nextVideo, 'init next Video')
    nextVideo?.initVideo()
  }
  if (preVideo && !preVideo.player?.instance){
    console.log(preVideo, 'init pre Video')
    preVideo?.initVideo()
  }
  if (isMax) {
    let nextVideo = proxy.$refs.videoItemRef[cureentIndex.value +2]
    if (nextVideo && !nextVideo.player?.instance){
      console.log(nextVideo, 'init next next Video')
      nextVideo?.initVideo()
    }
  }

  // 大于4个视频时删除第一个视频
  if (cureentIndex.value >= 3) {
    const player = proxy.$refs.videoItemRef[cureentIndex.value-3]?.player
    if (player?.instance) {
      console.log(player.instance, '移除播放器')
      player.instance.dispose()
      player.instance = null
    }
  }
}

let i = 0
/**
 * 维护渲染列表
 */
const onLoad = async () => { 
  try {
    const data = list.value.slice(i * 5, (i + 1) * 5)
    videoList.value = videoList.value.concat(data)
    if ((i+1) *5 > list.value.length) {
      emit('getVideoList', {}) // 拉取视频列表
    }
    i++
  } catch (err) {
    console.log("error")
  }
}

网络请求情况如下: 实战分享:上下滑动刷视频、性能优化、踩坑及总结

3.视频自动播放

因为浏览器的限制,不允许自动播放视频(带声音),只能静音播放

解决方案:静音播放,给用户一个解除静音的按钮。

注意点:play方法

控制video的播放时,会返回一个Promise

let video = document.getElementById('video');
const playPromise = video?.play()
if (playPromise !== undefined) {
  playPromise.then(_ => {
    // 播放成功回调
  }).catch(error => {
    // 播放失败回调
  });
}

后面产品不想自动播放,用户点击播放(带声音)

得嘞,那就不折腾了,考虑各浏览器间的兼容问题啥的...

4.切后台暂停

1.页面不可见时,暂停播放

const visibilityFn = () => {
  const player = proxy.$refs.videoItemRef[cureentIndex.value]?.player?.instance
  if (document.visibilityState === 'visible') {
    // player?.play()
  } else {
    player?.pause()
  }
}

onMounted(() => {
  onLoad()
  document.addEventListener('visibilitychange', visibilityFn)
})
onUnmounted(() => {
  document.removeEventListener('visibilitychange', visibilityFn)
})

  1. 原生切换到其它tab时,暂停播放

这个需要原生通知,切换至其它tab时,暂停播放

三、总结+踩过的坑

  1. 上下滑动 => 使用swiper
  2. 滑动的播放暂停 => 使用$refs 来控制
  3. m3u8格式 => 使用videojs来解析(可以使用西瓜播放器平替xgplay或者其它)
  4. 视频尺寸问题 => 通过宽高比 计算出 宽 再进行居中处理
  5. 视频卡顿问题 => 动态控制video标签个数 可以使用一个虚拟dom去动态load下面的视频,然后当播放到当前视频时会走缓存就更快一点了,(小伙伴们可能会说:这样不是浪费用户流量么,才进来就框框缓存视频、流量框框掉。咱只要顺畅,嘿嘿,产品说要快 哈哈!!!)

踩过的坑

构建编译报错babel

原因:框架在对videojs做babel转换时,文件大小超过了500KB 就忽略了,以至于报错

解决:在config.ts 添加 babelExtraModule: [/node_modlues/video.js/]

// config.ts

import type { UserConfig } from 'ssr-types'
export const userConfig: UserConfig = {
  serverPort: 3000,
  host: '0.0.0.0',
  babelExtraModule: [/node_modlues\/video.js/]
}

实战分享:上下滑动刷视频、性能优化、踩坑及总结

Uncaught ReferenceError: _babel_runtime_helpers_createClass__WEBPACK_IMPORTED_MODULE_10___default is not defined

实战分享:上下滑动刷视频、性能优化、踩坑及总结

node版本问题😭😭

刚开始是用的node 16.20.0,然后发现ssr-app默认需要的版本 18.13.0 或者大于 20.09.0

这里我升级了node版本 20.11.1,然后发现部署时宝塔不支持18以上,无奈只能降版本了。。。

dev 分支是 20.11.0

master 分支是 16.20.0

源码

github地址

结语:

如果本文对你有收获,麻烦动动发财的小手,点点关注、点点赞!!!👻👻👻

因为收藏===会了

如果有不对、更好的方式实现、可以优化的地方欢迎在评论区指出,谢谢👾👾👾