B站在实时音视频技术领域的探索与实践
B站在实时音视频技术领域的探索与实践 https://mp.weixin.qq.com/s/NbN4WjBzTjddS8nl6Oh_Cg
B站在实时音视频技术领域的探索与实践
马家忆
B端技术中心资深开发工程师
01 背景
直播行业从传统的娱乐直播发展到教育直播、电商直播等形式,产生了很多新的玩法。传统的直播是一位主播展示才艺,观众通过弹幕、送礼物等方式进行互动。随着网络质量不断地提高,用户也对直播平台产生的新的要求,实时互动直播的场景就出现了,观众可以同时观看多位主播之间互动的画面,让直播间的气氛更好。B站直播的连麦PK、视频连线业务就提供了这个能力。主播看到的是对方主播实时的流(延迟400ms以内),而观众看到的是“准实时”的流(延迟2~5s左右)。本文讲述搭建这样一套系统需要了解的背景知识以及系统的整体架构,希望对大家有帮助。
02 实时音视频关键技术
从0到1搭建一套实时音视频系统并支撑现有的业务,如果没有接触过这方面的东西会感觉无从下手。我们可以看到,1996年IETF就推出了RTP协议用于实时音视频传输,2011年Google推出了WebRTC用于网页端实时音视频通话。从这些现有的协议和项目中,我们可以发现实时音视频技术的关键点,评估自身现有的基础组件支持情况并结合业务场景寻找适合自己的解决方案。
2.1 传输协议
RTP协议(Real-time Transport Protocol)定义了在互联网上传输实时音视频数据的标准格式,属于应用层协议。RFC 3550描述RTP协议的传输层主要使用UDP,RFC 4571描述了RTP协议的TCP传输方式,在我们的实时音视频场景中应当优先选择UDP,理由如下:
-
TCP保证数据流的可靠性和顺序性。TCP的超时重传策略为了保证通用和公平,相对比较保守,重传超时时间(RTO)可能会变的很大。假如中途丢失一个包,后续的包即使先到达也要缓存起来等待重传完成以后才能送到应用层。在网络状况不佳的情况下,使用TCP传输会产生较大的延迟。
-
UDP允许数据包丢失、乱序和重复。即使数据丢失也不会阻塞接收缓冲区等待重传,这就为实时性提供了保障。在上层的RTP协议中,协议头部包含了时间戳和序列号,可以对数据包进行重排和丢弃,解决了乱序和重复的问题。如果接收端监测到丢包,并且丢失的包是必要的且无法恢复,则发送NACK消息通知发送端重传(下一节会详细探讨这个话题)。
UDP虽然在低延迟领域上有压倒性的优势,但是用户侧有可能存在防火墙拦截所有的UDP包。考虑到在网络环境足够好的情况下使用TCP也能达到不错的效果,因此我们做了一个降级策略,优先使用UDP,当且仅当UDP不通的时候使用TCP。
2.2 丢包补偿
前面讲到我们的传输层协议优先选择UDP,那么就需要引入一些机制解决丢包问题。
前向纠错FEC(Forward Error Correction)指的是发送端发送原始数据的同时附加部分冗余的信息,如果接收端检测到原始数据丢失则尝试使用冗余的信息进行恢复。发送端发送n个数据包,同时根据原始数据生成k个冗余的数据包,将n+k个数据包发送出去,接收端只要收到至少n个数据包就可以得到全部的原始数据。
FEC算法的关键在于异或。异或(Exclusive OR)是一个数学运算符,数学符号为“⊕”,两边数值转换成二进制按位运算,相同时为0,不同时为1。
以一阶冗余算法为例,n个数据包生成1个冗余包,发送n+1个数据包。我们发送三个数值分别为a、b、c,生成冗余数据x=a ⊕b ⊕ c一起发送。假如数值b在传输中丢失了,计算a ⊕c ⊕ x即可得到b。
在实际应用中,FEC没有这么简单,WebRTC实现了UlpFEC和FlexFEC,UlpFEC可以针对数据包的重要程度实施不同程度的保护以充分利用带宽,FlexFEC还支持对列做冗余,同时WebRTC默认的音频编码器Opus本身就支持FEC。
前向纠错适合少量随机丢包的场景,可以无视网络延迟时间,但是增加了带宽消耗。
后向纠错包括ARQ(Automatic Repeat Request)和PLC(Packet Loss Concealment)。ARQ指的是接收端检测到数据丢失的时候发送NACK报文请求发送端重传,适合突发大量丢包的场景,没有额外的带宽消耗,但是时效性取决于RTT,如果存在很多接收端还要考虑避免NACK风暴造成雪崩。PLC用于音频,当数据缺失时使用模型根据前后数据预测丢失的数据。
总之,前向纠错和后向纠错各有优缺点,需要搭配使用。
2.3 流量控制
流量控制指的是根据网络状况的波动估算可用带宽,根据带宽的变化自动调节音视频码率和发送速率。当网络质量变差的时候迅速降低数据量以确保实时性,网络较好时则慢慢提升数据量带来更清晰的画面。在WebRTC中提供了优秀的Google Congestion Control算法,包括基于延迟的评估和基于丢包率的评估,取两种评估方式的最小值作为目标带宽通知编码器和数据发送模块。
基于延迟的评估算法包括Transport-CC和Goog-REMB,目前最新版的WebRTC默认使用的是Transport-CC。Transport-CC在发送端进行带宽评估,接收端通过TransportFeedback RTCP包向发送端反馈每个RTP包的到达时间,发送端在一个时间窗口内计算每个RTP包到达时间与发送时间之差,通过Trendline滤波器处理后预测网络状况。假设我们当前处于Hold状态,如果检测到网络状态为OverUse,此时应该减小数据量,变更为Decrease状态;如果检测到网络状态为Normal,此时可以尝试增加数据量,变更为Increase状态。
基于丢包的评估算法是当网络突发大量丢包时的兜底策略。如果丢包率在2%以下的时候说明网络质量好,目标带宽增加8%;如果丢包率在在2%~10%说明当前发送数据的带宽和网络质量相匹配可以保持不变;如果丢包率大于10%说明网络质量差,目标带宽减小到(1-丢包率*0.5)* 当前带宽。
2.4 数据缓冲
如果我们只考虑实时性,那么收到数据就立刻解码并渲染必然是最好的选择,但是网络并不稳定,延迟、乱序、丢包、重复包都有可能发生。如果采用上面的策略,音频可能因为网络的抖动变的断断续续,视频可能因为丢包导致缺少参考帧从而出现黑屏或花屏,所以有必要引入一个缓冲区,增加一点可以接受的延迟来保证用户体验。
在WebRTC中,视频包会被放入JitterBuffer模块进行处理,JitterBuffer会进行视频包的排序、组装成完整的帧、确保参考帧有效,然后把数据送到解码器。同时,根据网络状况自适应地调节缓冲区的长度。音频包会被放入NetEQ中,它维护了音频的缓冲区,同时负责将音频同步到视频。我们做播放器一般都是以音频的时间为基准同步视频,但是WebRTC刚好相反,它是以视频为基准的。当音频数据堆积的时候加速音频播放,音频数据不足的时候降低速度把音频拉长。
2.5 回声消除
在语音通话的场景中,麦克风采集到的声音发送给远端,远端的扬声器播放出来以后又被远端的麦克风采集到这个声音并传送回来,这样讲话的人会感觉到有回声,影响体验。
WebRTC提供了回声消除算法AEC,时延估计(Delay Estimation)模块找到扬声器信号和麦克风信号的时延,线性自适应滤波器(Linear Adaptive Filter)参考扬声器信号估算回声信号并将其剪去,最后通过非线性处理(Nonlinear Processing)模块消除残留的回声。
2.6 最优路径
实时音视频对网络的要求非常高,如果通话双方距离很远,那么通话质量是很难保证的。城市A的设备给城市D的设备发送数据,直接发送未必是最优的选择,从城市B和城市C中转一下有可能更快。理想的解决方案是在全球部署加速节点,用户就近接入。根据加速节点之间的实时网络质量探测数据,找到一条最优传输路径,避开网络的拥堵。
03 WebRTC分析
刚才介绍了实时音视频系统实现过程中所需的关键技术,多次提到了WebRTC。显然,对于绝大多数团队来说,这些内容如果全部自主研发几乎是不可能的事情,而我们站在WebRTC的基础上去设计自己的系统是较为明智的选择。WebRTC的代码非常复杂,想要把它搞清楚是一件非常困难的任务,我第一次看到WebRTC的代码根本就不知道从哪里下手。幸运的是,WebRTC官方提供了架构图,可以先帮助我们对它进行一个宏观的了解。
WebRTC整体架构大概可以分为接口层、会话层、引擎层和设备I/O层。
接口层包括Web API和WebRTC C++ API,Web API给Web开发者提供了JavaScript接口,这样Web端就具备了接入WebRTC的能力;WebRTC C++ API面向的是浏览器开发者,让浏览器开发商具备集成WebRTC的能力。当然,WebRTC C++ API也可以用于Native客户端接入。
会话层主要包含信令相关的逻辑,比如媒体协商,P2P连接管理等。
引擎层是WebRTC最核心的功能,包括音频引擎、视频引擎和传输模块。音频引擎包含音频编解码器(Opus)、NetEQ和著名的3A(回声消除、自动增益、降噪)算法;视频引擎包括视频编解码器(VP8、VP9、H264)、JitterBufer和图像增强(降噪)算法;传输模块包含SRTP、多路复用和P2P模块。
设备I/O层主要和硬件交互,包括音视频采集和渲染,以及网络I/O。
上面一节描述的实时音视频关键技术中,WebRTC实现了除“最优路径”之外的全部内容。WebRTC几乎每个模块都是可以按需替换的,便于我们增加定制的内容。我们可以根据实际需求决定如何使用WebRTC,Native客户端可以通过PeerConnection接口接入,服务端拿到RTP/RTCP包以后完全可以直接调用引擎层处理拿到最终的YUV和PCM数据,甚至只把WebRTC内部模块抠出来用在自己的系统上也是没问题的。
04 系统架构
我们回到B站的连麦PK业务场景,两位主播进行互动PK,同时大量观众在直播间观看PK的过程。
显然,两位主播通话要求低延迟,必须使用实时音视频系统交互;而观众观看直播的延迟要求相对没那么严格,可以采取传统直播的模式,通过RTMP或者SRT推流到CDN,用户从CDN拉流。然后我们要思考两个问题:
问题1.主播之间的音视频通话是采用P2P还是服务器中转?
首先对P2P和服务器中转两种方案做个对比:
对于P2P方案来说,只需要部署STUN和TURN服务器,如果成功建立P2P连接那么后续媒体数据传输就不需要经过服务器,所以具有成本优势。然而,P2P的缺点也很明显,如果打洞失败还是需要TURN服务器中转,且建立连接的过程耗时较高,用户之间距离较远的情况下网络质量不可控,而且现有的第三方网络加速服务基本上都不支持P2P。
我们这里选择服务器中转的方案,因为实时音视频本身对网络的要求比较高,不会设置过高的码率,所以网络传输的数据量是可控的,成本能够接受。而且我们的实时音视频数据要对接AI审核,还要实现服务器混流,这是P2P方案做不到的。
问题2.推送给观众的流到CDN,这个工作放在主播客户端还是服务器?
两位主播PK对应的是两路流,观众只从CDN拉一路流,所以必须有个地方做混流。这里的混流指的是把两位主播的视频进行拼接、音频进行混合,然后打包成一路流。主播客户端能收到对方的流,可以和自己的流做混流;在前面提到的服务器中转方案中,服务器有双方的流,同样也可以完成混流。我们先对比一下两种方案的优劣:
服务器进行混流需要先解码再编码,这需要消耗大量计算资源,所以成本很高;主播客户端进行混流需要额外增加一路流的编码和上传,对设备性能和上行带宽来说也是很大的挑战。主播客户端需要等待服务器把对方的流发送过来才能混流,所以从延迟的角度来看服务器混流稍微占据优势,不过这个延迟相比CDN的延迟可以忽略不计。如果后期对通话质量要求变高,主播的设备性能和上行带宽跟不上,我们可以很容易增加服务器来扩展计算资源和带宽,所以在可扩展性方面服务器混流胜出。另外,当主播从正常直播切换到连麦PK状态的时候,采用服务器混流必须先把直播的流停掉再由服务器接管,中间的时间差可能会产生卡顿或黑屏影响观众体验,而主播客户端混流可以做到无缝切换。
所以,这两种方案各有优缺点,我们采取折衷的办法:如果主播的设备负载较低且上行带宽比较充足,优先采用主播客户端混流的方式,否则降级为服务器混流。
上面两个问题分析清楚了,就可以开始设计了,这是我们的系统整体架构:
rtc-service主要提供信令、频道管理、主播管理、公有云上媒体服务器集群的健康检查和节点分配、同步主播状态到业务服务器、记录通话流水。
rtc-job是对rtc-service的补充,定期检查当前在线主播的状态,发现主播异常下线时触发兜底逻辑。
rtc-router负责收发主播的音视频数据。主播可以收到同一个频道内其他人的音视频流。如果需要服务器混流,则访问注册中心并采用Google的有界负载一致性哈希算法(Consistent Hashing with Bounded Loads)选取rtc-mixer节点,并往对应节点推送主播的音视频流。
rtc-mixer负责混流,根据需求拼接画面和混音,然后推送到CDN,观众通过CDN拉流。
主播的客户端并没有直接向rtc-router发送数据,而是通过第三方的四层加速网络转发。我们前面提到了“最优路径”的概念,第三方的四层加速服务可以让用户接入最近的加速节点,然后寻找最优路径把数据转发到我们的公有云节点。客户端只能看到第三方的加速节点IP,看不到我们公有云媒体服务器的IP,这在一定程度上可以防止服务器遭到攻击;其次,我们可以在保证异地多活的前提下让公有云集群相对比较集中,节省成本。
服务的可用性和容错性也是一个很重要的问题,假如在主播PK即将胜利的时刻服务出现故障,弹出"PK异常终止请重新再来",这很令人绝望。我们不仅要保证服务可用,还要尽最大可能保证服务出现故障时减小对用户的影响,让流程能够走下去。接下来讨论系统中每个风险模块为了实现这个目标所采取的措施:
-
四层加速网络故障。这个属于第三方厂商提供的服务,每个厂商提供的接入方式大同小异,基本上就是附加的header有差别,所以同时对接多家厂商对客户端来说是很容易做到的。客户端进行连通性检查,只要存在至少一家厂商的服务可用,就不会影响业务。
-
公有云上的rtc-router和rtc-mixer故障。在公有云上部署服务,尽量要多厂商、多区域部署,防止单机房整体宕机。我们同样准备了多个集群,每个集群都部署了多台rtc-router、rtc-mixer和ZooKeeper,单个集群可以独立工作,如果单个集群不可用或者负载达到上限则会被熔断。核心机房的rtc-service会对公有云上的集群进行健康检查,如果rtc-router宕机,rtc-service会通过信令通道通知客户端切换到同集群中其他服务器,当同集群没有可用机器时则切换集群。如果rtc-mixer宕机,rtc-router会通过ZooKeeper重新选择一台接管混流任务。
-
核心机房的rtc-service和rtc-job故障。这部分内容和B站大部分核心服务部署在同样的集群,复用了B站比较成熟的高可用架构。这部分内容可以参考其他文章,这里不再赘述。
05 总结
如果我们把实时音视频技术比作一座富丽堂皇的城池,这篇文章只能带领大家来到城门口。我们也不会停止探索的脚步。希望大家读到这里能够有所收获,如有疏漏,欢迎批评指正。