本文来自熊猫TV音视频技术专家姜雨晴在LiveVideoStackCon 2017上的分享,并有LiveVideoStack整理成文。当下,打造一款播放器已经有比较好的开源实现,但熊猫TV为什么还要自研一款H5播放器呢?为了保证业务持续扩展能力,需要对播放器做解耦。同时,在播放器上线初期还遇到了音画不同步、故障定位、客户端性能不足等问题。
文 / 姜雨晴
整理 / LiveVideoStack
大家知道HTML5播放器曾被广泛运用于视频点播,而今天我想与大家分享的是运用在直播领域的HTML5播放器。现在熊猫已不再使用FLVJS作为播放器了,所以今天与大家探讨一下直播HTML5播放器的技术难点与架构探索。
我来自熊猫直播,从去年的7月份加入熊猫并在 11月中旬开始开发播放器,主要致力于HTML5播放器的研制开发。
接下来我将从以下几个方面介绍HTML5播放器的相关内容:
1. HTML5播放器产生背景
首先让我们来看看HTML5播放的产生背景,
通过最近的一些新闻大家可以看到,包括谷歌的Chrome还有Adobe这样的公司都在强调其产品不再专注Flash转而更关注HTML5。在这样一个后Flash时代,我们必须要有自己的新技术来支撑视频播放,尤其是视频直播的需求。
作为熊猫直播最重要的用户之一,熊猫直播的老板王思聪之前提出H5播放器的开发需求,那么H5播放器具有哪些优势呢?
(1)高效性
第一点是高效性。我们需要明确Video标签为浏览器带来的是什么?其实是在背后把H264的Codec打进了浏览器,无需内嵌应用而是利用浏览器Codec进行视频解码。
(2)兼容性
第二点是兼容性。之前我们遇见了很多非同寻常的案例与需求,包括将HTML5播放器技术运用于电视直播或游戏主机,这其实是反映了H5解决方案的良好兼容性。这种兼容性体现在一次开发后可以在多个不同平台应用,降低开发成本。
(3)浏览器新技术
第三点是快速接入浏览器新技术。例如大家或多或少听说过的流媒体加密的浏览器新接口Encrypted Media Extensions,还有 WebRTC、VP9、AV1、H.265等新技术,通过使用HTML5可以将这些新技术快速接入浏览器中。例如最新版本的Chrome浏览器便打入了H.265的Codec。相对于Flash播放器, HTML5可更便捷快速地引入新技术。
当然,HTML5播放器的开发过程并不是一帆风顺的。
2. 直播领域H5播放器的问题
我们之前从未尝试过将H5播放器技术运用于视频直播领域,因此在开发初期我们遇到了很多棘手的问题。2016年12月份上线的第一版便出现音画不同步、码率过高、播放器崩溃、浏览器崩溃、延迟高等问题。
我们团队曾经将这些问题集中并研究解决方案,下面我将会选其中几个比较具有代表性的问题进行详细阐述。
2.1 音画不同步
音画不同步的问题困扰了许久,很多开发者问到相关的问题,下面就是我们对于问题的定位与解决思路。
初期我们在观察来自内核的视频时会发现主播口型与声音无法准确同步,延迟可达到两三秒。这对用户而言是一场糟糕的体验,那么究竟为什么会出现音画不同步的问题呢?
1) 问题定位
我们发现,户外直播是发生音画不同步问题最为频繁的版区。第一个原因是户外主播手机性能及网络问题导致上行数据掉帧频发;第二个原因是音频和视频的掉帧时间长度存在差异;第三个原因是播放端音视频实际播放时长不一致导致音画不同步。
上图为问题示意图。灰色框为视频帧组成的视频流,红色框为音频帧组成的音频流,理想状态下的视频流与音频流应当是长度一致。其中虚线框表示帧片丢失的状态,例如现在视频流丢了3片,音频流丢了1片,此时实际传输的音视频为上图,但实际播放的音视频为下图:
但看着一小段音视频流,两三帧的差异似乎不是特别明显;一旦累计时间过长,视频流与音频流之间的时间差异越来越大,音画不同步的现象也就会越来越明显。相信现在使用FLVJS做视频直播的朋友也都会遇到这样一个问题:音画不同步的现象随时间的增长越来越显著,那么如何改进技术消除这个问题呢?
2) 解决方案
上图是影视动画后期制作时使用Au将配音员配音人声与视频画面做对接的处理过程。当出现音画不同步的现象时最常用的处理方案是调整轨道相对位置,再添加特效使得音画自然同步。
视频直播中出现音画不同步时可以运用类似方法进行处理,我们称为抽帧处理。当然抽帧后需要进行音频补帧处理。
在这里大家一定会有疑问,后期补进去的音频帧并不是原生的,那么应该补进去什么帧呢?为了让大家比较清晰地理解这个问题,也我们使用配音中的原理进行解释。
演员配音时,因为演员说每个字时发声的频率不同,声音听上去也会不同。如果每个字的不同频率切换得比较平滑便不会出现“嘶啦”的声音也就是“过电”现象;但如果是补一个空白帧,便会出现这样的现象,此时人耳会听到短暂的电流杂音,体验很不好;尤其是当直播频繁掉帧时用户会感觉到明显的电流杂音。
所以我们取前一帧进行音频补帧,较好避免了过电现象的发生。
3)改进效果
通过上述播放器对轨与补帧处理可以在掉帧频繁时明显降低音画不同步带来的对直播视频观看的影响。
2.2 码率问题
1) 问题定位
相信大家无论是使用Flash还是在H5播放器都曾遇见正在播放时突然弹框显示“页面已崩溃”的问题。这是为什么?因为浏览器会限制网页占用运行内存。普通的无音视频流的网页,除非代码出现严重的Bug否则不会占用过高的运行内存;但如果网页中有播放器的运行便很容易使浏览器处于一个高内存占用运行状态,一旦达到运行内存上限便会使得网页崩溃。
上图是蓝光直播上线第一天的反馈情况,可以看到大家反馈的信息,无论是选手毛孔还是主播妆容都是纤毫毕现。
上图是6000kbps的高清的直播,可以清晰看到主播面部的细节。对熊猫来说,高清直播是一座里程碑,也是我们产品的一个卖点。我们不可能用3000kbps的冒充蓝光线路,所以在这种大型活动熊猫基本上都维持在一个6000到8000kbps推流码率下的高清直播。
而对于普通主播而言高码率采集同样重要。上图是根据某天下午几个FPS主播们的直播房间统计出来的结果,可以看到很多主播都将码率采样推到6000以上,对此主播们也是乐此不疲,这是为什么?
这是我自己喜欢的几位主播平时的推流规律。其中有一个最高需要推到一万四的码率,这样一个高码率对熊猫来讲可以说是非常普遍的。我们需要保证页面不崩溃的同时维持这样一个高码率的推流,可以说难度不小。
这是FPS游戏《绝地求生》的直播画面。可以看到游戏中对手距离非常远,有的时候在画面中就是一个小黑点。像这样的FPS游戏一旦推至很高的码率便会大大降低用户体验。因为会带来明显的卡顿,包括主播也对这一点心知肚明。一般情况下如果出现卡顿的问题主播会给出“换线路板”、“调清晰度”等提示语。但无论如何我们需要支持主播的高码率直播需求,那么如何解决?
2) 解决方案
如果你打开熊猫HTML5播放器并右键点击打开监控,会看到显示“正在清洗能量槽”,很多人问我什么是正在清洗能量槽?其实是正在清理缓存的意思。这个功能的实现其实只需要几行代码,但背后会遇到了什么问题呢?
a.什么时候清洗
做前端的同学应该知道这个Setinterval。当Setinterval或新的GOP准备好时会触发清洗能量槽的功能。
b.一次清洗多少
先说Setinterval和新GOP。 Setinterval解决方式有优点与缺点,其问题在于此定时器在页面挂起的状态下并非按照设置的时间运行,而只是把这一段代码推至站并等待运行;此时如果超过时间而又在休眠状态便失去作用。而新GOP会过于频繁, 干扰系统正常运行,因此最后我们选择Setinterval解决方式。那么关于清理多少,我们暂时是确定10秒以前的全部清洗。
c.容易洗出什么问题
BufferUpdating是MSE的Buffer的一个状态。在新的GOP准备好时这是一个写操作,此时一定会存在这样一个无法清理的状态,这也是我们没有用新GOP的原因。
3) 改进效果
下面来看一下我们内存控制的效果,这是我们新版内核与老版内核的对比,请注意内存的变化。
在同样的测试环境下,上面的标签页是我们使用老版内核得出的占用内存值为285736k,下面的标签页是我们使用新版内核得出的占用内存值为75632k,大概是老板内核内存占用的1/4。
2.3 累计延时问题
CDN的同事应该知道累计延时也是一个困扰大家很久的问题。上图是我自己直播间的一个界面,左半图右侧是老版内核的,左侧是新版内核,右半图是我在新版内核网站刷新出的的一个状态,最左边的和最右边我都是已经放置了一段比较长的时间。先对比来看时间戳,老版内核页面与刚刷新完的页面相比存在大概4分钟的延迟,这4分钟的延迟可以说为观影体验带来的影响是毁灭性的。
1) 问题定位
延迟问题与码率有关。当下行网速小于平均码率时便会出现这种视频卡顿的现象。浏览器的Video标签是针对点播设计的,出现卡顿后一定是从卡顿点开始继续播放,这种小规模无法被轻易感知的卡顿累计多了便会造成明显的延迟,那我们该如何处理呢?
2) 解决方案
这一部分是我们写的一个重新拉流,处理方法为网络抖动。如果使用网络抖动而后面网络又平滑了该怎么办?此时需要看最后一帧是否满足需求,如果不满足就重新拉流并重新计算起始时间;然后将始终时间和当天时间作差,得出实际播出的时间以及实际消耗的时间,便是累计延时的时长。若大于一定阈值便会触发重新拉流的操作,当然这个阀值可根据应用环境进行修改,这里我设定的是15秒。
3) 改进效果
以上是在弱网环境下的测试结果。大家可以看到如果在放置比较久的情况下会产生一定的累计延迟,大概为3秒。但这种体验已经比之前好很多了,可以基本保证同步。
3. 熊猫HTML5播放器内核架构
3.1 明确问题
在整个开发过程中我们遇到了以下的一些问题使得我们将内核进行重新架构。
1) 不同业务
不同业务对播放器内核的需求是不一样的。虽然这是个外层问题,但当我们再去剖析时会发现,其实针对不同需求的不同业务下所需要的内核也不太一样。这个时候该怎么办呢?当然不可能将所有的业务都写在内核里,一个业务对应一个内核会带来庞大的开发体量。
2) 新技术接入
大家可以看到熊猫之前有十个多月处于Bata阶段。为什么我们一直没有发布正式版?因为我们想在播放器当中接入一些新技术。而每次新技术的接入就需要改变包中代码,可想而知其有多么不稳定。
3) 团队新人加入
我们团队会遇到的一个很正常的问题就是当有新人加入该怎么办?新人一开始不熟悉开发过程,在开发过程中有时对内核造成不必要的影响。
3.2 构架特征
我们对于新版内核的要求就是——“高度解耦”、“模块化”、“易扩展”,也就是下面我们重构的架构。
1) Modules层
大家可以看到在Modules层是由Loader模块、Demuxer模块、Remuxer模块与Build Packages模块组成,每个模块都有一些自己的插件。讲到这里,大家可能会想到一些以前的库,包括HLSJS、FLAVJS等都大概有这样的一个架构,那么我们在Mccree Core层做了哪些工作?
2) Mccree Core层
首先我们设置了一个消息通道Message Channal,其作用是当有模块要完成某些任务时会通知给下一个模块,然后会把数据给到缓冲区。
这个消息通道采用广播模式,任何一个模块在得到对应的消息时会触发对应功能。
3) 底层
底层的数据结构分为Loader Buffer、Tracks与Remuxed Buffer,分别用来放置原始的流数据、Demuxer后的数据与Demuxer前的数据,并提供给MICE。其中MICE是一个插件,其他的几个部分是我们的核心模块。可能大家刚开始看到这个构架有些复杂,接下来我会向大家介绍这些模块是如何工作的。
3.3 模块、插件与封装
注册、调用、销毁的流程会经常在工程化中被用到。那么在我们的Mccree Core中模块是如何被接入的?
首先初始化模块,接下来进行模块调用;这一步比较简单的是调用标准接口也就是Loader加载数据;最后在我不用的时候进行销毁。需要注意的是这里的Unload也是一个标准接口, Unload是promise,如果有人想比着这个东西去改FLVJS,可以把改掉,因为这个是个promise,泛指是个promise,其他的也都必须做成一个promise才能兼容这样的一个接口。
这是我们一个具体的数据传输方式。首先是向缓存中填充数据,再通过消息通道通知下一个模块获取数据;之后会给出获取数据的长度,否则下一块模块无法确定获取数据量;接下来收到这些消息后下一模块从缓存中提取数据。大家都知道FLV的视频Header等于13位,就是以上的一段代码,大家可以在开源库上看到这段代码,我就不再赘述了。
3.4 工程管理
这个较为复杂的流程本身会有一些工程管理上的麻烦:
1) 工程体积大,模块多
解决方案:多包管理系统
我们现在使用Lerna package管理系统,我想做前端的同学应该了解这是Babel的多包管理工具。
2) 参与开发复性工作多
解决方案:完整的开发套件
当你第一次布局这套环境时会发现需要一个个装所有的库,还要做Lerna Bootstrap的工作,参与开发复性工作多,如何改进?我们可以做一套完整的开发套件,将包括自动检测在内的全部工作做好。
3) 模块质量保证
解决方案:完整的测试用例、文档支持。
我们要求需要有一个完整的测试用例与文档支持,即使是上一个模块我们也会做A/B Test和软切换。保证其模块的质量。
4. 技术创新与展望
关于这一点我想与大家分享一个简单的例子:P2P技术想必大家并不陌生。
上图是我们实际中接入一位合作方P2P的代码。如果需要我在外层去控制使用P2P该如何解决?
我们在P2PLoader层先写了一些如刚才提到的Loade还有URLsource这样的标准接口,再写了这一套代码;之后把P2P完整接入到我们的HTML5播放器。
我们花了半天工时写了一个模块与几行控制代码就可以将这样一个P2P技术完整接入到播放器中。
4.1 WebVR
WebVR想必大家都了解一些。但现在距离产品化还有一段探索的路,故而一直没有推向市场。只需封装一个WebVR接口,也就是去做一个插件就可很快取代我们现有的纯MSE,很快就可以上线。
4.2 服务端应用接入
这应该是前端的同学比较熟悉的NodeJS。由于现在的框架包括大部分的模块和浏览器是不相关的,而唯一和浏览器相关的是部分Loader与基于浏览器的MSE。我们可以使用Node服务端运行提供音视频服务,将来需要去自建CDN时可以很轻易地将我们现在所做的包括解决音画不同步在内的一切优化都转接到一个边缘节点上。
Q&A
Q1.1:播放器刚启动时默认使用大码率还是小码率?
A:大码率
Q1.2:如果用户的网络环境比较差怎么办?
A:关于这一点我们有一个降级的解决方案。熊猫直播可切换三个清晰度,但默认是超清;用户上传多少码率,我就可以拉多少码率。比如说有P2P推一个8000kbps的码率,其用户可在超清链路上拉到8000,如果出现卡顿可切换到高清或更低的码流。
Q1.3:播放器是否会推荐给用户合适的码流?
A:这是一个业务层面需要解决的问题。我们在Loader里集成了一个实时监控的插件监控实时传输速率。基于保证沉浸且连续的用户体验与业务方的需求,我们不会默认在直播中向用户弹出推荐合适码流的提示框。
Q1.4:一般码流切换时播放器会缓存多长时间?
A:这个问题与我们的首屏优化有一定关系的,我预测今天会有很多人讲首屏优化。因为直播视频里是没有B帧的,不存在向后预测的帧,只存在向前预测的帧。我们进行首屏优化时,如果是在GOP比较长的情况下会在到下一个I帧前开始播放。我们只会给I帧缓存并且直接开始播放以实现秒开的效果,此时用户会看到直播画面闪一下。
当然在这个过程中需要切换码率, MOOV的Header需要改变,所以必须要清空之前MSE上所有的数据。
Q2:这些视频插件在Chrome、Safari、IE等平台上如何实现适配?
A:其实大部分国内的浏览器厂商使用的都是谷歌的Chromium内核解决方案,除此之外还有火狐、苹果的Safari、微软的Edge。Chrome与火狐已经支持了这些插件,而为什么我最后说Safari与Edge?因为这个问题的解决很大程度上取决于浏览器的市场覆盖率。但是这两个浏览器在Fetch Loader上存在问题,我们只能去加载HLS流。如果我的Remuxer不变,MSE控制插件也不变的情况下降级兼容HLS,只需要换一个Loader一个Master就可以解决。
Q3:关于解决音视频不同步问题的修正码插件,是集成在原生播放器中吗?
A:在Remaster中,暂时还没有提取出来。 FLV流拉过来时会给出一个PTS差值。当被检测到时我们就改动时间或重新输出数据包。
HTML5原生播放器支持MP4、WebM,不支持FLV,PC端也不支持HLS。我们会将数据进行拆包和分包再传输给浏览器以实现格式支持。
Q4:客户端会缓存多少?追赶策略是什么?
A:首先说一下几个不同拉流方式的差异:Fetch方式拉流时,因为是长链接所以是挨着拉。如果出现网络抖动,保持在比较卡的状态下拉流会和服务器端产生很大差距;但如果是网络抖动,后面的数据密度大,可与服务器保持一个相似的状态。这两种不同追帧方式,如果只是抖动,最后拉流多少就是多少。我们会监测实际播放时长和理论播放时长的差值,根据差值找最新的GOP里的I帧。如果有就不用重新拉流,如果没有则需要重新拉流。
Q4.1:可能缓存一个GOP?
A:有可能,如果说服务器本身缓存了三个GOP,我们会缓存三个。
Q5:移动端的相关问题解决方案有什么?
A:移动端我们暂时使用HLS拉流的方式,这一点策略与我们的业务相关。对我们而言移动端本身只是用来分享,没有必要使用这么高的码。我们直接用的HLS流,不需要拆分包以提高移动端效率。
Q5.1:大概介绍一下码流监控的埋点与监控的思路。
A:我们会监控一些参数,例如某个Buffer不够用了,此时就开始埋这个卡顿点,开始计时到重新播放的状态;此时会统计时间与卡顿次数并上报给我们自己的数据中心。其实在CDN会看到一个主播推流的上行状态,我们还会监控下行网络速度等。通过这些埋点我们可以看出到底是哪个环节出现问题,防患于未然。
Q6:补帧的策略是怎么样的?
A:以视频帧为基准。根据视频帧的位置计算音频帧的位置,如果这帧出现缺失我们就补帧。
Q6.1:补前一帧与后一帧的区别?
A:根据不同场景选择最优化的方案,从代码修改简便的角度我们会优先选择补前一帧。
Q7:国外有一种DASH的解决方案,但是国内CDN厂商对DASH的支持不太积极,为何不做相关的适配工作?
A:我们尽量去推动,但在时间成本上无法保证。技术过渡期是有必要存在这种技术的。
Q8:熊猫HTML5播放器是否参考flv.js?能否对比一下二者优劣?
A:我们之前有调研过他的东西,但最后未使用。原因一是开发包臃肿,很多东西对我们来说是没有必要的。为了防止日后维护上的混乱我们重构了架构。原因二是维护风险过大,跟不上我们的业务节奏。