前端结合webassembly与webworker实现视频播放器,支持倍速、逐帧、快进、快退、进度条拖拽

276 阅读5分钟

想要实现浏览器视频播放器需要实现一下步骤。

  1. 首先,实现前端代码引入利用webassembly技术编译的解码文件.wasm和.js.实现思路可以参考github.com/goldvideo/d… 的README.md文件。

  2. 在编译前需要安装Emscripten,拉取ffmpeg代码。 Emscripten的安装与使用可以参考

    1)juejin.cn/post/685708…

    2)emscripten.org/docs/gettin…

  3. 将编译好的.wasm和.js引入项目中使用,实现顺序播放与暂停功能。可参考: 1)juejin.cn/post/694602…

    2)juejin.cn/post/696199…

经历以上一步一坑的步骤后,我们终于实现了浏览器播放h264、h265的视频播放。恭喜你终于来到了前端视频播放器最坑环节,惊不惊喜?意不意外? 大家平常浏览视频网站,视频不止具有播放暂停功能还可以任意拖拽进度条、倍速等。然而按照以上步骤实现的视频播放功能在用户拖拽进度条、倍速播放、倒放时会存在花屏问题,而且非常花!!!糊眼的那种。

这是由于视频数据的特性导致的,简单来说视频数据分为非关键帧和关键帧,非关键帧的解码需该帧前面最近的关键帧及其它非关键帧的数据。前面步骤1、2、3实现播放采用的是实时解码方案,这样如果用户拖拽进度条、倍速播放时很有可能传给解码器的是一帧非关键帧数据,解码时由于缺少依赖数据从而导致花屏。

想要解决以上问题总方案是采用预解码。在实践过程中我总共实验了以下5种方案

方案一 使用一个主线程,一个解码子线程(webworker),用户拖拽进度条时,查询进度条时刻对应的片段数据,从关键帧开始解码并保存解码后数据。播放时按照时间戳拿到预解码后的数据直接渲染。

遇到的问题:

  1. 用户拖拽进度条后需要等待片段数据预解码完成,时间较长
  2. 用户快速频繁拖拽进度条时,由于子线程webworker接收数据按照先来后到的原则接,不能快速响应预解码用户最后跳转的那个片段数据,等待时间与拖动次数基本成正比增加,另外ffmpeg 缓冲区存在丢尾帧问题,导致最终播放时视频画面出现前后画面来回跳动播放的现象

方案二 一个主线程,一个解码子线程(webworker),播放前将所有video数据预解码并保存,播放时按照时间戳拿到预解码后的数据直接渲染。

遇到的问题:

  1. 开始播放前等待时间长
  2. 如果视频时间很长比如几分钟、半小时、一两个小时。不仅等待时间长,更难的是解码后内存剧增,很大概率还没有等到播放浏览器就崩溃了。所以全部预解码只针对视频时长不超过3分钟的数据做了试验

方案三 一个主线程,每次拖拽时重新创建一个新的解码子线程。

遇到的问题: 用户拖拽后,虽然可以解决方案一遇到的前后画面反复跳动的问题,也较大缩短了此时用户等待的时间,但以预解码400帧左右为例,仍然需要等待3s左右不能满足业务要求

方案四 一个主线程,一个子线程,用户拖拽时先找到前面距离最近的关键帧,然后依次解码到当前时间开始播放。后面的数据实时解码

遇到的问题: 如果用户拖拽后立刻倒放,由于只解码了当前时间戳依赖的关键帧数据,如果用户倒放到更靠前的时间,会再出现loading画面,给用户一个频繁loading的印象,体验不够好。

优点: 该方案其实是后端播放器常用的方案,在前端实现时,由于只需要预解码当前时间戳往前较少帧数的数据,缩短预解码时间的同时还可以减少浏览器内存的消耗

方案五 一个主线程,多个子线程。与方案三不同之处为,本方方案使用多个子线程并行预解码,解码后保存数据。当视频时间长较短时全部解码后占用内存不超过1G时,可以在播放前使用多个子线程全部预解码后播放。 如果视频时长较大,可以并行预解码当前时间前后一定范围内的数据后开始播放。

优点: 既能快速响应用户操作,又能减少loading次数

缺点: 由于预解码后数据保存在内存里,预解码的时间越长占用内存越大。不过只要合理设置预解码的时间范围可以屏蔽该缺点。所以当前项目暂时采用了此方案

方案六: 一个主线程,一个子线程。视频拖拽、倍速、逐帧等逻辑使用c语言调用ffmpeg 的api实现。主线程在播放时传输时间戳给解码器,在解码器里实现seek和精准seek。解码器使用单线程如果不能满足实效性,可以考虑c语言的多线程

优点:js代码逻辑被简化,占用内存小、响应快速

缺点:要求熟悉c语言编码,对于不熟悉c语言的前端开发人员来说实现难度大