h264 码流打包 rtp

参考:

1. 概述

h264 打包 rtp 在 rfc6184 中有详细描述。
这里主要说明 Annex-B 格式的 264 码流打包 rtp。
关于 h264 rtp 解包组帧,参看:https://www.cnblogs.com/moonwalk/p/15903766.html。

2. h264 码流格式简述(Annex-B 格式)

2.1 nal unit stream(Network Abstraction Layer Unit Stream)

h.264 编码器把原始的 yuv 图像文件编码成码流文件,生成的码流文件称为 NAL 单元流(NAL unit Stream),NALU stream 由一个个 NALU(nal 单元) 组成(https://www.cnblogs.com/TaigaCon/p/5215448.html):

2.2 nal 单元分割方式

多个 nalu 之间,通过分割字节,组成了 nalu stream,分隔符定义如下:

  • zero_byte(0x00),一个字节。如果当前的 NAL 单元为 sps、pps 或者一个访问单元(access unit)的第一个 NAL 单元,这个字节就会存在
  • start_code_prefix_one_3bytes(0x000001),三个字节。固定存在的 NAL 单元起始码,用来指示下面为一个 NAL 单元

所以我们常看到的 nalu 分隔符一般由 0x00000001 或则 0x000001 组成。

2.3 nal 单元结构

NALU 由 header + payload 组成(http://iphome.hhi.de/wiegand/assets/pdfs/DIC_H264_07.pdf):

2.3.1 nalu header

结构如下(rfc6184):

      +---------------+
      |0|1|2|3|4|5|6|7|
      +-+-+-+-+-+-+-+-+
      |F|NRI|  Type   |
      +---------------+
  • F:1 bit,forbidden_zero_bit。固定为 0
  • NRI:2 bit,nal_ref_idc。用于指示该 nalu 的重要性,实际应用层代码一般不关心此值
  • Type:5 bit,nal_unit_type。指定 nalu 的类型,如下所示:

    我们通过读取 nalu 的第一个字节,取得 nalu header,然后判断 type,就能知道此 nalu 所属的帧类型,常见的如 IDR/I 帧的 type=5,P/B 帧的 type=1,sps 的 type=7,pps 的 type=8 等。
    在 rfc6184 中,又为 nal_unit_type 从类型值 24 开始进行了扩充:
      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                                     -

注意,扩充的 nal_unit_type 类型并不是编码器输出的类型,而是为了适应 rtp payload 打包而定义的打包类型。

2.3.2 nalu payload

nalu payload 由 rbsp 结构组成。

2.4 rbsp

rbsp 可以分为 video-codec-layer rbsp(如 I/P/B 帧等) 和 non-video-codec-layer rbsp(如 sps/pps/sei 等)。

2.4.1 防竞争字节

emulation_prevention_three_byte,1字节,固定0x03,在 rbsp 中出现连续的 0x0000 两字节结构时,在后面添加 0x03 作为防竞争字节,避免与 nal 单元分割字节冲突:

0x000000 =>  0x00000300
0x000001 =>  0x00000301
0x000002 =>  0x00000302
0x000003 =>  0x00000303
      .........

2.4.2 rbsp 尾部

  • rbsp_stop_one_bit,1位,固定为1
  • rbsp_alignment_zero_bit,用于字节对齐,可选

2.5 SPS/PPS

  • SPS/PPS 不同于 slice,虽然也是编码器输出的内容,但是不包含任何原始码流。他们中包含的是解码器需要的解码信息,例如图像的宽高、一些编码参数等。
  • 在编码的时候,可以通过 ffmpeg 设置 AV_CODEC_FLAG_GLOBAL_HEADER 参数,那么 sps、pps 会作为 extra data 出现在 AVCodecContext::extradata 变量中,而不是出现在每个 IDR 帧的前面。用户发送数据的时候,最好每次发送 idr 帧时,都将 sps、pps 一起发送。
  • sps、pps 会有一个 id 值,如 sps 中的 seq_parameter_set_id,用于标识 sps 版本。pps 中的 pic_parameter_set_id,用于标识 pps 版本(且 pps 也有一个 seq_parameter_set_id,用于标识参考的 sps)。idr 也会有一个 pic_parameter_set_id,用于标识参考的 pps id(然后通过 pps 的 seq_parameter_set_id 跟踪参考的 sps)。当编码参数发生变化时,这些值也会发生变化,所以发送给接收端的数据,一定要及时更新 sps、pps,否则会发生解码错误。

2.6 slice(条带)

编码后原始码流被保存到了称为 slice 的结构中,编码后的一帧图像可以对应一个 slice。但是因为一些编码器设置,例如设置了输出 slice 的数量、进行了多线程并行编码等,编码后的一帧图像,也会分为多个 slice,每个 slice 各自负责了图像中某一块的编码。
slice 也分为 header 和 data 部分:

通过 header 部分,我们可以得到当前 slice 在一帧编码图像中的位置(第几个)、当前编码图像是 I/P/B 帧等中的哪一种、当前编码图像参考的 SPS/PPS 等重要信息。

2.7 access unit(访问单元)

访问单元代表一张编码图像,不包含 sps、pps 等外部数据。由于一帧编码后的图像可能会生成多个 slice,所以,access unit(访问单元)可以由属于一帧编码图像的多个 slice(nalu) 组成。

2.8 idr 帧

idr 帧是立即刷新帧,意味着接收端收到 idr 帧时,前面的参考帧缓存都可以丢弃了(注意,仅针对 close-gop),且 idr 后面的帧不会参考 idr 前面的任何帧。
idr 与 I 帧不同,I 帧不具有刷新参考缓冲区的功能,使用 ffmpeg 编码时,可以设置 AVCodecContext::gop_size 来指定多少帧产生一个 IDR 帧。

3. h264 rtp payload 格式

3.1 Packetization Modes(打包模式)

rfc6184 定义了三种打包模式,分别为:

  • Single NAL Unit mode,单 nalu 模式
  • Non-Interleaved mode,非交织模式
  • Interleaved mode,交织模式

三种不同打包模式对不同 nal unit 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

3.1.1 Single NAL Unit mode(单 nalu 模式)

单 nalu 模式即将编码器输出的 nalu stream 流直接通过分隔符拆分出来,一个一个 nalu 复制到 rtp payload 中进行发送:

     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

Single NAL Unit mode 有如下问题:

  • 如果采用 udp 进行媒体传输,那么会有 ip 分片的问题
  • 当 nalu 比较小时,网络传输效率不高(ip 头 + udp 头 + rtp 头会占用较多的带宽)

3.1.2 Non-Interleaved mode(非交织模式)

非交织模式下不仅支持编码器直接输出的的 nalu 复制到 payload 的打包方式,也支持 stap-a 聚合包模式和 fu-a 拆分包模式。聚合包模式可以合并较小的 nalu 到一个 rtp payload 中,解决网络传输效率不高的问题;拆分包模式可以拆分大 nalu 到多个 rtp payload 中,解决 ip 分片的问题。

3.1.3 Interleaved mode(交织模式)

没有研究过,且 webrtc、sip 等都不支持此模式。

3.2 sdp 媒体协商

在 sdp 媒体协商中,有如下示例(来自 webrtc):

a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f

如上,packetization-mode=1,profile=42,level=e0,id=1f(固定,不做分析)。下面主要分析 packetization-mode、profile 和 level 三个字段。

3.2.1 packetization-mode(协商打包模式):

rfc6184 定义了 single nalu mode=0; non-interleaved mode=1; interleaved mode=2。一般 sdp 中只支持 packetization-mode=1 的打包模式,如果没有此项,默认支持 Single NAL Unit mode。

3.2.2 profile

一般 sdp 中会出现三种 profile,分别为 42(baseline profile)、4d(main profile)、64(high profile),参考 wiki 的定义(https://en.wikipedia.org/wiki/Advanced_Video_Coding):

可以看到,三种 profile 都只支持 yuv420 一种源图像格式。baseline profile 不支持 B 帧,其它两种支持 B 帧。
但是实际项目中,即使协商的 profile 非 baseline,webrtc 等实时音视频系统也不会出现 B 帧,因为 B 帧的解码依赖于前后帧,解码顺序与显示顺序不一致,会造成显示延迟。且发生丢包时,B 帧无法解码的概率很大。还有一个项目上的问题是,与自己系统对接的其它音视频系统也不一定支持 B 帧。

3.2.3 level

参考 wiki 的定义(https://en.wikipedia.org/wiki/Advanced_Video_Coding):

以上是一部分截图,可以看到,level 主要影响的是支持的最高分辨率和帧率,如 3.1(16进制为1f),最高支持 1,280×720@30.0 的视频。

3.3 packet type(rtp payload 打包类型)

在 h264 输出码流的 nalu 类型基础上,为了适应 rtp payload 打包,又扩充了 nal_unit_type 的定义:

  • nal_unit_type 从 1 到 23,是 h264 定义的 nalu 类型,直接一一对应 rtp payload 打包类型
  • nal_unit_type 从 24 到 29,是 rfc6184 定义的 nalu 类型,主要适应于网络传输要求

由于一般 webrtc、sip 等只支持 non-interleaved mode(非交织模式),所以下面只讨论 stap-a 和 fu-a 打包方式(单包方式已在前面进行了讨论)。

3.3.1 STAP-A(Single-Time Aggregation Packet A)

如果一个编码器输出的 h264 nalu 的大小太小,那么可以尝试与下一个 nalu 合并后一起放到一个 rtp 包的 payload 中,只要总大小不超过 MTU(1500字节) 即可。基于此,定义了 stap-a 类型,用于将多个 nalu 组合到一个 rtp payload 中:

     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

但是 STAP 聚合包有个规则是同一时间编码出来的 nalu 才能聚合在一起,这意味着前后帧编码后的 nalu 不能尝试合并在一起。

  • STAP-A NAL HDR,1 字节。依然由 F、NRI、Type 三项组成,其中 F 置 0(因为 h264 中所有 nalu 的 F 都置 0);NRI 为组合的 nalu 中 NRI 值最大的那个,实际上置 0 即可;Type=24
  • NALU 1 size,2 字节。即第一个 nalu 的大小,包括 nalu header
  • NALU 1 HDR,1 字节,nalu 的头部,直接复制原始 nalu 的 header 即可
  • NALU 1 DATA,(NALU 1 Size - 1) 字节大小,直接复制原始 nalu 的 payload 即可
  • 后面是第二个 ... 第 n 个 相同的结构
  • OPTIONAL RTP padding,rtp payload 可以进行 4 字节对齐(非强制要求)

3.3.2 FU-A(Fragmentation Units A)

fu-a 打包模式能够将一个大的 nalu 拆分成多个放到 rtp payload 中,每个拆分包的大小以总数据包大小不超过 MTU 为准:

     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-a 的头部有 2 个字节,分别为 FU indicator 和 FU header,各占一个字节。

FU indicator:

   The FU indicator octet has the following format:

       +---------------+
       |0|1|2|3|4|5|6|7|
       +-+-+-+-+-+-+-+-+
       |F|NRI|  Type   |
       +---------------+

其中,F 位、NRI 位都是复制自拆分的 nalu 的 header,Type=28。

FU header:

   The FU header has the following format:

      +---------------+
      |0|1|2|3|4|5|6|7|
      +-+-+-+-+-+-+-+-+
      |S|E|R|  Type   |
      +---------------+

其中,S 位表示此 payload 是否是 nalu 的第一个拆分包,如果是,则 S 位置 1,否则,置 0。
E 位表示此 payload 是否是 nalu 的最后一个拆分包,如果是,则 E 位置 1,否则,置 0。
R 位是保留位。Type 复制自 nalu 的 Type。

FU payload 赋值自拆分的 nalu body。

4. 打包步骤

打包时,不需要知道 nalu 的具体类型(sps/pps/IDR 还是非 IDR 等类型),只需要根据分隔符和 nalu 的大小选择合适的打包方式。
同时,我们需要先预设一个与 MTU 相关的阈值,可以设置为 1400 byte,即一个 rtp payload 最大容量为 1400 字节。

4.1 识别 nalu stream 分隔符

编码器输出的是 nalu stream,通过前面说明的分隔符将每个 nalu 之间进行了分割,所以打包的第一步就是识别分隔符,得到每个 nalu 的起始地址和大小。
需要注意的是,分隔符可以是 0x00000001 或者 0x000001,进行分隔符识别时,代码必须兼容这两种分割方式。

4.2 Single NAL Unit mode(单 nalu 模式)

如果媒体协商 sdp 中没有说明 packetization-mode=1,则只能打单包。前面说明了如果 nalu 大小超过 MTU,则有 ip 分片的风险,所以这就需要编码器输出的 nalu 大小不能超过 payload 阈值。在 x264 编码器中,通过设置 slice-max-size 参数可以控制输出的 slice(即 nalu)的最大字节数(http://www.chaneru.com/Roku/HLS/X264_Settings.htm#slice-max-size):

注意,设置 slice-max-size 后,设置的 slices 数量参数就无效了。
单 nalu 模式直接将根据分隔符识别的每个 nalu 原样复制到 rtp payload 中即可。
在下一节中,会一起介绍判断何时打 single 包的伪代码。

4.3 STAP-A(聚合包模式)

如果媒体协商 sdp 中说明了 packetization-mode=1,则支持 stap-a 打包模式。
有如下伪代码:

vector<NALU> nalus; // 存储所有 nalu 的数组
int payload_threshold; // rtp payload 阈值

// 遍历所有 nalus
while (i < nalus.size()) {
  if (当前 nalu 的 size < payload_threshold) {
    j = i + 1;
    sum_len = nalus[i].len;
    // 累加 n 个 nalu.len,直到累计大小大于阈值
    while (j < nalus.size()) {
      sum_len += nalus[j].len;
      if (sum_len > payload_threshold) {
        break; // 跳出第二个 while
      }
    }
    if (j - i - 1 > 0) {
      // 打包 [i, j-1] 内的 nalus 为一个 stap-a 包
      do_package_stapa(nalus, i, j-1);
    } else {
      // 对于如下情况,只能将第 i 个包单独打包:
      //   1. 如果第 i+1 个包累加到第 i 个包后,总大小超过阈值
      //   2. 如果第 i 个包已经是最后一个包
      do_package_single(nalus, i);
    }
    // 更新 i,然后继续从 while 循环开始检测
    i = i + (j-i);
  }
}

4.4 FU-A(拆分包模式)

如果媒体协商 sdp 中说明了 packetization-mode=1,则支持 fu-a 打包模式。
有如下伪代码:

vector<NALU> nalus; // 存储所有 nalu 的数组
int payload_threshold; // rtp payload 阈值

// 遍历所有 nalus
while (i < nalus.size()) {
  if (当前 nalu 的 size > payload_threshold) {
    // 得到 nalu 被拆分的个数
    int count = nalus[i].size / payload_threshold;
    if (nalu.size % GO_MAX_RTP_PACKET_SIZE) {
      count += 1;
    }

    // 遍历 count
    for (j < count) {
      // 标识 fu header 开始和结束位
      bool start = (j == 0);
      bool end = (j == (count-1));
      
      // 得到每个拆分包的开始地址和大小
      char* data = nalus[i].size + j * payload_threshold;
      int size = payload_threshold;
      if ((j + 1) * payload_threshold > nalus[i].size) {
        size = nalus[i].size % payload_threshold;
      }
  
      // 开始构建 fu-a payload
      // ...
    }
  }
  // 更新 i,然后继续从 while 循环开始检测
  i = i + 1;
}

4.5 sps、pps 和 idr

在 ffmpeg 中,如果没有设置 AV_CODEC_FLAG_GLOBAL_HEADER,那么编码器编码出 idr 时,前面都会附带 sps、pps、sei(可选),因为他们是同一时间编码出来的,所以可以使用 stap-a 格式进行打包。且一般 sps 和 pps 的大小都很小,所以 sps 和 pps 常会打到同一个 stap-a payload 格式的包中。idr 一般都比较大,会拆分成多个 fu-a 包。
pps、idr 前面的分隔符,不一定都是 0x00000001 开头,取决于编码器的实际输出,但是 sps 前面的分隔符一定是 0x00000001 开头。
sps、pps、idr 可以看作是同一帧,标识 rtp header marker 时,只有 idr nalu 才会有可能被标识 marker,且他们的 rtp header timestamp 都一样。

4.6 rtp header marker

rtp header marker 用于标识当前 rtp 包是否是一帧的最后一个 rtp 包:

  • 打 stap-a 包时,如果聚合的最后一个 nalu 是 nalu stream 的最后一个 nalu,那么标识 marker
  • 打 fu-a 包时,如果待拆分的 nalu 是 nalu stream 的最后一个 nalu,且 fu-a header end 位置 1 时,才能标识 marker
  • 打 single 包时,如果当前 nalu 是 nalu stream 的最后一个 nalu,那么标识 marker

4.7 rtp header timestamp

rtp 打包 h264 不支持 B 帧,因为 pts、dts 不同的话,无法同时存储两个时间戳。
所以 rtp timestamp 直接使用 pts * clockrate 即可(注意 pts 时间单位为秒,应来自于送入编码器前赋值的时间戳)。
如果发送 rtp 的是编码端,参考编码端打时间戳的考虑 https://www.cnblogs.com/moonwalk/p/16409385.html。

posted @ 2022-02-17 11:13  小夕nike  阅读(1367)  评论(0编辑  收藏  举报