文章目录
TCP为何不适用于实时音视频
可靠性是以牺牲实时性为代价的。按照TCP原理,当出现极端网络情况时,理论上每个包的时延可达到秒级以上,而且这种时延是不断叠加的。这对于音视频实时通信来说是不可接受的。
TCP为了实现数据传输的可靠性,采用的是“发送→确认→丢包→重传”这样一套机制。而且为了增加网络的吞吐量,还采用了延迟确认和Nagle算法(将多个小包组成一个大包发送,组合包的大小不超过网络最大传输单元)
为了增加网络的吞吐量,接收端不必每收到一个包就确认一次,而是对一段时间内收到的所有数据集体确认一次即可。为了实现该功能,TCP通常会在接收端启动一个定时器。定时器的时间间隔一般设置为200ms,即每隔200ms确认一次接收到的数据。这就是延迟确认机制。‘
除此之外,TCP在发送端也启动了一个定时器,不过该定时器的功能不是发送确认消息,而是用来判别是否有丢包的情况。发送端定时器的时长为一个RTO(RTO(Retransmission Timeout),重传超时时长。其值约等于RTT的平均值,每次超时后以指数级增长。RTT表示一个数据包从发送端到接收端,然后再回到发送端所用的时长)如果在定时器超时后仍然没有收到包的确认消息,则认为包丢失了,需要发送端重发丢失的包。这就是TCP的丢包重传机制。
假如接收端发送的确认消息丢失了,按TCP的协议规则,通信双方会怎么做呢?首先,发送端只有等到定时器超时后,才能发现该包丢失了。确认丢包后,发送端会将前面所有未确认的包重发一遍。如果在收到数据后,接收端发送的确认消息又丢失了,那么发送端还要等到定时器超时后才能知道包丢失了。因此,在遇到这种极端网络的情况下,TCP传输的时延要累加很多,这种时延是不可控的。
UDP->RTP
UDP没有这套逻辑,所以实时性最高。WebRTC通过NACK、FEC、Jitter Bufer以及NetEQ技术既可以解决丢包和抖动问题,又不会产生影响服务质量的时延。
UDP传输一些有前后逻辑关系的数据时有缺陷,所以在UDP之上的应用层上使用RTP传输音视频数据
RTP协议结构
保持有序:Sequence Number
我们希望在使用RTP传输音视频数据时,一旦有数据丢失,可以快速定位是哪个数据包丢失了。
如果给每个发送的数据包都打上一个编号,并且编号是连续的,那么,接收端就可以很容易地判断出哪些包丢失了。在RTP头中,有一个专门记录该编号的字段,称作Sequence Number。在发送端,每产生一个RTP包,其Sequence Number字段中的值就被自动加1,以保证每个包的编号唯一且连续。当接收端收到RTP包时,会对Sequence Number字段进行检查,如果发现Sequence Number不连续了,就说明有包丢失或乱序了。
区分不同类型数据:PayloadType
我们在做网络应用开发时,通常会使用同一个端口传输不同类型的数据,如音视频数据。但接收端是如何区分出不同类型的数据的?RTP在其协议头中设置了PT(PayloadType)字段.比如VP8的PT一般为96,而Opus的PT一般为111
区分不同源数据包:SSRC
同一个端口不仅可以同时传输不同类型的数据包,还可以传输同一类型但不同源的数据包。
流媒体服务就可以将多个不同源(参与人)的视频通过同一个端口发送给客户端。那么客户端(接收端)又是如何将不同源的数据区分出来的呢?这就要说到RTP中另一个字段SSRC了。
RTP要求所有不同的源的数据流之间可以通过SSRC字段进行区分,且每个源的SSRC必须唯一。
每个SSRC所代表的数据流的Sequence Number都是单独计数的,如下图:
完整的协议格式如下:
V(Version)字段,占2位,表示RTP的版本号,现在使用的都是第2个版本,所以该域固定为2。
P(Padding)字段,占1位,表示RTP包是否有填充值。为1时表示有填充,填充以字节为单位。一般数据加密时需要固定大小的数据块,此时需要将该位置1。
X(eXtension)字段,占1位,表示是否有扩展头。如果有扩展头,扩展头会放在CSRC之后。扩展头主要用于携带一些附加信息。
CC(CSRCCount)字段,占4位,记录了CSRS标识符的个数。每个CSRC占4字节,如果CC=2,则表示有两个CSRC,共占8字节。
M(Marker)字段,其含义是由配置文件决定的,一般情况下用于标识边界。比如一帧H264被分成多个包发送,那么最后一个包的M位就会被置位,表示这一帧数据结束了。
timestamp字段,占4字节,用于记录该包产生的时间,主要用于组包和音视频同步。
CSRC字段,指该RTP包中的数据是由哪些源贡献的。比如混音数据是由三个音频混成的,那么这三个音频源都会被记录在CSRS列表中。
Jittbuffer
介绍一下使用RTP消除包抖动的一个简要过程:
对于WebRTC而言,其在接收RTP包时,会为之创建一个接收队列来消除包抖动。一开始,队列中只收到了100、101、102和104号包。由于103号包还没到,所以无法将100∼104号包组成一帧数据。103号包没有到有两种可能的原因:一种原因是103号包丢失了;另一种原因是网络抖动导致包乱序了。判断缓冲队列有没有满。如果缓冲队列满了,就说明包真的丢失了。对于103号包来说,由于现在缓冲队列还不满,因此该包处于待定状态。同理,当107号包到达时,105号包和106号包也处于待定状态。
很快103号包来了,通过对其RTP头中Sequence Number字段的计算,它会被插到队列中对应的空缺位置,此时100∼104号包连成了一串。又由于104号包上有M标记,因此可以将这几个RTP包组成一个完整的帧。接下来,100∼104号包将从缓冲队列中弹出,交由组帧模块处理,空出的位置可以继续接收新包。WebRTC也是通过类似的方法从网络上将一个个RTP包接收下来。
WebRTC中解决RTP包抖动的缓冲队列就是我们通常所说的JitterBufer。
RTP扩展头
当X被置位1,说明有扩展头
RTP扩展头由三部分组成,分别为profile、length以及headerex tension。
在RFC5285中定义了两种profile,分别是**{0xBE,0xDE}和{0x10,0x0X}**(分别代表存放在headerextension中的两种不同的数据格式,即one-byte-header和two-byte-header)
接收端解析RTP扩展头时,通过profile来区分header extension中的内容该如何解析。
length字段表示扩展头所携带的header extension的个数。如果length为4,表示有4个headerextension;
header extension字段是扩展头信息,以4字节为单位,其具体含义由profile决定。
one-byte-header格式:
存放在扩展头header extension字段中的数据,由一个字节的Header和N字节的Body组成,而Header又由4位的ID和4位的len组成。注意,length的值为跟在Header后面的数据(以字节为单位)长度减1。
第一个one-byte-header的length值为0,其数据长度为(0+1)=1字节;第二个one-byte-header(的length值为1,其数据占(1+1)=2字节;第三个one-byte-header的length值为3,其数据占(3+1)=4字节。此外,由于扩展头要保持4字节对齐,所以最后两个字节是填充字节,设置为0。
two-byte-header格式:
Header部分由两个字节组成,第一个字节表示ID,第二个字节表示长度,two-byte-header中length存放的是实际长度。
通过上面的介绍我们知道RTP扩展头有三个要点。一是RTP标准头中的X位,该位置1时,RTP中才会有扩展头。二是扩展头中的profile字段指明了扩展头中数据的格式。如果profile为0xBEDE,则说明使用的扩展头格式为one-byte-header;如果profile为0x100X(X表示任意值),则说明使用的扩展头格式为two-byte-header。三是one-byte-header与two-byte-header的区别。如果ID和len放在一个字节中,说明它是one-byte-header格式;如果ID和len放在两个字节中,说明它是two-byte-header格式。
RTP填充数据
RTP头中的P位用于标识RTP包中是否有填充数据。如果P位为1,说明RTP包中含有填充数据。
当RTP包中包含有填充数据时,其数据包的最后一个字节记录着包中填充字节的个数,即图中的Padding Size部分。
如果Padding Size为5,说明RTP包*有5个填充字节,其中包括它自己。在解析RTP Payload部分之前,应将填充部分去掉。去掉填充字节的算法也非常简单,首先读取RTP包的最后一个字节,取出填充字节数,然后从最后一个字节算起,将其前面的Padding Size个字节丢掉即可。
参考
李超《WebRTC音视频实时互动技术:原理、实战与源码分析》
https://weread.qq.com/web/reader/377320f07260a55337761c1kc81322c012c81e728d9d180