转WebRTC Native 源码导读(十五):RTP H.26
本文是 Piasy 原创,发表于 https://blog.piasy.com,请阅读原文支持原创 https://blog.piasy.com/2019/01/01/WebRTC-RTP-Mux-Demux/
之前我在为 janus-pp-rec 增加视频旋正功能一文中简单介绍了一点 RTP 协议的内容,重点关注的是视频方向的 RTP header extension,这次我们更深入的了解一下 RTP 协议的内容,看看 H.264 视频数据是如何封装和解封装的。
其他视频编码格式的封装和解封装大体思路应该是一样的,大家可以举一反三;音频数据的封装和解封装,日后可能我会再整理一篇文章。
注:本文是我对这块代码、协议理解的总结,如果你也打算钻研这块代码,那本文可能有一点助益,如果你没有这个打算,那我建议趁早关闭本页面。
再谈 RTP 协议
我们首先了解一下 RTP H.264 相关的 RFC,下面的内容是对两篇 RFC 的总结:RTP: A Transport Protocol for Real-Time Applications, RTP Payload Format for H.264 Video。
RTP 包结构
包头有固定 12 个字节部分,以及可选的 csrc
和 ext
数据(在为 janus-pp-rec 增加视频旋正功能一文中有更详细的介绍):
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|X| CC |M| PT | sequence number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
接着是载荷数据,载荷长度在包头中有记录。载荷数据的格式,由不同的 profile 单独定义,profile 的 payload type 值,通过 SDP 协商确定。
下面我们了解一下 H.264 载荷的格式。
H.264 载荷
H.264 载荷数据的第一个字节格式和 NAL 头一样,其 type 定义如下:
Table 1\. Summary of NAL unit types and the corresponding packet
types
NAL Unit Packet Packet Type Name Section
Type Type
-------------------------------------------------------------
0 reserved -
1-23 NAL unit Single NAL unit packet 5.6
24 STAP-A Single-time aggregation packet 5.7.1
25 STAP-B Single-time aggregation packet 5.7.1
26 MTAP16 Multi-time aggregation packet 5.7.2
27 MTAP24 Multi-time aggregation packet 5.7.2
28 FU-A Fragmentation unit 5.8
29 FU-B Fragmentation unit 5.8
30-31 reserved -
H.264 载荷数据的封包有三种模式:Single NAL unit mode (0), Non-interleaved mode (1), Interleaved mode (2)。它们各自支持的 type 见下表:
Table 3\. Summary of allowed NAL unit types for each packetization
mode (yes = allowed, no = disallowed, ig = ignore)
Payload Packet Single NAL Non-Interleaved Interleaved
Type Type Unit Mode Mode Mode
-------------------------------------------------------------
0 reserved ig ig ig
1-23 NAL unit yes yes no
24 STAP-A no yes no
25 STAP-B no no yes
26 MTAP16 no no yes
27 MTAP24 no no yes
28 FU-A no yes yes
29 FU-B no no yes
30-31 reserved ig ig ig
注意:WebRTC iOS H.264 编码时,无论是 baseline 还是 high profile,都是使用的 Non-interleaved mode,WebRTC Android 也是如此。
因此 WebRTC 里实际使用的只有三种封包模式:NAL unit, STAP-A, FU-A。那我们接下来就看一下这三种模式。
NAL unit
如果 type 为 [1, 23]
,则该 RTP 包只包含一个 NALU:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| Type | |
+-+-+-+-+-+-+-+-+ |
| |
| Bytes 2..n of a single NAL unit |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 2\. RTP payload format for single NAL unit packet
包聚合(Aggregation Packets)
为了体现/应对有线网络和无线网络的 MTU 巨大差异,RTP 协议定义了包聚合策略:
- STAP-A:聚合的 NALU 时间戳都一样,无 DON(decoding order number);
- STAP-B:聚合的 NALU 时间戳都一样,有 DON;
- MTAP16:聚合的 NALU 时间戳不同,时间戳差值用 16 bit 记录;
- MTAP24:聚合的 NALU 时间戳不同,时间戳差值用 24 bit 记录;
- 包聚合时,RTP 的时间戳是所有 NALU 时间戳的最小值;
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|NRI| Type | |
+-+-+-+-+-+-+-+-+ |
| |
| one or more aggregation units |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 3\. RTP payload format for aggregation packets
STAP-A 示例:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| RTP Header |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|STAP-A NAL HDR | NALU 1 Size | NALU 1 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 1 Data |
: :
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| | NALU 2 Size | NALU 2 HDR |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| NALU 2 Data |
: :
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 7\. An example of an RTP packet including an STAP-A
containing two single-time aggregation units
包拆分(Fragmentation Units,FUs)
在应用层实现包拆分而不是依赖下层网络的拆分机制,好处有二:
- 可以支持超过 64 KB(IPv4 包最大长度为 64 KB)的 NALU,高清视频文件可能有超大的 NALU;
- 可以利用 FEC(forward error correction);
每个分包都有一个编号,一个 NALU 拆分的 RTP 包其序列必须顺序且连续,中间不得插入其他数据的 RTP 包序号。FU 只能拆分 NALU,STAP 和 MTAP 不能拆分,FU 也不能嵌套。FU-A 没有 DON,FU-B 有 DON。
FU-A 格式如下:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| FU indicator | FU header | |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
| |
| FU payload |
| |
| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| :...OPTIONAL RTP padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Figure 14\. RTP payload format for FU-A
FU header 格式如下:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|R| Type |
+---------------+
- S: start bit, 置一表明这是 NALU 的首个 fragment;
- E: end bit, 置一表明是 NALU 的最后一个 fragment;
- R: reserved,必须置零;
- Type: 取值含义与 NALU header 的 type 字段一致;
WebRTC H.264 封包实现
了解完了理论部分,接下来我们看看 WebRTC 里是如何实现的,WebRTC 把视频数据封装成 RTP packet 的逻辑在 RTPSenderVideo::SendVideo
函数中。
RTPSenderVideo::SendVideo
其实封包的过程,就是计算一帧数据需要封多少个包、每个包放多少载荷,为此我们需要知道各种封包模式下,每个包的最大载荷(包大小减去头部大小)。
首先计算一个包的最大容量,这个容量是指可以用来容纳 RTP 头部和载荷的容量,FEC、重传的开销排除在外:
// Maximum size of packet including rtp headers.
// Extra space left in case packet will be resent using fec or rtx.
int packet_capacity = rtp_sender_->MaxRtpPacketSize() - fec_packet_overhead -
(rtp_sender_->RtxStatus() ? kRtxHeaderSize : 0);
rtp_sender_->MaxRtpPacketSize
默认会被设置为 1460,但如果需要发送视频,则会被设置为 1200。(why ???)
接着准备四种包的模板:
-
single_packet
: 对应 NAL unit 和 STAP-A 的包; -
first_packet
: 对应 FU-A 的首个包; -
middle_packet
: 对应 FU-A 的中间包; -
last_packet
: 对应 FU-A 的最后一个包;
准备过程包括:
- 在
RTPSender::AllocatePacket
里设置ssrc
和csrcs
字段,预留AbsoluteSendTime
,TransmissionOffset
和TransportSequenceNumber
extension 的空间,并且按需设置PlayoutDelayLimits
和RtpMid
extension; - 在
RTPSenderVideo::SendVideo
里设置payload_type
,rtp_timestamp
和capture_time_ms
字段; - 在
AddRtpHeaderExtensions
里按需设置VideoOrientation
,VideoContentTypeExtension
,VideoTimingExtension
和RtpGenericFrameDescriptorExtension
extension; -
first_packet
,middle_packet
和last_packet
均是拷贝自single_packet
,因此代码里只调用了AddRtpHeaderExtensions
设置它们的 extension;
这些模板一是后续封包时可以直接拿来用,二是可以准确地知道 RTP 头部需要多少空间,正如注释所言:
Simplest way to estimate how much extensions would occupy is to set them.
知道了每种包的头部需要多少空间后,就知道每个包最多可以容纳多少载荷了(为 RtpPacketizer::PayloadSizeLimits
的各个字段赋值):
-
max_payload_len
,最大载荷可用空间:包的最大容量减去中包头部大小; -
single_packet_reduction_len
,封单包时,载荷可用空间还需要在max_payload_len
的基础上打个折扣:单包与中包头部大小之差;即包的最大容量减去单包头部大小; -
first_packet_reduction_len
,封多包时,首包载荷可用空间也需要在max_payload_len
的基础上打个折扣:首包与中包头部大小之差; -
last_packet_reduction_len
,封多包时,末包载荷可用空间也需要在max_payload_len
的基础上打个折扣:末包与中包头部大小之差;
准备好了模板、知道了 limits 之后,就创建 RtpPacketizer
,通过其 NumPackets
接口得知这一帧图像需要封装为多少个包,再调用其 NextPacket
封装每个包。调用 NextPacket
之后还不算完,还得调用 RTPSender::AssignSequenceNumber
分配序列号,如果需要设置 VideoTimingExtension
,还得设置 packetization_finish_time_ms
。最后,就是调用 FEC 处理,或直接调用 RTPSenderVideo::SendVideoPacket
发送 RTP 报文了。
视频编码为 H.264 时,RtpPacketizer
的实现类是 RtpPacketizerH264
,接下来,我们就看一下 H.264 的封包逻辑。
RtpPacketizerH264
RtpPacketizerH264
构造时会根据 RTPFragmentationHeader
的内容,生成 RtpPacketizerH264::Fragment
数组 input_fragments_
,Fragment
里面包含了每个 NALU 载荷起始字节的指针、NALU 的长度。
RTPFragmentationHeader
其实就是这帧图像里面每个 NALU 的信息:载荷在 buffer 里的 offset、载荷长度。这些信息在编码器输出数据之后解析生成,扫描整个 buffer,查找 NALU start code(001
或 0001
),统计每个 NALU 的 offset 和长度。安卓的实现在 sdk/android/src/jni/videoencoderwrapper.cc
的 VideoEncoderWrapper::ParseFragmentationHeader
中,iOS 的实现在 sdk/objc/components/video_codec/nalu_rewriter.cc
的 H264CMSampleBufferToAnnexBBuffer
中。
H.264 规范里定义了一幅图像分片为多个 NALU 的功能,但我观察了一下 iPhone 6 编出来的数据,非关键帧都只有一个 NALU,关键帧有两个 NALU,而且前面都添加了 SPS 和 PPS,所以关键帧会有四个 NALU。
有了 input_fragments_
后,就会在 GeneratePackets
中遍历之,对每个 Fragment
,根据 packetization_mode
执行不同的封包逻辑:
-
如果是
SingleNalUnit
,那就为这个Fragment
(其实就是一个 NALU)生成一个PacketUnit
; -
如果是
NonInterleaved
(WebRTC Native SDK 实际使用的 mode),那就看这个Fragment
能否放进单个包里,先计算单个包能容纳多少数据:int single_packet_capacity = limits_.max_payload_len; if (input_fragments_.size() == 1) single_packet_capacity -= limits_.single_packet_reduction_len; else if (i == 0) single_packet_capacity -= limits_.first_packet_reduction_len; else if (i + 1 == input_fragments_.size()) single_packet_capacity -= limits_.last_packet_reduction_len;
-
逻辑并不复杂,
max_payload_len
扣除各种情况的折扣之后,剩下的就是single_packet_capacity
; -
如果
fragment_len > single_packet_capacity
,就说明无法放进单个包,那就要做 Fragmentation 了,即调用PacketizeFuA
,否则说明可以放进单个包,那就可以做 Aggregation,即调用PacketizeStapA
; -
PacketizeFuA
就是看怎么把一个Fragment
分成多个包了,然后生成每个PacketUnit
,这个分的逻辑实现在SplitAboutEqually
函数中,里面处理了不少边界情况,大体思想就是把数据放进尽可能少的包、每个包的大小尽可能相近;它生成的PacketUnit
的aggregated
字段都是 false; -
PacketizeStapA
则是看能把多少个Fragment
放进一个包,这里也会为每个Fragment
生成一个PacketUnit
,但只会对num_packets_left_
做一次加一操作;它生成的PacketUnit
除了最后一个的aggregated
字段为 false,其他都为 true;
GeneratePackets
执行完毕后,就算出了 num_packets_left_
的值,即此帧图像需要多少个 RTP包,并且也准备好了 PacketUnit
数组。
之后在 RTPSenderVideo::SendVideo
里就会调用 num_packets_left_
次 NextPacket
来实际组装每一个 RTP 包了,我们现在就看看 NextPacket
的逻辑:
- 检查首个
PacketUnit
: - 如果
PacketUnit
的first_fragment
和last_fragment
字段都是 true,那就直接把载荷拷进去; - 这种情况有可能是
SingleNalUnit
,也有可能是NonInterleaved
的 STAP-A 包,因为NonInterleaved
时,如果Fragment
可以放进一个包,那就会封为 STAP-A,而如果只生成了一个PacketUnit
,那它的first_fragment
和last_fragment
都会是 true; - 否则,如果
aggregated
字段为 true,那就调用NextAggregatePacket
封 STAP-A 包;- 这里只提一点我看了比较久才看清楚的逻辑:这个函数里通过一个循环不停的消费
PacketUnit
,退出循环的条件是!packet->aggregated
或packet->last_fragment
,由于需要放进一个包的一系列PacketUnit
里只有最后一个last_fragment
字段为 true(这个逻辑在PacketizeStapA
里),因此可以正确退出循环;
- 这里只提一点我看了比较久才看清楚的逻辑:这个函数里通过一个循环不停的消费
- 如果
aggregated
字段为 false,就调用NextFragmentPacket
封 FU-A 包;
好了,至此我们就已经看完了 H.264 封装 RTP 包的逻辑,可以长舒一口气了 :)
WebRTC H.264 解包实现
了解了封包的实现,我们接下来看看解包是怎么实现的,解包比封包稍微复杂一点,关键就在于包的到达可能是乱序的(丢包重传也可以认为是一种乱序)。
解包过程包括两大步:先解析出 RTP 的头部和载荷;再解析载荷部分,根据不同的封包模式,对封包过程做一个逆操作,就能得到一帧完整的数据。前者在 Call::DeliverRtp
中调用 RtpPacket::ParseBuffer
中实现,后者则比较复杂,因为需要处理乱序问题,逻辑起始点是 RtpVideoStreamReceiver::ReceivePacket
函数。
RtpPacket::ParseBuffer
ParseBuffer
的任务有三点:
- 解析 RTP 标准头的各个字段,包括
payload_type_
,sequence_number_
,timestamp_
,ssrc_
等; - 解析 RTP 扩展头的元数据,即偏移量和长度;
- 确定 RTP 载荷的偏移量和长度,完成了第二点后做个减法就可以得到;
RtpVideoStreamReceiver::ReceivePacket
首先根据不同的 payload type,创建不同的 RtpDepacketizer
去解析载荷内容,H.264 的解析逻辑在 RtpDepacketizerH264::Parse
中实现,其主要任务就是找到实际数据的位置和大小:
- 检查载荷的第一个字节里的 type 字段(低五位),以判断包类型(NAL unit, STAP-A, FU-A);
- FU-A 的解析在
ParseFuaNalu
里完成; - NAL unit 和 STAP-A 的解析在
ProcessStapAOrSingleNalu
里完成; - 具体解析代码这里不做展开;
然后解析 RTP 扩展头的实际数据,包括 VideoOrientation
等。
最后构造 VCMPacket
,并调用 PacketBuffer::InsertPacket
放入包缓冲区中。
PacketBuffer::InsertPacket
PacketBuffer
封装了 RTP 包处理乱序到达的逻辑,大体思路就是:
- 收到每个包之后,检查序列号:
- 确定已经收到过的包,就会直接丢弃;
- 否则就把包放进
data_buffer_
数组里,并在sequence_buffer_
数组里记下这个序列号的一些属性; - 每个包在上述两个数组里存放的下标是序列号模以数组大小,因此是按序列号顺序存放的;
- 调用
FindFrames
,从已收到的包列表中,找出完整的帧; - 把完整的帧交给
RtpFrameReferenceFinder::ManageFrame
,由其确保帧可以解码后,再回调出去,进入后续的解码环节;
PacketBuffer::FindFrames
每次收到包后,会触发 FindFrames
,我们会从刚收到的包的序列号向后查找:
- 只有一个包满足以下两个条件之一才会进行检查:
- 该包是「帧起始」包;
- 该包前一个序号的包是连续的,何谓连续?就是说它是帧起始包,或它之前的所有序列号都已经收到了;
- 举个例子,假设 1 是帧起始包,那收到 1 的时候肯定会检查,之后收到 2 时,由于 1 是连续的,所以 2 也会检查,但如果收到 4(假设 4 不是帧起始),那 4 就不会检查,再收到 3 时,就会依次检查 3 和 4;
- 我们首先感兴趣的是「帧末尾」包,即有
packet->is_last_packet_in_frame
标志; - 找到帧末尾包后,再反过来向前查找「帧起始」包;
- VP8/VP9 靠
frame_begin
(即packet->is_first_packet_in_frame
)标志判断帧起始,H.264 则靠时间戳的变化来判断帧起始; - 正常情况下,由于我们只检查帧起始包和连续包,所以一旦找到了帧末尾包,向前就一定能找到帧起始包;
- VP8/VP9 靠
- 找到了帧末尾包和帧起始包,就可以构造完整的帧了,不过这里只是记录元数据,不会做帧数据的拷贝;
RtpFrameReferenceFinder::ManageFrame
从载荷里解析出来的帧数据都是完整的帧,但不一定能解码,比如 H.264 有前向参考(P 帧需要参考前面的 I 帧才能解码),也有后向参考(B 帧需要参考前面的 I/P 帧和后面的 P 帧才能解码),因此需要等这一帧的参考帧都收到之后,才能回调出去。
虽然 PacketBuffer
处理了 RTP 报文乱序到达的问题,输出了一个个完整的帧,但并没有保证帧是按序到达的,所以仍需 RtpFrameReferenceFinder
来处理帧乱序到达的问题。
RtpFrameReferenceFinder
的代码细节这里就不展开了,有兴趣/需求的朋友可以自行阅读。
好了,至此我们就已经看完了 H.264 解封装 RTP 包的逻辑,可以再长舒一口气了 :)
WebRTC RTP 封包解包相关数据结构
最后,我们再总结一下 WebRTC RTP 封包解包相关数据结构:
-
RtpPacket
: RTP 报文的数据结构,里面定义了各种标准头部字段、扩展头部、数据缓冲区等; -
RtpPacketToSend
: 发送端封包用到的数据结构,继承自RtpPacket
,加了一些扩展头部设置逻辑的封装; -
RtpPacketReceived
: 接收端解包用到的数据结构,也继承自RtpPacket
,加了获取扩展头部逻辑的封装;
最后的最后,我再分享一个内容:序列号的比较算法。
序列号的比较算法
由于序列号可能发生回绕,所以不能直接比较,有一个 RFC 文档专门定义了这个比较算法:Serial Number Arithmetic。
这个 RFC 里首先定义了序列号的定义法:n 位无符号数,最低序列号为 0,最高序列号为 2^n-1
,序列号没有最大最小值,每个序列号至少需要 n 位来保存。
接着它定义了序列号的加法:在 [0, 2^n-1]
范围内的合法序列号值 s,加 m 的值为 (s+m) % (2^n)
,这里的加法和取模,都是常规定义的加法和取模。
最后它定义了序列号的比较算法(RFC 里为了严谨,引入了另外两个普通正整数,这里简单起见我们就不引入了):
- 判等:序列号
s
和s+m
(m
为普通正整数),只有m
为 0 时,它们才相等;即给定两个序列号值,完全无法判断其是否相等,不过通常我们不需要判等,而是判断大小; - 判小:当且仅当
(s1 < s2 && s2 - s1 < 2^(n-1)) || (s1 > s2 && s1 - s2 > 2^(n-1))
时,序列号s1
小于s2
;即值小不过一半范围,或大过一半范围,例如 n=3,2-1 < 4
,故 1 比 2 小,7-2 > 4
,故 7 比 2 小; - 判大:当且仅当
(s1 < s2 && s2 - s1 > 2^(n-1)) || (s1 > s2 && s1 - s2 < 2^(n-1))
时,序列号s1
大于s2
;即值小过一半范围,或大不过一半范围,例如 n=3,7-2 > 4
,故 2 比 7 大,2-1 < 4
,故 2 比 1 大;
细心的朋友也许会举出一个例子:7 和 3 谁大谁小?它们其实无法区分大小。就像 3 和 3 是否相等一样,无法区分。RFC 里故意不对这种序列号对的大小问题作出定义,因为着实不好定义。
WebRTC 的实现逻辑主要在 rtc_base/numerics/sequence_number_util.h
和 rtc_base/numerics/mod_ops.h
中:
template <typename T, T M = 0>
inline bool AheadOf(T a, T b) {
static_assert(std::is_unsigned<T>::value,
"Type must be an unsigned integer.");
return a != b && AheadOrAt<T, M>(a, b);
}
template <typename T, T M>
inline typename std::enable_if<(M == 0), bool>::type AheadOrAt(T a, T b) {
static_assert(std::is_unsigned<T>::value,
"Type must be an unsigned integer.");
const T maxDist = std::numeric_limits<T>::max() / 2 + T(1);
if (a - b == maxDist)
return b < a;
return ForwardDiff(b, a) < maxDist;
}
template <typename T, T M>
inline typename std::enable_if<(M == 0), T>::type ForwardDiff(T a, T b) {
static_assert(std::is_unsigned<T>::value,
"Type must be an unsigned integer.");
return b - a;
}
- 首先序列号必须是无符号数;
- 然后 WebRTC 定义了「前向距离」这个概念,即后数加多少能加到前数(考虑无符号数的溢出);
- 还定义了「最大距离」这个概念,可以理解为两个数之差的绝对值的最大可能取值,也就是最大取值范围的一半(向上取整);
- 最后,a 领先于 b 的的条件就是:若 ab 前向距离为最大距离,那 a 大于 b 就是领先于 b,否则,若 ab 前向距离小于最大距离,那 a 就领先于 b;
其实就是通过无符号数减法的溢出,把 RFC 定义的两种或起来的情况统一了,以及对于 RFC 未定义的情况,定义成了值大小的比较。