一、RTP 头结构总览(RFC 3550)
RTP 固定头为12 字节(不含 CSRC 列表和扩展头),字段按大端序排列:
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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | SSRC | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | CSRC (0~15 个,每个 4 字节) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | 扩展头(若 X=1,可选) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Payload ... | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
二、各字段详解
V(Version,2 bits)
RTP 协议版本。固定为 2,对应 RFC 3550。发送端必须设为 2,接收端可忽略非 2 的包。
P(Padding,1 bit)
填充标志。若为 1,载荷末尾有 1 个或多个填充字节,这些字节不属于有效载荷。最后一个填充字节的值表示需要忽略的填充字节总数(含自身)。
应用场景:加密算法要求块对齐,或底层协议需要固定长度。音视频 RTSP 推流中通常设为 0。
X(Extension,1 bit)
扩展头标志。若为 1,固定头之后紧跟一个扩展头(格式见 RFC 3550 §5.3.1)。
应用场景:携带传输层无关的扩展信息(如绝对发送时间)。常规 H264/AAC 推流中设为 0。
CC(CSRC Count,4 bits)
CSRC 计数器。表示固定头之后跟随的 CSRC 标识符数量(0~15)。
应用场景:多方混音/混流场景,标识参与混流的多个源。单源推流中设为 0。
M(Marker,1 bit)
标记位。含义由 Payload Type 定义,用于标记帧边界等重要事件。
| 场景 | M 位含义 |
|---|---|
| H264 视频 | 一帧的最后一个 RTP 包 |
| AAC 音频 | 一帧的最后一个 RTP 包(通常一个包一帧) |
| 视频帧分片 | 第一个分片 M=0,最后一个分片 M=1 |
接收端用法:收到 M=1 表示一帧完整接收,可交付解码器。
PT(Payload Type,7 bits)
载荷类型,标识载荷的编码格式,决定接收端如何解释载荷。
| PT 值 | 编码 | 时钟频率 | 说明 |
|---|---|---|---|
| 96 | H264(动态) | 90000 | SDP 中a=rtpmap:96 H264/90000 |
| 97 | AAC(动态) | 采样率 | SDP 中a=rtpmap:97 mpeg4-generic/44100/2 |
| 0~95 | 静态映射 | 固定 | RFC 3551 定义(如 0=PCMU) |
推流端:在 SDP 中协商 PT,通常视频用 96,音频用 97。接收端:根据 SDP 的rtpmap行确定 PT 对应的编码。
Sequence Number(16 bits)
序列号,每发送一个 RTP 包递增 1,用于接收端检测丢包和恢复包序。
初始值应随机(防明文攻击),递增到 65535 后回绕到 0。
接收端用法:
期望值 = 上一个包的序列号 + 1
若收到的序列号 > 期望值 →丢包
若收到的序列号 < 期望值 →乱序或重复
Timestamp(32 bits)
时间戳,反映 RTP 数据包中第一个字节的采样时刻,用于同步和抖动计算。
时钟频率由编码格式决定:
| 编码 | 时钟频率 | 时间戳增量 |
|---|---|---|
| H264 | 90000 Hz | 每帧增量 = 90000 / fps |
| AAC 44100Hz | 44100 Hz | 每帧增量 = 1024(每 AAC 帧 1024 采样) |
| AAC 48000Hz | 48000 Hz | 每帧增量 = 1024 |
初始值应随机,但同一帧的所有分片 RTP 包必须使用相同的时间戳。
示例(H264 30fps):
第 1 帧:timestamp = T0
第 2 帧:timestamp = T0 + 90000/30 = T0 + 3000
第 3 帧:timestamp = T0 + 6000
Frame::dts是 90kHz 时基,H264 可直接作为 RTP timestamp;AAC 需从 90kHz 转换为采样率时基:
rtpTs = (dts90k * sampleRate) / 90000
SSRC(Synchronization Source,32 bits)
同步源标识符,唯一标识 RTP 流的发送者,随机生成。
同一 SSRC 的包属于同一媒体流
传输地址变更时 SSRC 应更新
同一会话中不同媒体流(视频/音频)使用不同 SSRC
生成建议:QRandomGenerator::global()->generate()
CSRC(Contributing Source,0~15 × 4 bytes)
贡献源标识符列表,仅在 CC > 0 时存在。每个 CSRC 是 4 字节,标识参与混流的源。
单源推流中 CC=0,此字段不存在。接收端解析时不能硬编码偏移 12,必须根据 CC 和 X 动态计算:
int headerLen = 12 + cc * 4; if (x) { uint16_t extLen = load_be16(p + headerLen + 2); headerLen += 4 + extLen * 4; }三、H264 RTP 封包详解(RFC 6184)
H264 的 RTP 载荷有三种模式:
模式 1:单 NALU 包(1 ≤ NAL type ≤ 23)
一个 RTP 包承载一个完整的 NALU,RTP 头之后直接跟 NALU(不含起始码)。
+---------------+------------------+ | RTP Header | NALU (无起始码) | +---------------+------------------+
适用条件:NALU 大小 ≤ MTU(通常 1400 字节)。
模式 2:STAP-A(NAL type = 24)
单时间聚合包,把多个小 NALU 聚合成一个 RTP 包发送。
+---------------+------+------+------+------+-----+ | RTP Header |STAP-A|NALU1 |NALU2 | ... |NALU N| | | Hdr |Len+Data|Len+Data| | | +---------------+------+------+------+------+-----+
STAP-A 头(1 字节):nal_type = 24。之后每个 NALU 前有2 字节长度(大端)。
适用条件:SPS/PPS 一起发送,或多个小 slice 聚合。
模式 3:FU-A(NAL type = 28)
分片单元,把一个大 NALU 拆成多个 RTP 包。
FU-A 结构(每个分片包):
+-----------+-----------+-----------+-----------------+ | RTP Header| FU Indicator| FU Header | FU Payload | | (12 字节) | (1 字节) | (1 字节) | (NALU 分片数据) | +-----------+-----------+-----------+-----------------+
FU Indicator(1 字节):
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |F|NRI| Type=28 | +-+-+-+-+-+-+-+-+
F:禁止位,与原始 NALU 的 F 位一致
NRI:与原始 NALU 的 NRI 一致
Type:固定为 28(FU-A)
FU Header(1 字节):
0 1 2 3 4 5 6 7 +-+-+-+-+-+-+-+-+ |S|E|R| Type | +-+-+-+-+-+-+-+-+
S(Start):1 表示分片起始(第一个包)
E(End):1 表示分片结束(最后一个包)
R:保留,设为 0
Type:原始 NALU 的 NAL type(如 5 表示 IDR)
分片规则:
第一个分片:S=1,E=0,
FU Payload = 原始 NALU 去掉第 1 字节中间分片:S=0,E=0
最后一个分片:S=0,E=1,M 位设为 1
完整示例(一个 IDR 帧,2048 字节 NALU,MTU 1400):
| 包 | S | E | M | FU Payload |
|---|---|---|---|---|
| 1 | 1 | 0 | 0 | NALU[1..1399] |
| 2 | 0 | 1 | 1 | NALU[1400..2047] |
四、AAC RTP 封包详解(RFC 3640)
AAC 使用mpeg4-generic载荷格式,RTP 载荷由AU Header Section + AU Data Section组成。
完整结构
+-------------------+---------------------+---------------------+ | AU-headers-length | AU-header(s) | AAC raw data | | (2 字节, 大端) | (每个 2 字节) | (无 ADTS 头) | +-------------------+---------------------+---------------------+
AU-headers-length(2 字节)
表示后面AU-header 部分的总比特数。对于单个 AAC 帧,固定为 16(即 2 字节)。
AU-header(2 字节)
0 1 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | AU-size (13 bits) |AU-Idx (3)| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
AU-size(13 bits):AAC 帧的原始字节数(不含 AU-header)
AU-Index(3 bits):固定为 0(非交错模式)
完整 AAC RTP 包示例
假设 raw AAC 帧为 380 字节:
+-----------+-----------+-----------+---------------+ | RTP Header| 00 10 | AU-size | AAC raw data | | (12 字节) | (16 bits) | (13+3 bits)| (380 字节) | +-----------+-----------+-----------+---------------+
AU-header 的13 位 AU-size = 380(0x17C),3 位 Index = 0:
第 1 字节:0x01 (高 5 位 of 13-bit size) 第 2 字节:0x7C (低 8 位 of 13-bit size) + 3 位 Index=0 填充
接收端解析:
cpp
uint16_t auHeadersLen = load_be16(payload); // 应为 16 uint16_t auHeader = load_be16(payload + 2); int auSize = (auHeader >> 3) & 0x1FFF; // 13 bits const uint8_t* aacData = payload + 4;
五、代码实现
5.1 RtpPacketizer 接口
// protocol/rtsp/rtp/RtpPacketizer.h class RtpPacketizer { public: using PacketCB = std::function<void(const QByteArray& rtpPacket)>; void setOnPacket(PacketCB cb) { _onPacket = std::move(cb); } void setSsrc(uint32_t ssrc) { _ssrc = ssrc; } void setPayloadType(uint8_t pt) { _pt = pt; } // H264: 输入 Frame(Annex-B),输出 0~N 个 RTP 包 void packetizeH264(const Frame::Ptr& frame); // AAC: 输入 Frame(raw AAC),输出 1 个 RTP 包 void packetizeAAC(const Frame::Ptr& frame); private: uint16_t _seq = 0; // 初始随机 uint32_t _ssrc = 0; uint8_t _pt = 96; PacketCB _onPacket; QByteArray buildRtpHeader(bool marker, uint32_t timestamp); };5.2 H264 封包核心实现
void RtpPacketizer::packetizeH264(const Frame::Ptr& frame) { // 1. 拆分 Annex-B 为 NALU 列表 auto nalus = H264Utils::splitNalus(frame->data); // 2. 时间戳:Frame::dts 已是 90kHz,直接使用 uint32_t ts = static_cast<uint32_t>(frame->dts); // 3. 过滤 SPS/PPS(单独发送或聚合发送) QList<QByteArray> mediaNalus; for (auto& nalu : nalus) { int type = uint8_t(nalu[0]) & 0x1F; if (type == 7 || type == 8) continue; // SPS/PPS 单独处理 mediaNalus.append(nalu); } // 4. 逐 NALU 封包 for (int i = 0; i < mediaNalus.size(); ++i) { const auto& nalu = mediaNalus[i]; bool isLastNalu = (i == mediaNalus.size() - 1); if (nalu.size() + 12 <= kMtu) { // 单 NALU 包 QByteArray pkt = buildRtpHeader(isLastNalu && frame->keyFrame, ts); pkt.append(nalu); if (_onPacket) _onPacket(pkt); } else { // FU-A 分片 packetizeFuA(nalu, ts, isLastNalu && frame->keyFrame); } } } void RtpPacketizer::packetizeFuA(const QByteArray& nalu, uint32_t ts, bool lastNaluMarker) { uint8_t nalHeader = uint8_t(nalu[0]); uint8_t nri = (nalHeader & 0x60) >> 5; uint8_t nalType = nalHeader & 0x1F; uint8_t fuIndicator = (nri << 5) | 28; // Type = 28 const int maxPayload = kMtu - 14; // 12 RTP + 2 FU int offset = 1; // 跳过原始 NALU 头 bool first = true; while (offset < nalu.size()) { int chunkSize = qMin(maxPayload, nalu.size() - offset); bool last = (offset + chunkSize >= nalu.size()); uint8_t fuHeader = nalType; if (first) fuHeader |= 0x80; // S=1 if (last) fuHeader |= 0x40; // E=1 bool marker = (last && lastNaluMarker); QByteArray pkt = buildRtpHeader(marker, ts); pkt.append(char(fuIndicator)); pkt.append(char(fuHeader)); pkt.append(nalu.constData() + offset, chunkSize); if (_onPacket) _onPacket(pkt); offset += chunkSize; first = false; } }5.3 AAC 封包核心实现
void RtpPacketizer::packetizeAAC(const Frame::Ptr& frame) { // frame->data 是 raw AAC(无 ADTS 头) // 需要知道采样率,从 Track 获取 int sampleRate = _audioTrack->sampleRate; // 时间戳转换:dts 是 90kHz → 采样率时基 uint32_t ts = static_cast<uint32_t>(frame->dts * sampleRate / 90000); // 构建 AU Header Section QByteArray auHeader; uint16_t auHeadersLen = 16; // 单个 AU-header,固定 16 bits auHeader.append(char(auHeadersLen >> 8)); auHeader.append(char(auHeadersLen & 0xFF)); // AU-header: 13 bits size + 3 bits index(=0) uint16_t sizeAndIndex = (frame->data.size() << 3) & 0xFFF8; auHeader.append(char(sizeAndIndex >> 8)); auHeader.append(char(sizeAndIndex & 0xFF)); // 构建完整 RTP 包 bool marker = true; // AAC 一帧一包,M=1 QByteArray pkt = buildRtpHeader(marker, ts); pkt.append(auHeader); pkt.append(frame->data); // raw AAC if (_onPacket) _onPacket(pkt); }5.4 RTP 头构建
QByteArray RtpPacketizer::buildRtpHeader(bool marker, uint32_t timestamp) { QByteArray hdr(12, '\0'); uint8_t* p = reinterpret_cast<uint8_t*>(hdr.data()); // 第 1 字节: V=2, P=0, X=0, CC=0 p[0] = 0x80; // 10 000000 // 第 2 字节: M=marker, PT=payload type p[1] = (marker ? 0x80 : 0x00) | (_pt & 0x7F); // 序列号(大端) p[2] = uint8_t(_seq >> 8); p[3] = uint8_t(_seq & 0xFF); _seq++; // 时间戳(大端) p[4] = uint8_t(timestamp >> 24); p[5] = uint8_t(timestamp >> 16); p[6] = uint8_t(timestamp >> 8); p[7] = uint8_t(timestamp & 0xFF); // SSRC(大端) p[8] = uint8_t(_ssrc >> 24); p[9] = uint8_t(_ssrc >> 16); p[10] = uint8_t(_ssrc >> 8); p[11] = uint8_t(_ssrc & 0xFF); return hdr; }六、与 RTSP 会话的集成
推流端 SETUP 时,SDP 中声明 PT 和编码:
m=video 0 RTP/AVP 96 a=rtpmap:96 H264/90000 a=fmtp:96 packetization-mode=1;profile-level-id=42a01e; sprop-parameter-sets=Z0KAHpY0Cg==,aM48gA== m=audio 0 RTP/AVP 97 a=rtpmap:97 mpeg4-generic/44100/2 a=fmtp:97 profile-level-id=15;mode=AAC-hbr;SizeLength=13; IndexLength=3;IndexDeltaLength=3;config=1210
推流端从Track::extradata提取 SPS/PPS 填充sprop-parameter-sets,音频从AudioSpecificConfig转 Base64 填充config。
接收端解析 SDP 后,用 PT 和编码类型初始化RtpDepacketizer(FU-A 重组、AU-header 解析)。