【音视频】RTP封包H265信息

RTP

含义

RTP 是一种专门为 实时数据传输 设计的网络协议。这里的 "实时数据" 主要指的是 音频视频 这类对传输延迟非常敏感的数据。想象一下,你在进行视频通话或者观看在线直播,你希望画面和声音能够流畅地、几乎同步地到达,而不是卡顿或者延迟很久。RTP 就是为了实现这种目标而生的

RTP在网络传输结构中的位置

  • 应用层协议: RTP 运行在 OSI 模型中的 应用层 ,通常建立在 用户数据报协议 (UDP) 之上
  • 不是传输协议,而是传输框架: 需要注意的是,RTP 本身 不是一个完整的传输协议 ,它更像是一个 传输框架 或者 协议轮廓
    • 因为 RTP 只定义了数据包的格式、序列号、时间戳等基本信息,而具体的传输控制、可靠性保证、会话建立等功能,RTP 自身并不负责。这些通常需要借助 RTP 控制协议 (RTCP) 以及其他的信令协议 (如 RTSP, SIP--一般GB28181通过SIP) 来完成

数据结构

cpp 复制代码
     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             |
    |                             ....                                |
    +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • Version (V, 2 bits): RTP 协议版本号,当前版本通常为 2
  • Padding (P, 1 bit): 填充标志位。如果设置为 1,表示 RTP 数据包末尾包含一个或多个填充字节,用于满足某些协议对数据包长度的要求 (例如加密算法)。填充字节的最后一个字节指示了填充字节的长度
  • Extension (X, 1 bit): 扩展头标志位。如果设置为 1,表示在 RTP 头后面有一个扩展头 (RTP header extension)。扩展头可以用来添加额外的协议信息
  • CSRC count (CC, 4 bits): CSRC 计数器,指示 CSRC 标识符列表的长度。取值范围为 0-15,表示最多可以有 15 个 CSRC 标识符
  • Marker (M, 1 bit): 标记位。其含义由 Payload Type 决定
    • 例如,对于视频,标记位可能用来指示帧的边界;对于音频,可能用来指示会话的开始或结束
  • Payload type (PT, 7 bits): Payload 类型。指示 RTP 数据包中携带的媒体数据类型
  • Sequence number (16 bits): 序列号。每个 RTP 数据包的序列号递增 1。初始值随机
  • Timestamp (32 bits): 时间戳。记录了 RTP 数据包中第一个字节的采样时刻。采样时钟频率由 Payload Type 决定
    • 例如,音频的采样频率可能是 8kHz, 44.1kHz 等,视频的帧率可能是 25fps, 30fps 等
  • SSRC (Synchronization source identifier, 32 bits): 同步源标识符。
    • 随机选择的 32 位数值,唯一标识 RTP 流的源头。在同一个 RTP 会话中,SSRC 值必须唯一
  • CSRC list (Contributing source identifiers, 0-15 项, 每项 32 bits): 贡献源标识符列表。只有当 CC 字段不为 0 时才存在。每个 CSRC 标识符也是一个 32 位数值,用于标识参与混合或转换的贡献源

主要特性

目的即是满足实时性传输的要求

  • 实时性优先: RTP 的设计目标是 尽可能快地传输数据,即使这意味着可能会有少量的数据包丢失。它牺牲了一定的可靠性来换取低延迟,这对于实时音视频应用至关重要
  • Payload 类型标识 (Payload Type): RTP 数据包头中包含一个 Payload Type 字段 ,用来 指示当前 RTP 包中携带的是什么类型的媒体数据
    • 例如,Payload Type 可以标识是 H.264 视频、H.265 视频、AAC 音频、MP3 音频等等
  • 序列号 (Sequence Number): RTP 数据包头中包含 序列号
    • 数据包排序: 由于 UDP 是无序的,网络传输过程中数据包可能会乱序到达。序列号可以帮助接收端 按照正确的顺序重组数据包
    • 丢包检测: 序列号的连续性可以帮助接收端 检测数据包是否丢失。如果序列号不连续,就意味着有数据包在传输过程中丢失了
  • 时间戳 (Timestamp): RTP 数据包头中包含 时间戳
    • ​​​​​​​同步播放: 时间戳记录了 数据包中第一个字节的采样时刻 。接收端可以根据时间戳信息,将音频和视频流同步播放,避免音视频不同步的问题
    • 抖动补偿 (Jitter Compensation): 网络传输延迟可能会有抖动 (延迟变化)。时间戳可以帮助接收端 计算数据包的延迟,并进行抖动补偿,平滑播放,减少卡顿感
  • 同步源标识符 (Synchronization Source, SSRC): RTP 数据包头中包含 SSRC 字段 ,用于 唯一标识 RTP 流的源头
    • 即使在同一个 RTP 会话中,不同的发送端也会有不同的 SSRC 值。这在多方通信 (例如多人视频会议) 中非常重要,接收端可以通过 SSRC 区分来自不同发送端的数据流
  • 贡献源标识符 (Contributing Source, CSRC)
    • ​​​​​​​ RTP 数据包头中可以包含 CSRC 列表 。CSRC 用于 标识参与混合器 (mixer) 或转换器 (translator) 的贡献源。在一些复杂的网络拓扑中,例如音频会议桥接,多个音频流会被混合成一个流再发送给接收端

H265的RTP

RTP结构分析

  • 首行分析(第0和1个字节)
    • V (Version - 2 bits, 位 0-1): 版本号。 通常 RTP 版本号为 2,所以这里 V=2
    • P (Padding - 1 bit, 位 2): 填充标志。 如果 P=1,表示 RTP 包的末尾包含一个或多个填充字节。填充可能用于使 RTP 包符合某些块大小要求,或者用于加密等目的
    • X (Extension - 1 bit, 位 3): 扩展头标志。 如果 X=1,表示在固定 RTP 包头之后,还存在一个扩展头。扩展头可以用于携带额外的协议特定信息
    • CC (CSRC Count - 4 bits, 位 4-7): CSRC 计数。 指示紧随固定 RTP 包头之后 CSRC (Contributing Source) 标识符的数量。CSRC 标识符用于标识为混合器 (mixer) 贡献了媒体源的源。对于简单的点对点传输,通常 CC=0
    • M (Marker - 1 bit, 位 7): 标记位。 M 位的含义由 负载类型 (PT) 决定。在视频编码中,M 位经常用于标记重要事件
      • 帧的结束: 在 H.265 视频中,M=1 可能被用来指示当前 RTP 包是否包含一个视频帧的最后一个 NAL 单元 (Network Abstraction Layer Unit)访问单元 (Access Unit) 的最后一个分片。这对于解码器识别帧边界非常重要
    • PT (Payload Type - 7 bits, 位 0-6): 负载类型。 PT 字段指示 RTP 包中数据的 格式 。 对于 H.265 视频,会分配特定的 负载类型值,注意这些值是通过会话协议(SDP)等方式协商出来的(通常是SDP)
  • 第二行(第2和3字节)
    • sequence number (序列号 - 16 bits): 序列号。 序列号 随着每个 RTP 数据包递增。接收端可以使用序列号来
      • 检测丢包: 如果序列号不连续,则表明发生了丢包
      • 重排序数据包: 在网络传输中,数据包可能会乱序到达。序列号允许接收端按照正确的顺序重组数据包
  • 第三行(第4、5、6、7字节)
    • timestamp (时间戳 - 32 bits): 时间戳。 时间戳 反映了 RTP 数据包中第一个数据字节的 采样时刻 。 对于视频,时间戳 通常与视频帧的捕获或编码时间相关
      • 同步播放: 与音频或其他媒体流同步视频播放
      • 抖动补偿: 帮助接收端处理网络抖动,平滑播放
      • 帧率控制: 解码器可以根据时间戳来调整帧率
  • 第四行 (字节 8, 9, 10, 和 11)
    • synchronization source (SSRC) identifier (同步源标识符 - 32 bits): SSRC 标识符。 SSRC 标识符唯一地标识了 RTP 包的 同步源 。 在一个 RTP 会话中,每个同步源应该有唯一的 SSRC 值
      • 区分不同的发送源: 在多方会议或广播场景中,可以区分来自不同发送者的 RTP 流
  • 第五行 (可变长度)
    • contributing source (CSRC) identifiers (贡献源标识符 - 可变数量,每个 32 bits): CSRC 标识符。 只有当 CC > 0 时,才会有 CSRC 标识符 字段。
    • 这些字段列出了为混合器贡献了媒体源的源的 SSRC 标识符。 在点对点传输中,通常没有 CSRC 标识符
  • 补充(RTP负载)
    • 包头后面就是数据载荷部分,对于 H.265 视频,这个 payload 部分会包含 H.265 编码的视频数据 ,通常是以 NAL 单元 (NALU) 的形式组织的。 RFC 7798 详细描述了 H.265 的 RTP 负载格式
    • 单元类型: H.265 视频数据被组织成不同类型的 NAL 单元,例如视频参数集 (VPS)、序列参数集 (SPS)、图像参数集 (PPS)、视频编码层 (VCL) NAL 单元 (包含实际的视频编码数据) 等
    • 封装方式(此处根据RFC 7798中描述实现)
      • 单一NALU模式:一个RTP包中包含一个完整的NALU
      • 聚合模式:一个RTP包中包含多个较小的NALU
      • 分片模式:一个大的NALU被分割成多个RTP包进行传输

RTP负载封包方式

单一NALU

  • PayloadHdr (Payload Header)
    • 占据1个字节(注意该字段可能会被更改),官方文档中举例说明将CRA图片处理BLA图片的时候就会对其修改
    • 这里复制的就是NALU的头部信息(例如SPS PPS VPS等)'
  • DONL (Decoding Order Number Low) (conditional 16-bit field)
    • 可选的2字节(存在一定条件才存在)
    • 参考
      • 存在条件: DONL 字段的出现与参数 sprop-max-don-diff 有关
      • 如果 sprop-max-don-diff 大于 0 (对于任何 RTP 流),则 DONL 字段必须存在 (MUST be present)。 此时,NAL 单元的变量 DON 的值就等于 DONL 字段的值
      • 如果 sprop-max-don-diff 等于 0 (对于所有 RTP 流),则 DONL 字段必须不存在 (MUST NOT be present)
  • NAL unit payload data (NAL 单元负载数据)
    • 这部分包含了 NAL 单元的实际负载数据
    • 注意这里是不包含Nalu的类型,因为类型被放到了 PayloadHdr字段
  • OPTIONAL RTP padding (可选的 RTP 填充)
    • 如图所示,在 NAL unit payload data 之后,可能有 可选的 RTP 填充字节
    • 这部分是标准的 RTP 填充,用于满足 RTP 协议或应用层的一些对包长度的要求,例如为了对齐到特定的块大小,或者用于加密等目的。 RTP 填充的使用由 RTP 包头中的 P (Padding) 标志位 控制
代码实现
bash 复制代码
--- Simulated NALU ---
Simulated NALU: 27 64 00 0a ac b4 00 00 00 40 00 00 03 00 80 00 00 03 00 00 80 8a 00 00 00 28 00 00 07 84 00 00 1e 84 21 10 

--- Packetization Parameters ---
Payload Type (PT): 96
Initial Sequence Number: 12345
Initial Timestamp: 0
SSRC: 0x12345678

--- RTP Header ---
RTP Header: 80 60 30 39 00 00 00 00 12 34 56 78 

--- RTP Payload ---
PayloadHdr (NALU Header): 0x27
NALU Payload Data: 64 00 0a ac b4 00 00 00 40 00 00 03 00 80 00 00 03 00 00 80 8a 00 00 00 28 00 00 07 84 00 00 1e 84 21 10 

--- Complete RTP Packet ---
Complete RTP Packet: 80 60 30 39 00 00 00 00 12 34 56 78 27 64 00 0a ac b4 00 00 00 40 00 00 03 00 80 00 00 03 00 00 80 8a 00 00 00 28 00 00 07 84 00 00 1e 84 21 10 

--- After Packetization ---
Next Sequence Number: 12346
Next Timestamp: 3000
  • 日志解释补充
    • RTP Header: 显示了生成的 RTP 包头的字节内容
      • 80 60: 第一个字节 80 (二进制 10000000) 表示 V=2 (bit 7-8), P=0 (bit 6), X=0 (bit 5), CC=0 (bit 1-4)。 第二个字节 60 (十六进制) = 96 (十进制),是负载类型 PT
      • 30 39: 序列号 12345 的十六进制表示 (网络字节序)
      • 00 00 00 00: 初始时间戳 0 的十六进制表示
      • 12 34 56 78: SSRC 0x12345678 的十六进制表示
    • RTP的负载就是Nalu
cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdint>
#include <iomanip> // 用于 std::setw, std::setfill, std::hex

// 辅助函数:打印字节数组为十六进制字符串
void printHex(const std::vector<uint8_t>& data, const std::string& label = "") {
    std::cout << label;
    for (uint8_t byte : data) {
        std::cout << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(byte) << " ";
    }
    std::cout << std::dec << std::endl; // 恢复十进制输出
}

// 创建 RTP 包头
std::vector<uint8_t> createRTPHeader(uint8_t payloadType, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpHeader(12); // 基本 RTP 头是 12 字节

    // 字节 0-1: 版本 (V=2), 填充 (P=0), 扩展 (X=0), CSRC 计数 (CC=0), 标记 (M=0), 负载类型 (PT)
    rtpHeader[0] = (2 << 6) | (0 << 5) | (0 << 4) | (0 << 0); // V=2, P=0, X=0, CC=0
    rtpHeader[1] = payloadType; // PT

    // 字节 2-3: 序列号
    rtpHeader[2] = (sequenceNumber >> 8) & 0xFF; // 高字节
    rtpHeader[3] = sequenceNumber & 0xFF;        // 低字节

    // 字节 4-7: 时间戳
    rtpHeader[4] = (timestamp >> 24) & 0xFF;
    rtpHeader[5] = (timestamp >> 16) & 0xFF;
    rtpHeader[6] = (timestamp >> 8) & 0xFF;
    rtpHeader[7] = timestamp & 0xFF;

    // 字节 8-11: SSRC 标识符
    rtpHeader[8] = (ssrc >> 24) & 0xFF;
    rtpHeader[9] = (ssrc >> 16) & 0xFF;
    rtpHeader[10] = (ssrc >> 8) & 0xFF;
    rtpHeader[11] = ssrc & 0xFF;

    return rtpHeader;
}

// 封装单一 NAL 单元 RTP 包
std::vector<uint8_t> packetizeSingleNalu(const std::vector<uint8_t>& nalu, uint8_t payloadType, uint16_t& sequenceNumber, uint32_t& timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpPacket;

    // 1. 创建 RTP 包头
    std::vector<uint8_t> rtpHeader = createRTPHeader(payloadType, sequenceNumber, timestamp, ssrc);
    rtpPacket.insert(rtpPacket.end(), rtpHeader.begin(), rtpHeader.end());

    std::cout << "\n--- RTP Header ---" << std::endl;
    printHex(rtpHeader, "RTP Header: ");

    // 2. PayloadHdr (NAL 单元头部)
    uint8_t payloadHdr = nalu[0]; // 假设 NALU 的第一个字节是 NALU 头部
    rtpPacket.push_back(payloadHdr);

    std::cout << "\n--- RTP Payload ---" << std::endl;
    std::cout << "PayloadHdr (NALU Header): 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(payloadHdr) << std::dec << std::endl;

    // 3. NAL unit payload data (NALU 数据,去除 NALU 头部)
    std::vector<uint8_t> naluPayloadData;
    if (nalu.size() > 1) {
        naluPayloadData.insert(naluPayloadData.end(), nalu.begin() + 1, nalu.end()); // 从第二个字节开始复制
        rtpPacket.insert(rtpPacket.end(), naluPayloadData.begin(), naluPayloadData.end());
    } else {
        std::cout << "Warning: NALU only contains header, no payload data." << std::endl;
    }
    printHex(naluPayloadData, "NALU Payload Data: ");


    // 4. (可选) RTP 填充 - 在此示例中我们不添加填充,假设 P=0

    // 更新序列号和时间戳 (为下一个 RTP 包做准备)
    sequenceNumber++;
    timestamp += 3000; // 假设时间戳单位是 1/90000 秒,这里假设帧率近似 30fps

    std::cout << "\n--- Complete RTP Packet ---" << std::endl;
    printHex(rtpPacket, "Complete RTP Packet: ");

    return rtpPacket;
}

int main() {
    // 模拟一个 NALU (示例: SPS - 序列参数集)
    std::vector<uint8_t> simulatedNalu = {
        0x27, // NALU 头部 (类型 SPS - 假设)
        0x64, 0x00, 0x0a, 0xac, 0xb4, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x03, 0x00, 0x80, 0x00, 0x00, 0x03, 0x00, 0x00, 0x80, 0x8a,
        0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x07, 0x84, 0x00, 0x00, 0x1e, 0x84, 0x21, 0x10
    };
    std::cout << "--- Simulated NALU ---" << std::endl;
    printHex(simulatedNalu, "Simulated NALU: ");


    uint8_t payloadType = 96; // 示例负载类型 (动态负载类型范围)
    uint16_t sequenceNumber = 12345;
    uint32_t timestamp = 0;
    uint32_t ssrc = 0x12345678; // 示例 SSRC

    std::cout << "\n--- Packetization Parameters ---" << std::endl;
    std::cout << "Payload Type (PT): " << static_cast<int>(payloadType) << std::endl;
    std::cout << "Initial Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Initial Timestamp: " << timestamp << std::endl;
    std::cout << "SSRC: 0x" << std::hex << ssrc << std::dec << std::endl;


    // 进行 RTP 封包
    std::vector<uint8_t> rtpPacket = packetizeSingleNalu(simulatedNalu, payloadType, sequenceNumber, timestamp, ssrc);

    std::cout << "\n--- After Packetization ---" << std::endl;
    std::cout << "Next Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Next Timestamp: " << timestamp << std::endl;


    return 0;
}

聚合包

存在意义

聚合包存在的主要目的就是减少小型NALU的封装开销,主要就是针对像非 VCL NAL 单元(例如 SPS, PPS, VPS 等)这样通常体积很小的 NAL 单元,如果每个都单独封装成 RTP 包,会造成比较大的协议开销(每个 RTP 包头 12 字节)(注意,具体代码实现如果不考虑开销,也可以将其直接封装成一个NALU

具体实现,将许多给小的Nalu聚合到一个RTP包的负载中。这样,多个 NAL 单元只需要一个 RTP 包头,从而提高了传输效率,降低了开销。 特别是在网络带宽受限的情况下,这种方式可以更有效地利用带宽

聚合包的约束

  • 必须包含至少两个聚合单元: AP 的目的是聚合多个 NAL 单元,所以至少要有两个
  • 总大小限制: AP 的总数据量必须在一个 IP 包的限制内,并且应该小于 MTU 大小,避免 IP 分片
  • 不能包含 FU (Fragmentation Unit): AP 不能与分片单元 (FU) 混合使用。 如果 NAL 单元太大需要分片,应该使用 FU 模式,而不是 AP 模式(注意,一般使用了聚合包就不会和分片有关系了)
  • AP 不能嵌套: 一个 AP 中不能再包含另一个 AP
基本结构
cpp 复制代码
    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |    PayloadHdr (Type=48)       |                               |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
   |                                                               |
   |             two or more aggregation units                     |
   |                                                               |
   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                               :...OPTIONAL RTP padding        |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • PayloadHdr (负载头): 1 个字节,位于 AP 的最开始
  • Two or more aggregation units (两个或多个聚合单元): 紧随 PayloadHdr 之后,包含两个或更多个聚合单元。每个聚合单元封装一个 NAL 单元
  • OPTIONAL RTP padding (可选的 RTP 填充): 在所有聚合单元之后,可以有可选的 RTP 填充字节

PayloadHdr (负载头)

此处的负载头,和Nalu的负载头表示的意义不同

  • Type 字段: MUST be equal to 48。 类型值 48 用于标识这是一个聚合包 (AP)。接收端通过 PayloadHdr 的类型值来区分不同类型的 RTP payload
  • F bit (Forbidden zero bit): F 位的值需要根据聚合的所有 NAL 单元的 F 位来确定
    • 如果聚合的所有 NAL 单元的 F 位都为 0,则 AP 的 F 位 MUST be equal to 0
    • 否则 (只要有一个 NAL 单元的 F 位为 1),AP 的 F 位 MUST be equal to 1
  • LayerId 和 TID (TemporalId): 这两个字段的值需要从聚合的所有 NAL 单元中取最小值
    • LayerId: AP 的 LayerId 必须等于所有聚合的 NAL 单元中 LayerId 的最小值
    • TID (TemporalId): AP 的 TID 必须等于所有聚合的 NAL 单元中 TID 的最小值

aggregation units(聚合单元)

聚合单元是 AP 的核心组成部分,每个聚合单元封装一个 NAL 单元。 AP 中必须包含 至少两个 聚合单元,并且可以根据需要包含更多,但总大小要限制在 IP 包的 MTU 大小内,以避免 IP 层分片

cpp 复制代码
    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                   :       DONL (conditional)      |   NALU size   |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |   NALU size   |                                               |
   +-+-+-+-+-+-+-+-+         NAL unit                              |
   |                                                               |
   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                               :
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

第一种结构
  • DONL (Decoding Order Number Low): 用于第一个聚合单元,是一个 16 位字段。 它直接给出了第一个聚合单元中 NAL 单元的解码顺序号 (DON) 的最低 16 位
    • 如果 sprop-max-don-diff > 0,表示码流可能需要重排序,因此需要使用 DON 信息,DONL 和 DOND 必须存在
    • 如果 sprop-max-don-diff = 0,表示码流不需要重排序,传输顺序即解码顺序,因此不需要 DON 信息,DONL 和 DOND 不应该存在
  • NALU size:16位无符号整数,用网络字节序表示,注意这个大小是不包括NALU size自身所占用的2个字节
  • NALU unit:完整的Nalu,其中包括头部信息
cpp 复制代码
    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
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                   : DOND (cond)   |          NALU size            |
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                                                               |
   |                       NAL unit                                |
   |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
   |                               :
   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • DOND (Decoding Order Number Delta): 用于非第一个聚合单元,是一个 8 位字段。 它表示当前 NAL 单元的 DON 与 前一个聚合单元 的 NAL 单元的 DON 之间的差值。 计算公式是:Current NALU DON = (Previous NALU DON + DOND + 1) modulo 65536
    • 判断条件同上DONL
代码实现

注意序列号的递增存在问题未修改

bash 复制代码
root@hcss-ecs-b4a9:/home/test/rtp/nalu# ./test2

--- Common Packetization Parameters ---
Payload Type (PT): 96
Initial Sequence Number: 20000
Initial Timestamp: 0
SSRC: 0x87654321


==================== Scenario 1: Aggregation Packet - No DONL/DOND ====================

--- Input NALUs (Scenario 1) ---
NALU Header: 0x27, Type: 39, Payload: 01 02 03 
NALU Header: 0x28, Type: 40, Payload: 04 05 

--- RTP Header ---
RTP Header: 80 60 4e 20 00 00 00 00 87 65 43 21 

--- RTP Payload Header (PayloadHdr) ---
PayloadHdr (Type=48): 0x60
  F bit (AP): 0
  LayerId (AP): 0
  TID (AP): 0

--- Aggregation Units ---

--- Aggregation Unit 1 ---
NALU Size: 4 bytes
NALU Header: 0x27
NALU Payload Data: 01 02 03 

--- Aggregation Unit 2 ---
NALU Size: 3 bytes
NALU Header: 0x28
NALU Payload Data: 04 05 

--- Complete RTP Packet (No DONL/DOND) ---
Complete RTP Packet: 80 60 4e 20 00 00 00 00 87 65 43 21 60 00 04 27 01 02 03 00 03 28 04 05 

--- After Packetization (Scenario 1) ---
Next Sequence Number: 20001
Next Timestamp: 3000


==================== Scenario 2: Aggregation Packet - With DONL/DOND ====================

--- Input NALUs (Scenario 2) ---
NALU Header: 0x01, Type: 19, DON: 10, Payload: 11 12 13 14 
NALU Header: 0x01, Type: 19, DON: 12, Payload: 15 16 

--- RTP Header ---
RTP Header: 80 60 4e 22 00 00 0b b8 87 65 43 21 

--- RTP Payload Header (PayloadHdr) ---
PayloadHdr (Type=48): 0x60
  F bit (AP): 0
  LayerId (AP): 0
  TID (AP): 0

--- Aggregation Units (with DONL/DOND) ---

--- Aggregation Unit 1 ---
DONL: 10
NALU Size: 5 bytes
NALU Header: 0x01
NALU Payload Data: 11 12 13 14 

--- Aggregation Unit 2 ---
DOND: 1 (DON Delta from previous NALU)
NALU Size: 3 bytes
NALU Header: 0x01
NALU Payload Data: 15 16 

--- Complete RTP Packet (With DONL/DOND) ---
Complete RTP Packet: 80 60 4e 22 00 00 0b b8 87 65 43 21 60 00 0a 00 05 01 11 12 13 14 01 00 03 01 15 16 

--- After Packetization (Scenario 2) ---
Next Sequence Number: 20003
Next Timestamp: 6000


==================== Scenario 3: Aggregation Packet - Multiple NALU Types ====================

--- Input NALUs (Scenario 3) ---
NALU Header: 0x27, Type: 39, F: 0, LayerId: 1, TID: 2, Payload: 21 
NALU Header: 0x28, Type: 40, F: 1, LayerId: 0, TID: 1, Payload: 22 23 
NALU Header: 0x01, Type: 19, F: 0, LayerId: 2, TID: 0, Payload: 24 25 26 

--- RTP Header ---
RTP Header: 80 60 4e 24 00 00 17 70 87 65 43 21 

--- RTP Payload Header (PayloadHdr) ---
PayloadHdr (Type=48): 0xe0
  F bit (AP) (Calculated based on NALUs): 1
  LayerId (AP) (Minimum of NALUs): 0
  TID (AP) (Minimum of NALUs): 0

--- Aggregation Units (Multiple NALU Types) ---

--- Aggregation Unit 1 ---
NALU Size: 2 bytes
NALU Header: 0x27
  NAL Unit Type: 39
  F bit: 0
  LayerId: 1
  TID: 2
NALU Payload Data: 21 

--- Aggregation Unit 2 ---
NALU Size: 3 bytes
NALU Header: 0x28
  NAL Unit Type: 40
  F bit: 1
  LayerId: 0
  TID: 1
NALU Payload Data: 22 23 

--- Aggregation Unit 3 ---
NALU Size: 4 bytes
NALU Header: 0x01
  NAL Unit Type: 19
  F bit: 0
  LayerId: 2
  TID: 0
NALU Payload Data: 24 25 26 

--- Complete RTP Packet (Multiple NALU Types) ---
Complete RTP Packet: 80 60 4e 24 00 00 17 70 87 65 43 21 e0 00 02 27 21 00 03 28 22 23 00 04 01 24 25 26 

--- After Packetization (Scenario 3) ---
Next Sequence Number: 20005
Next Timestamp: 9000
cpp 复制代码
#include <iostream>
#include <vector>
#include <cstdint>
#include <iomanip>

// 辅助函数:打印字节数组为十六进制字符串 (已提供)
void printHex(const std::vector<uint8_t>& data, const std::string& label = "") {
    std::cout << label;
    for (uint8_t byte : data) {
        std::cout << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(byte) << " ";
    }
    std::cout << std::dec << std::endl;
}

// 创建 RTP 包头 (已提供)
std::vector<uint8_t> createRTPHeader(uint8_t payloadType, uint16_t sequenceNumber, uint32_t timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpHeader(12);
    rtpHeader[0] = (2 << 6) | (0 << 5) | (0 << 4) | (0 << 0);
    rtpHeader[1] = payloadType;
    rtpHeader[2] = (sequenceNumber >> 8) & 0xFF;
    rtpHeader[3] = sequenceNumber & 0xFF;
    rtpHeader[4] = (timestamp >> 24) & 0xFF;
    rtpHeader[5] = (timestamp >> 16) & 0xFF;
    rtpHeader[6] = (timestamp >> 8) & 0xFF;
    rtpHeader[7] = timestamp & 0xFF;
    rtpHeader[8] = (ssrc >> 24) & 0xFF;
    rtpHeader[9] = (ssrc >> 16) & 0xFF;
    rtpHeader[10] = (ssrc >> 8) & 0xFF;
    rtpHeader[11] = ssrc & 0xFF;
    return rtpHeader;
}

// 辅助结构:NALU
struct NALU {
    uint8_t header_byte;
    std::vector<uint8_t> payload_data;
    uint16_t don; // Decoding Order Number (DON) for simulation
    int nal_unit_type;
    int f_bit;
    int layer_id;
    int tid;

    NALU(uint8_t header, std::vector<uint8_t> payload, uint16_t d, int type, int f, int lid, int t)
        : header_byte(header), payload_data(payload), don(d), nal_unit_type(type), f_bit(f), layer_id(lid), tid(t) {}
};

// 封装聚合包 (AP) - 无 DONL/DOND
std::vector<uint8_t> packetizeAggregationPacket_NoDON(const std::vector<NALU>& nalus, uint8_t payloadType, uint16_t& sequenceNumber, uint32_t& timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpPacket;

    // 1. 创建 RTP 包头
    std::vector<uint8_t> rtpHeader = createRTPHeader(payloadType, sequenceNumber, timestamp, ssrc);
    rtpPacket.insert(rtpPacket.end(), rtpHeader.begin(), rtpHeader.end());

    std::cout << "\n--- RTP Header ---" << std::endl;
    printHex(rtpHeader, "RTP Header: ");

    // 2. 创建 PayloadHdr (Type=48)
    uint8_t payloadHdr = (48 << 1); // Type = 48 (Aggregation Packet), 初始化 F=0, LayerId=0, TID=0
    int fBitAP = 0;
    int minLayerId = 3; // 假设最大 LayerId 是 2, 初始化为大于最大值
    int minTid = 7;      // 假设最大 TID 是 6, 初始化为大于最大值

    for (const auto& nalu : nalus) {
        if (nalu.f_bit == 1) fBitAP = 1;
        minLayerId = std::min(minLayerId, nalu.layer_id);
        minTid = std::min(minTid, nalu.tid);
    }

    payloadHdr |= (fBitAP << 7); // 设置 F bit
    payloadHdr |= (minLayerId << 4) & 0x70; // 设置 LayerId (3 bits, bits 4-6)
    payloadHdr |= (minTid) & 0x07;         // 设置 TID (3 bits, bits 0-2)

    rtpPacket.push_back(payloadHdr);

    std::cout << "\n--- RTP Payload Header (PayloadHdr) ---" << std::endl;
    std::cout << "PayloadHdr (Type=48): 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(payloadHdr) << std::dec << std::endl;
    std::cout << "  F bit (AP): " << fBitAP << std::endl;
    std::cout << "  LayerId (AP): " << minLayerId << std::endl;
    std::cout << "  TID (AP): " << minTid << std::endl;


    std::cout << "\n--- Aggregation Units ---" << std::endl;
    for (size_t i = 0; i < nalus.size(); ++i) {
        const auto& nalu = nalus[i];
        std::cout << "\n--- Aggregation Unit " << i + 1 << " ---" << std::endl;

        // NALU Size (16 bits)
        uint16_t naluSize = nalu.payload_data.size() + 1; // +1 for NALU header byte
        rtpPacket.push_back((naluSize >> 8) & 0xFF); // 高字节
        rtpPacket.push_back(naluSize & 0xFF);        // 低字节
        std::cout << "NALU Size: " << naluSize << " bytes" << std::endl;

        // NALU Header
        rtpPacket.push_back(nalu.header_byte);
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec << std::endl;

        // NALU Payload Data
        rtpPacket.insert(rtpPacket.end(), nalu.payload_data.begin(), nalu.payload_data.end());
        printHex(nalu.payload_data, "NALU Payload Data: ");
    }

    // 4. (可选) RTP 填充 - 在此示例中我们不添加填充

    // 更新序列号和时间戳
    sequenceNumber++;
    timestamp += 3000;

    std::cout << "\n--- Complete RTP Packet (No DONL/DOND) ---" << std::endl;
    printHex(rtpPacket, "Complete RTP Packet: ");

    return rtpPacket;
}


// 封装聚合包 (AP) - 带 DONL/DOND
std::vector<uint8_t> packetizeAggregationPacket_WithDON(const std::vector<NALU>& nalus, uint8_t payloadType, uint16_t& sequenceNumber, uint32_t& timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpPacket;

    // 1. 创建 RTP 包头
    std::vector<uint8_t> rtpHeader = createRTPHeader(payloadType, sequenceNumber, timestamp, ssrc);
    rtpPacket.insert(rtpPacket.end(), rtpHeader.begin(), rtpHeader.end());

    std::cout << "\n--- RTP Header ---" << std::endl;
    printHex(rtpHeader, "RTP Header: ");

    // 2. 创建 PayloadHdr (Type=48)
    uint8_t payloadHdr = (48 << 1); // Type = 48 (Aggregation Packet), 初始化 F=0, LayerId=0, TID=0
    int fBitAP = 0;
    int minLayerId = 3;
    int minTid = 7;

    for (const auto& nalu : nalus) {
        if (nalu.f_bit == 1) fBitAP = 1;
        minLayerId = std::min(minLayerId, nalu.layer_id);
        minTid = std::min(minTid, nalu.tid);
    }

    payloadHdr |= (fBitAP << 7);
    payloadHdr |= (minLayerId << 4) & 0x70;
    payloadHdr |= (minTid) & 0x07;

    rtpPacket.push_back(payloadHdr);

    std::cout << "\n--- RTP Payload Header (PayloadHdr) ---" << std::endl;
    std::cout << "PayloadHdr (Type=48): 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(payloadHdr) << std::dec << std::endl;
    std::cout << "  F bit (AP): " << fBitAP << std::endl;
    std::cout << "  LayerId (AP): " << minLayerId << std::endl;
    std::cout << "  TID (AP): " << minTid << std::endl;


    std::cout << "\n--- Aggregation Units (with DONL/DOND) ---" << std::endl;
    uint16_t previousDon = 0; // 用于计算 DOND
    for (size_t i = 0; i < nalus.size(); ++i) {
        const auto& nalu = nalus[i];
        std::cout << "\n--- Aggregation Unit " << i + 1 << " ---" << std::endl;

        // DONL (conditional, only for the first aggregation unit)
        if (i == 0) {
            uint16_t donl = nalu.don;
            rtpPacket.push_back((donl >> 8) & 0xFF); // 高字节
            rtpPacket.push_back(donl & 0xFF);        // 低字节
            std::cout << "DONL: " << donl << std::endl;
        } else { // DOND for subsequent units
            uint8_t dond = (nalu.don - previousDon - 1) & 0xFF; // 计算 DOND
            rtpPacket.push_back(dond);
            std::cout << "DOND: " << static_cast<int>(dond) << " (DON Delta from previous NALU)" << std::endl;
        }
        previousDon = nalu.don; // 更新 previousDon for next iteration

        // NALU Size (16 bits)
        uint16_t naluSize = nalu.payload_data.size() + 1;
        rtpPacket.push_back((naluSize >> 8) & 0xFF);
        rtpPacket.push_back(naluSize & 0xFF);
        std::cout << "NALU Size: " << naluSize << " bytes" << std::endl;


        // NALU Header
        rtpPacket.push_back(nalu.header_byte);
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec << std::endl;

        // NALU Payload Data
        rtpPacket.insert(rtpPacket.end(), nalu.payload_data.begin(), nalu.payload_data.end());
        printHex(nalu.payload_data, "NALU Payload Data: ");
    }

    // 4. (可选) RTP 填充 - 在此示例中我们不添加填充

    // 更新序列号和时间戳
    sequenceNumber++;
    timestamp += 3000;

    std::cout << "\n--- Complete RTP Packet (With DONL/DOND) ---" << std::endl;
    printHex(rtpPacket, "Complete RTP Packet: ");

    return rtpPacket;
}


// 封装聚合包 (AP) - 多种 NALU 类型
std::vector<uint8_t> packetizeAggregationPacket_MultiNALUTypes(const std::vector<NALU>& nalus, uint8_t payloadType, uint16_t& sequenceNumber, uint32_t& timestamp, uint32_t ssrc) {
    std::vector<uint8_t> rtpPacket;

    // 1. 创建 RTP 包头
    std::vector<uint8_t> rtpHeader = createRTPHeader(payloadType, sequenceNumber, timestamp, ssrc);
    rtpPacket.insert(rtpPacket.end(), rtpHeader.begin(), rtpHeader.end());

    std::cout << "\n--- RTP Header ---" << std::endl;
    printHex(rtpHeader, "RTP Header: ");

    // 2. 创建 PayloadHdr (Type=48)
    uint8_t payloadHdr = (48 << 1); // Type = 48 (Aggregation Packet), 初始化 F=0, LayerId=0, TID=0
    int fBitAP = 0;
    int minLayerId = 3;
    int minTid = 7;

    for (const auto& nalu : nalus) {
        if (nalu.f_bit == 1) fBitAP = 1;
        minLayerId = std::min(minLayerId, nalu.layer_id);
        minTid = std::min(minTid, nalu.tid);
    }

    payloadHdr |= (fBitAP << 7);
    payloadHdr |= (minLayerId << 4) & 0x70;
    payloadHdr |= (minTid) & 0x07;

    rtpPacket.push_back(payloadHdr);

    std::cout << "\n--- RTP Payload Header (PayloadHdr) ---" << std::endl;
    std::cout << "PayloadHdr (Type=48): 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(payloadHdr) << std::dec << std::endl;
    std::cout << "  F bit (AP) (Calculated based on NALUs): " << fBitAP << std::endl;
    std::cout << "  LayerId (AP) (Minimum of NALUs): " << minLayerId << std::endl;
    std::cout << "  TID (AP) (Minimum of NALUs): " << minTid << std::endl;


    std::cout << "\n--- Aggregation Units (Multiple NALU Types) ---" << std::endl;
    for (size_t i = 0; i < nalus.size(); ++i) {
        const auto& nalu = nalus[i];
        std::cout << "\n--- Aggregation Unit " << i + 1 << " ---" << std::endl;

        // NALU Size (16 bits)
        uint16_t naluSize = nalu.payload_data.size() + 1;
        rtpPacket.push_back((naluSize >> 8) & 0xFF);
        rtpPacket.push_back(naluSize & 0xFF);
        std::cout << "NALU Size: " << naluSize << " bytes" << std::endl;

        // NALU Header
        rtpPacket.push_back(nalu.header_byte);
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec << std::endl;
        std::cout << "  NAL Unit Type: " << nalu.nal_unit_type << std::endl;
        std::cout << "  F bit: " << nalu.f_bit << std::endl;
        std::cout << "  LayerId: " << nalu.layer_id << std::endl;
        std::cout << "  TID: " << nalu.tid << std::endl;


        // NALU Payload Data
        rtpPacket.insert(rtpPacket.end(), nalu.payload_data.begin(), nalu.payload_data.end());
        printHex(nalu.payload_data, "NALU Payload Data: ");
    }

    // 4. (可选) RTP 填充 - 在此示例中我们不添加填充

    // 更新序列号和时间戳
    sequenceNumber++;
    timestamp += 3000;

    std::cout << "\n--- Complete RTP Packet (Multiple NALU Types) ---" << std::endl;
    printHex(rtpPacket, "Complete RTP Packet: ");

    return rtpPacket;
}


int main() {
    uint8_t payloadType = 96;
    uint16_t sequenceNumber = 20000;
    uint32_t timestamp = 0;
    uint32_t ssrc = 0x87654321;

    std::cout << "\n--- Common Packetization Parameters ---" << std::endl;
    std::cout << "Payload Type (PT): " << static_cast<int>(payloadType) << std::endl;
    std::cout << "Initial Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Initial Timestamp: " << timestamp << std::endl;
    std::cout << "SSRC: 0x" << std::hex << ssrc << std::dec << std::endl;


    // --- 场景 1: 聚合包 - 无 DONL/DOND ---
    std::cout << "\n\n==================== Scenario 1: Aggregation Packet - No DONL/DOND ====================" << std::endl;
    std::vector<NALU> nalus_no_don = {
        NALU(0x27, {0x01, 0x02, 0x03}, 0, 39, 0, 0, 0), // SPS NALU
        NALU(0x28, {0x04, 0x05}, 1, 40, 0, 0, 0)      // PPS NALU
    };
    std::cout << "\n--- Input NALUs (Scenario 1) ---" << std::endl;
    for (const auto& nalu : nalus_no_don) {
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec << ", Type: " << nalu.nal_unit_type << ", Payload: ";
        printHex(nalu.payload_data);
    }
    packetizeAggregationPacket_NoDON(nalus_no_don, payloadType, sequenceNumber, timestamp, ssrc);
    std::cout << "\n--- After Packetization (Scenario 1) ---" << std::endl;
    std::cout << "Next Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Next Timestamp: " << timestamp << std::endl;


    // --- 场景 2: 聚合包 - 带 DONL/DOND ---
    sequenceNumber++; // 序列号递增,用于下一个 RTP 包
    std::cout << "\n\n==================== Scenario 2: Aggregation Packet - With DONL/DOND ====================" << std::endl;
    std::vector<NALU> nalus_with_don = {
        NALU(0x01, {0x11, 0x12, 0x13, 0x14}, 10, 19, 0, 0, 0), // Slice NALU 1, DON=10
        NALU(0x01, {0x15, 0x16}, 12, 19, 0, 0, 0)          // Slice NALU 2, DON=12
    };
    std::cout << "\n--- Input NALUs (Scenario 2) ---" << std::endl;
    for (const auto& nalu : nalus_with_don) {
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec << ", Type: " << nalu.nal_unit_type << ", DON: " << nalu.don << ", Payload: ";
        printHex(nalu.payload_data);
    }
    packetizeAggregationPacket_WithDON(nalus_with_don, payloadType, sequenceNumber, timestamp, ssrc);
    std::cout << "\n--- After Packetization (Scenario 2) ---" << std::endl;
    std::cout << "Next Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Next Timestamp: " << timestamp << std::endl;


    // --- 场景 3: 聚合包 - 多种 NALU 类型 ---
    sequenceNumber++; // 序列号递增,用于下一个 RTP 包
    std::cout << "\n\n==================== Scenario 3: Aggregation Packet - Multiple NALU Types ====================" << std::endl;
    std::vector<NALU> nalus_multi_type = {
        NALU(0x27, {0x21}, 0, 39, 0, 1, 2),    // SPS, LayerId=1, TID=2
        NALU(0x28, {0x22, 0x23}, 0, 40, 1, 0, 1), // PPS, F=1, LayerId=0, TID=1
        NALU(0x01, {0x24, 0x25, 0x26}, 0, 19, 0, 2, 0) // Slice, LayerId=2, TID=0
    };
     std::cout << "\n--- Input NALUs (Scenario 3) ---" << std::endl;
    for (const auto& nalu : nalus_multi_type) {
        std::cout << "NALU Header: 0x" << std::setfill('0') << std::setw(2) << std::hex << static_cast<int>(nalu.header_byte) << std::dec
                  << ", Type: " << nalu.nal_unit_type << ", F: " << nalu.f_bit << ", LayerId: " << nalu.layer_id << ", TID: " << nalu.tid << ", Payload: ";
        printHex(nalu.payload_data);
    }
    packetizeAggregationPacket_MultiNALUTypes(nalus_multi_type, payloadType, sequenceNumber, timestamp, ssrc);
    std::cout << "\n--- After Packetization (Scenario 3) ---" << std::endl;
    std::cout << "Next Sequence Number: " << sequenceNumber << std::endl;
    std::cout << "Next Timestamp: " << timestamp << std::endl;


    return 0;
}

分片模式

通俗理解

FU 就像把一个大的视频数据 "包裹" (NAL Unit) 拆成几个小的 "箱子" (FU 包) 来运输

  • 每个 "箱子" 上都有编号 (RTP 序列号),保证按顺序到达
  • 第一个 "箱子" 上会贴上 "起点" 标签 (S=1),最后一个 "箱子" 上会贴上 "终点" 标签 (E=1)
  • 所有 "箱子" 都会标明里面装的是什么东西的一部分 (FuType),比如 "电视机屏幕组件"
  • 接收端收到这些 "箱子" 后,按顺序把里面的东西拿出来拼接在一起,就能还原成完整的 "包裹" (NAL Unit)
  • 如果中间丢了一个 "箱子",可能会导致 "电视机" 缺零件,接收端可以选择丢弃后续的 "箱子",或者尝试用不完整的 "电视机" 进行解码

分片存在原因分析

核心就是碎片化,这个就像我们要寄一个非常大的包裹,比如一个巨大的电视机。但是邮局规定,包裹不能超过一定的大小。这时候你就需要把这个大电视机拆成几个小一点的箱子分别邮寄

那么在流媒体传输中当遇到较大的Nalu就会出现问题

  • 网络限制: 网络通常有最大传输单元 (MTU) 的限制,太大的数据包可能会被网络设备拒绝或者需要进行 IP 层面的分片,这会降低效率
  • 丢包风险: 大的数据包一旦在传输过程中丢失任何一部分,整个数据包都需要重传,效率不高
  • 实时性要求: 视频传输通常需要实时性,大的数据包可能会导致延迟增加

综上所述,为了更加有效的传输Nalu,那么就需要对一个大的Nalu进行分片

重点理解

  • 目的: 将一个大的 NAL Unit 分割成多个小的 RTP 包,以便在网络上传输。这个过程可能发生在编码器不知情的情况下,意味着可以在发送端或中间网络设备上进行
  • 片段构成: FU 是 NAL Unit 的连续字节片段。这意味着分割是按字节进行的,而且片段之间是连续的,不会跳跃
  • 顺序传输: 同一个 NAL Unit 的所有 FU 片段 必须 按照 RTP 序列号递增的顺序连续发送。在同一个 RTP 流中,同一个 NAL Unit 的片段之间不能插入其他 RTP 包。 就像你拆分电视机的箱子,箱子上的编号必须是连续的 (箱子1, 箱子2, 箱子3...),并且要按顺序寄出
  • AP (Aggregation Packet) 不允许碎片化: AP 是一种特殊的 RTP 包,它本身就可能包含多个 NAL Units。文档明确指出 AP 不能 再进行碎片化。 这就像你已经把几个小零件装在一个箱子里了 (AP),就不能再把这个箱子拆开了
  • FU 不允许嵌套: 一个 FU 不能包含另一个 FU 的一部分。 碎片化要 "干净利落",每个片段都是独立的,不会互相包含
  • RTP 时间戳: 携带 FU 的 RTP 包的时间戳,应该设置为被碎片化的原始 NAL Unit 的时间戳 (NALU-time)。 所有属于同一个 NAL Unit 的 FU 片段都应该使用相同的原始时间戳
结构分析
cpp 复制代码
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|    PayloadHdr (Type=49)       |   FU header   | DONL (cond)   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-|
| DONL (cond)   |                                               |
|-+-+-+-+-+-+-+-+                                               |
|                         FU payload                            |
|                                                               |
|                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               :...OPTIONAL RTP padding        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • PayloadHdr (Payload Header): 这是 RTP 包的负载头,对于 FU 来说,Type 字段 必须 设置为 49,用来标识这是一个 FU 包
    • F, LayerId, TID 字段需要和被碎片化的原始 NAL Unit 的对应字段保持一致。 这些字段通常用于指示错误标志、层 ID 和时间 ID,它们是原始 NAL Unit 的属性,碎片化后需要保留
  • FU header (FU 头): 这是一个 1 个字节 的头(下面详细分析)
  • DONL (Decoding Order Number Low, 可选 16-bit 字段):
    • ​​​​​​​ 这个字段是 可选的 ,并且是 条件性存在 的(参考的前文叙述)
  • FU payload (FU 负载)
    • 这部分就是 NAL Unit 的实际数据片段
    • 连续的 FU 包的 FU payload 按照顺序拼接起来,就可以 重构 出原始的 NAL Unit 的 payload 部分
    • 注意,原始 NAL Unit 的 NAL Unit header 本身不包含在 FU payload 中
    • NAL Unit header 的信息通过 FU 包的 PayloadHdr 中的 F, LayerId, TID 字段以及 FU header 中的 FuType 字段来传递。 FU payload 不能为空

FU header (FU 头)

cpp 复制代码
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|S|E|  FuType   |
+---------------+
  • S (Start Bit, 1 bit)
    • ​​​​​​​S=1: 表示这是 NAL Unit 的第一个片段。FU payload 的第一个字节也是原始 NAL Unit payload 的第一个字节。 就像电视机箱子编号为 1 的箱子,表示这是电视机的第一个部分
    • S=0: 表示这不是 NAL Unit 的第一个片段,是中间或后续的片段
  • E (End Bit, 1 bit,注意只有在分包中的最后一个包才有标注,详细参考文章后的理解)
    • ​​​​​​​E=1: 表示这是 NAL Unit 的最后一个片段。FU payload 的最后一个字节也是原始 NAL Unit payload 的最后一个字节。 就像电视机箱子编号为最后一个的箱子,表示电视机的最后一部分
    • E=0: 表示这不是 NAL Unit 的最后一个片段,是开始或中间的片段
  • FuType (Fragmented Unit Type, 6 bits)
    • 这个字段的值 必须 等于被碎片化的原始 NAL Unit 的 Type 字段值。 它告诉接收端,这些片段原本属于哪种类型的 NAL Unit (例如,视频参数集 VPS、序列参数集 SPS、图像参数集 PPS、视频数据 slice 等)

传输规则总结

  • 非碎片化的 NAL Unit 不能用 FU 传输: 在一个 FU 包中,Start bit (S) 和 End bit (E) 不能同时都设置为 1。这意味着 FU 必须是用来传输被分割的 NAL Unit 的,而不是用来直接传输完整的 NAL Unit
  • 片段丢失处理: 如果接收端丢失了一个 FU 片段,推荐的做法是 丢弃 所有后续属于同一个 NAL Unit 的 FU 片段 (按照传输顺序)
    • 除非接收端的解码器能够处理不完整的 NAL Unit。 这是一种错误恢复机制,避免使用不完整的数据解码
  • 接收端片段聚合: 接收端可以尝试聚合已经收到的前 n-1 个片段,即使第 n 个片段丢失了
    • 在这种情况下,需要将重构出的 (不完整的) NAL Unit 的 forbidden_zero_bit 设置为 1,以指示这是一个语法错误的 NAL Unit。 这是一种尽力而为的策略,尝试在片段丢失的情况下仍然解码尽可能多的信息
代码实现
bash 复制代码
root@hcss-ecs-b4a9:/home/test/rtp/nalu# ./test4
[信息]: 原始 NALU 大小: 81 字节
[信息]: MTU 大小设置为: 50 字节
[信息]: 开始 NALU 分片处理...
[信息]: 原始 NALU 头部信息:
  F (forbidden_zero_bit): 0 (正常)
  LayerId: 0 (简化解析)
  TID (temporal_id_plus1): 0 (简化解析)
  NALU Type (from header - simplified): 38 (简化解析)
[信息]: 创建 FU 包 - 起始片段 (S=1)
[信息]:   设置为中间片段 (E=0)
[信息]:   FU Payload 大小: 48 字节
[信息]:   已分片字节数: 48 / 80
[信息]:   当前 FU 包总大小: 50 字节
----------------------------------------
[信息]: 创建 FU 包 - 中间/结束片段 (S=0)
[信息]:   设置为结束片段 (E=1)
[信息]:   FU Payload 大小: 32 字节
[信息]:   已分片字节数: 80 / 80
[信息]:   当前 FU 包总大小: 34 字节
----------------------------------------
[信息]: NALU 分片完成,共生成 2 个 FU 包。

[信息]: 生成的 FU 包列表:
FU 包 #1, 大小: 50 字节, 内容 (Hex): 0x31 0xa6 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17 0x18 0x19 0x1a 0x1b 0x1c 0x1d 0x1e 0x1f 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27 0x28 0x29 0x2a 0x2b 0x2c 0x2d 0x2e 0x2f 0x30 
FU 包 #2, 大小: 34 字节, 内容 (Hex): 0x31 0x66 0x31 0x32 0x33 0x34 0x35 0x36 0x37 0x38 0x39 0x3a 0x3b 0x3c 0x3d 0x3e 0x3f 0x40 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48 0x49 0x4a 0x4b 0x4c 0x4d 0x4e 0x4f 0x50 

[信息]: 模拟接收 FU 包并进行 NALU 重组...
[信息]: 开始 NALU 重组处理...
[信息]: 检测到起始 FU 包 (S=1).
[信息]: 重建 NALU 头部字节: 0x26
[信息]: 开始拼接 FU Payload...
[信息]:   拼接 FU 包 #1 Payload, 大小: 48 字节。
[信息]:   拼接 FU 包 #2 Payload, 大小: 32 字节。
[信息]: NALU 重组完成,总大小: 81 字节。

[信息]: 重组后的 NALU 大小: 81 字节, 内容 (Hex, 前 20 字节): 0x26 0x01 0x02 0x03 0x04 0x05 0x06 0x07 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f 0x10 0x11 0x12 0x13 ... (更多字节)

[验证]: 比较原始 NALU 和重组后的 NALU:
[验证]: 大小是否一致: 是
[验证]: 前缀是否一致: 是
[验证]: **重组成功!**
cpp 复制代码
#include <iostream>
#include <vector>
#include <string>
#include <iomanip> // for std::setw, std::setfill

// Helper function to convert integer to hex string for logging
std::string to_hex_string(int val) {
    std::stringstream stream;
    stream << "0x" << std::setfill('0') << std::setw(2) << std::hex << (val & 0xFF);
    return stream.str();
}

std::vector<std::vector<uint8_t>> fragmentNALU(const std::vector<uint8_t>& nalu_payload, int mtu); // Forward declaration

std::vector<uint8_t> reassembleNALU(const std::vector<std::vector<uint8_t>>& fu_packets) {
    std::vector<uint8_t> reassembled_nalu;

    if (fu_packets.empty()) {
        std::cout << "[警告]: 输入的 FU 包列表为空,无法重组 NALU。" << std::endl;
        return reassembled_nalu; // Return empty vector
    }

    std::cout << "[信息]: 开始 NALU 重组处理..." << std::endl;

    // 1. 检查第一个 FU 包是否是起始片段 (S=1)
    uint8_t first_fu_header = fu_packets[0][1]; // FU Header is the second byte in FU packet
    if (!((first_fu_header >> 7) & 0x01)) {
        std::cout << "[错误]: 第一个 FU 包不是起始片段 (S 位未设置)。重组失败。" << std::endl;
        return reassembled_nalu; // Return empty vector
    }
    std::cout << "[信息]: 检测到起始 FU 包 (S=1)." << std::endl;

    // 2. 重建 NALU 头部 (从第一个 FU 包获取信息)
    uint8_t payload_header = fu_packets[0][0];
    uint8_t fu_type = first_fu_header & 0x3F; // Extract FuType

    // Reconstruct the first byte of NALU header
    uint8_t reconstructed_nalu_header_byte = 0;
    if ((payload_header >> 7) & 0x01) { // Check F bit from Payload Header
        reconstructed_nalu_header_byte |= (1 << 7); // Set F bit in reconstructed header
    }
    reconstructed_nalu_header_byte |= (fu_type & 0x3F); // Set NALU Type (FuType)

    reassembled_nalu.push_back(reconstructed_nalu_header_byte);
    std::cout << "[信息]: 重建 NALU 头部字节: " << to_hex_string(reconstructed_nalu_header_byte) << std::endl;

    // 3. 拼接 FU Payload
    std::cout << "[信息]: 开始拼接 FU Payload..." << std::endl;
    for (size_t i = 0; i < fu_packets.size(); ++i) {
        const std::vector<uint8_t>& fu_packet = fu_packets[i];

        if (fu_packet.size() < 2) {
            std::cout << "[警告]: FU 包 #" << i + 1 << " 太短,缺少 FU Header,跳过。" << std::endl;
            continue; // Skip invalid FU packet
        }

        // Extract FU Payload (starts from the byte after Payload Header and FU Header)
        for (size_t j = 2; j < fu_packet.size(); ++j) {
            reassembled_nalu.push_back(fu_packet[j]);
        }
        std::cout << "[信息]:   拼接 FU 包 #" << i + 1 << " Payload, 大小: " << fu_packet.size() - 2 << " 字节。" << std::endl;
    }

    std::cout << "[信息]: NALU 重组完成,总大小: " << reassembled_nalu.size() << " 字节。" << std::endl;
    return reassembled_nalu;
}


std::vector<std::vector<uint8_t>> fragmentNALU(const std::vector<uint8_t>& nalu_payload, int mtu) {
    std::vector<std::vector<uint8_t>> fragmentation_units;

    if (nalu_payload.empty()) {
        std::cout << "[警告]: 输入的 NALU 负载为空,无法分片。" << std::endl;
        return fragmentation_units; // Return empty vector
    }

    if (mtu <= 0) {
        std::cout << "[错误]: MTU 值必须大于 0。" << std::endl;
        return fragmentation_units; // Return empty vector
    }

    std::cout << "[信息]: 开始 NALU 分片处理..." << std::endl;

    // 1. 提取 NALU 头部信息 (假设 NALU 头部第一个字节包含 F, LayerId, TID 和 Type 信息)
    if (nalu_payload.size() < 1) {
        std::cout << "[错误]: NALU 负载太短,无法解析头部信息。" << std::endl;
        return fragmentation_units;
    }
    uint8_t nalu_header_byte = nalu_payload[0];

    // 假设:
    // F 标志位 (forbidden_zero_bit): 最高位 (bit 7)
    bool f_bit = (nalu_header_byte >> 7) & 0x01;
    uint8_t layer_id = 0; // 简化处理
    uint8_t tid = 0;      // 简化处理
    uint8_t nalu_type_from_header = (nalu_header_byte & 0x3F); // 假设 NALU Type 占据低6位


    std::cout << "[信息]: 原始 NALU 头部信息:" << std::endl;
    std::cout << "  F (forbidden_zero_bit): " << (f_bit ? "1 (错误)" : "0 (正常)") << std::endl;
    std::cout << "  LayerId: " << static_cast<int>(layer_id) << " (简化解析)" << std::endl;
    std::cout << "  TID (temporal_id_plus1): " << static_cast<int>(tid) << " (简化解析)" << std::endl;
    std::cout << "  NALU Type (from header - simplified): " << static_cast<int>(nalu_type_from_header) << " (简化解析)" << std::endl;


    // 2. 准备 FU 包的头部信息
    uint8_t payload_header = 0;
    payload_header |= (49 << 0); // Payload Type = 49 (FU)
    if (f_bit) payload_header |= (1 << 7); // 设置 F 标志位


    uint8_t fu_header_base = 0;
    uint8_t fu_type = nalu_type_from_header; // FuType 应该等于原始NALU的Type (simplified)
    fu_header_base |= (fu_type & 0x3F); // FuType (6 bits)


    // 3. 分片 NALU 负载 (去除原始NALU头部的第一个字节开始分片)
    int nalu_payload_data_start_index = 1; // 从索引 1 开始,跳过原始NALU的第一个字节头部
    int nalu_data_length = nalu_payload.size() - nalu_payload_data_start_index;
    if (nalu_data_length <= 0) {
        std::cout << "[警告]: NALU 负载数据部分为空,无需分片。" << std::endl;
        return fragmentation_units;
    }
    int bytes_fragmented = 0;


    while (bytes_fragmented < nalu_data_length) {
        std::vector<uint8_t> fu_packet;

        // a. 添加 Payload Header (Type=49)
        fu_packet.push_back(payload_header);

        // b. 创建 FU Header
        uint8_t fu_header = fu_header_base;
        if (bytes_fragmented == 0) {
            fu_header |= (1 << 7); // Set S bit for the first fragment
            std::cout << "[信息]: 创建 FU 包 - 起始片段 (S=1)" << std::endl;
        } else {
            std::cout << "[信息]: 创建 FU 包 - 中间/结束片段 (S=0)" << std::endl;
        }

        int fragment_size = std::min(mtu - 2, nalu_data_length - bytes_fragmented); // 2 bytes for PayloadHdr + FU Header

        if (bytes_fragmented + fragment_size >= nalu_data_length) {
            fu_header |= (1 << 6); // Set E bit for the last fragment
            std::cout << "[信息]:   设置为结束片段 (E=1)" << std::endl;
        } else {
            std::cout << "[信息]:   设置为中间片段 (E=0)" << std::endl;
        }
        fu_packet.push_back(fu_header);


        // c. 添加 FU Payload
        std::cout << "[信息]:   FU Payload 大小: " << fragment_size << " 字节" << std::endl;
        for (int i = 0; i < fragment_size; ++i) {
            fu_packet.push_back(nalu_payload[nalu_payload_data_start_index + bytes_fragmented + i]);
        }

        fragmentation_units.push_back(fu_packet);
        bytes_fragmented += fragment_size;
        std::cout << "[信息]:   已分片字节数: " << bytes_fragmented << " / " << nalu_data_length << std::endl;
        std::cout << "[信息]:   当前 FU 包总大小: " << fu_packet.size() << " 字节" << std::endl;
        std::cout << "----------------------------------------" << std::endl;
    }

    std::cout << "[信息]: NALU 分片完成,共生成 " << fragmentation_units.size() << " 个 FU 包。" << std::endl;
    return fragmentation_units;
}

int main() {
    // 示例 NALU 负载 (模拟一个较大的 NALU)
    std::vector<uint8_t> sample_nalu = {
        0x26, // NALU Header (Type=Slice, Non-IRAP, etc. - Example value)
        0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A,
        0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14,
        0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
        0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
        0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, 0x30, 0x31, 0x32,
        0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C,
        0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46,
        0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, 0x50,
        // ... 更多数据 ...
    };
    int mtu_size = 50; // 示例 MTU 大小

    std::cout << "[信息]: 原始 NALU 大小: " << sample_nalu.size() << " 字节" << std::endl;
    std::cout << "[信息]: MTU 大小设置为: " << mtu_size << " 字节" << std::endl;

    std::vector<std::vector<uint8_t>> fus = fragmentNALU(sample_nalu, mtu_size);

    if (!fus.empty()) {
        std::cout << "\n[信息]: 生成的 FU 包列表:" << std::endl;
        for (size_t i = 0; i < fus.size(); ++i) {
            std::cout << "FU 包 #" << i + 1 << ", 大小: " << fus[i].size() << " 字节, 内容 (Hex): ";
            for (uint8_t byte : fus[i]) {
                std::cout << to_hex_string(byte) << " ";
            }
            std::cout << std::endl;
        }

        // 模拟接收 FU 包并重组
        std::cout << "\n[信息]: 模拟接收 FU 包并进行 NALU 重组..." << std::endl;
        std::vector<uint8_t> reassembled_nalu = reassembleNALU(fus);

        std::cout << "\n[信息]: 重组后的 NALU 大小: " << reassembled_nalu.size() << " 字节, 内容 (Hex, 前 20 字节): ";
        for (size_t i = 0; i < std::min((size_t)20, reassembled_nalu.size()); ++i) { // Print first 20 bytes or less
            std::cout << to_hex_string(reassembled_nalu[i]) << " ";
        }
        std::cout << (reassembled_nalu.size() > 20 ? "... (更多字节)" : "") << std::endl;


        // 验证重组后的 NALU (简单验证大小和前几个字节)
        bool is_same_size = (reassembled_nalu.size() == sample_nalu.size());
        bool is_prefix_same = true;
        for (size_t i = 0; i < std::min(sample_nalu.size(), reassembled_nalu.size()); ++i) {
            if (sample_nalu[i] != reassembled_nalu[i]) {
                is_prefix_same = false;
                break;
            }
        }

        std::cout << "\n[验证]: 比较原始 NALU 和重组后的 NALU:" << std::endl;
        std::cout << "[验证]: 大小是否一致: " << (is_same_size ? "是" : "否") << std::endl;
        std::cout << "[验证]: 前缀是否一致: " << (is_prefix_same ? "是" : "否") << std::endl;

        if (is_same_size && is_prefix_same) {
            std::cout << "[验证]: **重组成功!**" << std::endl;
        } else {
            std::cout << "[验证]: **重组可能失败或不完整。请检查日志和代码。**" << std::endl;
        }


    } else {
        std::cout << "[信息]: 没有生成 FU 包 (可能由于输入为空或 MTU 设置错误)." << std::endl;
    }

    return 0;
}
相关推荐
Antonio91533 分钟前
【音视频】封装格式与音视频同步
音视频
EasyCVR1 小时前
安防监控/视频集中存储EasyCVR视频汇聚平台如何配置AI智能分析平台的接入?
人工智能·音视频·webrtc·rtsp·gb28181
weixin_519311741 小时前
3.多线程获取音频AI的PCM数据
人工智能·音视频·pcm
轶软工作室6 小时前
全自动数据强制备份程序,无视占用直接硬复制各种数据文件、文档、音视频、软件、数据库等的VSS卷拷贝批处理脚本程序,解放双手,一劳永逸
数据库·音视频
wkd_0076 小时前
【音视频 | AAC】AAC解码库faad2介绍、使用步骤、例子代码
音视频·aac·faad·faad2·aac解码·aac解码库
摸鱼 特供版6 小时前
一键无损放大视频,让老旧画面重焕新生!
windows·学习·音视频·软件需求
敢嗣先锋6 小时前
鸿蒙5.0实战案例:基于OpenGL渲染视频画面帧
移动开发·音视频·harmonyos·arkts·opengl·arkui·鸿蒙开发
tinghai_2169 小时前
vivo手机怎么剪辑视频?从零开始的视频编辑之旅
智能手机·音视频
正在走向自律20 小时前
通义万相2.1:开启视频生成新时代
人工智能·文生图·音视频·ai绘画·文生视频·ai视频·通义万相 2.1