基于WebRTC的开源低延时播放器实践
//
文/毕伟
整理/LiveVideoStack
大家下午好!我是网易云信资深音视频引擎研发工程师毕伟,今天为大家介绍云信开源低延时播放器的相关内容。云信开源播放器基于WebRTC二次开发,并且现在已经将代码上传到GitHub上,感兴趣的各位可以关注一下。
接下来我会从直播行业背景、低延时直播现状、低延时播放器框架、关键指标优化和未来展望5个方面逐步介绍。
一、直播行业背景
这两个典型场景的主要诉求都是低延时。现在市面上主流的仍然是标准CDN直播,即主播通过RTMP协议推流到CDN,CDN再进行分发,最后观看者通过HTTP-FLV、HLS、RTMP等协议进行拉流。整个方案基于TCP进行传输。因为TCP重传效率低,ACK反馈延迟导致数据积压,所以TCP方案的延时基本上在3到10秒。虽然HTTP-FLV/RTMP可以做到3秒左右,但是HLS因为切片的原因,会延长到10秒。综上所述,TCP方案并不适合用于低延时直播。
现在各大云厂商陆续推出了低延时直播的服务,主要是改造下行链路。通过把下行链路的RTMP转换成RTP,再进行UDP传输。整个直播过程包含主播端的采集、编码和发送、CDN分发、播放端接收、解码和渲染,主播端的延时相对较低,CDN内部走专网专线所以延时也是可控的。整个延时主要是下行TCP网络相对不太可控造成的,需要播放器加大缓冲区对抗网络抖动。正因如此,TCP方案端到端网络延时较大。事实上,改造下行网络带来的收益是最大化的。只需要将下行改为UDP传输,整个端到端延时就可以降低至1秒左右。现在市面上基于UDP的方案也有很多,例如QUIC、SRT、WebRTC等。目前WebRTC方案非常火热,大多数浏览器都支持,生态也很不错,所以云信也选择WebRTC作为低延时直播的基础。
云信也推出了自己的低延时直播服务。这张图是云信整个低延时直播的系统流程图。上行采用的是RTMP推流,通过改造下行链路,中间建立一张低延时传输网WE-CAN,主播推流到源站,源站再转推到WE-CAN进行分发。拉流端在边缘节点进行拉流。从云信传统的CDN直播转入到云信的低延时直播十分简便,只需要再重新申请一个低延时拉流的域名即可。
二、低延时直播现状
传统CDN直播能快速发展,和一些优秀的开源项目密不可分,例如FFMPEG、OBS、 VLC。目前的低延时直播仍然存在以下几个问题。首先各大云厂商采用的都是私有协议,没有标准化。另外,在使用这些协议时需要强绑定对应的SDK。在接入多家云厂商的服务时需要接入多个SDK。多个SDK对现阶段移动端APP的包大小十分不友好,不利于低延时直播的大规模推广。为此,云信推出一个开源的低延时播放器,开放信令交互,可以用一套SDK对接多家低延时云服务厂商。
三、低延时播放器框架
这是云信低延时播放器的框架。云信低延时播放器是一个传输层的SDK,最底层是WebRTC。因为我们意在打造一个通用版的SDK,所以我们将WebRTC全量包入,通过PeerConnection层接入,里面是一些主要模块,例如JitterBuffer、NetEQ、RTP/RTCP、Transport等。中间是RtdEngine层,主要作用是对WebRTC进行封装,包含API、引擎创建、信令建连、 媒体数据的接收回调等。最上层是FFMPEG插件。直播已近发展了数些年,各厂商都有一些存量的播放器,市面上大多数播放器都是基于FFMPEG开发,为了降低用户SDK接入门槛,云信将API封装成FFMPEG插件,扩展了输入流格式——ff_rtd_demuxer,对应的云信拉流地址协议头是“nertc://”。在FFMPEG插件里注册协议头,拉流地址替换以后就可以接入云信的低延时链路。如果要接入其他云厂商,只需要替换拉流协议头,同时将协议头添加到插件中即可,播放器代码不需要任何的改动。
下面介绍云信低延时播放器的交互流程。通过采用标准SDP+ICE的交互方式,以达到通用SDK的目的。客户端创建Offer SDP,通过HTTP POST到服务器,服务器响应Answer SDP,信令交互完成后进行ICE,ICE建连成功后服务器会发送媒体数据给播放器。
SDK的底层有一个Transport模块,建连成功后会收到服务器发送来的音视频数据,音视频数据包会分开传送。视频的数据包会送到JitterBuffer,音频数据包会送到NetEQ。视频的RTP包会在JitterBuffer中进行排序,组帧、重传等操作,之后会回调到RtdEngine。整个SDK内部不对视频进行解码,而是交由上层播放器做。视频数据组帧完成后回调时,为了不破坏原有的结构,云信模拟了一个解码线程,继承了WebRTC原来的VideoDecoder基类,模拟从JitterBuffer取数据的过程。取到的视频帧放在RtdEngine中,播放器通过插件从RtdEngine读取。音频在NetEQ中会被解码,之后回调PCM数据。同样,我们也在RtdEngine中模拟一个playout 线程,读取PCM数据到RtdEngine中,供播放器读取。大家可以注意到,我们只对音频做了解码,视频没有做。由于延时和缓冲区大小相关。播放器上层有一个缓冲区,JitterBuffer和NetEQ中也有缓冲区,多个缓冲区会对延时的控制带来难度,尤其是播放器上层的延时。我们希望将播放器上层的缓存区降为0,所有数据都在JitterBuffer和NetEQ中管理。这里音频解码是为了复用NetEQ中音频加减速的能力,更好地控制延时。
上面是传统CDN直播播放器的结构。通过FFMPEG从CDN中拉流,放到缓冲区中,然后进行解码、音画同步和渲染等。缓冲区一般设置为3到5秒。如果接入云信的低延时SDK,只要把云信的SDK编到FFMPEG中,作为FFMPEG的第三方插件,后续的整个流程不需要任何的改动,只需要把缓冲区降为0。所有的缓冲区都是由SDK中的JitterBuffer接管。拉流时只需要使用对应的低延时拉流地址,就可以接入到整个低延时拉流链路上。由此可见SDK的接入十分简便,同时可以复用原有FFMPEG拉流流程。原有播放器的业务不需要进行任何的改动。
下面具体介绍一下播放器的SDK接入。云信通过作为FFMPEG的插件,扩展AVInputformat格式,实现了如下接口rtd_probe、 rtd_read_header、 rtd_read_packet、 rtd_read_close等。在rtd_probe中添加nertc://的拉流协议头,FFEMPEG可以根据其中的分数探测最终的协议。如果需要接入其他低延时厂商的服务,就可以在rtd_probe添加对应厂商的协议头。使用对应的拉流协议头就可以走进云信的低延时模块中。但仅仅走进模块还不够,后续还需要去和云厂商对接,了解其信令交互,自己进行一些对接上的调整即可。
我们提供了一个插件的源码,称之为rtd_dec.c。只需要把rtd_dec.c拷贝到FFMPEG的libavformat文件夹下,修改对应的脚本Makefile即可。另外,需要将生成的AVInputFormat类型ff_rtd_demuxer注册到FFMPEG中,使其能够认识。在allformats.c中添加AVInputFormat格式,FFMPEG重新编译。除此之外还有另外一种格式,即不编译FFMPEG ,直接调用FFMPEG接口av_register_input_format()。这种方法仅对于低版本的FFMPEG支持,对于高版本的不太支持。所以我们统一采用allformats.c中添加libavformat格式,FFMPEG重新编译的方法。
还有一部分播放器是非FFMPEG播放器。云信也提供了一套API。具体操作可以参考rtd_dec.c插件调用API的流程。
四、关键指标优化
下面来介绍关于直播指标的一些优化。分别是首帧优化、延迟优化和抗性优化。
1、首帧优化
首帧分为以下几个过程。用户点击播放之后进行建连,随后收到数据,再进行解码渲染。云信有一张WE-CAN传输网,在全球的主流国家和重点省市都进行了布点,大多数地区的用户都可以就近接入。同一地区的用户会尽量调度到同一个机房,减少回源。现阶段低延时拉流还是复用RTMP的上行,RTMP推流到CDN,再进行回源拉流。如果命中率较低,多次回源非常耗时,会大大影响首帧。
直播和RTC存在一些区别,直播在接入时没法请求关键帧等,如果服务器不缓存GOP,在订阅流时,服务器因为没有关键帧可以发,需要等待下一个关键帧到来才行。这会对首帧带来很大的影响。如果服务器缓存前一个GOP,订阅流时能立即发送数据。
以上都是服务器的优化。因为云信是一个通用的播放器,媒体建连会采用标准的ICE。ICE中存在DTLS,对于直播来说大部分场景不需要进行加密,可以关闭DTLS 减少建连耗时。
建连完成之后就可以收媒体数据包。网络中可能存在一些丢包的情况。检测丢包一般会通过序列号是否连续进行判断。但是当第一个关键帧最前面几个包丢掉,往往很难检测出来,或者能检测出丢包,但是不能判断前面丢了几个包,就不能确定如何重传。关键帧组帧不成功,会导致整个GOP都难以进行解码。有些用户的推流GOP很大,首个关键帧组帧失败会导致首帧时间非常长,这对用户体验影响很大。
有两个方法解决上述问题。第一个方法是拉流时服务器通过信令告知第一个序列号是多少,将拿到的序列号和收到的第一个包序列号进行比较,就能知道中间有没有包丢失,丢了几个包。就可以在收到包时立即进行重传。第二种方法,收到第一个RTP包,不管前面有没有丢包,直接往前重传10~20个包。
最后一个优化方式是首帧快速出帧。组帧完成以后出帧有一定的等待时间,为了快速出帧,将前面几帧等待时间设置为0,使得快速接触到视频包进行解码渲染。
通过以上几步的优化,整个首帧可以控制在200ms以内。
2、抗性优化
抗性优化第一个方面是NACK。WebRTC原生的重传效率较低,重传间隔在100ms左右,对于低延时直播来说,整个端到端延时也只有几百毫秒,因此这样的重传效率是不可接受的。我们对重传的间隔进行了优化,会根据实际的RTT进行重传。另外,对于重传的优先级,服务器会响应不同帧之间的优先级。
第二个方面是JitterBuffer、NetEQ。在整个播放过程中会实时监控网络质量,例如丢包率、RTT、Jitter等。有了丢包率就可以算出需要多少次重传才可以将这个包重传回来,根据重传次数和RTT,大致能估算出需要多少JitterBuffer来应对该次网络抖动,实时感知和调整。
第三种方式是添加冗余包。目前云信实现了标准RED。一开始我们打算做RSFEC,但RSFEC需要和服务器进行配合,对于我们的通用播放器不太适合。之后可能会把整个RSFEC链路流程建立好,具体实现通过插件的方式对接不同的云厂商。
3、延时优化
在起播阶段,服务器会发送GOPCache中的数据,导致起播阶段延时较大。整个播放过程中延时最高的时候就是在起播阶段。播放器通过加速播放来追赶延时,加速速度过快不仅会影响我们的感官体验,一些低端的机器还会因为解码速度不够导致无法加速,长时间处于高延时的状态,这样就违背了我们的低延时理念。为此云信采用了服务器丢帧的策略。假如在订阅流的时候是P帧,服务器发送缓存数据帧到某个P帧时,服务器收到了下一个关键帧,这个时候服务器会直接从当前P帧跳跃到下个关键帧,P帧和关键帧之间一段数据全部丢掉,重编关键帧及其后面帧的时间戳,保证时间戳连续。由于时间戳连续,所以播放器感知不到跳帧,只是起播阶段画面有一个小跳跃。通过上述的方法可以在起播阶段快速追赶上延时。
4、功能升级
WebRTC一开始并不是用来进行直播,它对直播有一些限制,比如音频只支持OPUS。现在低延时直播很多都是复用了RTMP上行,RTMP推流音频采用AAC。如果播放器不支持AAC,服务器就需要进行转码,例如AAC转OPUS。转码不仅会带来音质的损失,还会带来延时。所以我们在开源播放器中支持了AAC。
另外WebRTC不支持44.1K采样。云信也进行了44.1K采样的支持。
WebRTC不支持H265。现在分辨率越来越高,H265的压缩效率比H264高一些。云信也完成了对H265的支持。
WebRTC不支持多Slice。在播放Slice流时WebRTC在组帧时会出现帧完整性判断错误的情况,导致花屏现象的出现。云信也对多Slice进行了支持。
这是GitHub上开源播放器的DEMO。我们进行了一个测试。左边是OBS推流,利用OBS将其配置成低延时的模式,右边是拉流延时的对比。jitterBuffer配置成200ms,端到端延时在600ms左右。具体的延时可以根据自己的业务需求进行jitterBuffer调控,jitterBuffer下调一点,延时也会降低一点,反之亦然。
除了开源播放器,云信也有一个闭源的播放器。虽然闭源播放器也是基于WebRTC开发,但是因为不需要考虑通用,所以并没有使用整个WebRTC,只抽取其中的部分模块,例如JitterBuffer、NetEQ、Transport、NACK等模块。再进行相应的封装,包体积在iOS单架构增加550k,Android单架构增加1M。
信令协议方面,开源播放器使用的HTTP,HTTP在弱网环境下经常连接不上。我们采用KCP协议,增加冗余的方式,提升建连成功率。
首帧方面,我们简化了信令流程,提升建连速度。
弱网抗性中,得益于闭源包,会存在很多私有协议,QoS策略较为丰富。可以支持FEC、或者反馈支持不同分级的重传,可以抗住100ms+50%的丢包。
闭源播放器和RTC可以进行融合,支持连麦互动。
五、未来计划
5月份云信开源了windows端,之后我们将实现移动端的支持。移动端目前正在内测,内测结束后就会发布到GitHub上。未来我们也会持续优化各项播放指标,以及推行播放器标准化。也欢迎大家一起在开源播放器上开发,提供宝贵的意见。另外,云信正在构建一个低延时推流工具,未来也会开源,现阶段的方案是做成OBS插件,以提供全链路的解决方案。
谢谢大家!我的分享就到这里。
微信扫码关注该文公众号作者