H.265 RTP 打包与拆包重组详解
📅 更新时间:2025-10-14
🏷️ 标签:H.265
|HEVC
|RTP
|打包
|解包
|组帧
🎯 目标:深度掌握 H.265 在 RTP 中的打包、解包与组帧全流程
文章目录
- 📖 前言
- 第一部分:H.265 NALU 结构基础
- 1.1 H.265 NALU 基本结构
- 📋 完整结构(Annex-B 格式)
- 1.2 NALU Header 详解(2 字节,关键)
- 📊 位结构分布
- 🔑 字段详解
- 🎯 关键 NALU 类型(Type 值)
- 1.3 提取 NALU Header 字段(C++ 实现)
- 1.4 典型 H.265 码流示例
- 第二部分:RTP 打包三种模式详解
- 2.1 RTP 基础概念回顾
- RTP Header(12 字节)关键字段
- 2.2 打包模式选择逻辑
- 第三部分:模式一 单 NALU 模式
- 3.1 适用场景
- 3.2 Payload 结构
- 3.3 发送端实现
- 3.4 接收端实现
- 第四部分:模式二 - FU 分片模式(核心重点)
- 4.1 适用场景
- 4.2 FU Payload 完整结构
- 4.3 PayloadHdr(字节 0-1)详解
- 4.4 FU Header(字节 2)详解
- 4.5 FU Payload(字节 3~N)
- 4.6 完整发送端实现
- 4.7 完整接收端实现(关键:组帧逻辑)
- 4.8 关键边界情况处理
- ❌ 错误 1:丢包导致 NALU 不完整
- ❌ 错误 2:收到 S=1 时上一个 NALU 未结束
- ❌ 错误 3:TS 跳变但 NALU 未完成
- 第五部分:模式三 - AP 聚合模式
- 5.1 适用场景
- 5.2 AP Payload 结构
- 5.3 PayloadHdr(字节 0-1)
- 5.4 NALU 单元格式
- 5.5 发送端实现
- 5.6 接收端实现
- 5.7 三种模式对比总结
- 第六部分:调试与问题排查
- 6.1 常见问题诊断表
- 总结
📖 前言
H.265(HEVC)相比 H.264 压缩效率提升约 50%,但在 RTP 传输中的实现细节却复杂得多。本文专注于 H.265 RTP 传输的核心技术,深入剖析:
- 🔧 H.265 NALU 结构与 RTP Payload 格式
- 📦 单 NALU、FU 分片、AP 聚合三种打包模式
- 🔄 接收端解包、乱序处理与 NALU 重组
- 🐛 常见问题、调试技巧与性能优化
第一部分:H.265 NALU 结构基础
1.1 H.265 NALU 基本结构
H.265 的 NALU(Network Abstraction Layer Unit)是编码后的基本传输单元。
📋 完整结构(Annex-B 格式)
起始码:
00 00 00 01
(4字节) - 用于序列开头、参数集前、关键帧前
1.2 NALU Header 详解(2 字节,关键)
H.265 的 NALU Header 为 2 字节(16 bits),这是与 H.264(1字节)的最大区别。
📊 位结构分布
Byte 0 Byte 10 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F| Type | LayerId | TID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
🔑 字段详解
字段 | 位数 | 位置 | 说明 | 典型值 |
---|---|---|---|---|
F | 1 | bit 15 | 禁止位(forbidden_zero_bit) | 0 |
Type | 6 | bit 14-9 | NALU 类型(nal_unit_type) | 见下表 |
LayerId | 6 | bit 8-3 | 层标识(nuh_layer_id) | 0(单层) |
TID | 3 | bit 2-0 | 时间层 ID+1(nuh_temporal_id_plus1) | 1(最低层) |
🎯 关键 NALU 类型(Type 值)
Type | 名称 | 说明 | 重要性 | RTP 处理 |
---|---|---|---|---|
0-9 | TRAIL/TSA/STSA/RADL/RASL | 普通视频片(P/B帧) | 中 | 需打包 |
16-21 | BLA/CRA/IDR | 关键帧(I帧) | ⭐⭐⭐ 高 | 需打包 |
19 | IDR_W_RADL | 常用 IDR 类型 | ⭐⭐⭐ 高 | 需打包 |
20 | IDR_N_LP | 无前导图像的 IDR | ⭐⭐⭐ 高 | 需打包 |
32 | VPS | 视频参数集 | ⭐⭐⭐ 高 | 优先发送 |
33 | SPS | 序列参数集 | ⭐⭐⭐ 高 | 优先发送 |
34 | PPS | 图像参数集 | ⭐⭐⭐ 高 | 优先发送 |
39-40 | SEI | 补充增强信息 | 低 | 可选 |
48 | AP | 聚合包(RTP) | - | 接收端识别 |
49 | FU | 分片包(RTP) | - | 接收端识别 |
⚠️ 重要:Type=48 和 49 是 RTP 专用类型,不会出现在编码器输出中,仅用于 RTP Payload。
1.3 提取 NALU Header 字段(C++ 实现)
struct H265NALUHeader {uint8_t forbidden_zero_bit; // 1 bituint8_t nal_unit_type; // 6 bitsuint8_t nuh_layer_id; // 6 bitsuint8_t nuh_temporal_id_plus1; // 3 bits// 从 2 字节解析void parse(const uint8_t* data) {uint16_t header = (data[0] << 8) | data[1];forbidden_zero_bit = (header >> 15) & 0x01;nal_unit_type = (header >> 9) & 0x3F; // 6 位!nuh_layer_id = (header >> 3) & 0x3F;nuh_temporal_id_plus1 = header & 0x07;}// 编码为 2 字节void encode(uint8_t* data) const {uint16_t header = (forbidden_zero_bit << 15)| (nal_unit_type << 9)| (nuh_layer_id << 3)| nuh_temporal_id_plus1;data[0] = (header >> 8) & 0xFF;data[1] = header & 0xFF;}
};
1.4 典型 H.265 码流示例
00 00 00 01 40 01 0C 01 FF FF ... <- VPS (Type=32, 0x40>>1=32)
00 00 00 01 42 01 01 01 60 ... <- SPS (Type=33, 0x42>>1=33)
00 00 00 01 44 01 C1 72 B4 ... <- PPS (Type=34, 0x44>>1=34)
00 00 00 01 26 01 AF ... <- IDR (Type=19, 0x26>>1=19)
Type 提取技巧:
uint8_t byte0 = nalu[0]; // 第一个字节
uint8_t type = (byte0 >> 1) & 0x3F; // 右移1位取6位
第二部分:RTP 打包三种模式详解
2.1 RTP 基础概念回顾
RTP Header(12 字节)关键字段
0 1 2 30 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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| SSRC |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
视频场景关键规则:
字段 | 含义 | 规则 |
---|---|---|
M | Marker 位 | 帧最后一个 RTP 包 = 1,其他 = 0 |
Seq | 序列号 | 每包递增 1(用于排序、检测丢包) |
TS | 时间戳 | 同帧所有包使用相同 TS,90kHz 时钟 |
PT | Payload Type | H.265 通常 96-127(动态协商) |
2.2 打包模式选择逻辑
enum RTPPacketMode {SINGLE_NALU, // 单 NALU 模式FU_MODE, // FU 分片模式(Type=49)AP_MODE // AP 聚合模式(Type=48)
};RTPPacketMode select_mode(const NALU& nalu, int mtu) {const int RTP_HEADER_SIZE = 12;const int MAX_PAYLOAD = mtu - RTP_HEADER_SIZE - 20 - 8; // IP+UDPif (nalu.size <= MAX_PAYLOAD) {return SINGLE_NALU; // 小 NALU,直接发送} else {return FU_MODE; // 大 NALU,需要分片}// AP 模式:多个小 NALU 聚合(本文暂不深入)
}
第三部分:模式一 单 NALU 模式
3.1 适用场景
- ✅ NALU 大小 ≤ MTU(通常 ≤ 1400 字节)
- ✅ VPS、SPS、PPS(参数集通常很小)
- ✅ 小的 P 帧片段
3.2 Payload 结构
关键点:
- RTP Payload 直接就是 NALU 内容(已去除起始码)
- NALU Header 的 Type 字段保持原值(如 33=SPS, 19=IDR)
3.3 发送端实现
void send_single_nalu(const uint8_t* nalu, int nalu_size,RTPContext& ctx) {// nalu: 不含起始码,第一个字节是 NALU Header[0]RTPPacket packet;// 构造 RTP Headerpacket.header.version = 2;packet.header.padding = 0;packet.header.extension = 0;packet.header.cc = 0;packet.header.marker = 1; // 单包即完整帧,M=1packet.header.payload_type = 96; // H.265 动态类型packet.header.seq = ctx.seq++;packet.header.timestamp = ctx.timestamp;packet.header.ssrc = ctx.ssrc;// Payload = 完整 NALUmemcpy(packet.payload, nalu, nalu_size);packet.payload_size = nalu_size;// 发送send_udp_packet(&packet);// 日志uint8_t type = (nalu[0] >> 1) & 0x3F;printf("[SEND] Single NALU: seq=%d ts=%u type=%d size=%d M=%d\n",packet.header.seq, packet.header.timestamp, type, nalu_size, packet.header.marker);
}
3.4 接收端实现
void recv_single_nalu(const RTPPacket* packet, H265OutputStream& output) {const uint8_t* payload = packet->payload;int size = packet->payload_size;// 解析 NALU Headeruint16_t nalu_header = (payload[0] << 8) | payload[1];uint8_t type = (nalu_header >> 9) & 0x3F;// 日志printf("[RECV] Single NALU: seq=%d ts=%u type=%d size=%d M=%d\n",packet->header.seq, packet->header.timestamp,type, size, packet->header.marker);// 写入起始码 + NALUuint8_t start_code[] = {0x00, 0x00, 0x00, 0x01};output.write(start_code, 4);output.write(payload, size);// 如果 M=1,通知上层帧完成if (packet->header.marker) {output.flush_frame();}
}
第四部分:模式二 - FU 分片模式(核心重点)
4.1 适用场景
- ✅ NALU 大小 > MTU
- ✅ 大的 I 帧(IDR)
- ✅ 长的 P/B 帧片段
4.2 FU Payload 完整结构
4.3 PayloadHdr(字节 0-1)详解
作用: 标识这是一个 FU 包,并保留原 NALU 的层信息。
0 10 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|Type=49| LayerId | TID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
字段说明:
字段 | 值 | 来源 |
---|---|---|
F | 0 | 固定为 0 |
Type | 49 | 固定值,表示 FU 分片 |
LayerId | 通常 0 | 从原 NALU Header 复制 |
TID | 通常 1 | 从原 NALU Header 复制 |
构造示例:
// 假设原 NALU Header: 0x2601 (Type=19, LayerId=0, TID=1)
uint16_t orig_header = 0x2601;
uint8_t layer_id = (orig_header >> 3) & 0x3F; // = 0
uint8_t tid = orig_header & 0x07; // = 1// 构造 PayloadHdr (Type=49)
uint16_t payload_hdr = (0 << 15) // F=0| (49 << 9) // Type=49| (layer_id << 3)| tid;uint8_t payload_hdr_bytes[2];
payload_hdr_bytes[0] = (payload_hdr >> 8) & 0xFF; // 0x62
payload_hdr_bytes[1] = payload_hdr & 0xFF; // 0x01
4.4 FU Header(字节 2)详解
作用: 标识分片的开始/结束,保存原 NALU 类型。
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|S|E| OrigType | (OrigType 占 6 位!)
+-+-+-+-+-+-+-+-+
字段说明:
位 | 名称 | 说明 | 取值规则 |
---|---|---|---|
S | Start | 分片起始标志 | 第一片=1,其他=0 |
E | End | 分片结束标志 | 最后一片=1,其他=0 |
OrigType | 原始 NALU 类型 | 6 位(0-63) | 从原 NALU Header 提取 |
典型组合:
分片位置 | S | E | 说明 |
---|---|---|---|
第一片 | 1 | 0 | 分片开始 |
中间片 | 0 | 0 | 继续分片 |
最后一片 | 0 | 1 | 分片结束 |
单片(不可能) | 1 | 1 | 非法组合 |
构造示例:
// 原 NALU Type = 19 (IDR)
uint8_t orig_type = 19;// 第一片
uint8_t fu_header_first = (1 << 7) | (0 << 6) | orig_type; // S=1, E=0
// = 0x93// 中间片
uint8_t fu_header_mid = (0 << 7) | (0 << 6) | orig_type; // S=0, E=0
// = 0x13// 最后一片
uint8_t fu_header_last = (0 << 7) | (1 << 6) | orig_type; // S=0, E=1
// = 0x53
4.5 FU Payload(字节 3~N)
- 直接是原 NALU Payload 的连续切片
- 不包含原 NALU Header(已在 FU Header 中保存 Type)
- 切片大小 = MTU - RTP Header(12) - IP(20) - UDP(8) - PayloadHdr(2) - FU Header(1)
- 典型值:约 1400 - 3 = 1397 字节
4.6 完整发送端实现
void send_fu_packets(const uint8_t* nalu, int nalu_size,RTPContext& ctx) {// nalu[0..1] = NALU Header (2 字节)// nalu[2..] = NALU Payload// 解析原 NALU Headeruint16_t orig_header = (nalu[0] << 8) | nalu[1];uint8_t f = (orig_header >> 15) & 0x01;uint8_t orig_type = (orig_header >> 9) & 0x3F; // 6 位!uint8_t layer_id = (orig_header >> 3) & 0x3F;uint8_t tid = orig_header & 0x07;const int MAX_PAYLOAD_SIZE = 1400; // RTP Payload 最大const int FU_HEADER_SIZE = 3; // PayloadHdr(2) + FU Header(1)const int MAX_FRAGMENT_SIZE = MAX_PAYLOAD_SIZE - FU_HEADER_SIZE;int payload_offset = 2; // 跳过原 NALU Header(2字节)int payload_remaining = nalu_size - 2;bool is_first = true;while (payload_remaining > 0) {int fragment_size = std::min(payload_remaining, MAX_FRAGMENT_SIZE);bool is_last = (payload_remaining == fragment_size);RTPPacket packet;// RTP Headerpacket.header.version = 2;packet.header.padding = 0;packet.header.extension = 0;packet.header.cc = 0;packet.header.marker = is_last ? 1 : 0; // 只有最后一片 M=1packet.header.payload_type = 96;packet.header.seq = ctx.seq++;packet.header.timestamp = ctx.timestamp;packet.header.ssrc = ctx.ssrc;// 构造 PayloadHdr (Type=49)uint16_t payload_hdr = (f << 15) | (49 << 9) | (layer_id << 3) | tid;packet.payload[0] = (payload_hdr >> 8) & 0xFF;packet.payload[1] = payload_hdr & 0xFF;// 构造 FU Headeruint8_t fu_header = orig_type; // 低 6 位if (is_first) fu_header |= 0x80; // S=1if (is_last) fu_header |= 0x40; // E=1packet.payload[2] = fu_header;// 复制分片数据memcpy(packet.payload + 3, nalu + payload_offset, fragment_size);packet.payload_size = 3 + fragment_size;// 发送send_udp_packet(&packet);// 日志printf("[SEND] FU: seq=%d ts=%u S=%d E=%d type=%d size=%d M=%d\n",packet.header.seq, packet.header.timestamp,is_first, is_last, orig_type, packet.payload_size, packet.header.marker);payload_offset += fragment_size;payload_remaining -= fragment_size;is_first = false;}
}
4.7 完整接收端实现(关键:组帧逻辑)
class H265FUAssembler {
private:struct FrameBuffer {std::vector<uint8_t> data;uint32_t timestamp;uint16_t orig_nalu_header;bool started;};std::map<uint32_t, FrameBuffer> buffers; // key: timestamppublic:bool process_fu_packet(const RTPPacket* packet,std::vector<uint8_t>& complete_nalu) {const uint8_t* payload = packet->payload;int size = packet->payload_size;uint32_t ts = packet->header.timestamp;// 解析 PayloadHdr (字节 0-1)uint16_t payload_hdr = (payload[0] << 8) | payload[1];uint8_t type = (payload_hdr >> 9) & 0x3F;if (type != 49) {fprintf(stderr, "Error: Not a FU packet (type=%d)\n", type);return false;}// 解析 FU Header (字节 2)uint8_t fu_header = payload[2];bool is_start = (fu_header & 0x80) != 0;bool is_end = (fu_header & 0x40) != 0;uint8_t orig_type = fu_header & 0x3F; // 6 位!// 日志printf("[RECV] FU: seq=%d ts=%u S=%d E=%d type=%d size=%d M=%d\n",packet->header.seq, ts, is_start, is_end, orig_type, size, packet->header.marker);FrameBuffer& buffer = buffers[ts];if (is_start) {// 新 NALU 开始if (buffer.started) {fprintf(stderr, "Warning: New FU start before previous end (ts=%u)\n", ts);}buffer.data.clear();buffer.timestamp = ts;buffer.started = true;// 重建原 NALU Header (2 字节)uint8_t f = (payload_hdr >> 15) & 0x01;uint8_t layer_id = (payload_hdr >> 3) & 0x3F;uint8_t tid = payload_hdr & 0x07;buffer.orig_nalu_header = (f << 15) | (orig_type << 9) | (layer_id << 3) | tid;// 先写入起始码buffer.data.push_back(0x00);buffer.data.push_back(0x00);buffer.data.push_back(0x00);buffer.data.push_back(0x01);// 写入重建的 NALU Headerbuffer.data.push_back((buffer.orig_nalu_header >> 8) & 0xFF);buffer.data.push_back(buffer.orig_nalu_header & 0xFF);}if (!buffer.started) {fprintf(stderr, "Error: FU fragment without start (ts=%u)\n", ts);return false;}// 追加分片数据(从 payload[3] 开始)buffer.data.insert(buffer.data.end(), payload + 3, payload + size);if (is_end) {// 分片完成complete_nalu = std::move(buffer.data);buffers.erase(ts);printf("[ASSEMBLE] Complete NALU: ts=%u size=%zu type=%d\n",ts, complete_nalu.size(), orig_type);return true;}return false;}
};
4.8 关键边界情况处理
❌ 错误 1:丢包导致 NALU 不完整
问题: 收到 S=1 和 E=1 的包,但中间丢了包。
检测:
if (is_start) {expected_seq = packet->header.seq;
}uint16_t actual_seq = packet->header.seq;
uint16_t seq_diff = actual_seq - expected_seq;if (seq_diff != 0) {fprintf(stderr, "Packet loss detected: expected=%d actual=%d\n",expected_seq, actual_seq);// 丢弃当前帧buffer.started = false;buffer.data.clear();return false;
}
expected_seq++;
❌ 错误 2:收到 S=1 时上一个 NALU 未结束
问题: 前一帧的 E=1 包丢失,又收到新帧的 S=1。
处理:
if (is_start && buffer.started) {fprintf(stderr, "Warning: Incomplete NALU discarded (ts=%u)\n",buffer.timestamp);// 丢弃旧帧,开始新帧buffer.data.clear();
}
❌ 错误 3:TS 跳变但 NALU 未完成
问题: 时间戳变了,但上一帧还没收到 E=1。
处理:
// 检查旧帧是否超时
for (auto it = buffers.begin(); it != buffers.end(); ) {uint32_t age = current_ts - it->second.timestamp;if (age > 3000) { // 超过 3000 个时间戳单位(约 33ms@90kHz)fprintf(stderr, "Timeout: Incomplete NALU discarded (ts=%u)\n",it->second.timestamp);it = buffers.erase(it);} else {++it;}
}
第五部分:模式三 - AP 聚合模式
5.1 适用场景
- ✅ 多个小 NALU 需要一起发送(如 VPS + SPS + PPS)
- ✅ 节省 RTP 包数量,减少网络开销
- ✅ 总大小 ≤ MTU 的多个 NALU
5.2 AP Payload 结构
AP(Aggregation Packet,聚合包) 允许将多个小 NALU 打包到一个 RTP 包中。
5.3 PayloadHdr(字节 0-1)
0 10 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|F|Type=48| LayerId | TID |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
关键点:
- Type 字段 = 48(AP 标识)
- LayerId 和 TID 通常从第一个 NALU 获取
5.4 NALU 单元格式
每个 NALU 单元的格式:
字节位置 | 内容 | 说明 |
---|---|---|
Byte 0-1 | NALU Size | 16位无符号整数,大端序 |
Byte 2~N | NALU Data | 完整 NALU(含2字节Header) |
示例结构:
PayloadHdr (2B) | Size1 (2B) | NALU1 | Size2 (2B) | NALU2 | ...48 0x0020 32B 0x0015 21B
5.5 发送端实现
void send_ap_packet(const std::vector<NALU>& nalus, RTPContext& ctx) {// 计算总大小int total_size = 2; // PayloadHdrfor (const auto& nalu : nalus) {total_size += 2 + nalu.size; // Size(2B) + NALU Data}if (total_size > MAX_PAYLOAD_SIZE) {fprintf(stderr, "Error: AP packet too large\n");return;}RTPPacket packet;// RTP Headerpacket.header.version = 2;packet.header.padding = 0;packet.header.extension = 0;packet.header.cc = 0;packet.header.marker = 1; // AP 包通常是完整单元packet.header.payload_type = 96;packet.header.seq = ctx.seq++;packet.header.timestamp = ctx.timestamp;packet.header.ssrc = ctx.ssrc;// 构造 PayloadHdr (Type=48)// 从第一个 NALU 获取 LayerId 和 TIDuint16_t first_nalu_hdr = (nalus[0].data[0] << 8) | nalus[0].data[1];uint8_t layer_id = (first_nalu_hdr >> 3) & 0x3F;uint8_t tid = first_nalu_hdr & 0x07;uint16_t payload_hdr = (0 << 15) | (48 << 9) | (layer_id << 3) | tid;packet.payload[0] = (payload_hdr >> 8) & 0xFF;packet.payload[1] = payload_hdr & 0xFF;int offset = 2;// 添加每个 NALUfor (const auto& nalu : nalus) {// NALU Size (大端序,2字节)packet.payload[offset++] = (nalu.size >> 8) & 0xFF;packet.payload[offset++] = nalu.size & 0xFF;// NALU Datamemcpy(packet.payload + offset, nalu.data, nalu.size);offset += nalu.size;}packet.payload_size = offset;// 发送send_udp_packet(&packet);printf("[SEND] AP: seq=%d ts=%u nalus=%zu total_size=%d M=%d\n",packet.header.seq, packet.header.timestamp,nalus.size(), packet.payload_size, packet.header.marker);
}
5.6 接收端实现
bool process_ap_packet(const RTPPacket* packet,std::vector<std::vector<uint8_t>>& nalus) {const uint8_t* payload = packet->payload;int size = packet->payload_size;// 解析 PayloadHdruint16_t payload_hdr = (payload[0] << 8) | payload[1];uint8_t type = (payload_hdr >> 9) & 0x3F;if (type != 48) {fprintf(stderr, "Error: Not an AP packet (type=%d)\n", type);return false;}int offset = 2; // 跳过 PayloadHdrnalus.clear();// 解析每个 NALUwhile (offset < size) {// 读取 NALU Size (大端序)if (offset + 2 > size) {fprintf(stderr, "Error: Invalid AP packet (truncated size)\n");return false;}uint16_t nalu_size = (payload[offset] << 8) | payload[offset + 1];offset += 2;// 读取 NALU Dataif (offset + nalu_size > size) {fprintf(stderr, "Error: Invalid AP packet (truncated data)\n");return false;}std::vector<uint8_t> nalu;// 写入起始码nalu.push_back(0x00);nalu.push_back(0x00);nalu.push_back(0x00);nalu.push_back(0x01);// 写入 NALU 数据nalu.insert(nalu.end(), payload + offset, payload + offset + nalu_size);nalus.push_back(std::move(nalu));offset += nalu_size;// 解析 Type 用于日志uint16_t nalu_hdr = (payload[offset - nalu_size] << 8) | payload[offset - nalu_size + 1];uint8_t nalu_type = (nalu_hdr >> 9) & 0x3F;printf("[RECV] AP NALU: type=%d size=%d\n", nalu_type, nalu_size);}printf("[RECV] AP: seq=%d ts=%u nalus=%zu total_size=%d\n",packet->header.seq, packet->header.timestamp,nalus.size(), size);return true;
}
5.7 三种模式对比总结
模式 | Type值 | 适用场景 | Payload结构 | 优点 | 缺点 |
---|---|---|---|---|---|
单NALU | 0-47 | NALU ≤ MTU | 直接=NALU | 简单 | 浪费包(小NALU) |
FU分片 | 49 | NALU > MTU | PayloadHdr + FU Header + 片段 | 支持大帧 | 复杂,怕丢包 |
AP聚合 | 48 | 多个小NALU | PayloadHdr + [Size+NALU]… | 节省包数 | 需要额外处理 |
第六部分:调试与问题排查
6.1 常见问题诊断表
症状 | 可能原因 | 检查方法 | 解决方案 |
---|---|---|---|
无法解码 | 起始码缺失 | hexdump output.h265 | head | 确保重组时添加起始码 |
花屏 | FU 分片丢包 | 检查 Seq 连续性 | 实现丢包检测与丢弃机制 |
卡顿 | 时间戳错误 | 检查同帧 TS 是否一致 | 修正时间戳计算 |
Type 解析错误 | 只取 5 位而非 6 位 | 确认掩码为 & 0x3F | 改用 6 位掩码 |
PayloadHdr 错误 | Type 未设为 49 | 检查 (hdr >> 9) & 0x3F | 确保 Type=49 |
FU Header 偏移错 | 读取 payload[1] | 应读取 payload[2] | 修正偏移 |
NALU Header 错误 | 重建时只用 1 字节 | 应重建 2 字节 | 修正重建逻辑 |
总结
H.265 RTP 传输的核心要点:
- ✅ 2 字节 NALU Header,Type 占 6 位
- ✅ FU 模式 Type=49,PayloadHdr(2字节) + FU Header(1字节)
- ✅ FU Header 在
payload[2]
,数据从payload[3]
开始 - ✅ 重建时恢复完整 2 字节 NALU Header
- ✅ 严格的组帧逻辑:检测 S/E 标志,处理丢包
如果您觉得这篇文章对您有帮助,不妨点赞 + 收藏 + 关注,更多音视频开发实战技巧将持续更新 🔥!