☰
RTP详解
2026/10/9 2:53:45 网站建设 项目流程

一、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 值编码时钟频率说明
96H264(动态)90000SDP 中a=rtpmap:96 H264/90000
97AAC(动态)采样率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 数据包中第一个字节的采样时刻,用于同步和抖动计算。

时钟频率由编码格式决定:

编码时钟频率时间戳增量
H26490000 Hz每帧增量 = 90000 / fps
AAC 44100Hz44100 Hz每帧增量 = 1024(每 AAC 帧 1024 采样)
AAC 48000Hz48000 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):

包SEMFU Payload
1100NALU[1..1399]
2011NALU[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 解析)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询