【Vue前端】使用 videojs 做直播流遇到的问题及解决方案总结(video使用 + http-flv 低延时优化方向)

本文介绍了在Vue项目中使用videojs进行HLS直播流的实现过程,包括videojs的基本概念、Vue中的使用、常见问题及解决方案。详细讨论了videojs初始化、监听事件、视频销毁、防抖处理、加载错误重试以及低延时优化。此外,还提及了http-flv作为替代方案以降低延迟,并分享了相关资源链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

👉 个人博客主页 👈
📝 一个努力学习的程序猿


专栏:
HTML和CSS
JavaScript
jQuery
Vue
Vue3
React
TypeScript
uni-app
Linux
个人经历+面经+学习路线【内含免费下载初级前端面试题】
更多前端面试分享
前端学习+方案分享(VitePress、html2canvas+jspdf、vuedraggable、videojs)
前端踩坑日记(ElementUI)


在最近的开发中遇到一个需求:需要在页面上看到直播流(摄像头监控 / 直播推流)。本文将实现该功能,并不断解决遇到的问题 + 进行优化。

寻找测试直播流的朋友看这里:
rtsp、rtmp、m3u8、flv、mkv、3gp、mp4直播流测试地址
可进行测试的 m3u8 链接

除此以外,最简单的方法是使用 obs studio 自己推流,即可看到播放效果。


前言(video标签使用 + videojs介绍)

关于播放视频,第一时间就想到了 video 标签。示例代码:

<template>
   <div class="video-container">
     <div v-if="video" class="video-btn">
       <button @click="playPause">
         {{ isVideoPlay ? '暂停' : '播放' }}
       </button>
       <button @click="skipAhead(1)">
         快进1s
       </button>
       <button @click="changeVolume(-5)">
         减小音量
       </button>
       <button @click="changeVolume(5)">
         放大音量
       </button>
     </div>
     <video
       id="basicVideo"
       controls
       autoplay
       loop
       muted
       preload="auto"
       width="800"
       height="450"
     >
       <source src="http://www.w3school.com.cn/i/movie.mp4" type="video/mp4">
       <source src="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/x-mpegURL">
       对不起,您的浏览器不支持 HTML5 视频。
     </video>
   </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from '@vue/composition-api'

export default defineComponent({
  name: 'VideoPage',
  setup() {
    const video = ref(null) as any
    const isVideoPlay = ref(true)
    onMounted(() => {
      video.value = document.getElementById('basicVideo')
      if (video.value) {
        // 监听视频播放事件
        video.value.addEventListener('play', function() {
          console.log('视频开始播放')
        })

        // 监听视频暂停事件
        video.value.addEventListener('pause', function() {
          console.log('视频已暂停')
        })

        // 监听视频结束事件
        video.value.addEventListener('ended', function() {
          console.log('视频播放结束')
        })

        // 监听视频时间更新事件
        video.value.addEventListener('timeupdate', function() {
          console.log('当前播放时间:', video.value.currentTime)
        })
      }
    })
    function playPause() {
      if (video.value.paused) {
        video.value.play()
        isVideoPlay.value = true
      } else {
        video.value.pause()
        isVideoPlay.value = false
      }
    }

    function skipAhead(seconds) {
      video.value.currentTime += seconds
    }

    function changeVolume(delta) {
      video.value.volume = Math.max(0, Math.min(1, video.value.volume + delta))
    }

    return {
      video,
      isVideoPlay,
      playPause,
      skipAhead,
      changeVolume,
    }
  },
})
</script>
<style scoped lang="scss">
.video-container {
  max-width: 800px;
  margin: 0 auto;
  .video-btn {
    display: flex;
    align-items: center;
    button {
      margin-right: 10px;
    }
  }
  video {
    width: 100%;
    height: auto;
  }
}
</style>

请添加图片描述
其实如果只是简单场景,播放个视频,video 标签已经很适用,它能作用于几乎全主流的浏览器,而且用起来也很简单。但放弃它原因很简单:

1、video 标签支持的主要是基于 HTTP 的流媒体协议,如 HLS。(不过这也不算是缺陷,当前 RTMP 已经无法使用了,主流全部都是 HTTP。在下文中有说明 RTMP 的问题)

2、video 标签播放功能相对简单。换句话说,如果我想要为其增加功能做扩展,可能需要处理不同设备的兼容性问题,考虑的事情很多,不如直接开箱即用的组件库,即 videojs。

在这里插入图片描述
videojs 是一个开源 HTML5 播放器框架,在 videojs 官网 中可以看到它很多特点:

1、videojs 不仅可以播放传统的文件格式,比如:MP4、WebM、Ogg,也能够支持自适应流格式,如 Hls、rtmp;

2、播放器开箱即用,而且可以很轻松的设置额外的 CSS 样式;

3、使用 videojs 可以支持现代的所有浏览器,包括桌面和移动浏览器。

请添加图片描述

因为官网无中文汉化,且内容较多,不太好找到关键配置,所以本文也参考了很多成功的文章案例,感谢各位大佬们的付出。本文希望能给各位提供帮助。

videojs 官网:https://videojs.com/

视频播放插件Video.js:https://www.jq22.com/plugin/404

vue项目中video.js的使用总结:https://blog.csdn.net/weixin_39135926/article/details/118225035

videojs的一些监听事件汇总:https://blog.csdn.net/Q147351/article/details/106663908/


1、Vue 中使用 videojs 做直播流

由于网上真的有很多繁杂的内容,而且大多是直接在 html 中使用,而不是针对 Vue,所以我在寻找解决方案的过程中,也是费了一些时间。在这里直接给出代码:

npm install video.js --save
npm install videojs-contrib-hls --save

在main.js引入样式文件

import 'video.js/dist/video-js.css'

示例:

<template>
  <div class="video-container">
    <video
      id="my-video"
      class="video-js vjs-default-skin"
      width="800"
      height="450"
    />
  </div>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted } from '@vue/composition-api'
import videojs from 'video.js'
import 'videojs-contrib-hls'

export default defineComponent({
  name: 'VideoTest',
  setup() {
    const video = ref(null) as any
    onMounted(() => {
      video.value = videojs('my-video', {
        autoplay: true, // 是否自动播放
        controls: true, // 是否显示控件
        fluid: true, // 是否使用流式布局(自适应父容器)
        responsive: true, // 是否响应式
        playbackRates: [0.5, 1, 1.5, 2], // 可选的播放速率
        muted: false, // 是否静音
        loop: false, // 是否循环播放
        preload: 'auto', // 预加载方式:'auto', 'metadata', 'none'
        // poster: '', // 视频封面图片
        // width: 500, // 播放器宽度
        // height: 264, // 播放器高度
        aspectRatio: '16:9', // 视频宽高比
        controlBar: { // 控制栏配置
          volumePanel: { inline: false }, // 音量控制是否内联
          fullscreenToggle: true // 是否显示全屏按钮
        },
        language: 'zh-CN', // 播放器语言
        // 视频源
        // sources: [{
        //   type: 'video/mp4',
        //   src: 'http://www.w3school.com.cn/i/movie.mp4'
        // }],
        // techOrder: ['html5', 'flash'], // 技术优先级
      }, onPlayerReady)
      // 因为通常需要等待接口返回地址,所以可以用这样的方式,重新绑定直播流
      video.value.src({
        src: 'http://www.w3school.com.cn/i/movie.mp4',
        type: 'video/mp4',
      })
      /** 可选type值
       video/mp4: MP4 格式
       video/webm: WebM 格式
       video/ogg: Ogg 格式
       application/x-mpegURL 或 application/vnd.apple.mpegurl: HLS 流媒体格式
       rtmp/mp4: RTMP 流媒体(MP4)
       rtmp/flv: RTMP 流媒体(FLV)
       video/x-flv: FLV 格式
       video/3gpp: 3GPP 格式(常用于移动设备)
       video/quicktime: QuickTime 格式
       video/x-matroska: MKV 格式
       video/x-ms-wmv: Windows Media Video 格式
       application/dash+xml: MPEG-DASH 流媒体格式
       * */
    })
    // 事件监听
    const onPlayerReady = () => {
      if (video.value) {
        video.value.on('loadstart', function() {
          console.log('开始请求数据')
        })
        video.value.on('progress', function() {
          console.log('正在请求数据')
        })
        video.value.on('loadedmetadata', function() {
          console.log('获取资源长度完成')
        })
        video.value.on('canplaythrough', function() {
          console.log('视频源数据加载完成')
        })
        video.value.on('waiting', function() {
          console.log('等待数据')
        })
        video.value.on('play', function() {
          console.log('视频开始播放')
        })
        video.value.on('playing', function() {
          console.log('视频播放中')
        })
        video.value.on('pause', function() {
          console.log('视频已暂停')
        })
        video.value.on('ended', function() {
          console.log('视频播放结束')
        })
        video.value.on('error', function() {
          console.log('加载错误')
        })
        video.value.on('seeking', function() {
          console.log('视频跳转中')
        })
        video.value.on('seeked', function() {
          console.log('视频跳转结束')
        })
        video.value.on('ratechange', function() {
          console.log('播放速率改变')
        })
        video.value.on('timeupdate', function() {
          console.log('播放时长改变')
        })
        video.value.on('volumechange', function() {
          console.log('音量改变')
        })
        video.value.on('stalled', function() {
          console.log('网速异常')
        })
      }
    }

    return {}
  },
})
</script>
<style scoped lang="scss">
.video-container {
  max-width: 800px;
  margin: 0 auto;
  video {
    width: 100%;
    height: auto;
  }
}
</style>

在测试的时候,如果用测试直播流可能会碰到跨域的问题,烦请自行解决。


2、videojs 遇到的问题总结

接下来是本文的主要内容,记录了我自己在做直播流过程中,遇到的问题和相关解决方案。

问题一:The element or ID supplied is not valid

在最开始使用的时候,我遇到了下面的报错:

在这里插入图片描述

针对这个报错,在网上有一些相关的参考方案:

video.js多个视频初次加载报错The element or ID supplied is not valid

videojs中遇到的问题

而我在尝试了几种方法后,发现并没有解决。所以在回到原点后,针对报错信息思考了一下,其实就是在使用 videojs 获取 ID 时,遇到了问题

经过自己的排查发现,问题的原因其实很简单。我在使用 getElementByID 时,ID 写错了,其中包含了特殊字符。也就是说需要注意,在写 ID 名时,确保不含有 # 特殊字符(其他特殊字符或许也会有问题,尽量避免)。除此以外,请务必注意执行顺序(比如在 created 生命周期调用 getElementById 肯定拿不到这个元素,因为这时候元素还未生成。需要把这样的操作,放到 mounted 中)


问题二:video 销毁问题

实现销毁看起来是很简单的,就是在对应的时间点上去触发 dispose 事件

beforeDestroy() {
  if (video.value !== null) {
    video.value.dispose() // dispose()会直接删除Dom元素
    video.value = null
  }
},

但是需要注意的是,当我们对 videojs 对象使用了 dispose 后,它会直接删除这个 DOM 元素。那么这会有什么后果,又为什么要进行销毁?

1、首先,我们必须在不想播放该视频流的时候进行销毁

因为是直播,如果我们不对其进行销毁,那么离开当前页面后,这部分切片内容(即视频)仍然会不断的进行获取。那显而易见的,如果用户又点击查看了不同的视频流,最后获取到的内容越来越多。当多到一定限额时,后果可想而知。

如果你想着 F5 就完事大吉了,那也不对。因为生成直播流,一定是前端告知后端去生成(无论你们技术选型如何)。如果你不告知后端,前端已经关闭,后端可能依然会生成切片内容到主机(除非停止推流)。这也是需要考虑的前后端交互问题。

在这里插入图片描述

2、现在我们已经知道了需要对它进行销毁,那么直接使用 dispose 会有什么后果

因为这样操作会直接删除 DOM 元素,所以这就会导致使用 dispose 后,如果还想重启 videojs,你就没办法再像 mounted 里那样:video.value = videojs('my-video', ...)了,因为 id 为 'my-video' 的 DOM 元素已经被删除了。

所以现在针对问题,要做的事情是

(1)在离开当前页面或必要情景时,需要将之前的 videojs 进行销毁(如有必要,需要告知后端,前端已经停止获取)。

(2)在 videojs 被销毁后,想重新播放视频流时,要保证 video 标签的存在。

3、解决方案

(1)如果你想直接操作 DOM,那你可以这样做

// 假设你有一个重新初始化的函数
function reinitializePlayer() {
  // 1. 确保旧实例被销毁
  if (video.value) {
    video.value.dispose();
    video.value = null;
  }

  // 2. 重新创建 video 元素
  const videoContainer = document.getElementById('video-container');
  const videoElement = document.createElement('video');
  videoElement.id = 'my-video';
  videoElement.className = 'video-js';
  videoContainer.appendChild(videoElement);

  // 3. 重新初始化 video.js
  video.value = videojs('my-video', {
    // ... 配置项
  }, onPlayerReady);

  // 4. 设置新的视频源(如果需要)
  video.value.src({
    src: 'https://example.com/new-video-source.mp4',
    type: 'video/mp4'
  });
}

(2)更巧妙的方式是利用 Vue 中 v-if 的机制:它在 true、false 转化时,对组件是重新销毁和创建的。你可以把上述 videojs 的功能封装一个组件,当要播放时 v-if 为 true,此时就会正常进入 mounted 播放,DOM 也都存在。当想要实现销毁时,就 v-if 为 false,从而触发该组件的 beforeDestroy 事件

(3)最后的最后,如果需要告知后端停止生成,就可以放在 beforeDestroy 事件中。通信可以考虑使用 websocket ,或者在页面销毁时直接调用http接口通知后端,或者其他技术选型。


问题三:防抖问题

如果你是通过按钮来开启 videojs 视频流的话,防抖必不可少。

<button @click="startTransFlow">开始生成</button>

防抖的代码:

if (timer.value === null) {
  /* 防抖 */
  timer.value = setTimeout(() => {
    timer.value = null
  }, 25000)
   /*  调用接口,video处理  */
}

防抖就是来避免用户可能会误操作点击多次按钮的,在这里如果触发多次 video 事件,显然可能会出现页面问题。当然,就算不是点击按钮触发,平常也需要注意页面中是否可能会多次触发这些事件。如果多次触发,带来的后果将是不可预估的。


问题四:未获取到对应视频流的重新加载问题

为了更好的解释问题,代码简单展示如下:(代码不完整,仅用于展示问题)

<template>
  <div>
    <el-button
      type="primary"
      @click="startTransFlow"
    >测试</el-button>
    <video
   	  v-if="showVideo"
      id="cameraIndexCode"
      class="video-js vjs-default-skin"
      width="500"
      height="264"
    ></video>
  </div>
</template>
<script>
export default {
  // ...
  methods: {
    startTransFlow() {
      if (this.timer === null) {
        /* 防抖 */
        this.timer = setTimeout(() => {
          this.timer = null
        }, 35000)
		
		// ...
		// 调用接口,触发生成 m3u8 文件的事件。最后拿到接口返回的url
		// ...
		
        const _this = this;
        this.$message.info('摄像头加载中');
        this.showVideo = true;
		setTimeout(() => {
          // 选中的要播放的video标签
          this.videoPlayer = this.$video(document.getElementById('cameraIndexCode'), {
            autoplay: true, // 是否自动播放
            controls: true // 是否显示控件
          }, function onPlayerReady() {
            let num = 1;
            this.on('canplaythrough', () => {
              _this.$message.success('摄像头加载成功');
            });
            // 加载失败会在有限次数内,不断重试
            this.on('error', () => {
              if (num > 3) {
                _this.$message.error('摄像头加载失败,请重新尝试');
                num = 1;
              } else {
                _this.$message.info('摄像头第' + num + '次尝试加载....');
                num++;
                setTimeout(() => {
                  this.src(url); // 放入接口返回的url
                  this.play();
                }, 10000)
              }
            });
            this.src(url); // 放入接口返回的url
            this.play();
          })
        }, 5000)
      }
    }
  }
}
</script>

在这里使用到了 function onPlayerReady() 去反复加载,并进行相应的提示。先阐述一下,为什么会需要反复加载的功能。

在项目中,播放视频流需要通过按钮去触发,从而调用接口,告知后台去对视频流进行处理。此时,后台处理视频流必然需要一些时间,才能给前端返回一个对应的存放 m3u8 (或者其他格式视频)位置的路径。

所以就出现了在获取时 可能获取不到,从而报错的问题(也就是大家看直播经常需要加载和反复刷新的问题)。

那问题就很明确了,无论什么原因,此时直播视频流文件还没生成出来,所以前端也应该给用户一些友好提示,并作出短暂时间的等待。而为了解决这个问题,就如同上面代码一样,用到了 error 事件。只要遇到了报错,就会进入到该事件中。在代码中将会在有限的次数和时间下反复去尝试对应的视频源是否加载完毕,并在加载完成后去进行播放。

而反复去尝试的原理,就是使用:

this.src(url);
this.play();

只要 src 变化,并尝试播放,videojs 就会被调用。它的流程就是这样的,比较简单:

在这里插入图片描述


http-flv 低延时优化说明

说到这里,笔者还想补充说明一些内容。

在上文和之前自己的开发过程中,使用的是 videojs 做直播流,因为这种方式相对简单。但是在后续项目的迭代过程中发现,使用 hls 直播流是会有一些问题的。只要您真的实现了功能,一拿直播流进行测试,查看北京时间就会发现,hls 的延时和其他方案相比很高。这样一来,就无法满足低延时的需求。如果您对低延时没有要求,那就不用考虑太多问题了。

以下为自己测试多个视频流的测试结果:

hls 延时会达到 15 ~ 30 秒。
rtmp 延时只有 1 ~ 5 秒。
http-flv 延时只有 4 ~ 10 秒。

在这里插入图片描述

在这里插入图片描述

那很不幸的是,在我的开发需求中,视频流播放的延时一定要低。那显然的,hls 直播流就不满足需求了。所以我就尝试使用了 http-flv 和 rtmp 两种格式。

但实际上最终的解决方案只能是用 http-flv。因为在使用 rtmp 流时,会发生这样的报错:

在这里插入图片描述

起初,没觉得是什么大问题,最多也就是跨域问题。但通过在网上参考了很多篇文章,发现这个问题完全解决不掉。最后,一篇文章吸引了我的注意力:Chrome将在2020年底不再支持Flash,那如何播放rtmp格式的监控或直播视频?

在这里插入图片描述

那也就是说,出现这个问题的最根本原因,就是 chrome 及其他浏览器要逐步淘汰 flash(而经过测试,至少 2022年 chrome 最新版本已经不支持了)。

当然,我们肯定不能要求使用者只使用特定版本的浏览器。所以才有了下文对 http-flv 的使用说明。


那么言归正传,http-flv 该如何使用呢?由于笔者在参考了以下几篇文章后发现完全可行,且没有遇到其他问题,所以尊重原创,在这里就不大篇幅引用了。 关于更详细的使用内容,您需要前往以下文章查阅:

vue+flv.js实时播放 断流重连 关闭断流开发心得

vue使用flv.js(bilibili)拉流

flv.js解决直播流延迟、断流重连以及画面卡死

flv.js API (API 全英)

Debian使用Nginx和Nginx-http-flv-module来实现简单的直播服务

nginx-http-flv-module.github


至此前端在 Vue 做直播流的内容就完成了。希望这些参考文章能够为您提供帮助!

评论 11
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

前端Jerry_Zheng

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

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

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

打赏作者

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

抵扣说明:

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

余额充值