资讯动态

Qt/C++ 手撸 RTMP 拉流:从 Chunk 解包到 AVCC 转 AnnexB 的完整实现

发布时间:2026/10/21 5:54:57 来源:尧图企业网站定制
之前写了两篇推流文章RTSP 推流、RTMP 推流这次补齐——RTMP 拉流。拉流和推流方向相反但协议的坑一点不少。本文按拉流的实际时序展开每一步都给出真实的十六进制字节并逐字段中文注释。读完你能彻底搞懂RTMP 三次握手的字节交换过程Chunk 分片如何重组为完整消息AMF0 二进制编码的每条命令AVCC → AnnexB 的转换拉流方向AVCDecoderConfigurationRecord 的字段含义全文覆盖四条主线握手 → 信令 → Chunk 重组 → 载荷转换。一、RTMP vs RTSP 对比先建立全局认知维度RTSPRTMP传输层TCP 信令 UDP/TCP RTP纯 TCP握手无直接 OPTIONSC0/C1/C2 S0/S1/S2信令编码文本RTSP 头AMF0 二进制载荷格式RTP 分片Chunk 分片H.264 格式AnnexBAVCCconfig 传法SDPsprop-parameter-sets独立 Sequence Header 消息端口5541935拉流信令OPTIONS → DESCRIBE → SETUP → PLAYconnect → createStream → play核心洞察RTSP 靠文本协议 RTP 分片RTMP 靠二进制协议 Chunk 分片。两者对 H.264 的表示方式完全不同这是所有坑的根源。二、RTMP 四层模型┌──────────────────────────────────┐ │ Application Layer │ connect / createStream / play │ (AMF0 命令消息) │ ├──────────────────────────────────┤ │ Message Layer │ typeId length streamId body ├──────────────────────────────────┤ │ Chunk Layer │ ★ 把 Message 切成固定大小 Chunk ├──────────────────────────────────┤ │ Transport Layer │ 纯 TCP └──────────────────────────────────┘四个关键概念概念含义Message一条完整逻辑消息一个视频帧 / 一个命令ChunkMessage 的分片每个最多chunkSize字节Chunk Stream用CSID区分多条逻辑通道Message Stream用MSID区分同一连接的多个流为什么要分 ChunkTCP 是字节流一次写太大会阻塞低优先级消息。分片后可以交错发送不同 Chunk Stream 的数据。默认 chunkSize 128握手后双方可用SetChunkSize消息修改。三、★ Chunk 结构详解每个 Chunk 三部分┌──────────────────┬──────────────────┬─────────────┐ │ Basic Header │ Message Header │ Chunk Data │ │ (1~3 字节) │ (0/3/7/11 字节) │ (≤ chunkSize)│ └──────────────────┴──────────────────┴─────────────┘3.1 Basic Header1~3 字节第 1 字节 bit 布局0 1 2 3 4 5 6 7 -------- | fmt | csid | -------- 2bit 6bitfmt2 bitMessage Header 类型0~3csid6 bitChunk Stream ID1~63csid 三种编码csid 值字节数编码2~631直接写在低 6 bit64~3192低 6 bit 填 0第 2 字节 csid-64320~655993低 6 bit 填 1第 2/3 字节 (csid-64) 小端中文示例csid 5 → 1 字节 0x05 csid 63 → 1 字节 0x3F csid 64 → 2 字节 0x00 0x00 csid 100 → 2 字节 0x00 0x24 (100 - 64 36 0x24) csid 320 → 3 字节 0x01 0x00 0x01常用 CSIDCSID用途2协议控制SetChunkSize/Ack/WinSize3命令connect/createStream/play4音频数据5视频数据3.2 Message Header0/3/7/11 字节fmt 决定大小fmt大小包含字段011timestamp length typeId streamId17timestamp delta length typeId23timestamp delta30无完全继承fmt011 字节逐字段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 -------------------------------- | timestamp (3B, 大端) | -------------------------------- | message length (3B, 大端) | -------------------------------- | message type id (1B) | | ------------ | message stream id (4B, ★小端) | --------------------------------字段大小字节序含义timestamp3大端绝对时间戳毫秒0xFFFFFF 用扩展message length3大端消息体字节数message type id1-8音频, 9视频, 20命令message stream id4小端Message Stream ID唯一小端字段Extended Timestamp4 字节当 timestamp 0xFFFFFF时追加。fmt3 特殊规则如果上一个 Chunk 用了扩展时间戳本 Chunk 也必须带。3.3 ★ 完整分片示例重点假设300 字节视频帧chunkSize128csid5streamId1timestamp1000。分片计算第 1 片128 字节第 2 片128 字节第 3 片44 字节第 1 个 Chunkfmt0偏移 字节值 含义 ───────────────────────────────────── [0] 0x05 Basic Header: fmt0, csid5 [1] 0x00 timestamp 高位 [2] 0x03 timestamp 中位 [3] 0xE8 timestamp 低位 (1000 0x0003E8) [4] 0x00 length 高位 [5] 0x01 length 中位 [6] 0x2C length 低位 (300 0x00012C) [7] 0x09 type 9 (MSG_VIDEO) [8] 0x01 streamId 小端第 1 字节 (1 0x00000001) [9] 0x00 streamId 第 2 字节 [10] 0x00 streamId 第 3 字节 [11] 0x00 streamId 第 4 字节 [12..139] 视频数据前 128 字节十六进制拼接05 00 03 E8 00 01 2C 09 01 00 00 00 128B 数据第 2 个 Chunkfmt3[0] 0xC5 Basic Header: fmt3, csid5 (0b116|5 0xC5) [1..128] 视频数据第 129~256 字节第 3 个 Chunkfmt3[0] 0xC5 Basic Header [1..44] 视频数据剩余 44 字节核心结论第一片带完整 11 字节头后续片只带 1 字节 Basic Header。四、RTMP 三次握手4.1 握手流程C0 (1B: 0x03) C1 (1536B) → S0 (1B) S1 (1536B) S2 (1536B) C2 (1536B) → 完成4.2 C0C1客户端 → 服务器实际字节前 32 字节示例┌──────────────────────────────────────────────────────────────────┐ │ 03 ← C0: 版本号 0x03 │ ├──────────────────────────────────────────────────────────────────┤ │ 00 00 00 00 ← C1 前 4 字节客户端时间戳毫秒 │ │ 00 00 00 00 ← C1 第 5~8 字节保留必须为 0 │ │ XX XX XX XX XX XX XX XX ... ← C1 第 9~1536 字节随机字节 │ │ ... │ └──────────────────────────────────────────────────────────────────┘中文说明字段大小含义C0.version1B固定 0x03C1.timestamp4B客户端启动时间戳C1.zero4B固定 0x00000000C1.random1528B随机字节⚠️ 关键ZLMediaKit 会嗅探 C1 判断是否支持复杂握手。建议 C1 用全 0 或固定字节避免随机数碰巧匹配QByteArray c1(1536, \0); // 全填 0 uint32_t now QDateTime::currentMSecsSinceEpoch(); ByteOrder::set_be32(c1.data(), now); // 前 4 字节时间戳4.3 S0S1S2服务器 → 客户端实际字节03 ← S0: 版本号 0x03 00 00 00 00 ← S1 前 4 字节服务器时间戳 00 00 00 00 ← S1 第 5~8 字节保留零 A1 C2 3F 88 90 11 5E 7B ... ← S1 第 9~1536 字节随机 ... ← 直到 1536 字节 C1 的完整 1536 字节数据 ← S2: 服务器对 C1 的回显总长度1 1536 1536 3073 字节4.4 C2客户端 → 服务器简单握手C2 S1S1 的完整 1536 字节数据握手成功日志RtmpClient: handshake finished五、AMF0 编解码RTMP 命令消息用AMF0Action Message Format 0编码。5.1 类型标记值名称说明0x00Number8 字节 double 大端0x01Boolean1 字节0x02String2 字节长度 UTF-80x03Object键值对00 00 09结尾0x05Null无附加0x08ECMA Array4 字节 count 键值对0x0AStrict Array4 字节 count 元素5.2 connect 命令的中文示例客户端发送内容enc.writeString(connect); enc.writeNumber(1.0); AmfValue obj AmfValue::makeObject(); obj.set(app, AmfValue::makeString(live)); obj.set(tcUrl, AmfValue::makeString(rtmp://127.0.0.1:1935/live)); obj.set(flashVer, AmfValue::makeString(FMLE/3.0)); obj.set(audioCodecs, AmfValue::makeNumber(0x0400)); obj.set(videoCodecs, AmfValue::makeNumber(0x0080)); enc.writeObject(obj);生成的字节流逐字节中文注释02 00 07 63 6F 6E 6E 65 63 74 ← String connect长度 7 00 3F F0 00 00 00 00 00 00 ← Number 1.08B double 大端 03 ← Object 开始 00 03 61 70 70 ← key app 02 00 04 6C 69 76 65 ← String live 00 05 74 63 55 72 6C ← key tcUrl 02 00 1D 72 74 6D 70 3A 2F 2F ... ← String rtmp://127.0.0.1:1935/live 00 08 66 6C 61 73 68 56 65 72 ← key flashVer 02 00 0C 46 4D 4C 45 2F 33 2E 30 ← String FMLE/3.0 00 0B 61 75 64 69 6F 43 6F 64 65 63 73 ← key audioCodecs 00 40 B0 00 00 00 00 00 00 ← Number 1024 (0x400 AAC) 00 0B 76 69 64 65 6F 43 6F 64 65 63 73 ← key videoCodecs 00 40 80 00 00 00 00 00 00 ← Number 128 (0x80 H.264) 00 00 09 ← 空 key ObjectEnd关键02 00 07String 类型 长度 72 字节大端00 3F F0 ...Number 类型 double 大端1.0 0x3FF000000000000000 00 09Object 结束标记六、完整信令流程6.1 时序图客户端 服务器 │ │ │──── TCP 连接 ──────────────────────────►│ │ │ │──── C0C1 (11536B) ──────────────────►│ 握手 │◄─── S0S1S2 (115361536B) ───────────│ │──── C2 ( S1, 1536B) ────────────────►│ │ │ │──── connect (AMF0) ──────────────────►│ 建连 │◄─── _result (Success) ─────────────────│ │◄─── Window Ack Size ──────────────────│ │◄─── Set Peer Bandwidth ───────────────│ │ │ │──── createStream (AMF0) ─────────────►│ 建流 │◄─── _result (streamId1) ─────────────│ │ │ │──── play test -2000 (AMF0) ────────►│ 播放 │◄─── onStatus (Play.Reset) ────────────│ │◄─── onStatus (Play.Start) ────────────│ │◄─── onStatus (Data.Start) ────────────│ │◄─── onMetaData ───────────────────────│ │◄─── Video Sequence Header (avcC) ─────│ │◄─── Audio Sequence Header (ASC) ──────│ │ │ │◄─── Video Frame (AVCC) ───────────────│ 数据 │◄─── Audio Frame (raw AAC) ────────────│ │◄─── ... 持续 ... │6.2 connect 命令客户端发Chunk 头 AMF0 数据03 ← Basic Header: fmt0, csid3 00 00 00 ← timestamp 0 00 00 C0 ← length 192 (0xC0) 14 ← type 20 (MSG_CMD) 00 00 00 00 ← streamId 0 (小端) 上面 5.2 节的 AMF0 数据服务器回text03 00 00 00 00 00 B8 14 00 00 00 00 02 00 07 5F 72 65 73 75 6C 74 ← _result 00 3F F0 00 00 00 00 00 00 ← txn 1 05 ← Null 03 ... 00 00 09 ← 服务器信息 03 ... 00 00 09 ← 结果信息 level status code NetConnection.Connect.Success ← ★ 成功标志6.3 createStream 命令客户端发03 00 00 00 00 00 19 14 00 00 00 00 02 00 0C 63 72 65 61 74 65 53 74 72 65 61 6D ← createStream 00 40 00 00 00 00 00 00 00 ← txn 2 05 ← Null服务器回03 00 00 00 00 00 15 14 00 00 00 00 02 00 07 _result 00 40 00 00 00 00 00 00 00 ← txn 2 05 ← Null 00 3F F0 00 00 00 00 00 00 ← ★ streamId 16.4 play 命令客户端发03 00 00 00 00 00 20 14 01 00 00 00 ← ★ streamId 1 (小端) 02 00 04 70 6C 61 79 ← play 00 40 08 00 00 00 00 00 00 ← txn 3 05 ← Null 02 00 04 74 65 73 74 ← String test 00 C0 9F 40 00 00 00 00 00 ← Number -2000.0负数live6.5 服务器响应三个 onStatus02 00 08 6F 6E 53 74 61 74 75 73 ← onStatus 00 00 00 00 00 00 00 00 00 ← txn 0 05 ← Null 03 ... 00 00 09 ← Object level status code NetStream.Play.Start ← ★ 播放开始七、媒体描述信息7.1 onMetaData 示例03 00 00 00 00 01 06 12 00 00 00 00 02 00 0D 40 73 65 74 44 61 74 61 46 72 61 6D 65 ← setDataFrame 02 00 0A 6F 6E 4D 65 74 61 44 61 74 61 ← onMetaData 08 ← ECMA Array 00 00 00 07 ← count 7 00 08 64 75 72 61 74 69 6F 6E ← key duration 00 00 00 00 00 00 00 00 ← Number 0直播 00 05 77 69 64 74 68 ← key width 00 40 88 00 00 00 00 00 00 ← Number 1920 00 06 68 65 69 67 68 74 ← key height 00 40 87 00 00 00 00 00 00 ← Number 1080 00 09 66 72 61 6D 65 72 61 74 65 ← key framerate 00 40 39 00 00 00 00 00 00 ← Number 25.0 00 0C 76 69 64 65 6F 63 6F 64 65 63 69 64 ← key videocodecid 00 40 1C 00 00 00 00 00 00 ← Number 7 (H.264) 00 0C 61 75 64 69 6F 63 6F 64 65 63 69 64 ← key audiocodecid 00 40 24 00 00 00 00 00 00 ← Number 10 (AAC) 00 00 09 ← ECMA Array 结束7.2 ★ Video Sequence Header最重要的 config 帧05 ← csid5 (视频通道) 00 00 00 ← timestamp 0 00 00 2A ← length 42 09 ← type 9 (MSG_VIDEO) 01 00 00 00 ← streamId 1 ├─────────────────────────────────────────────────────┤ │ 17 ← ★ Video Tag Header │ │ │ bit7-4 1 (key) │ │ │ bit3-0 7 (AVC) │ │ 00 ← AVCPacketType 0 │ │ │ (0 Sequence Header)│ │ 00 00 00 ← CompositionTime 0 │ │ 01 ← ★ configurationVersion1 │ │ 42 C0 1E ← profile/compat/level │ │ FF ← lengthSizeMinusOne3 │ │ E1 ← numOfSPS 1 │ │ 00 19 ← SPS 长度 25 │ │ 67 42 C0 1E D9 00 50 05 BB 01 10 00 00 03 00 10 00 00 03 03 C8 F1 22 6A 2C │ │ ← SPS 数据25 字节 │ │ 01 ← numOfPPS 1 │ │ 00 05 ← PPS 长度 5 │ │ 68 CE 3C 80 ← PPS 数据5 字节 │ └─────────────────────────────────────────────────────┘关键data[0]必须是0x01configurationVersion。服务器会检查推流端发 AnnexB 会被拒绝。7.3 Audio Sequence Header04 ← csid4 (音频通道) 00 00 00 ← timestamp 0 00 00 04 ← length 4 08 ← type 8 (MSG_AUDIO) 01 00 00 00 ← streamId 1 ├─────────────────────────────────────────────┤ │ AF ← Audio Tag Header │ │ │ 10AAC │ │ │ 344kHz │ │ │ 116bit │ │ │ 1stereo │ │ 00 ← AACPacketType 0 │ │ 12 10 ← AudioSpecificConfig │ └─────────────────────────────────────────────┘ASC 解析12 100001 0010 0001 0000audioObjectType 2 (AAC-LC) samplingFreqIdx 4 (44100 Hz) channelConfig 2 (stereo)八、★ AVCC → AnnexB 转换8.1 两种格式对比AVCCRTMP 里[00 00 00 19] | 67 42 C0 1E ... ← 4 字节长度 SPS [00 00 00 05] | 68 CE 3C 80 ... ← 4 字节长度 PPS [00 00 0A B4] | 65 88 84 ... ← 4 字节长度 IDRAnnexBRTSP / 裸流用00 00 00 01 | 67 42 C0 1E ... ← 起始码 SPS 00 00 00 01 | 68 CE 3C 80 ... ← 起始码 PPS 00 00 00 01 | 65 88 84 ... ← 起始码 IDR核心区别起始码 ↔ 4 字节长度前缀大端。8.2 转换代码static QByteArray avccToAnnexB(const QByteArray avcc, int naluLenSize) { QByteArray out; if (naluLenSize ! 1 naluLenSize ! 2 naluLenSize ! 4) naluLenSize 4; const uint8_t* p reinterpret_castconst uint8_t*(avcc.constData()); int size avcc.size(); int off 0; static const uint8_t kStartCode[4] {0,0,0,1}; while (off naluLenSize size) { // 读长度字段大端 uint32_t len 0; if (naluLenSize 1) len p[off]; else if (naluLenSize 2) len (p[off] 8) | p[off1]; else len (p[off] 24) | (p[off1] 16) | (p[off2] 8) | p[off3]; off naluLenSize; if (off int(len) size) break; out.append((const char*)kStartCode, 4); // 加起始码 out.append((const char*)(p off), len); // 加 NALU off len; } return out; }九、★ AVCDecoderConfigurationRecord 解析9.1 结构[0] configurationVersion 0x01 [1] AVCProfileIndication SPS[1] [2] profile_compatibility SPS[2] [3] AVCLevelIndication SPS[3] [4] 0xFF (reserved(6) | lengthSizeMinusOne(2)) [5] 0xE1 (reserved(3) | numOfSPS(5)) [6..7] SPS length (大端) [8..] SPS 数据 [...] 0x01 (numOfPPS1) [...] PPS length [...] PPS 数据9.2 解析代码static QByteArray parseAvcConfig(const QByteArray cfg, int naluLenSize) { if (cfg.size() 7) return QByteArray(); const uint8_t* p reinterpret_castconst uint8_t*(cfg.constData()); // lengthSizeMinusOne 在 [4] 的低 2 bit naluLenSize (p[4] 0x03) 1; int off 5; int numSps p[off] 0x1F; QByteArray out; static const uint8_t kStartCode[4] {0,0,0,1}; // 解析 SPS for (int i 0; i numSps off 2 cfg.size(); i) { int len (p[off] 8) | p[off1]; off 2; if (off len cfg.size()) return out; out.append((const char*)kStartCode, 4); out.append((const char*)(p off), len); off len; } // 解析 PPS if (off cfg.size()) return out; int numPps p[off]; for (int i 0; i numPps off 2 cfg.size(); i) { int len (p[off] 8) | p[off1]; off 2; if (off len cfg.size()) return out; out.append((const char*)kStartCode, 4); out.append((const char*)(p off), len); off len; } return out; }9.3 实际收到字节示例17 ← FrameType1(key) | Codec7(AVC) 00 ← AVCPacketType0 (Sequence Header) 00 00 00 ← CompositionTime0 01 ← ★ configurationVersion1 42 C0 1E ← profile/compat/level FF ← lengthSizeMinusOne3 E1 ← numOfSPS1 00 19 ← SPS 长度25 67 42 C0 1E D9 ... ← SPS 数据25B 01 ← numOfPPS1 00 05 ← PPS 长度5 68 CE 3C 80 ← PPS 数据5B转换后的 AnnexB00 00 00 01 | 67 42 C0 1E D9 ... ← 起始码 SPS 00 00 00 01 | 68 CE 3C 80 ← 起始码 PPS现象原因修复握手后立即断开C1 随机字节触发复杂握手C1 用固定字节_result收不到connect 参数不合法检查 app/tcUrlplay 后无数据app/stream 拼错检查 URL黑屏Sequence Header 没收到检查 avcType0 分支花屏AVCC → AnnexB 转换错检查长度前缀处理音频无声ASC 没解析对从 ASC 取采样率时间戳跳变RTMP 用毫秒dts_90k / 90RTMP 拉流信令的四个阶段阶段命令关键点握手C0/C1/C2 ↔ S0/S1/S2C1 别用随机字节建连connect → _resultAMF0 编码检查 Success建流createStream → _result拿 streamId播放play → onStatus收 Sequence Header三层要点Chunk 层fmt0 首片 fmt3 续片streamId 小端AMF 层二进制编码String 2 字节长度Number 8 字节 double载荷层AVCC ↔ AnnexBavcC 首字节0x01与推流端的镜像关系推流拉流buildAvcC()拼 avcCparseAvcConfig()拆 avcCannexBToAvcc()转 AVCCavccToAnnexB()转 AnnexBsendVideoConfig()发handleVideo(avcType0)收sendVideoFrame()发 AVCChandleVideo(avcType1)转 AnnexB核心一句话RTMP 拉流 握手 AMF0 信令 Chunk 重组 AVCC 转 AnnexB。四条主线和 RTSP 一一对应只是每层都换了一套编码方式。把这三层理清RTMP 拉流就不再是黑盒。遇到协议问题先看日志的字节偏移和字段名几乎都能定位到具体层。rtmp拉流

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑