视频打包: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

优化策略

  1. 批量 NACK:多个连续丢包合并为一个 NACK 请求
  2. NACK 重发限制:同一序列号最多请求重传 2-3 次
  3. 智能等待:根据历史乱序率调整等待时间
  4. 快速恢复:收到重传包后立即尝试解码

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 次 避免拥塞