RTCP 报文详解

RTCP(RTP Control Protocol)是 RTP 的控制协议,用于传输质量反馈、会话控制和身份标识。本文详细介绍各种 RTCP 报文的格式和机制。

1. RTCP 概述

1.1 RTCP 的作用

flowchart LR
    subgraph 发送端
        A[RTP 数据] --> B[SR]
    end

    subgraph 接收端
        C[RR] --> D[质量统计]
        E[NACK] --> F[丢包重传]
        G[PLI] --> H[请求关键帧]
        I[RPSI] --> J[选择参考帧]
        K[TMMBR] --> L[码率请求]
    end

    B <--> C
    B <--> E
    B <--> G
    B <--> I
    B <--> K

RTCP 主要功能:

  • 质量监控:SR/RR 报告传输质量
  • 丢包恢复:NACK 请求重传
  • 解码恢复:PLI/FIR 请求关键帧,RPSI 选择参考帧
  • 码率控制:TMMBR/TMMBN 临时调整码率
  • 会话控制:BYE 离开会话,SDES 身份描述

1.2 RTCP 报文类型

PT 名称 缩写 用途
200 Sender Report SR 发送者报告
201 Receiver Report RR 接收者报告
202 Source Description SDES 源描述
203 BYE BYE 离开会话
204 Application APP 应用自定义
205 RTPFB - RTP 反馈(NACK 等)
206 PSFB - 负载特定反馈(PLI、FIR 等)

PSFB (PT=206) 子类型

FMT 名称 缩写 用途
1 Picture Loss Indication PLI 请求关键帧
2 Reference Picture Selection Indication RPSI 选择参考帧
3 Temporary Max Media Stream Bit Rate Request TMMBR 请求限制码率
4 Temporary Max Media Stream Bit Rate Notification TMMBN 确认码率限制
4 Full Intra Request FIR 强制关键帧
15 Receiver Estimated Maximum Bitrate REMB 码率估计

1.3 通用报文头

所有 RTCP 报文都以相同的头部结构开始:

 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|    RC   |       PT      |         length                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         SSRC/SSRC                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

V (Version): 2位,版本号,固定为 2
P (Padding): 1位,是否有填充
RC (Reception Report Count): 5位,报告块数量
PT (Payload Type): 8位,报文类型
length: 16位,报文长度(32位字为单位,减1)
SSRC: 32位,同步源标识

2. Sender Report (SR)

2.1 SR 的作用

发送者定期发送 SR,用于:

  • 同步 NTP 时间和 RTP 时间戳
  • 报告发送统计信息
  • 帮助接收端计算往返延迟(RTT)

2.2 SR 报文格式

 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|    RC   |       PT=SR=200       |         length        |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                         SSRC of sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|              NTP timestamp (most significant word)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             NTP timestamp (least significant word)            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         RTP timestamp                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     sender's packet count                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      sender's octet count                     |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         report block 1                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              ...                              |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                         report block N                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

2.3 字段详解

字段 位数 说明
NTP timestamp 64 NTP 时间戳,用于同步
RTP timestamp 32 对应的 RTP 时间戳
sender’s packet count 32 发送的 RTP 包总数
sender’s octet count 32 发送的字节总数

2.4 NTP 时间戳

NTP 时间戳是从 1900年1月1日 00:00:00 UTC 开始的 64 位时间值:

// NTP 时间戳计算
function getNtpTimestamp() {
  const ntpEpoch = new Date('1900-01-01T00:00:00Z').getTime();
  const now = Date.now();
  const seconds = Math.floor((now - ntpEpoch) / 1000);
  const fraction = Math.floor(((now - ntpEpoch) % 1000) / 1000 * 0x100000000);

  return {
    msw: seconds,   // Most Significant Word
    lsw: fraction   // Least Significant Word
  };
}

2.5 报告块结构

SR 可以包含多个报告块(最多 31 个),用于报告该发送者接收到的其他源的统计信息:

 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
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                 SSRC_1 (SSRC of first source)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| fraction lost |       cumulative number of packets lost       |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           extended highest sequence number received           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      interarrival jitter                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         last SR (LSR)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   delay since last SR (DLSR)                  |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

关键字段说明

fraction lost(丢包率)

  • 8 位,表示丢包比例
  • 计算公式:丢包数 / 期望包数 * 256
// 计算丢包率
function calculateFractionLost(lost, expected) {
  if (expected === 0) return 0;
  return Math.floor(lost / expected * 256) & 0xFF;
}

interarrival jitter(到达抖动)

  • 估计网络抖动的统计量
  • 单位:RTP 时间戳单位
// 抖动计算
class JitterCalculator {
  constructor(clockRate = 90000) {
    this.jitter = 0;
    this.clockRate = clockRate;
  }

  update(rtpTimestamp, arrivalTime) {
    if (this.lastArrival !== undefined) {
      const D = (arrivalTime - this.lastArrival) -
                (rtpTimestamp - this.lastRtpTimestamp);
      this.jitter = this.jitter + (Math.abs(D) - this.jitter) / 16;
    }
    this.lastArrival = arrivalTime;
    this.lastRtpTimestamp = rtpTimestamp;
    return this.jitter;
  }
}

RTT 计算

// 计算往返延迟(Round-Trip Time)
function calculateRTT(arrivalTime, lsr, dlsr) {
  // arrivalTime: 收到 SR 的 NTP 时间
  // lsr: SR 中的 Last SR 字段
  // dlsr: SR 中的 Delay since Last SR 字段

  const currentTime = toNtpSeconds(arrivalTime);
  const rtt = currentTime - lsr - dlsr;

  return rtt; // 单位:秒的 1/65536
}

3. Receiver Report (RR)

3.1 RR 的作用

接收者定期发送 RR,反馈接收质量信息:

  • 丢包统计
  • 抖动测量
  • 延迟反馈

3.2 RR 报文格式

 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|    RC   |       PT=RR=201       |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                     SSRC of packet sender                     |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                         report block 1                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              ...                              |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                         report block N                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

3.3 RR vs SR

特性 SR RR
发送者 数据发送者 仅接收者
发送统计
报告块
NTP/RTP 映射

4. NACK(Negative Acknowledgment)

4.1 NACK 的作用

NACK 用于请求重传丢失的 RTP 包,是一种选择性重传机制。

4.2 NACK 工作流程

sequenceDiagram
    participant S as 发送端
    participant R as 接收端
    participant B as 重传缓冲区

    S->>R: RTP seq=1
    S->>R: RTP seq=2
    S-xR: RTP seq=3 (丢失)
    S->>R: RTP seq=4
    Note over R: 检测到 seq=3 丢失
    R->>S: RTCP NACK (PID=3)
    S->>B: 查找 seq=3
    B->>S: 返回 seq=3 数据
    S->>R: RTP seq=3 (重传)

4.3 NACK 报文格式

NACK 属于 RTPFB (PT=205) 类型,使用 FMT=1:

 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|   FMT=1 |       PT=205          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            PID                |             BLP               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段说明

字段 说明
FMT 1 = Generic NACK
PID 第一个丢失包的序列号
BLP 后续丢失包位图(Bitmap of Lost Packets)

4.4 BLP 位图解析

BLP 是一个 16 位位图,表示 PID+1 到 PID+16 范围内的丢失包:

BLP 位图:
  Bit 0: PID+1 是否丢失
  Bit 1: PID+2 是否丢失
  ...
  Bit 15: PID+16 是否丢失

示例:PID=100, BLP=0b0000000010000001
  - seq=100 丢失 (PID)
  - seq=101 丢失 (BLP bit 0 = 1)
  - seq=109 丢失 (BLP bit 8 = 1)
// 解析 NACK 消息
function parseNack(packet) {
  const pid = packet.readUInt16BE(8);
  const blp = packet.readUInt16BE(10);

  const lostPackets = [pid];

  for (let i = 0; i < 16; i++) {
    if (blp & (1 << i)) {
      lostPackets.push((pid + i + 1) & 0xFFFF);
    }
  }

  return lostPackets;
}

// 构造 NACK 消息
function buildNack(lostPackets) {
  if (lostPackets.length === 0) return null;

  const pid = lostPackets[0];
  let blp = 0;

  for (let i = 1; i < lostPackets.length && i <= 16; i++) {
    const diff = (lostPackets[i] - pid - 1) & 0xFFFF;
    if (diff < 16) {
      blp |= (1 << diff);
    }
  }

  return { pid, blp };
}

4.5 NACK 策略

// NACK 发送策略
class NackStrategy {
  constructor() {
    this.pendingNacks = new Map(); // seq -> { sendCount, lastSendTime }
    this.maxRetries = 3;
    this.retryInterval = 100; // ms
  }

  // 检测到丢包
  onPacketLoss(seq) {
    if (!this.pendingNacks.has(seq)) {
      this.pendingNacks.set(seq, {
        sendCount: 0,
        lastSendTime: 0
      });
    }
  }

  // 获取需要发送的 NACK
  getNacksToSend() {
    const now = Date.now();
    const nacks = [];

    for (const [seq, info] of this.pendingNacks) {
      if (info.sendCount < this.maxRetries &&
          now - info.lastSendTime >= this.retryInterval) {
        nacks.push(seq);
        info.sendCount++;
        info.lastSendTime = now;
      }

      // 超过最大重试次数,放弃
      if (info.sendCount >= this.maxRetries) {
        this.pendingNacks.delete(seq);
      }
    }

    return nacks;
  }

  // 收到重传包
  onRetransmit(seq) {
    this.pendingNacks.delete(seq);
  }
}

5. PLI(Picture Loss Indication)

5.1 PLI 的作用

PLI 用于请求发送关键帧(I帧),当接收端无法解码当前帧时发送:

  • 关键帧丢失
  • 参考帧丢失导致无法解码
  • 新用户加入需要完整图像

5.2 PLI 工作流程

sequenceDiagram
    participant S as 发送端
    participant R as 接收端

    S->>R: P帧 (参考 I帧)
    Note over R: I帧丢失,无法解码 P帧
    R->>S: RTCP PLI
    Note over S: 收到 PLI,编码关键帧
    S->>R: I帧 (关键帧)
    Note over R: 恢复解码

5.3 PLI 报文格式

PLI 属于 PSFB (PT=206) 类型,使用 FMT=1:

 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|   FMT=1 |       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

5.4 PLI 触发条件

// PLI 触发条件
class PliTrigger {
  constructor() {
    this.lastPliTime = 0;
    this.minPliInterval = 100; // 最小间隔 100ms
    this.consecutiveDecodeErrors = 0;
    this.maxDecodeErrors = 3;
  }

  // 解码失败
  onDecodeError() {
    this.consecutiveDecodeErrors++;
    return this.shouldSendPli();
  }

  // 检查是否应该发送 PLI
  shouldSendPli() {
    const now = Date.now();

    if (now - this.lastPliTime < this.minPliInterval) {
      return false;
    }

    if (this.consecutiveDecodeErrors >= this.maxDecodeErrors) {
      this.lastPliTime = now;
      this.consecutiveDecodeErrors = 0;
      return true;
    }

    return false;
  }

  // 收到关键帧
  onKeyFrame() {
    this.consecutiveDecodeErrors = 0;
  }
}

6. FIR(Full Intra Request)

6.1 FIR 的作用

FIR 比 PLI 更强制,用于:

  • 新用户加入会议,需要完整图像
  • 码率大幅变化,需要重新编码
  • MCU 混流需要同步关键帧

6.2 FIR vs PLI

特性 PLI FIR
强制性 请求 命令
触发条件 解码失败 新用户加入、同步需求
响应要求 尽快发送 必须立即响应
使用场景 恢复解码 初始化/同步

6.3 FIR 报文格式

FIR 属于 PSFB (PT=206) 类型,使用 FMT=4:

 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|   FMT=4 |       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                               0                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Seq nr.      |    Reserved                                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

6.4 FIR 序列号

FIR 包含序列号,用于:

  • 检测重复请求
  • 关联请求和响应
class FirManager {
  constructor() {
    this.seqNumber = 0;
    this.pendingFir = null;
  }

  sendFir() {
    const fir = {
      seqNumber: this.seqNumber++,
      sendTime: Date.now()
    };
    this.pendingFir = fir;
    return fir;
  }

  onKeyFrameReceived(seqNumber) {
    if (this.pendingFir && this.pendingFir.seqNumber === seqNumber) {
      const delay = Date.now() - this.pendingFir.sendTime;
      this.pendingFir = null;
      return { success: true, delay };
    }
    return { success: false };
  }
}

7. RPSI(Reference Picture Selection Indication)

7.1 RPSI 的作用

RPSI 用于告知编码器选择特定的参考图像进行编码,是一种错误恢复机制:

  • 当检测到某个参考帧损坏时,通知编码器使用其他参考帧
  • 避免错误传播到后续帧
  • 比 PLI 更精细的控制,不需要完整的 IDR 帧

7.2 RPSI vs PLI

特性 PLI RPSI
恢复方式 请求 IDR 关键帧 指定使用特定参考帧
带宽消耗 高(IDR 帧大) 低(可继续用 P 帧)
恢复速度 较慢(等待 IDR) 较快(切换参考帧)
实现复杂度 简单 复杂(需维护参考帧列表)

7.3 RPSI 报文格式

RPSI 属于 PSFB (PT=206) 类型,使用 FMT=2:

 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|   FMT=2 |       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          Native RPSI message from the receiver                |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

7.4 RPSI 工作流程

sequenceDiagram
    participant S as 发送端
    participant R as 接收端

    S->>R: I 帧 (Ref 0)
    S->>R: P 帧 1 (参考 Ref 0)
    S->>R: P 帧 2 (参考 Ref 0)
    Note over R: Ref 0 损坏
    R->>S: RPSI (选择 Ref 1 作为参考)
    Note over S: 切换参考帧
    S->>R: P 帧 3 (参考 Ref 1)
    Note over R: 恢复解码

7.5 RPSI 使用场景

// RPSI 触发条件
class RpsiTrigger {
  constructor() {
    this.corruptedRefs = new Set();
  }

  // 检测参考帧损坏
  onReferenceFrameCorrupt(refId) {
    this.corruptedRefs.add(refId);

    // 查找可用的替代参考帧
    const alternativeRef = this.findAlternativeRef(refId);

    if (alternativeRef !== null) {
      return {
        type: 'RPSI',
        preferredRef: alternativeRef
      };
    } else {
      // 没有可用参考帧,回退到 PLI
      return {
        type: 'PLI'
      };
    }
  }

  findAlternativeRef(corruptedRef) {
    // 实现参考帧选择逻辑
    // 返回可用的替代参考帧 ID
    return null;
  }
}

8. TMMBR/TMMBN(临时码率控制)

8.1 TMMBR 的作用

TMMBR(Temporary Maximum Media Stream Bit Rate Request)用于:

  • 接收端请求发送端临时限制码率
  • 适应网络带宽变化
  • 避免拥塞导致的丢包

8.2 TMMBR 报文格式

TMMBR 属于 PSFB (PT=206) 类型,使用 FMT=3:

 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|   FMT=3 |       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  MxTBR Exp    |           MxTBR Mantissa                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         MxTBR Mantissa        |         Overhead              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

字段说明

字段 说明
MxTBR Exp 码率指数(6位),用于计算实际码率
MxTBR Mantissa 码率尾数(17位),与指数配合使用
Overhead 开销(4位),表示包头开销

8.3 码率计算

最大码率 = MxTBR Mantissa × 2^MxTBR Exp bps
// 解析 TMMBR 码率
function parseTmmbrBitrate(packet) {
  const exp = (packet[8] >> 2) & 0x3F;  // 6 bits
  const mantissa = ((packet[8] & 0x03) << 15) |
                   (packet[9] << 7) |
                   (packet[10] >> 1);   // 17 bits
  const overhead = ((packet[10] & 0x01) << 3) | (packet[11] >> 5); // 4 bits

  const bitrate = mantissa * Math.pow(2, exp); // bps

  return {
    maxBitrate: bitrate,
    overhead: overhead
  };
}

// 构建 TMMBR 报文
function buildTmmbr(bitrate, overhead = 0) {
  // 计算指数和尾数
  let exp = 0;
  let mantissa = bitrate;

  while (mantissa > 0x1FFFF) { // 17位最大值
    mantissa = Math.floor(mantissa / 2);
    exp++;
  }

  return {
    exp: exp,
    mantissa: mantissa,
    overhead: overhead
  };
}

8.4 TMMBN(码率通知)

TMMBN(Temporary Maximum Media Stream Bit Rate Notification)是发送端对 TMMBR 的响应:

  • 确认已接受码率限制
  • 通知接收端当前生效的码率上限

TMMBN 属于 PSFB (PT=206) 类型,使用 FMT=4:

 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|   FMT=4 |       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  MxTBR Exp    |           MxTBR Mantissa                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|         MxTBR Mantissa        |         Overhead              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

8.5 TMMBR/TMMBN 工作流程

sequenceDiagram
    participant R as 接收端
    participant S as 发送端

    Note over R: 检测到带宽下降
    R->>S: TMMBR (请求限制码率为 500kbps)
    Note over S: 调整编码器码率
    S->>R: TMMBN (确认码率限制 500kbps)
    S->>R: RTP 包 (按新码率发送)
    Note over R: 接收稳定

    Note over R: 带宽恢复
    R->>S: TMMBR (请求提高码率到 2Mbps)
    S->>R: TMMBN (确认 2Mbps)
    S->>R: RTP 包 (按高码率发送)

8.6 TMMBR 触发条件

// TMMBR 触发策略
class TmmbrStrategy {
  constructor() {
    this.currentBitrate = 2000000; // 2 Mbps
    this.minBitrate = 100000;      // 100 kbps
    this.maxBitrate = 5000000;     // 5 Mbps
    this.lossThreshold = 0.05;     // 5% 丢包率
    this.rttThreshold = 200;       // 200ms RTT
  }

  // 根据网络状况决定是否发送 TMMBR
  evaluate(stats) {
    const { packetsLost, packetsReceived, rtt, availableBandwidth } = stats;

    const lossRate = packetsLost / (packetsLost + packetsReceived);

    // 高丢包率,请求降低码率
    if (lossRate > this.lossThreshold) {
      const newBitrate = Math.max(
        this.minBitrate,
        this.currentBitrate * 0.8
      );
      return { shouldSend: true, bitrate: newBitrate, reason: 'high_loss' };
    }

    // 高延迟,请求降低码率
    if (rtt > this.rttThreshold) {
      const newBitrate = Math.max(
        this.minBitrate,
        this.currentBitrate * 0.9
      );
      return { shouldSend: true, bitrate: newBitrate, reason: 'high_rtt' };
    }

    // 带宽估计低于当前码率
    if (availableBandwidth < this.currentBitrate * 0.9) {
      return {
        shouldSend: true,
        bitrate: Math.max(this.minBitrate, availableBandwidth * 0.9),
        reason: 'low_bandwidth'
      };
    }

    // 网络状况良好,可以尝试提高码率
    if (lossRate < 0.01 && rtt < 100 && availableBandwidth > this.currentBitrate * 1.2) {
      const newBitrate = Math.min(
        this.maxBitrate,
        this.currentBitrate * 1.2
      );
      return { shouldSend: true, bitrate: newBitrate, reason: 'good_network' };
    }

    return { shouldSend: false };
  }
}

8.7 TMMBR vs REMB

特性 TMMBR REMB
定义来源 RFC 5104 Google 提案
粒度 单个媒体流 可包含多个 SSRC
使用场景 传统视频会议 WebRTC 默认
状态 已标准化 逐渐被 Transport-CC 替代

9. SDES(Source Description)

7.1 SDES 的作用

SDES 用于传输参与者的描述信息:

  • CNAME(Canonical Name):唯一标识
  • NAME:显示名称
  • EMAIL:邮箱
  • PHONE:电话
  • LOC:位置
  • TOOL:工具/应用

7.2 SDES 报文格式

 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|    RC   |       PT=202           |         length        |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
|                          SSRC/CSRC_1                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           SDES items                          |
|                              ...                              |
+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+

7.3 SDES Item 格式

 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Type=CNAME  |     length    | username@domain             ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// SDES Item 类型
const SDES_TYPES = {
  END: 0,    // 结束
  CNAME: 1,  // 规范名称
  NAME: 2,   // 用户名
  EMAIL: 3,  // 邮箱
  PHONE: 4,  // 电话
  LOC: 5,    // 位置
  TOOL: 6,   // 工具
  NOTE: 7,   // 备注
  PRIV: 8    // 私有扩展
};

// 构建 CNAME
function buildCname(ssrc, username, domain) {
  const cname = `${username}@${domain}`;
  const item = Buffer.alloc(2 + cname.length + 1);
  item.writeUInt8(SDES_TYPES.CNAME, 0);
  item.writeUInt8(cname.length, 1);
  item.write(cname, 2);
  return item;
}

10. BYE

8.1 BYE 的作用

BYE 用于通知离开会话:

  • 主动离开
  • 会话结束
  • SSRC 冲突后更换

8.2 BYE 报文格式

 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|    SC   |       PT=203           |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           SSRC/CSRC                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              ...                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     length    |               reason for leaving             ...
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// 构建 BYE 报文
function buildBye(ssrcs, reason = '') {
  const sc = ssrcs.length; // Source Count
  const reasonBuffer = Buffer.from(reason);
  const padding = (4 - ((reasonBuffer.length + 1) % 4)) % 4;

  const length = 1 + sc + Math.ceil((1 + reasonBuffer.length + padding) / 4);
  const packet = Buffer.alloc(length * 4);

  // 头部
  packet.writeUInt8((2 << 6) | sc, 0);
  packet.writeUInt8(203, 1);
  packet.writeUInt16BE(length - 1, 2);

  // SSRC 列表
  for (let i = 0; i < ssrcs.length; i++) {
    packet.writeUInt32BE(ssrcs[i], 4 + i * 4);
  }

  // 离开原因
  const reasonOffset = 4 + sc * 4;
  packet.writeUInt8(reasonBuffer.length, reasonOffset);
  reasonBuffer.copy(packet, reasonOffset + 1);

  return packet;
}

11. 其他反馈消息

9.1 REMB(Receiver Estimated Maximum Bitrate)

REMB 用于接收端告知发送端建议的最大码率:

 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|   FMT=15|       PT=206          |         length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of packet sender                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC of media source                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Unique identifier 'R' 'E' 'M' 'B'                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  Num SSRC     |     BR Exp    |           BR Mantissa         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  SSRC feedback                                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// 解析 REMB
function parseRemb(packet) {
  const id = packet.slice(8, 12).toString('ascii');
  if (id !== 'REMB') return null;

  const numSsrc = packet.readUInt8(12);
  const brExp = packet.readUInt8(13) >> 2;
  const brMantissa = ((packet.readUInt8(13) & 0x03) << 16) |
                     (packet.readUInt8(14) << 8) |
                     packet.readUInt8(15);

  const bitrate = brMantissa * Math.pow(2, brExp); // bps

  const ssrcs = [];
  for (let i = 0; i < numSsrc; i++) {
    ssrcs.push(packet.readUInt32BE(16 + i * 4));
  }

  return { bitrate, ssrcs };
}

9.2 Transport-CC

Transport-CC 用于更精确的拥塞控制:

flowchart LR
    A[发送端] -->|RTP with Transport-CC seq| B[接收端]
    B -->|RTCP Transport-CC feedback| A
    A -->|计算可用带宽| C[调整码率]

9.3 TWCC(Transport Wide Congestion Control)

TWCC 是现代 WebRTC 使用的拥塞控制反馈:

// TWCC 反馈包含每个包的到达时间
const twccFeedback = {
  referenceTime: 12345678,  // 参考时间
  packetResults: [
    { seq: 100, received: true,  arrivalTime: 10 },
    { seq: 101, received: true,  arrivalTime: 12 },
    { seq: 102, received: false, arrivalTime: 0 },  // 丢失
    { seq: 103, received: true,  arrivalTime: 18 },
  ]
};

12. RTCP 发送策略

10.1 发送间隔

RTCP 报文不应过于频繁,通常:

  • 最小间隔:5 秒
  • 自适应间隔:根据参与者数量调整
// RTCP 发送间隔计算
function calculateRtcpInterval(participants, bandwidth) {
  // RFC 3550 建议
  const avgRtcpSize = 100; // 平均 RTCP 包大小(字节)
  const minInterval = 5;   // 最小间隔(秒)

  // 根据参与者数量和带宽计算
  const interval = (participants * avgRtcpSize * 8) / (bandwidth * 0.05);

  return Math.max(minInterval, interval);
}

10.2 复合 RTCP 包

多个 RTCP 报文可以复合成一个包发送:

+----------------+----------------+----------------+
|      SR        |     SDES       |     BYE        |
+----------------+----------------+----------------+

复合规则:

  1. 必须以 SR 或 RR 开头(如果有)
  2. SDES 必须包含 CNAME
  3. BYE 放在最后
// 构建复合 RTCP 包
function buildCompoundRtcp(sr, sdes, bye) {
  const parts = [];

  if (sr) parts.push(sr);
  if (sdes) parts.push(sdes);
  if (bye) parts.push(bye);

  return Buffer.concat(parts);
}

13. WebRTC 中的 RTCP

11.1 获取 RTCP 统计信息

async function getRtcpStats(pc) {
  const stats = await pc.getStats();
  const rtcpStats = {};

  stats.forEach(report => {
    if (report.type === 'remote-inbound-rtp') {
      // 接收端报告的统计(从对方 RR 获取)
      rtcpStats.inbound = {
        packetsLost: report.packetsLost,
        jitter: report.jitter,
        roundTripTime: report.roundTripTime,
        fractionLost: report.fractionLost
      };
    }

    if (report.type === 'remote-outbound-rtp') {
      // 发送端报告的统计(从对方 SR 获取)
      rtcpStats.outbound = {
        packetsSent: report.packetsSent,
        bytesSent: report.bytesSent,
        remoteTimestamp: report.remoteTimestamp
      };
    }
  });

  return rtcpStats;
}

11.2 RTCP 事件处理

// WebRTC 内部自动处理 RTCP
// 但可以通过统计信息监控

class RtcpMonitor {
  constructor(pc) {
    this.pc = pc;
    this.lastPacketsLost = 0;
    this.lastPacketsReceived = 0;
  }

  async check() {
    const stats = await this.pc.getStats();

    stats.forEach(report => {
      if (report.type === 'inbound-rtp' && report.kind === 'video') {
        const lost = report.packetsLost - this.lastPacketsLost;
        const received = report.packetsReceived - this.lastPacketsReceived;

        if (lost > 0) {
          console.log(`丢包: ${lost}/${received +lost} (${(lost/(received+lost)*100).toFixed(2)}%)`);
        }

        this.lastPacketsLost = report.packetsLost;
        this.lastPacketsReceived = report.packetsReceived;
      }
    });
  }
}

14. 总结

RTCP 报文对比

报文 类型 方向 主要用途
SR 200 发送→接收 发送统计、时间同步
RR 201 接收→发送 接收质量反馈
SDES 202 双向 身份描述
BYE 203 双向 离开会话
NACK 205/1 接收→发送 请求重传
PLI 206/1 接收→发送 请求关键帧
FIR 206/4 接收→发送 强制关键帧
RPSI 206/2 接收→发送 选择参考帧
TMMBR 206/3 接收→发送 请求限制码率
TMMBN 206/4 发送→接收 确认码率限制
REMB 206/15 接收→发送 码率建议

关键要点

  1. SR/RR:质量监控的核心,计算 RTT 和抖动
  2. NACK:选择性重传,提高可靠性
  3. PLI/FIR:解码恢复机制,快速恢复画面
  4. RPSI:参考帧选择,比 PLI 更精细的错误恢复
  5. TMMBR/TMMBN:临时码率控制,适应网络变化
  6. SDES/BYE:会话管理,身份和离开通知

下一章将介绍音频打包技术。