webrtc

转WebRTC Native 源码导读(十五):RTP H.26

2019-08-02  本文已影响0人  hijiang

本文是 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 个字节部分,以及可选的 csrcext 数据(在为 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 modeWebRTC 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 协议定义了包聚合策略:

     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)

在应用层实现包拆分而不是依赖下层网络的拆分机制,好处有二:

每个分包都有一个编号,一个 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   |
      +---------------+

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 ???

接着准备四种包的模板:

准备过程包括:

这些模板一是后续封包时可以直接拿来用,二是可以准确地知道 RTP 头部需要多少空间,正如注释所言:

Simplest way to estimate how much extensions would occupy is to set them.

知道了每种包的头部需要多少空间后,就知道每个包最多可以容纳多少载荷了(为 RtpPacketizer::PayloadSizeLimits 的各个字段赋值):

准备好了模板、知道了 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(0010001),统计每个 NALU 的 offset 和长度。安卓的实现在 sdk/android/src/jni/videoencoderwrapper.ccVideoEncoderWrapper::ParseFragmentationHeader 中,iOS 的实现在 sdk/objc/components/video_codec/nalu_rewriter.ccH264CMSampleBufferToAnnexBBuffer中。

H.264 规范里定义了一幅图像分片为多个 NALU 的功能,但我观察了一下 iPhone 6 编出来的数据,非关键帧都只有一个 NALU,关键帧有两个 NALU,而且前面都添加了 SPS 和 PPS,所以关键帧会有四个 NALU。

有了 input_fragments_ 后,就会在 GeneratePackets 中遍历之,对每个 Fragment,根据 packetization_mode 执行不同的封包逻辑:

GeneratePackets 执行完毕后,就算出了 num_packets_left_ 的值,即此帧图像需要多少个 RTP包,并且也准备好了 PacketUnit 数组。

之后在 RTPSenderVideo::SendVideo 里就会调用 num_packets_left_NextPacket 来实际组装每一个 RTP 包了,我们现在就看看 NextPacket 的逻辑:


好了,至此我们就已经看完了 H.264 封装 RTP 包的逻辑,可以长舒一口气了 :)

WebRTC H.264 解包实现

了解了封包的实现,我们接下来看看解包是怎么实现的,解包比封包稍微复杂一点,关键就在于包的到达可能是乱序的(丢包重传也可以认为是一种乱序)。

解包过程包括两大步:先解析出 RTP 的头部和载荷;再解析载荷部分,根据不同的封包模式,对封包过程做一个逆操作,就能得到一帧完整的数据。前者在 Call::DeliverRtp 中调用 RtpPacket::ParseBuffer 中实现,后者则比较复杂,因为需要处理乱序问题,逻辑起始点是 RtpVideoStreamReceiver::ReceivePacket 函数。

RtpPacket::ParseBuffer

ParseBuffer 的任务有三点:

RtpVideoStreamReceiver::ReceivePacket

首先根据不同的 payload type,创建不同的 RtpDepacketizer 去解析载荷内容,H.264 的解析逻辑在 RtpDepacketizerH264::Parse 中实现,其主要任务就是找到实际数据的位置和大小:

然后解析 RTP 扩展头的实际数据,包括 VideoOrientation 等。

最后构造 VCMPacket,并调用 PacketBuffer::InsertPacket 放入包缓冲区中。

PacketBuffer::InsertPacket

PacketBuffer 封装了 RTP 包处理乱序到达的逻辑,大体思路就是:

PacketBuffer::FindFrames

每次收到包后,会触发 FindFrames,我们会从刚收到的包的序列号向后查找:

RtpFrameReferenceFinder::ManageFrame

从载荷里解析出来的帧数据都是完整的帧,但不一定能解码,比如 H.264 有前向参考(P 帧需要参考前面的 I 帧才能解码),也有后向参考(B 帧需要参考前面的 I/P 帧和后面的 P 帧才能解码),因此需要等这一帧的参考帧都收到之后,才能回调出去。

虽然 PacketBuffer 处理了 RTP 报文乱序到达的问题,输出了一个个完整的帧,但并没有保证帧是按序到达的,所以仍需 RtpFrameReferenceFinder 来处理帧乱序到达的问题。

RtpFrameReferenceFinder 的代码细节这里就不展开了,有兴趣/需求的朋友可以自行阅读。


好了,至此我们就已经看完了 H.264 解封装 RTP 包的逻辑,可以再长舒一口气了 :)

WebRTC RTP 封包解包相关数据结构

最后,我们再总结一下 WebRTC RTP 封包解包相关数据结构:


最后的最后,我再分享一个内容:序列号的比较算法。

序列号的比较算法

由于序列号可能发生回绕,所以不能直接比较,有一个 RFC 文档专门定义了这个比较算法:Serial Number Arithmetic

这个 RFC 里首先定义了序列号的定义法:n 位无符号数,最低序列号为 0,最高序列号为 2^n-1,序列号没有最大最小值,每个序列号至少需要 n 位来保存。

接着它定义了序列号的加法:在 [0, 2^n-1] 范围内的合法序列号值 s,加 m 的值为 (s+m) % (2^n),这里的加法和取模,都是常规定义的加法和取模。

最后它定义了序列号的比较算法(RFC 里为了严谨,引入了另外两个普通正整数,这里简单起见我们就不引入了):

细心的朋友也许会举出一个例子:7 和 3 谁大谁小?它们其实无法区分大小。就像 3 和 3 是否相等一样,无法区分。RFC 里故意不对这种序列号对的大小问题作出定义,因为着实不好定义。

WebRTC 的实现逻辑主要在 rtc_base/numerics/sequence_number_util.hrtc_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;
}

其实就是通过无符号数减法的溢出,把 RFC 定义的两种或起来的情况统一了,以及对于 RFC 未定义的情况,定义成了值大小的比较

上一篇下一篇

猜你喜欢

热点阅读