深入理解 RTP 协议与 H.264 视频传输:从核心原理到实战解析
一、为什么需要 RTP?------ 实时音视频传输的基石
在互联网上传输音视频数据,如果直接使用 TCP 协议,虽然可靠,但存在重传机制导致的延迟,不适合实时通话。如果直接使用 UDP 协议,虽然延迟低,但数据包乱序、丢包问题无法解决。
RTP(Real-time Transport Protocol,实时传输协议) 应运而生,它通常运行在 UDP 之上,提供了序列号(Sequence Number)和 时间戳(Timestamp)等关键信息,帮助接收端进行数据重组、抖动缓冲和丢包检测,从而保证音视频流的实时性和流畅性。
二、 RTP 协议头部结构:12 字节的"信封"
RTP 包本身不关心承载的内容是什么,它只提供一个标准的"信封"。这个信封就是 RTP 固定头部,长度为 12 字节。
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| synchronization source (SSRC) identifier |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
各字段含义解析:
| 字段 | 位数 | 含义 |
|---|---|---|
| V (Version) | 2 bits | 协议版本号,当前标准为 2。 |
| P (Padding) | 1 bit | 填充标志。若 P=1,则包尾包含填充字节,最后一字节指示填充长度。 |
| X (Extension) | 1 bit | 扩展标志。若 X=1,则在固定头部之后有一个扩展头部。 |
| CC (CSRC Count) | 4 bits | 贡献源标识符的计数。表示固定头部后跟的 CSRC 列表数量(0-15)。 |
| M (Marker) | 1 bit | 标记位。由应用层定义,常用于标记视频帧的结束或音频会话的开始。 |
| PT (Payload Type) | 7 bits | 载荷类型。标识 RTP 载荷的编码格式(如 G.711、H.264、AAC 等)。 |
| Sequence Number | 16 bits | 序列号。每发送一个 RTP 包,序列号加 1。用于接收端检测丢包和重组数据。 |
| Timestamp | 32 bits | 时间戳。反映 RTP 载荷中第一个字节的采样时刻,用于同步和抖动缓冲。 |
| SSRC | 32 bits | 同步源标识符。在一个 RTP 会话中唯一标识一个媒体流。 |
RTP 头部 12 字节位分布图:
#mermaid-svg-HKLjTU4sJEO6k42U{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-HKLjTU4sJEO6k42U .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-HKLjTU4sJEO6k42U .error-icon{fill:#552222;}#mermaid-svg-HKLjTU4sJEO6k42U .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-HKLjTU4sJEO6k42U .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-HKLjTU4sJEO6k42U .marker{fill:#333333;stroke:#333333;}#mermaid-svg-HKLjTU4sJEO6k42U .marker.cross{stroke:#333333;}#mermaid-svg-HKLjTU4sJEO6k42U svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-HKLjTU4sJEO6k42U p{margin:0;}#mermaid-svg-HKLjTU4sJEO6k42U .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .cluster-label text{fill:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .cluster-label span,#mermaid-svg-HKLjTU4sJEO6k42U p{color:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .label text,#mermaid-svg-HKLjTU4sJEO6k42U span,#mermaid-svg-HKLjTU4sJEO6k42U p{fill:#333;color:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .node rect,#mermaid-svg-HKLjTU4sJEO6k42U .node circle,#mermaid-svg-HKLjTU4sJEO6k42U .node ellipse,#mermaid-svg-HKLjTU4sJEO6k42U .node polygon,#mermaid-svg-HKLjTU4sJEO6k42U .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-HKLjTU4sJEO6k42U .flowchart-label text{text-anchor:middle;}#mermaid-svg-HKLjTU4sJEO6k42U .node .label{text-align:center;}#mermaid-svg-HKLjTU4sJEO6k42U .node.clickable{cursor:pointer;}#mermaid-svg-HKLjTU4sJEO6k42U .arrowheadPath{fill:#333333;}#mermaid-svg-HKLjTU4sJEO6k42U .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-HKLjTU4sJEO6k42U .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-HKLjTU4sJEO6k42U .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-HKLjTU4sJEO6k42U .edgeLabel p{margin:0;padding:0;display:inline;}#mermaid-svg-HKLjTU4sJEO6k42U .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-HKLjTU4sJEO6k42U .labelBkg{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-HKLjTU4sJEO6k42U .node .cluster{fill:rgba(255, 255, 222, 0.5);stroke:rgba(170, 170, 51, 0.2);box-shadow:rgba(50, 50, 93, 0.25) 0px 13px 27px -5px,rgba(0, 0, 0, 0.3) 0px 8px 16px -8px;stroke-width:1px;}#mermaid-svg-HKLjTU4sJEO6k42U .cluster text{fill:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .cluster span,#mermaid-svg-HKLjTU4sJEO6k42U p{color:#333;}#mermaid-svg-HKLjTU4sJEO6k42U div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-HKLjTU4sJEO6k42U .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-HKLjTU4sJEO6k42U .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-HKLjTU4sJEO6k42U .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-HKLjTU4sJEO6k42U :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} V=2
P
X
CC (4 bits)
M
PT (7 bits)
Sequence Number (16 bits)
Timestamp (32 bits)
SSRC (32 bits)
三、 RTP 载荷:编码数据的"内容"
RTP 头部之后就是载荷(Payload),它承载着编码后的音视频数据。载荷的格式完全由 PT 字段指定的编码类型决定。
3.1 核心原则:第一个字节由载荷类型决定
RTP 协议本身不规定载荷第一个字节的格式,它由具体的编码格式(H.264、AAC、G.711 等)定义。
| 载荷类型(PT) | 编码格式 | 第一个字节是什么? | 格式 |
|---|---|---|---|
0 (PCMU) |
G.711 μ-law | 直接是音频采样数据 | 无头,纯数据 |
8 (PCMA) |
G.711 A-law | 直接是音频采样数据 | 无头,纯数据 |
26 (JPEG) |
JPEG | JPEG 量化表头 | RFC 2435 |
96 (动态) |
H.264 | NAL 单元头 | RFC 6184 |
97 (动态) |
AAC | AU 头 | RFC 3640 |
关键点:RTP 包头里的 PT 字段告诉接收端"载荷是什么编码",然后接收端才知道第一个字节怎么解析。就像快递单上的"物品类型"告诉你箱子里是书还是衣服,你才知道怎么拆箱。
3.2 最简单的音频载荷:G.711
G.711 是 VoIP 电话中最常见的音频编码。它的 RTP 载荷格式非常简单,直接将 PCM 音频数据放入 RTP 包中,没有任何额外的头部。
RTP 包头 (12 字节) | G.711 音频数据 (例如 160 字节)
示例:一个 20ms 的音频帧,采样率为 8000 Hz,载荷大小 = 8000 × 0.02 × 1 字节 = 160 字节。
3.3 其他常见编码的第一个字节
| 编码格式 | PT 值 | 第一个字节格式 | 示例 |
|---|---|---|---|
| G.711 μ-law | 0 | 直接是 8 位 PCM 音频数据 | FF(静音) |
| G.711 A-law | 8 | 直接是 8 位 PCM 音频数据 | D5 |
| AAC | 动态(96-127) | AU 头(2 字节):[AU-size:13bits][AU-index:3bits] |
00 20(32 字节) |
| JPEG | 26 | JPEG 头:[类型特定:8bits][分片偏移:24bits][类型:8bits][Q:8bits][宽:8bits][高:8bits] |
00 00 00 00 01 5E 02 80 01 E0 |
载荷类型判断流程:
#mermaid-svg-7tWFFGxE3UQGOCZR{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-7tWFFGxE3UQGOCZR .error-icon{fill:#552222;}#mermaid-svg-7tWFFGxE3UQGOCZR .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-7tWFFGxE3UQGOCZR .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-7tWFFGxE3UQGOCZR .marker{fill:#333333;stroke:#333333;}#mermaid-svg-7tWFFGxE3UQGOCZR .marker.cross{stroke:#333333;}#mermaid-svg-7tWFFGxE3UQGOCZR svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-7tWFFGxE3UQGOCZR p{margin:0;}#mermaid-svg-7tWFFGxE3UQGOCZR .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster-label text{fill:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster-label span{color:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster-label span p{background-color:transparent;}#mermaid-svg-7tWFFGxE3UQGOCZR .label text,#mermaid-svg-7tWFFGxE3UQGOCZR span{fill:#333;color:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR .node rect,#mermaid-svg-7tWFFGxE3UQGOCZR .node circle,#mermaid-svg-7tWFFGxE3UQGOCZR .node ellipse,#mermaid-svg-7tWFFGxE3UQGOCZR .node polygon,#mermaid-svg-7tWFFGxE3UQGOCZR .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-7tWFFGxE3UQGOCZR .rough-node .label text,#mermaid-svg-7tWFFGxE3UQGOCZR .node .label text,#mermaid-svg-7tWFFGxE3UQGOCZR .image-shape .label,#mermaid-svg-7tWFFGxE3UQGOCZR .icon-shape .label{text-anchor:middle;}#mermaid-svg-7tWFFGxE3UQGOCZR .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-7tWFFGxE3UQGOCZR .rough-node .label,#mermaid-svg-7tWFFGxE3UQGOCZR .node .label,#mermaid-svg-7tWFFGxE3UQGOCZR .image-shape .label,#mermaid-svg-7tWFFGxE3UQGOCZR .icon-shape .label{text-align:center;}#mermaid-svg-7tWFFGxE3UQGOCZR .node.clickable{cursor:pointer;}#mermaid-svg-7tWFFGxE3UQGOCZR .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-7tWFFGxE3UQGOCZR .arrowheadPath{fill:#333333;}#mermaid-svg-7tWFFGxE3UQGOCZR .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-7tWFFGxE3UQGOCZR .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-7tWFFGxE3UQGOCZR .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7tWFFGxE3UQGOCZR .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-7tWFFGxE3UQGOCZR .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7tWFFGxE3UQGOCZR .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster text{fill:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR .cluster span{color:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-7tWFFGxE3UQGOCZR .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-7tWFFGxE3UQGOCZR rect.text{fill:none;stroke-width:0;}#mermaid-svg-7tWFFGxE3UQGOCZR .icon-shape,#mermaid-svg-7tWFFGxE3UQGOCZR .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-7tWFFGxE3UQGOCZR .icon-shape p,#mermaid-svg-7tWFFGxE3UQGOCZR .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-7tWFFGxE3UQGOCZR .icon-shape .label rect,#mermaid-svg-7tWFFGxE3UQGOCZR .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-7tWFFGxE3UQGOCZR .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-7tWFFGxE3UQGOCZR .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-7tWFFGxE3UQGOCZR :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 0 (PCMU)
8 (PCMA)
26 (JPEG)
96 (动态)
97 (动态)
其他
接收 RTP 包
解析 RTP 头部
读取 PT 字段
PT 值?
G.711 μ-law 解码
G.711 A-law 解码
按 RFC 2435 解析 JPEG
按 RFC 6184 解析 H.264
按 RFC 3640 解析 AAC
查询 SDP 中的 rtpmap 映射
输出 PCM 音频
输出 JPEG 图像
输出 H.264 NALU
输出 AAC 音频帧
四、H.264 基础概念
在深入 H.264 载荷格式之前,需要先了解一些基本概念。
4.1 H.264 编码原理
H.264 是一种视频压缩标准,它的核心思想是去除视频中的冗余信息:
- 空间冗余 :一帧图像中相邻像素通常很相似 → 使用帧内预测
- 时间冗余 :连续帧之间通常只有少量变化 → 使用帧间预测
- 视觉冗余 :人眼对某些细节不敏感 → 使用变换和量化
- 编码冗余 :某些数据出现频率高 → 使用熵编码
4.2 帧类型(I帧、P帧、B帧)
| 帧类型 | 全称 | 说明 | 大小 |
|---|---|---|---|
| I 帧 | Intra-coded frame(帧内编码帧) | 关键帧,完整编码,不依赖其他帧 | 最大 |
| P 帧 | Predicted frame(预测编码帧) | 参考前面的 I 帧或 P 帧,只编码差异 | 中等 |
| B 帧 | Bidirectional predicted frame(双向预测编码帧) | 参考前后帧,压缩率最高 | 最小 |
典型 GOP(Group of Pictures)结构 :I B B P B B P B B I ...
4.3 NAL 单元(Network Abstraction Layer)
H.264 编码器输出的数据被组织成 NAL 单元(Network Abstraction Layer Unit,简称 NALU),这是 H.264 数据在网络中传输的基本单位。
NALU 结构:
【NALU 格式】
+----------+------+---------+
| NAL 头 | 载荷 | 尾字节 |
| (1 字节) | (变长) | (可选) |
+----------+------+---------+
NAL 头格式:
+---------------+
|0|1|2|3|4|5|6|7|
+-+-+-+-+-+-+-+-+
|F|NRI| Type |
+---------------+
逐位拆解:
| 位 | 字段 | 含义 |
|---|---|---|
| 0 | F (forbidden_zero_bit) | 必须为 0,1 表示错误 |
| 1-2 | NRI (nal_ref_idc) | 优先级:00=不重要,11=最重要 |
| 3-7 | Type (nal_unit_type) | NAL 类型 |
常见 NAL 类型值:
| Type 值 | 含义 |
|---|---|
1 |
非 IDR 帧(P 帧/B 帧) |
5 |
IDR 帧(关键帧/I 帧) |
7 |
SPS(序列参数集) |
8 |
PPS(图像参数集) |
4.4 起始码(Start Code)
在 H.264 裸流中,每个 NALU 前面通常有起始码:
00 00 00 01(4 字节,最常用)00 00 01(3 字节,部分场景)
示例:
00 00 00 01 65 88 84 00 15 FF F8 7E 08 ...
其中 65 就是 NAL 头。
4.5 NAL 头解析示例
65(十六进制)解析:
65 = 0x65 = 二进制 0110 0101
│ │ └───── 00101 = Type = 5 → IDR 关键帧
│ └──────── 11 = NRI = 3 → 最高优先级
└────────── 0 = F = 0 → 正常
H.264 编码原理流程图:
#mermaid-svg-X4A4XaMa31tdSXZb{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-X4A4XaMa31tdSXZb .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-X4A4XaMa31tdSXZb .error-icon{fill:#552222;}#mermaid-svg-X4A4XaMa31tdSXZb .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-X4A4XaMa31tdSXZb .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-X4A4XaMa31tdSXZb .marker{fill:#333333;stroke:#333333;}#mermaid-svg-X4A4XaMa31tdSXZb .marker.cross{stroke:#333333;}#mermaid-svg-X4A4XaMa31tdSXZb svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-X4A4XaMa31tdSXZb p{margin:0;}#mermaid-svg-X4A4XaMa31tdSXZb .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-X4A4XaMa31tdSXZb .cluster-label text{fill:#333;}#mermaid-svg-X4A4XaMa31tdSXZb .cluster-label span{color:#333;}#mermaid-svg-X4A4XaMa31tdSXZb .cluster-label span p{background-color:transparent;}#mermaid-svg-X4A4XaMa31tdSXZb .label text,#mermaid-svg-X4A4XaMa31tdSXZb span{fill:#333;color:#333;}#mermaid-svg-X4A4XaMa31tdSXZb .node rect,#mermaid-svg-X4A4XaMa31tdSXZb .node circle,#mermaid-svg-X4A4XaMa31tdSXZb .node ellipse,#mermaid-svg-X4A4XaMa31tdSXZb .node polygon,#mermaid-svg-X4A4XaMa31tdSXZb .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-X4A4XaMa31tdSXZb .rough-node .label text,#mermaid-svg-X4A4XaMa31tdSXZb .node .label text,#mermaid-svg-X4A4XaMa31tdSXZb .image-shape .label,#mermaid-svg-X4A4XaMa31tdSXZb .icon-shape .label{text-anchor:middle;}#mermaid-svg-X4A4XaMa31tdSXZb .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-X4A4XaMa31tdSXZb .rough-node .label,#mermaid-svg-X4A4XaMa31tdSXZb .node .label,#mermaid-svg-X4A4XaMa31tdSXZb .image-shape .label,#mermaid-svg-X4A4XaMa31tdSXZb .icon-shape .label{text-align:center;}#mermaid-svg-X4A4XaMa31tdSXZb .node.clickable{cursor:pointer;}#mermaid-svg-X4A4XaMa31tdSXZb .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-X4A4XaMa31tdSXZb .arrowheadPath{fill:#333333;}#mermaid-svg-X4A4XaMa31tdSXZb .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-X4A4XaMa31tdSXZb .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-X4A4XaMa31tdSXZb .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X4A4XaMa31tdSXZb .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-X4A4XaMa31tdSXZb .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X4A4XaMa31tdSXZb .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-X4A4XaMa31tdSXZb .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-X4A4XaMa31tdSXZb .cluster text{fill:#333;}#mermaid-svg-X4A4XaMa31tdSXZb .cluster span{color:#333;}#mermaid-svg-X4A4XaMa31tdSXZb div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-X4A4XaMa31tdSXZb .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-X4A4XaMa31tdSXZb rect.text{fill:none;stroke-width:0;}#mermaid-svg-X4A4XaMa31tdSXZb .icon-shape,#mermaid-svg-X4A4XaMa31tdSXZb .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-X4A4XaMa31tdSXZb .icon-shape p,#mermaid-svg-X4A4XaMa31tdSXZb .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-X4A4XaMa31tdSXZb .icon-shape .label rect,#mermaid-svg-X4A4XaMa31tdSXZb .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-X4A4XaMa31tdSXZb .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-X4A4XaMa31tdSXZb .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-X4A4XaMa31tdSXZb :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原始视频帧 (YUV)
帧内预测
帧间预测
变换
量化
熵编码
NAL 单元输出
解码参考帧
NAL 单元结构层次图:
#mermaid-svg-h3uHtgFqYRGpsEgo{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-h3uHtgFqYRGpsEgo .error-icon{fill:#552222;}#mermaid-svg-h3uHtgFqYRGpsEgo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-h3uHtgFqYRGpsEgo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-h3uHtgFqYRGpsEgo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-h3uHtgFqYRGpsEgo .marker.cross{stroke:#333333;}#mermaid-svg-h3uHtgFqYRGpsEgo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-h3uHtgFqYRGpsEgo p{margin:0;}#mermaid-svg-h3uHtgFqYRGpsEgo .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster-label text{fill:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster-label span{color:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster-label span p{background-color:transparent;}#mermaid-svg-h3uHtgFqYRGpsEgo .label text,#mermaid-svg-h3uHtgFqYRGpsEgo span{fill:#333;color:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo .node rect,#mermaid-svg-h3uHtgFqYRGpsEgo .node circle,#mermaid-svg-h3uHtgFqYRGpsEgo .node ellipse,#mermaid-svg-h3uHtgFqYRGpsEgo .node polygon,#mermaid-svg-h3uHtgFqYRGpsEgo .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-h3uHtgFqYRGpsEgo .rough-node .label text,#mermaid-svg-h3uHtgFqYRGpsEgo .node .label text,#mermaid-svg-h3uHtgFqYRGpsEgo .image-shape .label,#mermaid-svg-h3uHtgFqYRGpsEgo .icon-shape .label{text-anchor:middle;}#mermaid-svg-h3uHtgFqYRGpsEgo .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-h3uHtgFqYRGpsEgo .rough-node .label,#mermaid-svg-h3uHtgFqYRGpsEgo .node .label,#mermaid-svg-h3uHtgFqYRGpsEgo .image-shape .label,#mermaid-svg-h3uHtgFqYRGpsEgo .icon-shape .label{text-align:center;}#mermaid-svg-h3uHtgFqYRGpsEgo .node.clickable{cursor:pointer;}#mermaid-svg-h3uHtgFqYRGpsEgo .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-h3uHtgFqYRGpsEgo .arrowheadPath{fill:#333333;}#mermaid-svg-h3uHtgFqYRGpsEgo .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-h3uHtgFqYRGpsEgo .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-h3uHtgFqYRGpsEgo .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h3uHtgFqYRGpsEgo .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-h3uHtgFqYRGpsEgo .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h3uHtgFqYRGpsEgo .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster text{fill:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo .cluster span{color:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-h3uHtgFqYRGpsEgo .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-h3uHtgFqYRGpsEgo rect.text{fill:none;stroke-width:0;}#mermaid-svg-h3uHtgFqYRGpsEgo .icon-shape,#mermaid-svg-h3uHtgFqYRGpsEgo .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-h3uHtgFqYRGpsEgo .icon-shape p,#mermaid-svg-h3uHtgFqYRGpsEgo .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-h3uHtgFqYRGpsEgo .icon-shape .label rect,#mermaid-svg-h3uHtgFqYRGpsEgo .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-h3uHtgFqYRGpsEgo .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-h3uHtgFqYRGpsEgo .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-h3uHtgFqYRGpsEgo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} NAL 头结构
F (1 bit)
NRI (2 bits)
Type (5 bits)
NALU (Network Abstraction Layer Unit)
NAL 头 (1 字节)
NAL 载荷 (变长)
尾字节 (可选)
五、H.264 RTP 载荷格式深度剖析
H.264 是目前应用最广泛的视频编码标准。由于视频帧大小差异巨大,H.264 的 RTP 载荷格式最为复杂,也是本文的重点。
5.1 RTP 载荷第一个字节 = NALU 头
H.264 的 RTP 载荷第一个字节格式与 NALU 头完全一致,即 F、NRI、Type 三个字段。
5.2 三种基本载荷结构
接收者可以通过 RTP 载荷的第一个字节后 5 位(Type)识别载荷结构:
| Type 值 | 结构类型 | 说明 |
|---|---|---|
| 1-23 | 单个 NAL 单元包 | 荷载中只包含一个完整的 NAL 单元 |
| 24-27 | 聚合包(STAP-A/B, MTAP16/24) | 聚合多个 NAL 单元到单个 RTP 荷载中 |
| 28-29 | 分片包(FU-A/B) | 用于分片单个 NAL 单元到多个 RTP 包 |
三种载荷结构决策树:
#mermaid-svg-ySCTwR2NE4RkcLB6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-ySCTwR2NE4RkcLB6 .error-icon{fill:#552222;}#mermaid-svg-ySCTwR2NE4RkcLB6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-ySCTwR2NE4RkcLB6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .marker.cross{stroke:#333333;}#mermaid-svg-ySCTwR2NE4RkcLB6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-ySCTwR2NE4RkcLB6 p{margin:0;}#mermaid-svg-ySCTwR2NE4RkcLB6 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster-label text{fill:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster-label span{color:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster-label span p{background-color:transparent;}#mermaid-svg-ySCTwR2NE4RkcLB6 .label text,#mermaid-svg-ySCTwR2NE4RkcLB6 span{fill:#333;color:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .node rect,#mermaid-svg-ySCTwR2NE4RkcLB6 .node circle,#mermaid-svg-ySCTwR2NE4RkcLB6 .node ellipse,#mermaid-svg-ySCTwR2NE4RkcLB6 .node polygon,#mermaid-svg-ySCTwR2NE4RkcLB6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .rough-node .label text,#mermaid-svg-ySCTwR2NE4RkcLB6 .node .label text,#mermaid-svg-ySCTwR2NE4RkcLB6 .image-shape .label,#mermaid-svg-ySCTwR2NE4RkcLB6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-ySCTwR2NE4RkcLB6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .rough-node .label,#mermaid-svg-ySCTwR2NE4RkcLB6 .node .label,#mermaid-svg-ySCTwR2NE4RkcLB6 .image-shape .label,#mermaid-svg-ySCTwR2NE4RkcLB6 .icon-shape .label{text-align:center;}#mermaid-svg-ySCTwR2NE4RkcLB6 .node.clickable{cursor:pointer;}#mermaid-svg-ySCTwR2NE4RkcLB6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .arrowheadPath{fill:#333333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ySCTwR2NE4RkcLB6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-ySCTwR2NE4RkcLB6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ySCTwR2NE4RkcLB6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster text{fill:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 .cluster span{color:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-ySCTwR2NE4RkcLB6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-ySCTwR2NE4RkcLB6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-ySCTwR2NE4RkcLB6 .icon-shape,#mermaid-svg-ySCTwR2NE4RkcLB6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-ySCTwR2NE4RkcLB6 .icon-shape p,#mermaid-svg-ySCTwR2NE4RkcLB6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-ySCTwR2NE4RkcLB6 .icon-shape .label rect,#mermaid-svg-ySCTwR2NE4RkcLB6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-ySCTwR2NE4RkcLB6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-ySCTwR2NE4RkcLB6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-ySCTwR2NE4RkcLB6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 1-23
24-27
28-29
接收 H.264 RTP 包
读取载荷第一个字节的 Type 字段 (后5位)
Type 值?
单个 NAL 单元包
聚合包 STAP-A/B, MTAP16/24
分片包 FU-A/B
直接提取完整 NALU
解析多个 NALU 长度前缀
拆分出多个 NALU
收集分片 + 按 S/E 位组装
还原 NAL 头
组装完整 NALU
交给解码器
5.3 单个 NAL 单元包(Type 1-23)
当 NAL 单元大小小于 MTU 时,直接使用单个 NAL 单元包。载荷第一个字节就是 NAL 头。
使用场景:SPS、PPS、SEI 等小 NAL 单元,或者低分辨率视频帧。
5.4 聚合包 STAP-A(Type 24)
当有多个小 NAL 单元(如 SPS + PPS)需要合并到一个 RTP 包时,使用聚合包。
【第一个字节:STAP-A 头】
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|F| NRI | 24 | ← Type 固定为 24(STAP-A)
+-+-+-+-+-+-+-+-+
注意:实际应用中,聚合包使用较少,因为 SPS、PPS 等通常单独发送。
5.5 分片包 FU-A(Type 28)------ 重点
当 NALU 的长度超过 MTU(通常为 1460 字节)时,就必须对 NALU 单元进行分片封包,称为 Fragmentation Units (FUs)。
5.5.1 为什么需要分片?
MTU(最大传输单元) = 1500 字节(以太网)
- IP 包头 = 20 字节
- UDP 包头 = 8 字节
- RTP 包头 = 12 字节
- 剩余给 RTP 载荷的空间 = 1500 - 20 - 8 - 12 = 1460 字节
如果 NALU 大于 1460 字节,一个 RTP 包装不下,必须拆成多个 FU-A 分片。
5.5.2 FU-A 格式
FU-A 由 1 字节的 FU indicator、1 字节的 FU header 和分片单元荷载组成。
+---------------+ +---------------+ +------------------+
| FU indicator | | FU header | | FU payload |
| (1 字节) | | (1 字节) | | (分片数据) |
+---------------+ +---------------+ +------------------+
5.5.3 FU indicator 格式
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|F| NRI | 28 | ← Type 固定为 28(FU-A)
+-+-+-+-+-+-+-+-+
5.5.4 FU header 格式
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|S|E|R| Type | ← Type 是原始 NAL 类型
+-+-+-+-+-+-+-+-+
FU header 字段说明:
| 位 | 字段 | 含义 |
|---|---|---|
| S | 开始位 | 1 表示分片 NAL 单元的开始 |
| E | 结束位 | 1 表示分片 NAL 单元的结束 |
| R | 保留位 | 必须为 0,接收者必须忽略 |
| Type | NAL 类型 | 原始 NAL Header 中的 Type |
5.5.5 打包与解包规则
打包时,原始的 NAL 头的前三位为 FU indicator 的前三位,原始的 NAL 头的后五位(Type)为 FU header 的后五位(Type)。
解包时,取 FU indicator 的前三位和 FU Header 的后五位,还原出原始 NAL 头。
示例分析:
原始 NAL 头 = 0110 0101 (0x65)
│ │ └────── Type = 5 (IDR 帧)
│ └───────── NRI = 3
└─────────── F = 0
打包后:
FU indicator = 0111 1100 (0x7C)
│ │ └────── Type = 28 (FU-A)
│ └───────── NRI = 3
└─────────── F = 0
FU header = 1000 0101 (0x85)
│ │ │ └────── Type = 5 (原始 NAL 类型)
│ │ └──────── R = 0
│ └────────── E = 0
└──────────── S = 1 (第一个分片)
5.5.6 关键规则
- 相同 NALU 的分片必须使用递增的 RTP 序号连续顺序发送,中间不能插入其他 RTP 包。
- 分片不可以嵌套,即一个 FU 不可以包含另一个 FU。
- STAPs、MTAPs 不可以被分片。
- NAL 单元必须按照 RTP 顺序号的顺序装配。
完整流程图:NALU 分片 → 打包 → 传输 → 重组
#mermaid-svg-OjTt6LdMTSrotxLr{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-OjTt6LdMTSrotxLr .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OjTt6LdMTSrotxLr .error-icon{fill:#552222;}#mermaid-svg-OjTt6LdMTSrotxLr .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OjTt6LdMTSrotxLr .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OjTt6LdMTSrotxLr .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OjTt6LdMTSrotxLr .marker.cross{stroke:#333333;}#mermaid-svg-OjTt6LdMTSrotxLr svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OjTt6LdMTSrotxLr p{margin:0;}#mermaid-svg-OjTt6LdMTSrotxLr .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OjTt6LdMTSrotxLr .cluster-label text{fill:#333;}#mermaid-svg-OjTt6LdMTSrotxLr .cluster-label span{color:#333;}#mermaid-svg-OjTt6LdMTSrotxLr .cluster-label span p{background-color:transparent;}#mermaid-svg-OjTt6LdMTSrotxLr .label text,#mermaid-svg-OjTt6LdMTSrotxLr span{fill:#333;color:#333;}#mermaid-svg-OjTt6LdMTSrotxLr .node rect,#mermaid-svg-OjTt6LdMTSrotxLr .node circle,#mermaid-svg-OjTt6LdMTSrotxLr .node ellipse,#mermaid-svg-OjTt6LdMTSrotxLr .node polygon,#mermaid-svg-OjTt6LdMTSrotxLr .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OjTt6LdMTSrotxLr .rough-node .label text,#mermaid-svg-OjTt6LdMTSrotxLr .node .label text,#mermaid-svg-OjTt6LdMTSrotxLr .image-shape .label,#mermaid-svg-OjTt6LdMTSrotxLr .icon-shape .label{text-anchor:middle;}#mermaid-svg-OjTt6LdMTSrotxLr .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OjTt6LdMTSrotxLr .rough-node .label,#mermaid-svg-OjTt6LdMTSrotxLr .node .label,#mermaid-svg-OjTt6LdMTSrotxLr .image-shape .label,#mermaid-svg-OjTt6LdMTSrotxLr .icon-shape .label{text-align:center;}#mermaid-svg-OjTt6LdMTSrotxLr .node.clickable{cursor:pointer;}#mermaid-svg-OjTt6LdMTSrotxLr .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OjTt6LdMTSrotxLr .arrowheadPath{fill:#333333;}#mermaid-svg-OjTt6LdMTSrotxLr .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OjTt6LdMTSrotxLr .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OjTt6LdMTSrotxLr .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OjTt6LdMTSrotxLr .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OjTt6LdMTSrotxLr .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OjTt6LdMTSrotxLr .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OjTt6LdMTSrotxLr .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OjTt6LdMTSrotxLr .cluster text{fill:#333;}#mermaid-svg-OjTt6LdMTSrotxLr .cluster span{color:#333;}#mermaid-svg-OjTt6LdMTSrotxLr div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-OjTt6LdMTSrotxLr .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OjTt6LdMTSrotxLr rect.text{fill:none;stroke-width:0;}#mermaid-svg-OjTt6LdMTSrotxLr .icon-shape,#mermaid-svg-OjTt6LdMTSrotxLr .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OjTt6LdMTSrotxLr .icon-shape p,#mermaid-svg-OjTt6LdMTSrotxLr .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OjTt6LdMTSrotxLr .icon-shape .label rect,#mermaid-svg-OjTt6LdMTSrotxLr .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OjTt6LdMTSrotxLr .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OjTt6LdMTSrotxLr .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OjTt6LdMTSrotxLr :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ≤ 1460 字节
> 1460 字节
Type 1-23
Type 28
H.264 编码器输出 NALU
NALU 大小 > MTU (1460 字节)?
单个 NAL 单元包 (Type 1-23)
FU-A 分片 (Type 28)
FU-A Start (S=1, E=0)
FU-A Middle (S=0, E=0)
FU-A End (S=0, E=1)
封装 RTP 包 (RTP头 + FU indicator + FU header + payload)
UDP/IP/以太网封装后网络传输
接收端解析 RTP 包
载荷 Type 字段?
直接提取 NALU
收集同一 NALU 全部分片
按序列号排序 + 根据 S/E 位组装
还原 NAL 头 (FU indicator前3位 + FU header后5位)
组装完整 NALU
交给 H.264 解码器
六、RTCP 协议
6.1 什么是 RTCP?
RTCP(RTP Control Protocol,RTP 控制协议) 是 RTP 的姐妹协议,与 RTP 配合使用,负责传输控制信息。它同样运行在 UDP 之上,使用 RTP 端口号 + 1 的端口。
6.2 RTCP 的主要功能
| 功能 | 说明 |
|---|---|
| QoS 反馈 | 报告丢包率、抖动、延迟等网络质量信息 |
| 同步 | 通过 RTCP 的 NTP 时间戳和 RTP 时间戳关联,实现音视频同步 |
| 标识 | 通过 CNAME(规范名)关联同一参与者的多个媒体流 |
| 会话控制 | 参与者加入/离开通知,会话规模估算 |
6.3 RTCP 包类型
| 类型 | 缩写 | 说明 |
|---|---|---|
| 200 | SR(Sender Report) | 发送者报告,包含发送统计和 NTP 时间戳 |
| 201 | RR(Receiver Report) | 接收者报告,包含接收质量反馈 |
| 202 | SDES(Source Description) | 源描述,包含 CNAME 等 |
| 203 | BYE | 离开会话通知 |
| 204 | APP | 应用自定义 |
6.4 RTCP 发送间隔
RTCP 不是每帧都发送,而是周期性发送(通常 5 秒左右),且带宽占用控制在会话带宽的 5% 以内,避免占用过多带宽。
RTCP 与 RTP 交互时序图:
接收端 发送端 接收端 发送端 #mermaid-svg-TIQiR7aX6xzI2mYj{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-TIQiR7aX6xzI2mYj .error-icon{fill:#552222;}#mermaid-svg-TIQiR7aX6xzI2mYj .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-TIQiR7aX6xzI2mYj .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-TIQiR7aX6xzI2mYj .marker{fill:#333333;stroke:#333333;}#mermaid-svg-TIQiR7aX6xzI2mYj .marker.cross{stroke:#333333;}#mermaid-svg-TIQiR7aX6xzI2mYj svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-TIQiR7aX6xzI2mYj p{margin:0;}#mermaid-svg-TIQiR7aX6xzI2mYj .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TIQiR7aX6xzI2mYj text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-TIQiR7aX6xzI2mYj .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-TIQiR7aX6xzI2mYj .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-TIQiR7aX6xzI2mYj #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-TIQiR7aX6xzI2mYj .sequenceNumber{fill:white;}#mermaid-svg-TIQiR7aX6xzI2mYj #sequencenumber{fill:#333;}#mermaid-svg-TIQiR7aX6xzI2mYj #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-TIQiR7aX6xzI2mYj .messageText{fill:#333;stroke:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TIQiR7aX6xzI2mYj .labelText,#mermaid-svg-TIQiR7aX6xzI2mYj .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .loopText,#mermaid-svg-TIQiR7aX6xzI2mYj .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-TIQiR7aX6xzI2mYj .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-TIQiR7aX6xzI2mYj .noteText,#mermaid-svg-TIQiR7aX6xzI2mYj .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-TIQiR7aX6xzI2mYj .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TIQiR7aX6xzI2mYj .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TIQiR7aX6xzI2mYj .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-TIQiR7aX6xzI2mYj .actorPopupMenu{position:absolute;}#mermaid-svg-TIQiR7aX6xzI2mYj .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-TIQiR7aX6xzI2mYj .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-TIQiR7aX6xzI2mYj .actor-man circle,#mermaid-svg-TIQiR7aX6xzI2mYj line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-TIQiR7aX6xzI2mYj :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} RTP 数据流传输 周期性 RTCP 报告 (约 5s) 基于 RTCP 反馈调整 会话结束 RTP 包 (Seq=1, 音频)RTP 包 (Seq=2, 视频)RTP 包 (Seq=3, 音频)RTP 包 (Seq=4, 视频)RTCP SR (发送者报告: 包计数+NTP时间戳)RTCP RR (接收者报告: 丢包率+抖动)RTCP SDES (CNAME 标识)根据丢包率调整码率根据抖动调整缓冲区RTCP BYE
七、RTSP 信令交互流程
7.1 什么是 RTSP?
RTSP(Real Time Streaming Protocol,实时流协议) 是一个应用层协议,用于控制流媒体服务器。它负责建立和控制媒体会话,但不传输媒体数据本身(媒体数据通过 RTP 传输)。
7.2 RTSP 与 RTP 的关系
| 协议 | 作用 | 类比 |
|---|---|---|
| RTSP | 控制流媒体会话(播放、暂停、停止) | 遥控器 |
| RTP | 传输媒体数据 | 电视信号 |
| RTCP | 传输控制信息(质量反馈) | 信号质量报告 |
7.3 RTSP 标准交互流程
客户端(播放器) 服务器(摄像头)
| |
|--- OPTIONS ----------------------->| 1. 查询服务器能力
|<-- 200 OK ------------------------| (支持的方法)
| |
|--- DESCRIBE ---------------------->| 2. 请求媒体描述
|<-- 200 OK (SDP) ------------------| (返回 SDP 信息)
| |
|--- SETUP ------------------------->| 3. 建立传输通道
|<-- 200 OK ------------------------| (协商端口、协议)
| |
|--- PLAY -------------------------->| 4. 开始播放
|<-- 200 OK ------------------------| (开始传输 RTP 数据)
| |
| <<<< RTP 数据流开始传输 >>>> |
| <<<< RTCP 控制信息交换 >>>> |
| |
|--- TEARDOWN ---------------------->| 5. 结束会话
|<-- 200 OK ------------------------| (断开连接)
7.4 SDP 示例
v=0
o=- 0 0 IN IP4 192.168.1.1
s=Live Stream
c=IN IP4 0.0.0.0
t=0 0
m=video 8081 RTP/AVP 96
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42001F
关键字段解析:
| 字段 | 含义 |
|---|---|
m=video 8081 RTP/AVP 96 |
视频流,端口 8081,RTP/AVP 协议,载荷类型 96 |
a=rtpmap:96 H264/90000 |
PT=96 对应 H.264,时钟频率 90000 Hz |
a=fmtp:96 packetization-mode=1 |
分片模式为 1(支持 FU-A) |
九、实战:完整网络传输帧格式
设定场景:摄像头输出 1080p H.264 视频,通过 RTSP 单播推送到 Web 预览。一个 IDR 关键帧大小为 50KB,需要拆成 35 个 FU-A 分片包。
8.1 第一个分片(FU-A Start)
【RTP 包 1】
80 60 00 01 00 00 00 00 12 34 56 78 7C 85 88 84 00 15 FF F8 ... (总共 1472 字节)
逐字节解析:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 0 | 80 |
V=2, P=0, X=0, CC=0 | 版本 2,无填充,无扩展 |
| 1 | 60 |
M=0, PT=96 | 非标记,动态载荷类型 96 |
| 2-3 | 00 01 |
序列号 = 1 | 第一个包 |
| 4-7 | 00 00 00 00 |
时间戳 = 0 | 起始时刻 |
| 8-11 | 12 34 56 78 |
SSRC = 0x12345678 | 流唯一标识 |
| 12 | 7C |
FU indicator | Type=28 (FU-A), NRI=3 |
| 13 | 85 |
FU header | S=1, E=0, R=0, Type=5 (IDR) |
| 14-1471 | 88 84 00 15 FF F8 ... |
FU payload | 分片数据(1458 字节) |
8.2 中间分片(FU-A Middle)
【RTP 包 2】
80 60 00 02 00 00 00 00 12 34 56 78 7C 05 ... (1472 字节)
关键字段:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 2-3 | 00 02 |
序列号 = 2 | 递增 |
| 12 | 7C |
FU indicator | Type=28, NRI=3 |
| 13 | 05 |
FU header | S=0, E=0, R=0, Type=5 |
8.3 最后一个分片(FU-A End)
【RTP 包 35】
80 E0 00 23 00 00 00 00 12 34 56 78 7C 45 ... (约 512 字节)
关键字段:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 1 | E0 |
M=1, PT=96 | M=1 标记帧结束 |
| 2-3 | 00 23 |
序列号 = 35 | 最后一个包 |
| 12 | 7C |
FU indicator | Type=28, NRI=3 |
| 13 | 45 |
FU header | S=0, E=1, R=0, Type=5 |
8.4 UDP 封装
【UDP 数据报】
+-------------------+
| UDP 包头 (8 字节) |
+-------------------+
| RTP 包 (1472 字节) |
+-------------------+
UDP 包头格式:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 0-1 | 1F 90 |
源端口 = 8080 | 推流端端口 |
| 2-3 | 1F 91 |
目的端口 = 8081 | 预览端端口 |
| 4-5 | 05 C0 |
长度 = 1472 | UDP 头 + 数据 |
| 6-7 | 00 00 |
校验和 = 0 | 简化(实际应计算) |
8.5 IP 封装
【IP 数据包】
+-------------------+
| IP 包头 (20 字节) |
+-------------------+
| UDP 数据报 (1480 字节) |
+-------------------+
IP 包头格式:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 0 | 45 |
版本=4, 头长=5 | IPv4, 20 字节头 |
| 1 | 00 |
服务类型 | 默认 |
| 2-3 | 05 DC |
总长度 = 1500 | IP 头 + 数据 |
| 4-5 | 00 01 |
标识 = 1 | 分片标识 |
| 6-7 | 40 00 |
标志 + 片偏移 | 不分片 |
| 8 | 40 |
TTL = 64 | 生存时间 |
| 9 | 11 |
协议 = 17 | UDP |
| 10-11 | 00 00 |
头部校验和 | 简化 |
| 12-15 | C0 A8 01 01 |
源 IP = 192.168.1.1 | 推流端 |
| 16-19 | C0 A8 01 02 |
目的 IP = 192.168.1.2 | 预览端 |
8.6 以太网帧封装
【以太网帧】
+-------------------+
| 以太网头 (14 字节) |
+-------------------+
| IP 包 (1500 字节) |
+-------------------+
| FCS (4 字节) |
+-------------------+
以太网帧头格式:
| 字节 | 十六进制 | 字段 | 含义 |
|---|---|---|---|
| 0-5 | 00 11 22 33 44 55 |
目的 MAC | 预览端 MAC |
| 6-11 | AA BB CC DD EE FF |
源 MAC | 推流端 MAC |
| 12-13 | 08 00 |
类型 = 0x0800 | IPv4 |
8.7 完整数据帧结构总结
【从摄像头到 Web 预览的完整数据帧】
以太网帧 (1518 字节)
├── 以太网头 (14 字节)
│ ├── 目的 MAC (6 字节)
│ ├── 源 MAC (6 字节)
│ └── 类型 (2 字节) = 0x0800
├── IP 包 (1500 字节)
│ ├── IP 头 (20 字节)
│ │ ├── 版本/头长/服务类型 (2 字节)
│ │ ├── 总长度 (2 字节) = 1500
│ │ ├── 标识/标志/片偏移 (4 字节)
│ │ ├── TTL/协议 (2 字节) = 64/17
│ │ ├── 校验和 (2 字节)
│ │ ├── 源 IP (4 字节) = 192.168.1.1
│ │ └── 目的 IP (4 字节) = 192.168.1.2
│ └── UDP 数据报 (1480 字节)
│ ├── UDP 头 (8 字节)
│ │ ├── 源端口 (2 字节) = 8080
│ │ ├── 目的端口 (2 字节) = 8081
│ │ ├── 长度 (2 字节) = 1480
│ │ └── 校验和 (2 字节)
│ └── RTP 包 (1472 字节)
│ ├── RTP 头 (12 字节)
│ │ ├── V/P/X/CC (1 字节) = 0x80
│ │ ├── M/PT (1 字节) = 0x60
│ │ ├── 序列号 (2 字节) = 0x0001
│ │ ├── 时间戳 (4 字节) = 0x00000000
│ │ └── SSRC (4 字节) = 0x12345678
│ └── RTP 载荷 (1460 字节)
│ ├── FU indicator (1 字节) = 0x7C
│ ├── FU header (1 字节) = 0x85
│ └── FU payload (1458 字节) = 分片数据
└── FCS (4 字节) = 帧校验序列
十、核心规则总结查
在 RTP/H.264 实际传输中,经常会遇到花屏、音画不同步、丢包等问题。下面基于已讲解的原理,梳理常见问题的排查思路与解决方案。
| 问题现象 | 原因分析 | 排查方法 | 解决方案 |
|---|---|---|---|
| 花屏/绿屏/马赛克 | 1. 关键帧丢失或损坏,导致解码器无法正确参考 2. FU-A 分片组装错误(S/E 位处理不当、序列号乱序) 3. NAL 头还原错误(F/NRI/Type 拼接错误) | 1. 抓包检查 RTP 序列号连续性,确认是否丢包 2. 检查 FU indicator/header 构造是否正确 3. 检查接收端是否丢弃了不完整的分片组 | 1. 确保 I 帧(IDR)完整传输,必要时可降低分辨率/码率减少 I 帧大小 2. 严格按 FU indicator 前 3 位 + FU header 后 5 位还原 NAL 头 3. 对未完整接收的分片组直接丢弃,等待下一个 I 帧 4. 启用 RTCP 反馈,根据丢包率调整 FEC/重传策略 |
| 音画不同步 | 1. RTP 时间戳与 NTP 时间戳映射错误 2. 音频/视频使用不同的 SSRC 但未通过 RTCP 关联 3. 网络抖动导致音频缓冲与视频缓冲不一致 | 1. 检查音频和视频的 RTP 时间戳是否基于同一时钟源 2. 检查 RTCP SR 中的 NTP 时间戳与 RTP 时间戳的对应关系 3. 检查接收端抖动缓冲(Jitter Buffer)的大小设置 | 1. 在 RTCP SR 中正确填写 NTP 时间戳,确保接收端可关联音视频流 2. 通过 SDES 的 CNAME 将同一设备的音频/视频 SSRC 关联 3. 调整接收端缓冲策略,视频可适当增加缓冲深度 |
| 持续丢包/画面卡顿 | 1. 网络带宽不足,UDP 无拥塞控制 2. MTU 设置不当导致 IP 分片 3. 发送端码率过高,超过接收端处理能力 | 1. 通过 RTCP RR 查看丢包率、抖动 2. 检查 RTP 序列号缺口,统计丢包间隔 3. 检查路由器/交换机是否丢包,排除链路问题 | 1. 降低编码码率或分辨率,匹配实际带宽 2. 确保 RTP 载荷不超过 1460 字节,避免 IP 层再次分片 3. 启用 RTCP 反馈,发送端根据丢包率动态调整码率(REMB/TMMBR) |
| FU-A 分片无法完整组装 | 1. 分片包未按序列号连续发送,中间插入其他 NALU 的包 2. 接收端未正确识别 S=1 的开始包和 E=1 的结束包 3. 某个分片丢失导致整组丢弃 | 1. 抓包确认相同 NALU 的分片序列号是否连续 2. 检查 FU header 的 S/E 位是否正确设置 3. 检查接收端是否按序列号递增顺序收集分片 | 1. 发送端必须保证同一 NALU 的分片序列号连续,中间不能插入其他包 2. 接收端维护一个缓冲区,按 S 位置开始收集,E 位置结束并组装 3. 超时未收到结束包则丢弃整个分片组,等待下一个 I 帧 |
九、核心规则总结
9.1 打包规则
NAL 单元大小 ≤ MTU(1460 字节)→ 单个 NAL 单元包(Type=1-23)
NAL 单元大小 > MTU → 分片包 FU-A(Type=28)
这是 H.264 RTP 打包的核心逻辑,其他聚合包几乎不用。
9.2 分片规则
- 相同 NALU 的分片必须使用递增的 RTP 序号连续顺序发送,中间不能插入其他 RTP 包。
- 分片不可以嵌套,即一个 FU 不可以包含另一个 FU。
- STAPs、MTAPs 不可以被分片。
- NAL 单元必须按照 RTP 顺序号的顺序装配。
9.3 解包规则
接收端必须先读 RTP 头部的 PT 字段,确定载荷类型,然后才能解析第一个字节的格式。对于 FU-A 分片,需要根据 FU indicator 和 FU header 还原原始 NAL 头,并根据 S/E 位组装完整 NALU。
9.4 协议栈总结
| 层 | 协议 | 作用 |
|---|---|---|
| 应用层 | RTSP | 会话控制(播放、暂停、停止) |
| 传输层 | RTP | 传输媒体数据 |
| 传输层 | RTCP | 传输控制信息(QoS 反馈) |
| 传输层 | UDP | 无连接传输 |
| 网络层 | IP | 路由寻址 |
| 数据链路层 | 以太网 | 局域网帧传输 |
十一、完整流程回顾
10.1 整体架构
摄像头 → H.264 编码器 → NAL 单元 → RTP 打包 → UDP → IP → 以太网帧 → 网络 → Web 浏览器
10.2 发送端流程
- 摄像头采集原始视频帧(YUV 格式)
- H.264 编码器压缩编码,输出 NAL 单元
- 根据 MTU(1460 字节)判断是否分片
- 如果 NALU ≤ 1460 字节 → 单个 NAL 单元包
- 如果 NALU > 1460 字节 → FU-A 分片包
- 打包成 RTP 包(12 字节 RTP 头 + 载荷)
- 封装 UDP 数据报(8 字节 UDP 头 + RTP 包)
- 封装 IP 数据包(20 字节 IP 头 + UDP 数据报)
- 封装以太网帧(14 字节以太网头 + IP 包 + 4 字节 FCS)
- 发送到网络
10.3 接收端流程
- 接收以太网帧 → 剥离以太网头
- 接收 IP 包 → 剥离 IP 头
- 接收 UDP 数据报 → 剥离 UDP 头
- 接收 RTP 包 → 解析 RTP 头
- 读取 PT 字段,确定载荷类型为 H.264
- 读取第一个字节后 5 位(Type):
- Type=1-23 → 单个 NAL 单元,直接取出
- Type=28 → FU-A 分片,根据 S/E 位组装
- 还原 NAL 头(FU indicator 前 3 位 + FU header 后 5 位)
- 组装完整 NALU
- 交给 H.264 解码器
- 解码得到 YUV 帧
- 渲染到 Canvas/Video 元素
10.4 RTSP 信令流程
- OPTIONS:查询服务器能力
- DESCRIBE:请求媒体描述(SDP)
- SETUP:建立传输通道(协商端口、协议)
- PLAY:开始播放(开始传输 RTP 数据)
- RTP 数据流传输
- RTCP 控制信息交换
- TEARDOWN:结束会话