当前位置: 首页 > news >正文

音视频学习(五十六):单RTP包模式和FU-A分片模式

在RTP (Real-time Transport Protocol) 协议中,为了高效、可靠地传输H.264、H.265等视频流,需要解决一个关键问题:如何将大小不定的视频数据单元(即NAL单元,Network Abstraction Layer unit)适配到固定大小的RTP数据包中。

这就是RTP封装所要解决的核心问题。根据NAL单元的大小,RTP封装通常分为两种主要模式:单RTP包模式FU-A分片模式

什么是NAL单元?

NAL单元是H.264或H.265编码器输出的基本数据单元。每个NAL单元都包含一个NAL头部和负载数据。

  • NAL头部: 1到2个字节,用于标识该NAL单元的类型,例如,是视频帧数据(I帧、P帧、B帧)、还是编码参数信息(SPS、PPS、VPS)等。
  • NAL负载: 实际的视频或参数数据。

一个完整的视频帧(例如一个I帧)可能由一个或多个NAL单元组成。在编码器输出的裸流(Annex B格式)中,NAL单元之间通常由 0x000000010x000001起始码分隔。

单RTP包发送模式 (Single NAL Unit Packet)

当一个完整的NAL单元小于或等于RTP包的有效载荷(payload)最大尺寸时,可以直接将整个NAL单元封装在一个RTP包中发送。这种模式被称为单RTP包模式

这种模式的RTP包结构非常简单:

  • RTP头部: 标准的12字节RTP头部,包含序列号、时间戳、同步源标识(SSRC)等。
  • NAL单元: 完整的NAL单元,包括其NAL头部和负载数据。

封装过程

  1. 从视频裸流中找到一个完整的NAL单元(即从一个起始码到下一个起始码之前的数据)。
  2. 去掉起始码。
  3. 将该NAL单元的NAL头部和负载数据作为RTP包的有效载荷。
  4. 根据RTP协议填充RTP头部。

优缺点

  • 优点:
    • 简单高效: 封装过程简单,无需额外的分片处理,降低了延迟和计算复杂度。
    • 减少RTP开销: 每个RTP包只包含一个NAL单元,RTP头部开销相对较低。
  • 缺点:
    • 不适用于大NAL单元: 许多视频帧,特别是I帧(关键帧),其NAL单元尺寸可能远大于网络MTU(通常为1500字节),导致一个NAL单元无法放入单个RTP包中。在这种情况下,必须使用分片模式。

FU-A分片模式 (Fragmentation Unit Type A)

当一个完整的NAL单元大于RTP包的有效载荷最大尺寸时,就需要将其拆分为多个较小的分片进行传输。FU-A (Fragmentation Unit Type A) 是RTP协议中专门用于H.264和H.265的分片机制。

一个大的NAL单元被分成若干个分片,每个分片封装在一个单独的RTP包中。这些RTP包拥有相同的时间戳,但序列号是递增的。

FU-A模式的RTP包结构比单包模式更复杂,它引入了两个额外的字节:FU指示器FU头部

FU指示器 (FU Indicator)

  • 作用: 用于标识RTP包的有效载荷是FU-A分片。
  • 结构: 1字节。
    • F位 (1位): 禁止位,通常为0。
    • NRI位 (2位): H.264中表示NAL单元的重要性。
    • Type位 (5位): 用于标识负载类型,对于FU-A,其值为28 (H.264) 或 49 (H.265)。

FU头部 (FU Header)

  • 作用: 用于标识分片在原始NAL单元中的位置,并携带原始NAL单元的类型。
  • 结构: 1字节。
    • S位 (Start bit): 1位。如果这是原始NAL单元的第一个分片,则该位为1;否则为0。
    • E位 (End bit): 1位。如果这是原始NAL单元的最后一个分片,则该位为1;否则为0。
    • R位 (Reserved bit): 1位,保留位,通常为0。
    • Type位 (5位): 原始NAL单元的类型,例如,I帧为5,SPS为7,PPS为8等。

封装过程

  1. 从视频裸流中获取一个完整的、大于RTP有效载荷的NAL单元。
  2. 提取原始NAL头部: 移除该NAL单元的原始头部。
  3. 创建FU指示器和FU头部:
    • FU指示器的Type字段设为28 (H.264) 或 49 (H.265)。
    • FU头部的Type字段设为原始NAL单元的类型。
  4. 分片: 将NAL单元的负载数据切分成多个大小不超过RTP有效载荷最大尺寸的分片。
  5. 封装每个分片:
    • 对于第一个分片: 在RTP有效载荷中依次放入FU指示器、FU头部(S位设为1,E位设为0)和NAL单元的第一个分片数据。
    • 对于中间分片: 在RTP有效载荷中依次放入FU指示器、FU头部(S位和E位都为0)和NAL单元的中间分片数据。
    • 对于最后一个分片: 在RTP有效载荷中依次放入FU指示器、FU头部(S位设为0,E位设为1)和NAL单元的最后一个分片数据。
  6. 发送: 为每个分片生成一个RTP包,设置相同的时间戳和递增的序列号,然后发送出去。

优缺点

  • 优点:
    • 处理大NAL单元: 解决了大尺寸NAL单元无法通过单个RTP包传输的问题,确保了所有视频数据都能被封装和发送。
    • 灵活性: 允许根据网络MTU动态调整分片大小。
  • 缺点:
    • 增加开销: 每个RTP包都额外增加了2字节的FU指示器和FU头部,增加了协议开销。
    • 重组复杂性: 接收端需要缓存这些分片,并根据S/E位和序列号将它们重新组合成完整的NAL单元。如果任何一个分片丢失,整个NAL单元将无法重组,导致视频解码失败。

示例(c++)

#include <iostream>
#include <vector>
#include <numeric>
#include <map>
#include <arpa/inet.h> // For htons, ntohl// 定义 RTP 头部
struct RtpHeader {uint8_t version : 2;uint8_t padding : 1;uint8_t extension : 1;uint8_t csrcCount : 4;uint8_t marker : 1;uint8_t payloadType : 7;uint16_t sequenceNumber;uint32_t timestamp;uint32_t ssrc;
};// H.264 NALU 类型
enum NalTypeH264 {NAL_TYPE_H264_SINGLE_PACKET_MIN = 1,NAL_TYPE_H264_SINGLE_PACKET_MAX = 23,NAL_TYPE_H264_FU_A = 28,
};// H.265 NALU 类型
enum NalTypeH265 {NAL_TYPE_H265_SINGLE_PACKET_MIN = 0,NAL_TYPE_H265_SINGLE_PACKET_MAX = 47,NAL_TYPE_H265_FU_A = 49,
};// FU-A 指示器 (H.264)
struct FuAIndicatorH264 {uint8_t type : 5;uint8_t ref_idc : 2;uint8_t forbidden_bit : 1;
};// FU-A 头部 (H.264)
struct FuAHeaderH264 {uint8_t original_nal_type : 5;uint8_t reserved : 1;uint8_t end_bit : 1;uint8_t start_bit : 1;
};// FU-A 指示器 (H.265)
struct FuAIndicatorH265 {uint8_t type : 6;uint8_t layer_id_h : 2;uint8_t layer_id_l : 4;uint8_t temporal_id : 3;uint8_t forbidden_bit : 1;
};// FU-A 头部 (H.265)
struct FuAHeaderH265 {uint8_t original_nal_type : 6;uint8_t reserved : 1;uint8_t end_bit : 1;uint8_t start_bit : 1;
};// RTP 包解析器类
class RtpParser {
private:std::map<uint32_t, std::vector<uint8_t>> fragmentation_buffers;public:void process_rtp_packet(const std::vector<uint8_t>& packet) {if (packet.size() < sizeof(RtpHeader)) {std::cerr << "Invalid RTP packet size." << std::endl;return;}const RtpHeader* rtp_header = reinterpret_cast<const RtpHeader*>(packet.data());uint16_t sequence_number = ntohs(rtp_header->sequenceNumber);uint32_t ssrc = ntohl(rtp_header->ssrc);uint32_t timestamp = ntohl(rtp_header->timestamp);uint8_t payload_type = rtp_header->payloadType;const uint8_t* payload = packet.data() + sizeof(RtpHeader);size_t payload_size = packet.size() - sizeof(RtpHeader);if (payload_size <= 0) {std::cerr << "RTP payload is empty." << std::endl;return;}std::cout << "--- Processing RTP Packet ---" << std::endl;std::cout << "Sequence: " << sequence_number << ", Timestamp: " << timestamp << ", SSRC: " << ssrc << std::endl;std::cout << "Payload Type: " << static_cast<int>(payload_type) << std::endl;// 根据 PayloadType 区分 H.264 和 H.265if (payload_type == 96) {// H.264 流uint8_t nal_type_h264 = payload[0] & 0x1F;std::cout << "Codec: H.264, NALU Type: " << static_cast<int>(nal_type_h264) << std::endl;if (nal_type_h264 >= NAL_TYPE_H264_SINGLE_PACKET_MIN && nal_type_h264 <= NAL_TYPE_H264_SINGLE_PACKET_MAX) {handle_single_packet_h264(payload, payload_size);} else if (nal_type_h264 == NAL_TYPE_H264_FU_A) {handle_fu_a_packet_h264(payload, payload_size, ssrc);} else {std::cout << "Unsupported H.264 NALU type or aggregate packet." << std::endl;}} else if (payload_type == 97) {// H.265 流uint8_t nal_type_h265 = (payload[0] >> 1) & 0x3F;std::cout << "Codec: H.265, NALU Type: " << static_cast<int>(nal_type_h265) << std::endl;if (nal_type_h265 >= NAL_TYPE_H265_SINGLE_PACKET_MIN && nal_type_h265 <= NAL_TYPE_H265_SINGLE_PACKET_MAX) {handle_single_packet_h265(payload, payload_size);} else if (nal_type_h265 == NAL_TYPE_H265_FU_A) {handle_fu_a_packet_h265(payload, payload_size, ssrc);} else {std::cout << "Unsupported H.265 NALU type or aggregate packet." << std::endl;}} else {std::cout << "Unsupported payload type." << std::endl;}}private:// H.264 处理函数void handle_single_packet_h264(const uint8_t* payload, size_t size) {std::cout << "--> H.264 Single NALU packet detected." << std::endl;uint8_t nal_type = payload[0] & 0x1F;std::cout << "    NALU Type: " << static_cast<int>(nal_type) << std::endl;std::vector<uint8_t> nalu_with_start_code = {0x00, 0x00, 0x00, 0x01};nalu_with_start_code.insert(nalu_with_start_code.end(), payload, payload + size);std::cout << "    Reconstructed NALU size: " << nalu_with_start_code.size() << " bytes." << std::endl;std::cout << "---------------------------------------" << std::endl;}void handle_fu_a_packet_h264(const uint8_t* payload, size_t size, uint32_t ssrc) {std::cout << "--> H.264 FU-A fragmentation packet detected." << std::endl;if (size < 2) {std::cerr << "    Invalid FU-A packet size." << std::endl;return;}const FuAHeaderH264* fu_header = reinterpret_cast<const FuAHeaderH264*>(payload + 1);bool is_start = fu_header->start_bit;bool is_end = fu_header->end_bit;uint8_t original_nal_type = fu_header->original_nal_type;std::cout << "    Original NALU Type: " << static_cast<int>(original_nal_type) << std::endl;std::cout << "    Start Bit: " << (is_start ? "Yes" : "No") << ", End Bit: " << (is_end ? "Yes" : "No") << std::endl;std::vector<uint8_t>& buffer = fragmentation_buffers[ssrc];if (is_start) {buffer.clear();uint8_t original_nal_header = (payload[0] & 0xE0) | (original_nal_type & 0x1F);buffer.push_back(original_nal_header);}buffer.insert(buffer.end(), payload + 2, payload + size);if (is_end) {std::cout << "    Reassembled NALU complete. Total size: " << buffer.size() << " bytes." << std::endl;std::vector<uint8_t> complete_nalu_with_start_code = {0x00, 0x00, 0x00, 0x01};complete_nalu_with_start_code.insert(complete_nalu_with_start_code.end(), buffer.begin(), buffer.end());std::cout << "    NALU with start code size: " << complete_nalu_with_start_code.size() << " bytes." << std::endl;fragmentation_buffers.erase(ssrc);}std::cout << "---------------------------------------" << std::endl;}// H.265 处理函数void handle_single_packet_h265(const uint8_t* payload, size_t size) {std::cout << "--> H.265 Single NALU packet detected." << std::endl;uint8_t nal_type = (payload[0] >> 1) & 0x3F;std::cout << "    NALU Type: " << static_cast<int>(nal_type) << std::endl;std::vector<uint8_t> nalu_with_start_code = {0x00, 0x00, 0x00, 0x01};nalu_with_start_code.insert(nalu_with_start_code.end(), payload, payload + size);std::cout << "    Reconstructed NALU size: " << nalu_with_start_code.size() << " bytes." << std::endl;std::cout << "---------------------------------------" << std::endl;}void handle_fu_a_packet_h265(const uint8_t* payload, size_t size, uint32_t ssrc) {std::cout << "--> H.265 FU-A fragmentation packet detected." << std::endl;if (size < 3) { // H.265 FU-A: 1B FU指示器 + 1B FU头部 + 数据std::cerr << "    Invalid H.265 FU-A packet size." << std::endl;return;}const FuAHeaderH265* fu_header = reinterpret_cast<const FuAHeaderH265*>(payload + 2);bool is_start = fu_header->start_bit;bool is_end = fu_header->end_bit;uint8_t original_nal_type = fu_header->original_nal_type;std::cout << "    Original NALU Type: " << static_cast<int>(original_nal_type) << std::endl;std::cout << "    Start Bit: " << (is_start ? "Yes" : "No") << ", End Bit: " << (is_end ? "Yes" : "No") << std::endl;std::vector<uint8_t>& buffer = fragmentation_buffers[ssrc];if (is_start) {buffer.clear();// 构造原始的 NALU 头部 (2 字节)buffer.push_back(payload[0]);buffer.push_back(payload[1]);// 修正 NALU 类型字段,原始类型在 FU Header 中buffer[0] = (buffer[0] & 0x81) | (original_nal_type << 1);}buffer.insert(buffer.end(), payload + 3, payload + size);if (is_end) {std::cout << "    Reassembled NALU complete. Total size: " << buffer.size() << " bytes." << std::endl;std::vector<uint8_t> complete_nalu_with_start_code = {0x00, 0x00, 0x00, 0x01};complete_nalu_with_start_code.insert(complete_nalu_with_start_code.end(), buffer.begin(), buffer.end());std::cout << "    NALU with start code size: " << complete_nalu_with_start_code.size() << " bytes." << std::endl;fragmentation_buffers.erase(ssrc);}std::cout << "---------------------------------------" << std::endl;}
};// 模拟发送数据包
void send_packet_to_parser(RtpParser& parser, uint16_t seq, uint32_t ts, uint32_t ssrc, uint8_t payload_type, const std::vector<uint8_t>& payload) {std::vector<uint8_t> packet;packet.resize(sizeof(RtpHeader) + payload.size());RtpHeader* header = reinterpret_cast<RtpHeader*>(packet.data());header->version = 2;header->padding = 0;header->extension = 0;header->csrcCount = 0;header->marker = 0;header->payloadType = payload_type;header->sequenceNumber = htons(seq);header->timestamp = htonl(ts);header->ssrc = htonl(ssrc);// 最后一个分片设置 markeruint8_t last_byte = payload.back();if ((payload_type == 96 && (last_byte & 0x40) > 0) || (payload_type == 97 && (last_byte & 0x40) > 0)) {header->marker = 1;} else if (payload_type == 96 && (payload[0] & 0x1F) < 24) {header->marker = 1;} else if (payload_type == 97 && ((payload[0] >> 1) & 0x3F) < 48) {header->marker = 1;}memcpy(packet.data() + sizeof(RtpHeader), payload.data(), payload.size());parser.process_rtp_packet(packet);
}int main() {RtpParser parser;uint32_t ssrc = 0x12345678;// --- 模拟 H.265 单 RTP 包模式 (NALU 类型 32, VPS) ---std::cout << "\n--- Emulating H.265 Single RTP Packet Mode ---" << std::endl;std::vector<uint8_t> h265_single_nalu(100);h265_single_nalu[0] = 0x40; // NALU 类型 32 (VPS)h265_single_nalu[1] = 0x01;std::iota(h265_single_nalu.begin() + 2, h265_single_nalu.end(), 0);send_packet_to_parser(parser, 100, 1000, ssrc, 97, h265_single_nalu);// --- 模拟 H.265 FU-A 分片模式 (NALU 类型 49) ---std::cout << "\n--- Emulating H.265 FU-A Fragmentation Mode ---" << std::endl;// 第一个分片 (S=1, E=0, 原始类型 19, IDR)std::vector<uint8_t> h265_fu_a_part1(500);h265_fu_a_part1[0] = 0xFE; // NALU 类型 49h265_fu_a_part1[1] = 0x01; // temporal_id_plus1h265_fu_a_part1[2] = 0x80 | 19; // S=1, E=0, Type=19std::iota(h265_fu_a_part1.begin() + 3, h265_fu_a_part1.end(), 0);send_packet_to_parser(parser, 200, 2000, ssrc, 97, h265_fu_a_part1);// 第二个分片 (S=0, E=0)std::vector<uint8_t> h265_fu_a_part2(500);h265_fu_a_part2[0] = 0xFE;h265_fu_a_part2[1] = 0x01;h265_fu_a_part2[2] = 0x00 | 19;std::iota(h265_fu_a_part2.begin() + 3, h265_fu_a_part2.end(), 0);send_packet_to_parser(parser, 201, 2000, ssrc, 97, h265_fu_a_part2);// 最后一个分片 (S=0, E=1)std::vector<uint8_t> h265_fu_a_part3(400);h265_fu_a_part3[0] = 0xFE;h265_fu_a_part3[1] = 0x01;h265_fu_a_part3[2] = 0x40 | 19; // S=0, E=1, Type=19std::iota(h265_fu_a_part3.begin() + 3, h265_fu_a_part3.end(), 0);send_packet_to_parser(parser, 202, 2000, ssrc, 97, h265_fu_a_part3);return 0;
}

总结

特性单RTP包模式FU-A分片模式
适用场景NAL单元大小小于或等于RTP有效载荷最大尺寸。NAL单元大小大于RTP有效载荷最大尺寸。
RTP有效载荷完整的NAL单元 (NAL头 + 负载)。FU指示器 + FU头部 + NAL单元的分片负载。
主要功能直接封装。拆分和重组大的NAL单元。
开销仅RTP头部。RTP头部 + 额外2字节的FU指示器和FU头部。
实现复杂度简单。复杂,需要处理分片重组、丢包等问题。
NAL单元类型RTP包的有效载荷类型即为NAL单元类型。FU指示器中的类型为28/49,原始NAL类型在FU头部中。
http://www.xdnf.cn/news/1366831.html

相关文章:

  • Linux驱动开发笔记(七)——并发与竞争(上)——原子操作
  • 深度学习-----《PyTorch深度学习核心应用解析:从环境搭建到模型优化的完整实践指南》
  • 链表OJ习题(2)
  • 操作系统中,进程与线程的定义与区别
  • 似然函数对数似然函数负对数似然函数
  • Ant Design for UI 选择下拉框
  • BIO、NIO 和 AIO
  • 2025.8.25回溯算法-集合
  • Typora + PicList + Gitee 图床完整配置教程
  • 【ElasticSearch】json查询语法和可用的客户端
  • ESP32开发WSL_VSCODE环境搭建
  • Mysql系列--8、索引
  • Java延迟任务实现方案详解:从DelayQueue到实际应用
  • 2.3零基础玩转uni-app轮播图:从入门到精通 (咸虾米总结)
  • 【Docker基础】Docker-compose进阶配置:健康检查与服务就绪
  • K8s Pod驱逐机制详解与实战
  • C++ extern 关键字面试深度解析
  • 开源 C++ QT Widget 开发(六)通讯--TCP调试
  • 安全合规:AC(上网行为安全)--下
  • vue 一键打包上传
  • Genymotion 虚拟机如何安装 APK?(ARM 插件安装教程)
  • ICCV 2025|TRACE:无需标注,用3D高斯直接学习物理参数,从视频“预知”未来!
  • 二、添加3D形状
  • More Effective C++ 条款07:不要重载、和,操作符
  • 【系统架构设计师】数据库设计(一):数据库技术的发展、数据模型、数据库管理系统、数据库三级模式
  • 审核问题——首次进入APP展示隐私政策弹窗
  • 大模型(一)什么是 MCP?如何使用 Charry Studio 集成 MCP?
  • 深分页实战
  • 计算机网络:HTTP、抓包、TCP和UDP报文及重要概念
  • GPT5的Test-time compute(测试时计算)是什么?