flv.js直播拉流场景下的技术优化

文章目录

    • 前言
    • 问题
      • 编译问题
      • 延时大,并且会随着播放时间累积放大
      • 直播视频画面可能会卡停、黑屏
      • Chromium Console中经常有各种报错
      • 拉取不规范的http-flv,在某些浏览器上视频画面只能显示一小部分
      • 拉流过程中浏览器内存占用太大
      • 拉取http flv流,收到了MetaData和AVCDecoderConfigurationRecord,但后续视频fps始终为0
      • 遇到一次 MSE SourceBuffer is full, suspend transmuxing task
    • 功能补充
      • 如何获得直播流的实时音频、视频码率?
    • 结尾

前言

众所周知,Adobe Flash按照Google Chrome的计划(https://www.chromium.org/flash-roadmap),在已经release的Chrome 76开始默认禁用,在2020年12月将在Chrome 87中彻底移除。到那时,如果用户使用了最新版本的Chromium内核的浏览器,意味着将无法播放各种来自CDN推送的媒体流(rtmp、flv等)。当然也不排除人们又想出来一些能让Flash继续活下去的“外挂”技术。

好在现在已经有了大量的开源纯js实现的库,如hls.js、flv.js等,以及围绕这些基础库又经过各种封装改进的版本(如Dplayer、TcPlayer等等)供大家选择。

因为种种原因,flv.js的作者从2018年底开始以后已经没有过代码提交了(参看 flv.js github commit log),也许作者觉得也没什么需要修改的了?不过我注意到flv.js的issues区还存在着大量的提问和讨论,其中有一些可能是在被反复询问的,有一些可能不是有共性的问题。

我个人其实并没有做过前端开发,以前也没正经学过js。但是作为一名音视频工作者,因为在工作中使用flv.js播放直播流遇到了一些问题,所以就一边学js一边看flv.js的源码,根据自己的理解,进行了一些修改。这篇文章把我遇到的问题都记录一下,有些问题已经有答案和解决办法(可能不一定对),有些可能还没有答案。

问题

flv.js是可以同时支持直播流(通过isLive:true来设置)和点播的,我面对的主要针对直播场景,遇到的问题主要包括:

  • 延时大,并且会随着播放时间累积放大
  • 直播视频画面可能会卡停、黑屏
  • Chromium Console中经常有各种报错
  • 拉取不规范的http-flv,在某些浏览器上视频画面只能显示一小部分
  • 拉流过程中浏览器内存占用太大
  • 拉取http flv流,收到了MetaData和AVCDecoderConfigurationRecord,但后续视频fps始终为0
  • 遇到一次 MSE SourceBuffer is full, suspend transmuxing task

这么多的问题,是不是flv.js实现的不够好啊?解决了一圈下来,我的感觉不是这样。只是作为一个播放器来说,面对各种不同的flv源、以及复杂网络条件下,很难照顾的面面俱到。尤其是播放源本身就存在各种问题的情况下,指望一个播放器解决掉所有问题是很困难的。

下面来逐一说一下这些问题。

编译问题

如果你的nodejs版本比较高(比如我目前使用的是11.15.0),在执行了gulp release后会提示以下错误:

gulp release[75412]: c:\ws\src\node_contextify.cc:640: Assertion `args[1]->IsString()' failed.
 1: 00007FF6D747F43A v8::internal::GCIdleTimeHandler::GCIdleTimeHandler+4618
 2: 00007FF6D742D186 uv_loop_fork+86646
 3: 00007FF6D742D233 uv_loop_fork+86819
 4: 00007FF6D74348E8 uv_loop_fork+117208
 5: 00007FF6D7A9549F v8::internal::PassesFilter+847
 6: 00007FF6D7A966D7 v8::internal::PassesFilter+5511
 7: 00007FF6D7A9594C v8::internal::PassesFilter+2044
 8: 00007FF6D7A9586B v8::internal::PassesFilter+1819
 9: 000000B8EEA50481

这个时候是要补充执行一句:

npm install natives

就可以了。

延时大,并且会随着播放时间累积放大

我所在的公司主要提供直播SaaS/PaaS服务,其中推流到CDN这块,主要接入了网宿、京东云、腾讯云这三家。这三家都提供rtmp转flv的服务,所以我的测试都是基于这三家CDN提供厂商的。应用场景是:Chromium内核浏览器中,通过WebRTC发送音视频(RTP/RTCP协议),流媒体服务器负责将RTP转rtmp(单路不解码,多路会解码合屏再编码)送给CDN,由CDN向外提供多种协议的流数据,其中一种就包括http-flv。

在不做任何修改之前,我在普通的办公网络下,使用flv.js测试从这三家拉取http flv的单向延迟基本在5~7秒,这个延时对于flv来说还是比较大的。并且如果暂停播放直播流再恢复,会发现延时会累积。或者在长时间播放直播流,会发现延时也会变得越来越大。

那么如何解决这些问题呢?其实在flv.js的issues区已经有过很多关于延时和累积放大的讨论,如:215, 264, 274, 427, 498。应该还有一些,未能全部列举。

通过这些讨论,得到的针对降低直播流的最有效方法就是:定时检测HTMLMediaElement 的缓冲区末尾( HTMLMediaElement.buffered.end(index) )和当前播放位置(HTMLMediaElement.currentTime) 的差,如果超过一定程度,就将当前播放位置指向一个接近缓冲区末尾的位置。简单来说,就是:主动发现、主动追赶,主动跳帧。

这个做法经验证是非常有效的,但存在以下副作用:如果网络不稳定,可能会触发短时间内连续的追赶,肉眼表现上就会产生频繁的跳帧,影响观看。所以在实际应用中,最好能够控制追赶的频率和步伐,减缓剧烈的跳帧。 (2020-1-15补充:我实现了一版根据追赶次数和频率动态调节延迟阈值的逻辑,可以一定程度上避免追赶太频繁带来的音视频卡顿问题,代码已开源,见文章最下方)

直播视频画面可能会卡停、黑屏

遇到这种情况,大多数都是出现了各种错误,例如播放源出现问题、本地网络出现了问题。在发生这些问题的时候,如果上层业务逻辑没有针对flv.js上报错误进行处理,表现就是画面卡停、黑屏。
所以,业务层需要监听flv.js的一些重要的事件,如:

player.on(flvjs.Events.ERROR, (errType, errDetail) => { 
// errType是 NetworkError时,对应errDetail有:Exception、HttpStatusCodeInvalid、ConnectingTimeout、EarlyEof、UnrecoverableEarlyEof
// errType是 MediaError时,对应errDetail是MediaMSEError
});

player.on(flvjs.Events.MEDIA_SOURCE_CLOSE或MEDIA_SOURCE_ENDED, () =>{})

除了上面标准事件以外,我还为flv.js补充了2个事件:

VIDEO_RESOLUTION_CHANGED
VIDEO_FROZEN

VIDEO_RESOLUTION_CHANGED
在从MetaData和首个AVCDecoderConfigurationRecord中获取到视频分辨率时记录下来,当遇到后续再来新的AVCDecoderConfigurationRecord时,如果发现视频分辨率变化了,则上报此事件。添加这个事件的原因是我遇到了有的时候分辨率变化会引起画面卡停(此时IO正常、flvjs也检测到了新的AVCDecoderConfigurationRecord,控制台无报错),此时可以根据需要进行重新拉流来规避卡停问题。分辨率判断的代码在flv-demuxer.js中的_parseAVCDecoderConfigurationRecord()中编写即可。

VIDEO_FROZEN
我遇到过控制台没有任何报错,视频流规格也没有变化,但画面就突然卡停不动了。这种情况我增加了一个定时器,参考flv-player.js中_fillStatisticsInfo()中的实现,检查当前 decode frames数值是否已经停止变化,当超过一定阈值,就表示视频卡停了。注意判断卡停阈值不宜太小,因为有些场景,如推流端主动关闭摄像头(发送黑帧)、或者推流端是屏幕共享采用了低fps等情况,容易误判。

Chromium Console中经常有各种报错

我遇到的主要就是类似于以下这样的报错:

Failed to read the ‘buffered’ property from ‘SourceBuffer’: This SourceBuffer has been removed from the parent media source

这种错误提示一般是在flv源发生异常中断的时候产生的,因此错误提示的位置大多数都在 mse-controller.js 这个模块中。所以我添加了一个方法:

checkMediaSource(action) {
    if (!this._mediaSource || this._mediaSource.readyState !== 'open') {
        Log.w(this.TAG, 'try to do [' + action + '] but mediaSource is not prepared.');
        if (this._mediaSource) {
            Log.w(this.TAG, 'current mediaSource readyState = ' + this._mediaSource.readyState);
        }
        return false;
    }
    return true;
}

然后在 appendInitSegment() 、appendMediaSegment()、seek()、_needCleanupSourceBuffer()、_doCleanupSourceBuffer()、_updateMediaSourceDuration()、_doRemoveRanges()、_doAppendSegments()这些方法的入口处调用检查以下MediaSource的合法性。

拉取不规范的http-flv,在某些浏览器上视频画面只能显示一小部分

这个问题可能是属于个例。起因是因为拉流的 MetaData以及初始AVCDecoderConfigurationRecord中得到的视频分辨率,和后续实际视频流的分辨率不一致引起的。例如,拉流开始我从MetaData和AVCDecoderConfigurationRecord中的SPS得到视频分辨率是320x180,但是实际上发来的视频流是1280x720,经测试,在低于Chromium 70内核的浏览器上,如360浏览器、搜狗浏览器、Windows微信内置浏览器等,会发生这样的现象:
flv.js直播拉流场景下的技术优化_第1张图片
就是video标签只能显示实际图像的左上角一小块(1280x720中左上角320x180的那个部分)。但这个问题在内核Chromium 70及以上版本的浏览器(如QQ浏览器、Google Chrome)上却没有

解决这个问题的根本还是需要让源端给出实际的视频大小,但如果源不受控,非要在播放器上处理,那我的做法是在flv-demuxer.js中解析完AVCDecoderConfigurationRecord后,在调用 this._onTrackMetadata(‘video’, meta) 之前,将meta.codecWidth和meta.codecHeight强制给一个高于视频流的大小即可让那些显示异常的浏览器恢复正常。但这不是根本的解决办法,也是无奈之举。

拉流过程中浏览器内存占用太大

在flv.js的较早版本中曾经发生或内存泄露,但在后续版本中作者已经修复了。如果还是觉得内存占用太大,可以试着降低 autoCleanupMaxBackwardDurationautoCleanupMinBackwardDuration 这两个值,减少MSE缓存大小。它们的默认值分别是180秒和120秒,对于直播流而言,我觉得没有必要给这么大的值。

拉取http flv流,收到了MetaData和AVCDecoderConfigurationRecord,但后续视频fps始终为0

这个我只遇到一次,也是我为flv.js增加 VIDEO_FROZEN 事件的主要原因之一。在收到VIDEO_FROZEN时,如果业务判断流并没有主动停止,可以尝试重新拉流即可恢复,避免长时间卡停。

遇到一次 MSE SourceBuffer is full, suspend transmuxing task

这个报错位于 flv-player.js 中的 _onmseBufferFull(),触发它的位置在 mse-controller.js 的 _doAppendSegments()中,当尝试往SourceBuffer中appendBuffer时发生错误。这个报错我只遇到一次,尚不清楚导致的原因,所以我补充了 MS_BUFFER_FULL 事件通知业务层进行销毁充拉流处理。

功能补充

如何获得直播流的实时音频、视频码率?

首先,flv.js在解析ScriptData时候会尝试获取video和audio的datarate,但这个不是实时的码率,并且播放源很有可能没有填写这两个字段,所以这个不是我们想要的。

其次,flv.js的STATISTICS_INFO事件(默认每600毫秒上报一次,可以通过statisticsInfoReportInterval修改时长)中,已经实现了一个属性speed(单位是KBps),这个数值的计算参考源码 speed-sampler.js。这个是总得数据接收量,不区分是什么数据类型。所以也不是我们想要的。

怎么办呢?那只能自己实现了。其实原理也比较简单,大家知道,flv流的解封装都要经过 flv-demuxer.js 的parseChunks()这个方法,这里会得到 tagType,我们只要在这里,得到了tagType以后,分别针对tagType是8(音频)、9(视频)的数据量做累加计算,然后做一个1秒的定时器,计算出比特率,然后清空之前的计数,让 parseChunks() 继续更新这个计数直到下一个定时器到来重新计算即可。

下面是我在console里打印的一个720p http-flv的实时音视频码率:
flv.js直播拉流场景下的技术优化_第2张图片
主要代码 :
flv-demuxer.js中,
构造函数添加:

this._bpsCalculator = null;
this._bpsInfo = {
    lastVideoBytes: 0,
    lastAudioBytes: 0,
    bps_video: 0,
    bps_audio: 0
};

启动定时器:

if (!this._bpsCalculator) {
    this._bpsCalculator = self.setInterval(this._calculateRealtimeBitrate.bind(this), 1000);
}

别忘了destroy时销毁:

if (this._bpsCalculator) {
    self.clearInterval(this._bpsCalculator);
    this._bpsCalculator = null;
    this._bpsInfo = null;
}

定时器函数:

_calculateRealtimeBitrate() {
    if (!this._bpsInfo) {
        return;
    }

    this._bpsInfo.bps_video = 8 * this._bpsInfo.lastVideoBytes / 1024;
    this._bpsInfo.bps_audio = 8 * this._bpsInfo.lastAudioBytes / 1024;
    // Log.d(this.TAG, 'realtime av bitrate: v:' + video_bps + ', a:' + audio_bps);

    this._bpsInfo.lastVideoBytes = this._bpsInfo.lastAudioBytes = 0;
}

parseChunks()中:flv.js直播拉流场景下的技术优化_第3张图片
transmuxing-controller.js中:
flv.js直播拉流场景下的技术优化_第4张图片

结尾

其实这篇文章应该还不能算结束,因为经过优化后的版本,还需要经过大量的用户使用后才能暴露出更多的问题,如果未来有新类型的问题,我会再补充。

如果文中有描述不对的地方,欢迎指正和补充。

2019-12-21 今天终于有空,把大部分针对flv.js的修改放到了码云上一份:https://gitee.com/epubcn/flv.js,epubcn分支。为什么不是github?因为我近期访问github速度越来越慢,只有 几~ 十几KB/s的速度,试过一些方法效果都不好,实在让人无法忍受。

你可能感兴趣的:(H5)