帧频flash(flv.js的追帧、断流重连及实时更新的直播优化方案)
1. 前言
最近在处理前端直播的业务 ,根据业务需要 ,使用 flv.js 的方案播放实时的flv视频流 。不得不承认 ,flv.js 是一个伟大的库 。
在使用flv.js开发的过程中 ,遇到了一些问题 ,也无外乎是视频延迟 ,视频卡顿等问题 ,经过在github issues里摸爬滚打 ,加上长时间的试错 ,将这些问题归纳出了对应的解决方案 ,也自己封装了一个扩展插件 flvExtend 。
于是写这篇文章来对我遇到的一些问题进行总结 ,我提出的解决方案不一定适合所有场景 ,如果有更好的解决方案,欢迎讨论 ,这也是我写这篇文章的目的 ,也是我写文章的初心 。
2. 前端直播
在讲解 flv.js 的优化方案之前,我想先简单的介绍一下前端直播的方案 ,为什么要使用 flv.js ,方便大家理解以及作为一项技术来储备 。
2.1 常见直播协议
RTMP: 底层基于 TCP ,在浏览器端依赖 Flash 。 HTTP-FLV: 基于 HTTP 流式 IO 传输 FLV ,依赖浏览器支持播放 FLV 。 WebSocket-FLV: 基于 WebSocket 传输 FLV ,依赖浏览器支持播放 FLV 。WebSocket 建立在 HTTP 之上 ,建立 WebSocket 连接前还要先建立 HTTP 连接 。 HLS: Http Live Streaming ,苹果提出基于 HTTP 的流媒体传输协议 。HTML5 可以直接打开播放 。 RTP: 基于 UDP ,延迟 1 秒 ,浏览器不支持 。可以看到 ,在浏览器端 ,可以考虑的方案有:HTTP-FLV 、WebSocket-FLV 以及 HLS , 我们可以对比一下这几个直播协议之间的性能:
(以下数据来源于网络,只做对比参考) 传输协议 播放器 延迟 内存 CPU RTMP Flash 1s 430M 11% HTTP-FLV Video 1s 310M 4.4% HLS Video 20s 205M 3%可以看出在浏览器里做直播 ,使用 HTTP-FLV 协议是不错的 ,性能优于 RTMP+Flash,延迟可以做到和 RTMP+Flash 一样甚至更好。
2.2 flv.js 的原理
flv.js 的主要工作就是 ,在获取到 FLV 格式的音视频数据后通过原生的 JS 去解码 FLV 数据 ,再通过 Media Source Extensions API 喂给原生 HTML5 Video 标签 。(HTML5 原生仅支持播放 mp4/webm 格式 ,不支持 FLV)
flv.js 为什么要绕一圈 ,从服务器获取 FLV 再解码转换后再喂给 Video 标签呢?原因如下:
兼容目前的直播方案:目前大多数直播方案的音视频服务都是采用 FLV 容器格式传输音视频数据 。 FLV 容器格式相比于 MP4 格式更加简单 ,解析起来更快更方便。2.3 flv.js 的简单使用
<script src="https://www.cnblogs.com/xiahj/p/flv.min.js"></script> <video id="videoElement"></video> <script> if (flvjs.isSupported()) { var videoElement = document.getElementById("videoElement"); var flvPlayer = flvjs.createPlayer({ type: "flv", isLive: true, url: "http://example.com/flv/video.flv", }); flvPlayer.attachMediaElement(videoElement); flvPlayer.load(); flvPlayer.play(); } </script>主要流程就是:
创建flvjs.Player对象 ,可以传递两个参数:MediaDataSource ,以及 Config ,具体的可以看下官方文档 挂载元素 加载视频流 播放视频流附:官方 API 文档
3. flv.js 的优化方案
我们根据官方的例子 ,可以很容易地把 flv 直播流播起来 ,但是在实际项目中使用时 ,还会遇到一些问题 ,我们需要手动对这些问题进行优化处理
3.1 追帧-解决延迟累积问题
flv.js 有一个最大的问题,就是延迟问题 ,一方面是直播端的延迟 ,一方面是浏览器的延迟,而且浏览器的延迟如果不做特殊处理 ,会造成延时累积的问题 ,对直播的实时性影响很大 。
解决方案需要从以下两部分入手:
3.1.1 修改 config 配置
{ enableWorker: true, // 启用分离的线程进行转换 enableStashBuffer: false, // 关闭IO隐藏缓冲区 stashInitialSize: 128, // 减少首帧显示等待时长 } 开启 flv.js 的 Worker ,多线程运行 flv.js 提升解析速度可以优化延迟 关闭 buffer 缓存 ,这个选项可以明显地降低延迟 ,缺点就是由于关闭了 buffer 缓存 ,网络不好的时候可能会出现 loading 加载 调低 IO 缓冲区的初始尺寸 ,减少首帧显示的等待时长3.1.2 追帧设置
解决延时累加最有效的方式就是进行追帧设置
追帧 ,就是去判断缓冲区末尾的 buffer 值与当前播放时间的差值 ,如果大于某个值 ,就进行追帧设置 ,具体的思路如下:
首先 ,在 progress 事件,或者定时器中进行追帧逻辑 判断 buffer 的差值 delta let end = this.player.buffered.end(0); //获取当前buffered值(缓冲区末尾) let delta = end - this.player.currentTime; //获取buffered与当前播放位置的差值如果 delta 值大于某个设定的值 ,则进行追帧操作 追帧有两种方式
1)一种是直接更新当前的时间:this.player.currentTime = this.player.buffered.end(0) - 1 ,缺点是如果频繁触发会导致跳帧,观感差;
2)一种是调快播放速度的方式来慢慢追帧: this.videoElement.playbackRate = 1.1 ,优点是稳定 ,缺点是如果 delta 值过大 ,通过这种方式追得太慢
在实际使用中两种方式可以结合起来 。代码实现:
videoElement.addEventListener("progress", () => { let end = player.buffered.end(0); //获取当前buffered值(缓冲区末尾) let delta = end - player.currentTime; //获取buffered与当前播放位置的差值 // 延迟过大 ,通过跳帧的方式更新视频 if (delta > 10 || delta < 0) { this.player.currentTime = this.player.buffered.end(0) - 1; return; } // 追帧 if (delta > 1) { videoElement.playbackRate = 1.1; } else { videoElement.playbackRate = 1; } });3.2 断流重连
断流重连即在flvjs播放失败的回调中 ,进行重建视频的操作
代码实现:
this.player.on(flvjs.Events.ERROR, (e) => { // destroy this.player.pause(); this.player.unload(); this.player.detachMediaElement(); this.player.destroy(); this.player = null; // 进行重建的逻辑 ,这里不再展开 this.init(); });3.3 实时更新
直播需要保证视频的实时性 ,以下两种操作都会导致视频的实时性得不到保证:
用户点击了暂停 ,过一段时间后再点播放 ,这时候的直播视频不是最新的 网页切到后台 ,再重新切换回前台 ,视频不是最新的所以需要根据这两种情况来实时更新视频
代码实现:
// 点击播放按钮后 ,更新视频 videoElement.addEventListener("play", () => { let end = player.buffered.end(0) - 1; this.player.currentTime = end; }); // 网页重新激活后,更新视频 window.onfocus = () => { let end = player.buffered.end(0) - 1; this.player.currentTime = end; };3.4 解决 stuck 问题
有的时候 ,视频在播放的过程中会突然卡住 ,或者控制台有时会报错 “Playback seems stuck at 0, seek to 1.1 ” 。
我们需要判断视频是否卡住了,然后重建视频实例
思路就是判断 decodedFrames 是否产生变化 ,如果视频是播放状态并且该值没有产生变化 ,则可以判断视频卡住了 。
代码实现:
function handleStuck() { let lastDecodedFrames = 0; let stuckTime = 0; this.interval && clearInterval(this.interval); this.interval = setInterval(() => { const decodedFrames = this.player.statisticsInfo.decodedFrames; if (!decodedFrames) return; if (lastDecodedFrames === decodedFrames && !this.videoElement.paused) { // 可能卡住了 ,重载 stuckTime++; if (stuckTime > 1) { console.log(`%c 卡住 ,重建视频`, "background:red;color:#fff"); // 先destroy ,再重建视频实例 this.rebuild(); } } else { lastDecodedFrames = decodedFrames; stuckTime = 0; } }, 800); }4. 封装插件 flvExtend.js
我将这些优化方案封装成了一个插件 flvExtend.js ,它相当于是 flv.js 的一个功能扩展
插件地址:https://github.com/shady-xia/flvExtend
使用起来是这个样子:
import FlvExtend from "flv-extend"; // 配置需要的功能 const flv = new FlvExtend({ element: videoElement, // *必传 frameTracking: true, // 开启追帧设置 updateOnStart: true, // 点击播放后更新视频 updateOnFocus: true, // 获得焦点后更新视频 reconnect: true, // 开启断流重连 reconnectInterval: 2000, // 断流重连间隔 }); // 调用 init 方法初始化视频 // init 方法的参数与 flvjs.createPlayer 相同 ,并返回 flvjs.player 实例 const player = flv.init( { type: "flv", url: "http://192.168.0.11/stream", isLive: true, }, { enableStashBuffer: false, // 如果您需要实时(最小延迟)来进行实时流播放 ,则设置为false stashInitialSize: 128, // 减少首帧显示等待时长 } ); // 直接调用play即可播放 player.play();5. 其他问题
这里打算长期记录一下遇到的问题以及解决思路 ,欢迎大家讨论 ,我会更新补充
1)多路视频同时直播
由于浏览器对 http 1.0 的限制 ,以Chrome为例 ,同一个浏览器下,最多只能播6路同源地址下的视频(包括多个标签页也会被合算在内)
目前的解决方案有:
使用http 2.0 ,由于http 2.0的多路复用 ,可以同屏播放多个视频流 使用 websocket 通过为流分配不同的服务端地址参考
github issues 使用 flv.js 做直播创心域SEO版权声明:以上内容作者已申请原创保护,未经允许不得转载,侵权必究!授权事宜、对本内容有异议或投诉,敬请联系网站管理员,我们将尽快回复您,谢谢合作!