大家好,我是来自哔哩哔哩的郑龙,2012年至2017年我在广播电视行业从事工作,2017年我转型至互联网行业并加入了哔哩哔哩的视频云团队。在视频云团队的三年里,主要参与了哔哩哔哩的亿秒级日吞吐视频转码系统的开发与自营视频窄带高清技术的探索,以上两项服务都已上线并长期运行。
本次分享将从以下四个方面为大家分享SRT协议在互联网领域的实践。
1. SRT协议介绍
1.1 SRT联盟
SRT联盟是由Haivision和Wowza两大公司牵头组成的一个开源组织,通过汇集行业资源以求更好地实现提高音视频的传输质量的共同目标。联盟里的公司多为广播电视企业与国内外流媒体企业,包括国内较为知名的企业腾讯云、声网等,国际知名公司微软等。
1.2 SRT协议
SRT本身诞生于广电行业而非互联网行业,其主要运用UDP进行可靠传输或低延时传输,适用于复杂网络。这里的复杂网络主要相对于专线网络而言,也就是我们所谓的互联网。SRT包含ARQ重传与用来实现对丢包数据恢复的FEC前向纠错。其支持AES加密,同时也支持TLS链路加密。
1.3 SRT优点
1.3.1 抗丢包能力强
SRT协议的最显著优势便是抗丢包能力强。对比使用SRT协议推流与RTMP协议推流播放同一段视频:
初期SRT的丢包率增长较为迅速而RTMP的丢包率增长则较为缓慢,直到二者都出现卡顿。其中RTMP在丢包率为20%时画面已经完全卡住不动,而SRT哪怕在丢包率高达80%时,一些画面也能在经历短暂卡顿后恢复播放。
1.3.2 类SOCKET抽象API
对于开发人员来说,SRT协议的另一项优势是其类SOCKET编程的抽象API。
观察API的头文件我们不难看出,在SOCKET编程里能够遇到的几乎所有connect、SOCKETlsn、sendmsg、recvmsg等等函数,都可以在SRT的API中找到可以无缝替换的函数;除此之外SRT的【input】包含了Linux本身自带的【input】,也就是说SRT除了能用Linux本身的【input】对SOCKET进行处理之外还能对SRT自身的事件进行处理,这样便大大降低了协议开发与移植的时间成本。
1.3.3 负载无关与用户态协议栈
很多人将SRT与RTMP拿来做比较,在我看来该两个协议并非同一层级。SRT是一个用户态上的传输层协议,而RTMP则更像是一个流媒体协议,也就是说RTMP比SRT位于更高的层级。我们可以基于SRT使用RTMP进行开发,这里的底层不再是TCP,而是将SRT底层之上的所有流媒体相关逻辑全部使用RTMP替换。
所谓的“负载无关”也就是让我们将SRT看作是一个传输协议,SRT本身在协议开发阶段,其所使用的流通常为一个标准的TS流。
1.4 SRT缺点
SRT协议的缺点主要有:协议额外带宽较高,且SRT协议的传输策略激进,会对同网的其他用户带来影响;除此之外,SRT的协议栈在用户态并且占用了3~4个线程,我们知道线程的切换存在开销,线程之间的一些数据需要使用锁进行保护,加锁解锁的过程也会产生很大的开销;还有一个不可忽视的缺点就是很多防火墙对UDP并不友好,这就导致UDP的优先级更低或是含有UDP的包可能会被过滤掉。
2. SRT协议与互联网结合
接下来会详细介绍SRT协议与互联网的结合,上图展示了一个典型的包含CDN的直播与观看的场景。
来自主播的视频流推至上行CDN,再由源站推至下行CDN,继而分发给众多用户。大家可以看到视频流在该路径中的不同位置,其需要复制的数量也不尽相同。主播到上行CDN采用1:1复制,上行CDN至源站也是1:1,但是源站到下行CDN则采用1:N,而下行分发至用户则需要1:N*M,这里集中了几乎所有的带宽成本。
激进的传输策略会导致SRT的带宽增加,如果我们将SRT运用于上述所有流传输路径,势必会造成带宽成本的激增。由此可以得出SRT并不适用于下行CDN至用户这一传输路径,而更加适合主播至上行CDN以及上行CDN至源站这两部分传输路径;源站到下行CDN也可以使用SRT,相对于下行CDN至用户的1:N*M路径,其带宽占用也更低。
接下来我们就针对SRT适用的三条路径做进一步的探索。
在进行SRT有关实践之前,首先需要了解RTMP直播常见的技术栈。直播上行部分,用户多会使用OBS、移动智能终端上的App或者FFmpeg与其衍生品采集制作并生成视频流,继而通过RTMP推流至NGINX服务器,NGINX服务器会进行数据交换处理,这便是一条典型的工具链。
该工具链底层主要涉及LibRTMP与Nginx-RTMP模块,如果我们想入手实践RTMP就必须对该两部分进行改造。
3. 落地与实践
工欲善其事,必先利其器——SRT协议虽然基于UDP,但是其拥有很多自己的语义,为了更好分析复杂网络情况下SRT传输协议的情况,我们使用SRT协议解析插件实现了对于一层层嵌套的SRT语义的展示,由此我们可以看到很多SRT本身的内容例如一些包的大小或序号等等。
除此之外我们还实现了telent over SRT,也完善了一些纯SRT的推流工具,这些都便于我们能够更好地开发SRT协议。
3.1 LibRTMP支持SRT
落地与实践中第一点便是LibRTMP支持SRT,我们知道所有的TCP Socket都可以无缝替换为SRT Socket API。很快我们发现一个问题是:在正常建连时,如果手动产生一些丢包且丢包发生在RTMP握手的时刻,建连就会失败。
经过研究发现:正常情况下A向B发送一个消息1,A发出1之后会等待B回传消息2,收到B回传的2之后继续发送消息3,以此类推;我们知道SRT诞生于广电系统,其所使用的TS流传输不需要握手而是依次传输1、2、3、4、5、6、7、8……如果发送8之前未收到B传回的7即会要求B重传;但实际上SRT却是A发送1之后等待接收B发送的2,一旦握手信息出现丢包B无法接收来自A的1,也就不会发送2,此时便出现了死循环的情况,导致建连失败。
为解决SRT(1.34版本)不丢包模式下只启用被动重传的情况,我们修改了SRT的源码使得其能够在无被动重传的情况下进行主动重传。该问题已在新版本的SRT源码中得以修复。
第二个问题便是RTMP层丢包后带宽恢复过慢。SRT基于广电网络,其长时间用TS流的固定码率传输,SRT需要对码率进行评估;但是RTMP层也会出现丢包,一旦RTMP层产生丢包,就会导致SRT对负载带宽估计过小,重传率变小 。
解决该问题的方案是找到SRT源码并关闭SRT自动负载带宽估计,并根据业务手动设置负载带宽估计。例如业务需要8M的码率,我们可将其设置为8M并允许上浮50%,然后再根据RTMP或上层业务进行一定的联动。
3.2 nginx-rtmp-module支持SRT
以上介绍的内容与RTMP推流相关,接下来需要解决服务器端的一些问题。现在所有的RTMP服务器都是由Nginx事件驱动,所有的Socket都由其自己接管,而SRT的API内核也接管了所有Socket操作,这里便存在Nginx事件驱动和SRT API冲突。所以我们需要构建两个开发方案:第一个是SRT<-proxy->TCP,开发代理程序;第二个是在Nginx上开发SRT协议栈模块。