视频打包:RTP、RTCP、MTU 与分片
视频打包:RTP、RTCP、MTU 与分片
视频打包是将编码后的视频帧封装为网络传输数据包的过程。正确的打包策略对实时性和质量至关重要。
1. 打包概述
1.1 为什么需要打包
编码后的视频帧通常较大,不能直接在网络上传输:
flowchart LR
A[编码帧<br>100KB+] --> B[打包器]
B --> C[RTP 包 1<br>1200字节]
B --> D[RTP 包 2<br>1200字节]
B --> E[RTP 包 N<br>...]
1.2 打包流程
flowchart TB
A[编码视频帧] --> B{帧大小 > MTU?}
B -->|是| C[分片]
B -->|否| D[单包封装]
C --> E[添加 RTP 头]
D --> E
E --> F[发送]
2. RTP(Real-time Transport Protocol)
2.1 RTP 是什么
RTP 是实时传输协议,专门用于传输音视频数据:
- 实时性:低延迟设计
- 顺序性:序列号保证顺序
- 时间同步:时间戳用于同步
- 负载标识:Payload Type 标识编码格式
2.2 RTP 包结构
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 |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
| contributing source (CSRC) identifiers |
| .... |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Payload |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.3 RTP 头部字段
| 字段 | 位数 | 说明 |
|---|---|---|
| V (Version) | 2 | 版本号,固定为 2 |
| P (Padding) | 1 | 是否有填充 |
| X (Extension) | 1 | 是否有扩展头 |
| CC (CSRC Count) | 4 | CSRC 数量 |
| M (Marker) | 1 | 标记位(帧结束等) |
| PT (Payload Type) | 7 | 负载类型 |
| Sequence Number | 16 | 序列号 |
| Timestamp | 32 | 时间戳 |
| SSRC | 32 | 同步源标识 |
| CSRC | 32×N | 贡献源标识 |
2.4 Payload Type
常用视频 Payload Type:
| PT | 编码格式 | 说明 |
|---|---|---|
| 96 | H.264 | 动态分配 |
| 97 | H.265 | 动态分配 |
| 98 | VP8 | 动态分配 |
| 99 | VP9 | 动态分配 |
| 100 | AV1 | 动态分配 |
2.5 时间戳
- 时钟频率:视频通常 90000 Hz
- 单调递增
- 同一帧的所有包时间戳相同
时间戳计算:
- 每帧时间戳增量 = 时钟频率 / 帧率
- 例如:30fps 视频,增量 = 90000 / 30 = 3000
- 帧 0 时间戳 = 0,帧 1 时间戳 = 3000,帧 2 时间戳 = 6000,以此类推
2.6 序列号
- 16 位,范围 0-65535
- 循环使用(65535 → 0)
- 用于检测丢包和排序
序列号回绕处理:
- 当序列号从 65535 跳到 0 时,需要特殊处理
- 比较两个序列号时,考虑回绕情况
- 如果差值超过 32768,说明发生了回绕
3. RTCP(RTP Control Protocol)
3.1 RTCP 是什么
RTCP 是 RTP 的控制协议,用于:
- 传输质量反馈
- 会话控制
- 身份标识
3.2 RTCP 包类型
| 类型 | 名称 | 说明 |
|---|---|---|
| 200 | SR (Sender Report) | 发送者报告 |
| 201 | RR (Receiver Report) | 接收者报告 |
| 202 | SDES (Source Description) | 源描述 |
| 203 | BYE | 离开会话 |
| 204 | APP | 应用自定义 |
| 205 | RTPFB | RTP 反馈(NACK 等) |
| 206 | PSFB | 负载特定反馈(PLI、FIR 等) |
3.3 主要报文作用
| 报文 | 作用 |
|---|---|
| SR | 发送者报告,包含发送统计和 NTP/RTP 时间戳映射 |
| RR | 接收者报告,反馈丢包率、抖动、延迟等接收质量信息 |
| NACK | 负确认,请求重传丢失的 RTP 包 |
| PLI | 请求关键帧,接收端无法解码时发送 |
| FIR | 强制请求关键帧,比 PLI 更强制(如新用户加入) |
📖 RTCP 报文的详细格式和机制请参阅 2.11 RTCP 报文详解
4. MTU(Maximum Transmission Unit)
4.1 MTU 是什么
MTU 是网络层一次能传输的最大数据量:
| 网络类型 | MTU |
|---|---|
| 以太网 | 1500 字节 |
| PPPoE | 1492 字节 |
| VPN | ~1400 字节 |
| WebRTC 安全值 | 1200 字节 |
4.2 RTP 包大小计算
RTP 包大小 =
IP 头 (20) +
UDP 头 (8) +
RTP 头 (12) +
负载
安全最大负载 = 1500 - 20 - 8 - 12 = 1460 字节
考虑 VPN 等情况,WebRTC 通常使用 1200 字节
4.3 超过 MTU 的后果
- IP 分片:路由器分片,效率低,易丢包
- 丢包放大:一个分片丢失,整个包丢弃
- 延迟增加:需要等待所有分片
5. H.264 RTP 打包模式
5.1 打包模式概述
H.264 在 RTP 中的打包方式由 SDP 参数 packetization-mode 决定,共有三种模式:
| 模式 | 值 | 名称 | 支持的负载类型 | 典型应用 |
|---|---|---|---|---|
| 单 NAL 单元模式 | 0 | Single NAL Unit Mode | 仅单 NAL 包 | 低延迟视频通话 |
| 非交错模式 | 1 | Non-Interleaved Mode | 单 NAL、STAP-A、FU-A | WebRTC 默认 |
| 交错模式 | 2 | Interleaved Mode | 单 NAL、STAP-B、FU-B | 流媒体(较少使用) |
flowchart TB
A[H.264 NAL 单元] --> B{打包模式}
B -->|packetization-mode=0| C[单 NAL 单元模式]
B -->|packetization-mode=1| D[非交错模式]
B -->|packetization-mode=2| E[交错模式]
C --> F[单 NAL 单元包]
D --> G[单 NAL 单元包<br>STAP-A 聚合包<br>FU-A 分片包]
E --> H[单 NAL 单元包<br>STAP-B 聚合包<br>FU-B 分片包]
5.2 负载类型分类
H.264 RTP 负载分为三类:
┌─────────────────────────────────────────────────────────────────┐
│ H.264 RTP 负载类型 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 单 NAL 单元包 (Single NAL Unit Packet) │
│ ┌──────────────────┐ │
│ │ RTP Header │ │
│ ├──────────────────┤ │
│ │ NAL Unit Payload │ ← 一个完整的 NAL 单元 │
│ └──────────────────┘ │
│ │
│ 2. 聚合包 (Aggregation Packet) │
│ ┌──────────────────┐ │
│ │ RTP Header │ │
│ ├──────────────────┤ │
│ │ Payload Header │ ← STAP-A/B 或 MTAP16/24 │
│ ├──────────────────┤ │
│ │ NAL Unit 1 │ │
│ ├──────────────────┤ │
│ │ NAL Unit 2 │ ← 多个 NAL 单元聚合 │
│ ├──────────────────┤ │
│ │ ... │ │
│ └──────────────────┘ │
│ │
│ 3. 分片单元 (Fragmentation Unit) │
│ ┌──────────────────┐ │
│ │ RTP Header │ │
│ ├──────────────────┤ │
│ │ FU Indicator │ ← 分片指示 │
│ ├──────────────────┤ │
│ │ FU Header │ ← 分片头 │
│ ├──────────────────┤ │
│ │ NAL Unit Fragment│ ← NAL 单元的一部分 │
│ └──────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
5.3 单 NAL 单元包
适用场景:NAL 单元大小小于 MTU 时
结构:
单 NAL 单元包结构:
┌──────────────────────────────────────────┐
│ RTP Header (12 字节) │
├──────────────────────────────────────────┤
│ │
│ NAL Unit (完整) │
│ ┌────────────────────────────┐ │
│ │ NAL Header │ RBSP Payload │ │
│ │ (1 字节) │ (变长) │ │
│ └────────────────────────────┘ │
│ │
└──────────────────────────────────────────┘
NAL Header 直接作为 RTP 负载的第一个字节
示例:
一个 I 帧宏块数据(500 字节)封装为单 NAL 包:
RTP 负载:
┌────────────────────────────────────────┐
│ 0x65 │ I 帧宏块数据 (499 字节) │
│ NAL │ │
│ Hdr │ │
└────────────────────────────────────────┘
0x65 = 0110 0101
= Type 5 (IDR) + NRI 3 (重要)
总包大小 = 12 (RTP头) + 500 (NAL) = 512 字节 < MTU ✓
限制:只能用于 NAL 单元小于 MTU - 12 - IP/UDP 头的情况。
5.4 聚合包(STAP-A)
适用场景:多个小 NAL 单元需要同时发送
为什么需要聚合:
问题场景:
┌─────────────────────────────────────────────────────────────┐
│ 一帧图像包含多个小 NAL 单元: │
│ │
│ SPS (30 字节) + PPS (10 字节) + SEI (20 字节) = 60 字节 │
│ │
│ 如果每个单独发送: │
│ - 3 个 RTP 包 = 3 × (12 + 20 + 8) = 120 字节头开销 │
│ - 效率 = 60 / 180 = 33% │
│ │
│ 使用 STAP-A 聚合: │
│ - 1 个 RTP 包 = 12 + 20 + 8 + 60 + 6 = 106 字节 │
│ - 效率 = 60 / 106 = 57% │
│ │
│ 聚合后效率提升近一倍 │
└─────────────────────────────────────────────────────────────┘
STAP-A 结构:
STAP-A (Single-Time Aggregation Packet Type A) 结构:
┌─────────────────────────────────────────────────────────────┐
│ RTP Header (12 字节) │
├─────────────────────────────────────────────────────────────┤
│ Payload Header (1 字节) │
│ ┌───────────────────────────────────────┐ │
│ │ F │ NRI │ Type = 24 (STAP-A) │ │
│ └───────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ NAL Unit 1 │
│ ┌──────────────────────┬────────────────────────┐ │
│ │ Size (2 字节, 大端) │ NAL Unit 1 数据 │ │
│ └──────────────────────┴────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ NAL Unit 2 │
│ ┌──────────────────────┬────────────────────────┐ │
│ │ Size (2 字节, 大端) │ NAL Unit 2 数据 │ │
│ └──────────────────────┴────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ ... │
├─────────────────────────────────────────────────────────────┤
│ NAL Unit N │
│ ┌──────────────────────┬────────────────────────┐ │
│ │ Size (2 字节, 大端) │ NAL Unit N 数据 │ │
│ └──────────────────────┴────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
特点:
- 所有 NAL 单元共享相同的 RTP 时间戳
- 每个 NAL 前有 2 字节长度指示
- 按 RTP 序列号顺序解码
STAP-A 示例:
将 SPS + PPS 聚合为一个 STAP-A 包:
原始数据:
- SPS: 67 64 00 1F AC D9 40 ... (30 字节)
- PPS: 68 EE 3C 80 ... (10 字节)
STAP-A 负载:
┌────────┬─────────────────────────────────────────┐
│ 0x78 │ Payload Header (Type=24, NRI=3) │
├────────┼─────────────────────────────────────────┤
│ 0x001E │ SPS 长度 = 30 │
├────────┼─────────────────────────────────────────┤
│ 67 64...│ SPS 数据 (30 字节) │
├────────┼─────────────────────────────────────────┤
│ 0x000A │ PPS 长度 = 10 │
├────────┼─────────────────────────────────────────┤
│ 68 EE...│ PPS 数据 (10 字节) │
└────────┴─────────────────────────────────────────┘
总负载大小 = 1 + 2 + 30 + 2 + 10 = 45 字节
STAP-A 使用规则:
| 规则 | 说明 |
|---|---|
| 时间戳相同 | 所有聚合的 NAL 单元必须属于同一时间 |
| 顺序保持 | NAL 单元按解码顺序排列 |
| 大小限制 | 聚合后总大小不能超过 MTU |
| NRI 取值 | Payload Header 的 NRI 取各 NAL 中最大的 NRI |
5.5 分片单元(FU-A)
适用场景:单个 NAL 单元大小超过 MTU
为什么需要分片:
问题场景:
┌─────────────────────────────────────────────────────────────┐
│ 一个 I 帧的 NAL 单元: │
│ │
│ NAL Header (1 字节) + I 帧数据 (50000 字节) = 50001 字节 │
│ │
│ MTU = 1200 字节 │
│ │
│ 50001 字节 >> 1200 字节 │
│ │
│ 必须分片才能传输 │
└─────────────────────────────────────────────────────────────┘
FU-A 结构:
FU-A (Fragmentation Unit Type A) 结构:
原始 NAL 单元:
┌──────────────────────────────────────────────────────────┐
│ NAL Header │ RBSP Payload │
│ (1 字节) │ (大) │
└──────────────────────────────────────────────────────────┘
│
│ 分片
▼
┌──────────────────────────────────────────────────────────┐
│ FU-A 包 1 │
├──────────────────────────────────────────────────────────┤
│ RTP Header │
├──────────────────────────────────────────────────────────┤
│ FU Indicator │ FU Header │ NAL Fragment 1 (首字节开始) │
│ (1 字节) │ (1 字节) │ │
└──────────────────────────────────────────────────────────┘
┌──────────────────────────────────────────────────────────┐
│ FU-A 包 2 │
├──────────────────────────────────────────────────────────┤
│ RTP Header │
├──────────────────────────────────────────────────────────┤
│ FU Indicator │ FU Header │ NAL Fragment 2 (中间部分) │
│ (1 字节) │ (1 字节) │ │
└──────────────────────────────────────────────────────────┘
...
┌──────────────────────────────────────────────────────────┐
│ FU-A 包 N │
├──────────────────────────────────────────────────────────┤
│ RTP Header │
├──────────────────────────────────────────────────────────┤
│ FU Indicator │ FU Header │ NAL Fragment N (末尾部分) │
│ (1 字节) │ (1 字节) │ │
└──────────────────────────────────────────────────────────┘
FU Indicator 结构:
FU Indicator (1 字节):
┌───┬───────┬───────────────────┐
│ F │ NRI │ Type │
│ 1 │ 2 │ 5 │
└───┴───────┴───────────────────┘
F (forbidden_zero_bit): 1 位
- 必须为 0
NRI (nal_ref_idc): 2 位
- 从原始 NAL Header 复制
- 指示该 NAL 的重要性
Type: 5 位
- 固定为 28,表示 FU-A
FU Header 结构:
FU Header (1 字节):
┌───────┬───────┬───────┬───────────────────┐
│ S │ E │ R │ Type │
│ 1 │ 1 │ 1 │ 5 │
└───────┴───────┴───────┴───────────────────┘
S (Start): 1 位
- 1 = 这是第一个分片
- 0 = 不是第一个分片
E (End): 1 位
- 1 = 这是最后一个分片
- 0 = 不是最后一个分片
R (Reserved): 1 位
- 保留,必须为 0
Type: 5 位
- 从原始 NAL Header 复制
- 原始 NAL 单元的类型
FU-A 分片示例:
原始 IDR NAL 单元 (50000 字节):
┌──────────────────────────────────────────────────────────┐
│ 0x65 │ I 帧宏块数据 (49999 字节) │
│ IDR │ │
└──────────────────────────────────────────────────────────┘
NAL Header 0x65 解析:
F = 0
NRI = 3 (重要)
Type = 5 (IDR)
分片为约 42 个 FU-A 包 (每包约 1200 字节):
第 1 个 FU-A 包 (Start=1):
┌──────────────────────────────────────────────────────────┐
│ RTP Header (时间戳=T, 序列号=N, M=0) │
├──────────────────────────────────────────────────────────┤
│ 0x7C │ FU Indicator (F=0, NRI=3, Type=28) │
│ 0x85 │ FU Header (S=1, E=0, R=0, Type=5) │
│ I 帧数据前 1186 字节 │
└──────────────────────────────────────────────────────────┘
第 2-41 个 FU-A 包 (Start=0, End=0):
┌──────────────────────────────────────────────────────────┐
│ RTP Header (时间戳=T, 序列号=N+1, M=0) │
├──────────────────────────────────────────────────────────┤
│ 0x7C │ FU Indicator │
│ 0x05 │ FU Header (S=0, E=0, R=0, Type=5) │
│ I 帧数据中间 1188 字节 │
└──────────────────────────────────────────────────────────┘
第 42 个 FU-A 包 (End=1):
┌──────────────────────────────────────────────────────────┐
│ RTP Header (时间戳=T, 序列号=N+41, M=1) │
├──────────────────────────────────────────────────────────┤
│ 0x7C │ FU Indicator │
│ 0x45 │ FU Header (S=0, E=1, R=0, Type=5) │
│ I 帧数据最后部分 │
└──────────────────────────────────────────────────────────┘
注意:
- 所有分片的 RTP 时间戳相同
- 序列号连续递增
- 只有最后一个分片的 Marker 位为 1
FU-A 分片规则:
| 规则 | 说明 |
|---|---|
| 顺序发送 | 分片必须按顺序连续发送 |
| 时间戳相同 | 同一 NAL 的所有分片使用相同时间戳 |
| 序列号连续 | 分片的 RTP 序列号必须连续 |
| 不跨 NAL | 一个分片只能包含一个 NAL 的数据 |
| Marker 位 | 只有最后一个分片 M=1 |
| 首包特殊 | 第一个分片包含原始 NAL Header 的 F 和 NRI |
| 去除原头 | 分片数据不包含原始 NAL Header |
5.6 打包模式选择流程
flowchart TB
A[待发送 NAL 单元] --> B{大小 > MTU?}
B -->|否| C{还有其他小 NAL?}
B -->|是| D[使用 FU-A 分片]
C -->|是| E{聚合后 > MTU?}
C -->|否| F[使用单 NAL 包]
E -->|是| F
E -->|否| G[使用 STAP-A 聚合]
D --> H[按顺序发送分片]
F --> I[发送单包]
G --> J[发送聚合包]
5.7 接收端重组逻辑
STAP-A 解包:
STAP-A 解包流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 读取 Payload Header (1 字节) │
│ - 验证 Type = 24 │
│ │
│ 2. 循环读取 NAL 单元: │
│ a. 读取 Size (2 字节) │
│ b. 读取 Size 字节的 NAL 数据 │
│ c. 保存为独立的 NAL 单元 │
│ d. 如果还有数据,回到 a │
│ │
│ 3. 所有 NAL 单元共享相同的 RTP 时间戳 │
└─────────────────────────────────────────────────────────────┘
FU-A 重组:
FU-A 重组流程:
┌─────────────────────────────────────────────────────────────┐
│ 1. 检测 FU-A 包到达 │
│ - FU Indicator 的 Type = 28 │
│ │
│ 2. 解析 FU Header │
│ - S 位: 是否为起始包 │
│ - E 位: 是否为结束包 │
│ - Type: 原始 NAL 类型 │
│ │
│ 3. 起始包处理 (S=1) │
│ - 保存 NRI 和 Type │
│ - 创建缓冲区,存入分片数据 │
│ │
│ 4. 中间包处理 (S=0, E=0) │
│ - 追加分片数据到缓冲区 │
│ │
│ 5. 结束包处理 (E=1) │
│ - 追加最后分片数据 │
│ - 重构 NAL Header: (F=0) + NRI + Type │
│ - NAL Header + 缓冲区数据 = 完整 NAL 单元 │
│ │
│ 6. 验证 │
│ - 检查序列号连续性 │
│ - 检查时间戳一致性 │
└─────────────────────────────────────────────────────────────┘
5.8 常见问题与处理
| 问题 | 原因 | 处理方式 |
|---|---|---|
| 分片丢失 | 网络丢包 | 丢弃整个 NAL,请求关键帧 |
| 分片乱序 | 网络抖动 | 缓冲等待,超时丢弃 |
| 分片不完整 | 结束包丢失 | 超时后丢弃,请求重传或关键帧 |
| STAP-A 超大 | 打包错误 | 拆分为多个包或分片 |
| 类型不匹配 | 协议错误 | 丢弃并记录错误 |
6. 丢包处理
6.1 丢包检测
检测原理
RTP 通过序列号的连续性来检测丢包。每个 RTP 包携带一个 16 位序列号,发送端每发送一个包序列号加 1。
基本假设:
- 正常情况下,接收端收到的包序列号应该是连续递增的
- 如果出现序列号跳跃(间隙),说明可能有包丢失或乱序
丢包 vs 乱序的判定
核心问题:当收到的包序列号不连续时,如何判断是丢包还是乱序?
场景分析:
┌─────────────────────────────────────────────────────────────┐
│ 已收到: 1, 2, 3 │
│ 新到达: 5 │
│ │
│ 可能性 1 - 丢包: │
│ 包 4 在网络中丢失,永远不会到达 │
│ │
│ 可能性 2 - 乱序: │
│ 包 4 只是比包 5 晚到,稍后可能到达 │
└─────────────────────────────────────────────────────────────┘
判定策略:
| 策略 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 等待超时 | 等待一段时间,如果包还没到则判定丢失 | 准确区分丢包和乱序 | 增加延迟 |
| 立即判定 | 发现间隙立即判定丢失 | 延迟最低 | 可能误判乱序为丢包 |
| 混合策略 | 小间隙等待,大间隙立即判定 | 平衡准确性和延迟 | 实现复杂 |
序列号回绕处理
16 位序列号范围是 0-65535,超过最大值后会回绕到 0。
序列号回绕示例:
┌─────────────────────────────────────────────────────────────┐
│ 正常情况: ... 65533, 65534, 65535, 0, 1, 2, ... │
│ │
│ 如果按数值比较: │
│ 65535 > 0 ← 错误!实际 0 在 65535 之后 │
│ │
│ 正确处理方法: │
│ 计算差值,考虑回绕 │
└─────────────────────────────────────────────────────────────┘
回绕判定算法:
判断 seq_a 是否在 seq_b 之后:
1. 计算差值: diff = seq_a - seq_b
2. 处理回绕:
- 如果 diff > 32768 (即 65536/2):
说明 seq_a 实际上在 seq_b 之前(发生了回绕)
- 如果 diff < -32768:
说明 seq_a 在 seq_b 之后(反向回绕)
- 否则:
差值即为实际距离
示例:
seq_b = 65535, seq_a = 0
diff = 0 - 65535 = -65535
-65535 < -32768,所以 seq_a 在 seq_b 之后 ✓
seq_b = 0, seq_a = 65535
diff = 65535 - 0 = 65535
65535 > 32768,所以 seq_a 实际在 seq_b 之前 ✓
丢包检测状态机
┌─────────────────────────────────────────────────────────────┐
│ 丢包检测状态机 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [正常状态] │
│ │ │
│ │ 收到期望序列号的包 │
│ ▼ │
│ 更新 max_seq = 当前 seq + 1 │
│ │ │
│ │ 收到的 seq > max_seq (出现间隙) │
│ ▼ │
│ [间隙状态] ──────────────────────────────────────┐ │
│ │ │ │
│ │ 将 [max_seq, seq-1] 标记为"待确认" │ │
│ │ 启动定时器 │ │
│ │ │ │
│ ├─ 定时器超时 ──► 标记为丢失 │ │
│ │ │ │
│ └─ 收到待确认的包 ──► 从待确认列表移除 │ │
│ 更新 max_seq │ │
│ │ │ │
│ ▼ │ │
│ [正常状态] ◄───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
检测流程详解
步骤 1:初始化
- 记录第一个收到的包序列号为基准
- 设置 max_seq(期望的下一个序列号)
步骤 2:接收新包
当新包到达时:
┌─────────────────────────────────────────────────────────────┐
│ 1. 比较 seq 与 max_seq │
│ │
│ 如果 seq == max_seq: │
│ → 正常,更新 max_seq = seq + 1 │
│ │
│ 如果 seq > max_seq (考虑回绕): │
│ → 出现间隙 │
│ → 将 [max_seq, seq-1] 加入待确认列表 │
│ → 启动或重置这些序列号的定时器 │
│ → 更新 max_seq = seq + 1 │
│ │
│ 如果 seq < max_seq (考虑回绕): │
│ → 可能是乱序到达的包 │
│ → 检查是否在待确认列表中 │
│ → 如果在,从待确认列表移除 │
│ → 如果不在,可能是重复包或延迟太久 │
└─────────────────────────────────────────────────────────────┘
步骤 3:定时器处理
- 每个待确认的序列号有独立的定时器
- 定时器超时时间通常为 RTT 的 1-2 倍
- 超时后将该序列号标记为丢失,触发 NACK
实际示例
场景 1:简单丢包
时间线:
T1: 收到 seq=100 → max_seq=101
T2: 收到 seq=101 → max_seq=102
T3: 收到 seq=103 → 间隙!待确认=[102], max_seq=104
T4: 定时器超时 → seq=102 标记为丢失
场景 2:乱序后到达
时间线:
T1: 收到 seq=100 → max_seq=101
T2: 收到 seq=101 → max_seq=102
T3: 收到 seq=103 → 间隙!待确认=[102], max_seq=104
T4: 收到 seq=102 → 从待确认移除,无需重传
场景 3:连续丢包
时间线:
T1: 收到 seq=100 → max_seq=101
T2: 收到 seq=105 → 间隙!待确认=[101,102,103,104], max_seq=106
T3: 定时器超时 → seq=101,102,103,104 全部标记为丢失
场景 4:序列号回绕
时间线:
T1: 收到 seq=65535 → max_seq=0 (回绕)
T2: 收到 seq=1 → 间隙!待确认=[0], max_seq=2
T3: 收到 seq=0 → 从待确认移除(乱序到达)
NACK 触发时机
| 情况 | 触发条件 | 动作 |
|---|---|---|
| 立即触发 | 间隙超过阈值(如 3 个包) | 立即发送 NACK |
| 延迟触发 | 小间隙 | 等待一段短时间(如 10-20ms) |
| 超时触发 | 定时器到期 | 发送 NACK |
| 取消触发 | 乱序包到达 | 取消对应的 NACK |
优化策略
- 批量 NACK:多个连续丢包合并为一个 NACK 请求
- NACK 重发限制:同一序列号最多请求重传 2-3 次
- 智能等待:根据历史乱序率调整等待时间
- 快速恢复:收到重传包后立即尝试解码
6.2 NACK 重传
sequenceDiagram
participant S as 发送端
participant R as 接收端
participant B as 丢包缓冲
S->>R: RTP 1, 2, 3, 4, 5
S->>B: 存入缓冲
Note over R: 包 3 丢失
R->>S: NACK (seq=3)
S->>B: 查找包 3
B->>S: 返回包 3
S->>R: 重传 RTP 3
6.3 重传策略
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 立即重传 | 收到 NACK 立即发送 | 低延迟 |
| 延迟重传 | 等待一段时间再发 | 网络波动 |
| 限制次数 | 最多重传 N 次 | 避免拥塞 |
