RTCP 报文详解
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 |
+----------------+----------------+----------------+
复合规则:
- 必须以 SR 或 RR 开头(如果有)
- SDES 必须包含 CNAME
- 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 | 接收→发送 | 码率建议 |
关键要点
- SR/RR:质量监控的核心,计算 RTT 和抖动
- NACK:选择性重传,提高可靠性
- PLI/FIR:解码恢复机制,快速恢复画面
- RPSI:参考帧选择,比 PLI 更精细的错误恢复
- TMMBR/TMMBN:临时码率控制,适应网络变化
- SDES/BYE:会话管理,身份和离开通知
下一章将介绍音频打包技术。
