由于在自己的工作和学习过程中,只查看某个大佬的教程或文章无法满足自己的学习需求和解决遇到的问题,所以自己在追赶大佬们步伐的基础上,又自己总结、整理、汇总了一些资料,方便自己理解和后续回顾,同时也希望给大家带来帮助,所以才写下该篇文章。在本文中,所有参考或引用大佬们文章内容的位置,都附上了原文章链接,您可以直接前往查阅观看。在原文章内容的基础上,若无任何补充内容,同时避免直接大段摘抄大佬们的文章,该情况下也只附上了原文章链接供大家学习。本文旨在总结归纳,并希望给大家提供帮助,未用作任何商用用途。文章内容如有错误之处,望各位大佬指出。如果涉及侵权行为,将会第一时间对文章进行删除。


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


其他前端组件使用和踩坑记录文章,欢迎您查看:



前言

在最近的开发中遇到了一个需求,就是能够在页面上看到直播流。在听到这样的一个开发需求时,只觉得这貌似很有难度,因为自己从来没有接触过这样的用法。此时,我第一时间想到了 HTML5 的 video 标签。不过我猜测目前肯定有更好的方法,所以为了一步到位,还是先网上冲浪一下。

经过一段时间探索,发现还真有其他的解决方案,即 videojs。而苦于网上没有找到言简意赅的教程,所以只能通过各位前辈们的博客去搜集资料,去不断试错。最终,通过参考大佬们的文章,找到了可以实现的解决方案,而且解决了部分常见问题。在这里进行总结,希望能够给各位提供帮助和思路。

在下文中,所有参考的文章如下:

videojs 官网:https://videojs.com/ (全英无中)

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

可进行测试的 m3u8 链接:https://blog.csdn.net/myjie0527/article/details/117385253

项目经验和踩坑 - Vue中使用videojs做rtmp和hls直播流:https://blog.csdn.net/weixin_44238796/article/details/105808404

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

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


何为 videojs

首先 videojs 是一个开源 HTML5 播放器框架。

【图片截取自 videojs 官网】
在这里插入图片描述

在官网中可以看到它的很多特点,在这里帮大家快速总结一下:

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

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

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

如果各位对除了基本使用以外的内容感兴趣,可前往 videojs 官网:https://videojs.com/ 查看。


Vue 中使用 videojs 做 hls 直播流 => 实现代码

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

因为在 Vue 项目中使用 videojs 做 hls 直播流时,我也是参考相关文章中的代码,且在亲自尝试后,确定该用法可以实现,所以我这里也就不粘贴了。尊重原创,关于更详细的使用内容,为了避免大篇幅引用,您需要前往以下文章查阅:项目经验和踩坑 - Vue中使用videojs做rtmp和hls直播流


videojs 的监听事件

而在使用过程中,不可避免的就是需要使用其中的事件。关于更详细的内容,为了避免大篇幅引用,您需要前往以下文章查阅:videojs的一些监听事件汇总。后续在解决部分问题时,就将会用到其中的部分事件。


videojs 遇到的问题总结

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


问题一: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 销毁问题

为了更好的解释问题,相关代码展示如下:(代码不完整,仅用于展示问题,无法直接运行)

<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 {
  data() {
    return {
      // ...
      showVideo: false,
      videoPlayer: null
    }
  },
  // ...
  beforeDestroy() {
    if (this.videoPlayer !== null) {
      this.videoPlayer.dispose() // dispose()会直接删除Dom元素
    }
  },
  methods: {
    startTransFlow() {
	   // ...
	   // 调用接口,触发生成 m3u8 文件的事件。拿到url路径
	   // ...
	
       this.showVideo = true;

       this.videoPlayer = this.$video(document.getElementById('cameraIndexCode'), {
           autoplay: true, // 是否自动播放
           controls: true, // 是否显示控件
       })

       this.videoPlayer.src({
           src: url, // 上方调用接口拿到的url路径
           type: 'rtmp/flv', // 这个type值必写, 告诉videojs这是一个rtmp流视频
       })
    }
  }
}
</script>

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

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

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

1、首先,我们必须要在不想播放该视频流的时候进行销毁。因为现在是直播,如果我们不对其进行销毁,那么离开页面后,这部分切片内容(即视频)仍然会不断的进行获取。那显而易见的,如果用户又点击查看了不同的视频流,最后获取到的内容越来越多,当多到一定限额时,页面很可能崩溃。(不过对于该问题没有进行过多测试,因为在当前的需求中,视频流其实是在一个 el-dialog 上展现的,如果在关闭对话框时不销毁,视频流显然会不断获取。但是未测试切换普通页面或 F5 后是否仍会获取,不过这肯定是个需要留意的问题。其实更重要的是后端该怎么判断前端不需要获取视频流,从而停止生成切片(视频)。这可以通过 websocket 或者在页面销毁时用接口来解决)
在这里插入图片描述

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

在上面已经提到了,这样操作会直接删除这个 DOM 元素。这就会导致使用 dispose 后,如果还想去测试视频流,此时 DOM 元素被删除,也就肯定无法再查看视频流。

所以现在问题的解决方案就是:在合适的时间进行销毁,在合适的时间再创建这个 DOM 元素。如果有大佬对 DOM 操作比较熟练可以这样做,不过由于本人不太擅长且这样做有些麻烦,所以我就想起来利用 Vue 中 v-if 的机制。

3、针对这个问题,再总结一下就是:

(1)在离开页面或者想重新获取视频流时,我们需要将之前的 videojs 进行销毁。

(2)在想播放视频流时,要保证 video 标签的存在。

针对以上可能出现的复杂判定条件,我利用了 v-if 的特性。 v-if 的特性就是它在 true、false 转化时,对组件是重新销毁和创建的。那么这样就很清晰了。当我们想要实现销毁时,就去切换 v-if,从而触发该组件的 beforeDestroy 事件。也就是需要将可实现代码放入另一个组件里,然后在要使用这个 video 的组件里去引用这个自己创建的 video 组件,然后在父组件判断销毁或创建时,去切换这个组件的 v-if 状态即可。当切换 v-if 后,组件就会重新创建,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路径
		// ...
		
        this.showVideo = true;
        // 选中的要播放的video标签
        this.videoPlayer = this.$video(document.getElementById('cameraIndexCode'), {
          autoplay: true, // 是否自动播放
          controls: true // 是否显示控件
        }, function onPlayerReady() {
          this.src(url); // 在这里放入接口返回的url路径
          this.play();
        })
      }
    }
  }
}
</script>

防抖的代码简单来看就是这样的:

if (this.timer === null) {
  /* 防抖 */
  this.timer = setTimeout(() => {
    this.timer = 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 的延时和其他方案相比很高。这样一来,就无法满足低延时的这种需求。如果您对低延时没有要求,那就不用考虑太多问题了。

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

hls 延时会达到 15 ~ 30 秒。
rtmp 延时只有 1 ~ 5 秒。
http-flv 延时只有 4 ~ 10 秒。
在这里插入图片描述

从目前来看几个主要的视频流对比如下:(如果还有其他格式,确实是笔者才疏学浅)

在这里插入图片描述

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

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

起初,没觉得这是什么大问题,应该就是什么小 bug,最多也就是跨域问题。但通过在网上参考了很多篇文章,发现这个问题完全解决不掉。那到底问题出在哪里?这个报错信息属实是难以定位。

最后,一篇文章吸引了我的注意力:Chrome将在2020年底不再支持Flash,那如何播放rtmp格式的监控或直播视频?

在这里插入图片描述

那也就是说,出现这个问题的最根本原因,也许就是 chrome 及其他浏览器要逐步淘汰 flash(而经过测试,至少 2022年 chrome 最新版本已经不支持了)。如果您想尝试到底有没有办法播放 rtmp 流,您可以选择进入上面的文章查看,或者在网上查阅。

当然,作为一个前端程序猿,我们肯定不能要求使用者只使用特定版本的浏览器。所以才有了下文对 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 做直播流的内容就完成了。关于十分具体的前端实现源码、后端如何生成 http-flv、nginx 如何解决跨域等内容,恕笔者无法提供,因为涉及项目核心功能代码。总之笔者仅参考了以上内容就完成了相关需求。希望这些参考文章能够为您提供帮助!


由于在自己的工作和学习过程中,只查看某个大佬的教程或文章无法满足自己的学习需求和解决遇到的问题,所以自己在追赶大佬们步伐的基础上,又自己总结、整理、汇总了一些资料,方便自己理解和后续回顾,同时也希望给大家带来帮助,所以才写下该篇文章。在本文中,所有参考或引用大佬们文章内容的位置,都附上了原文章链接,您可以直接前往查阅观看。在原文章内容的基础上,若无任何补充内容,同时避免直接大段摘抄大佬们的文章,该情况下也只附上了原文章链接供大家学习。本文旨在总结归纳,并希望给大家提供帮助,未用作任何商用用途。文章内容如有错误之处,望各位大佬指出。如果涉及侵权行为,将会第一时间对文章进行删除。


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


其他前端组件使用和踩坑记录文章,欢迎您查看:

Logo

致力于链接即构和开发者,提供实时互动和元宇宙领域的前沿洞察、技术分享和丰富的开发者活动,共建实时互动世界。