WebRTC 全景实战 (8):RTP/RTCP 媒体传输与 QoS
Ch7 DTLS/SRTP 建立加密通道后,音频和视频以 RTP 包在 UDP 上传输。RFC 3550 定义了 RTP/RTCP——它的历史可追溯到 1992 年 Ron Frederick 的 nv 工具与 MBONE 多播实验(详见 Curious — RTP 历史)。
Frederick 在访谈中说:「如果一个东西需要实时数据流,又无需精确时序传输,它就可以从 RTP 中受益。」他最初为 MBONE 多播场景设计 RTP,后来 RTP 成为互联网实时音视频的事实标准。WebRTC 在 RTP 之上叠加了 SRTP 加密、AVPF 反馈 profile 和 TWCC 拥塞控制,但 RTP 包头结构与核心语义 thirty 年来未曾改变。
RTP 不要求数据按精确时序到达——它面向实时而非可靠。丢包用 FEC/NACK/PLC 补偿,而非 TCP 式重传阻塞。这正是 Ron Frederick 所说「如果一个东西需要实时数据流,又无需精确时序传输,它就可以从 RTP 中受益」。
本章覆盖 RFC 3550 RTP 包头、RTCP SR/RR/NACK/PLI/TWCC、Jitter Buffer、FEC 策略,以及 getStats() 诊断实战。
零、Ron Frederick 与 RTP 的诞生
WebRTC for the Curious — 历史 对 Ron Frederick 有一段专门访谈。1992 年,Frederick 在 Xerox PARC 为 nv(network video)工具开发传输层——当时 MBONE(Multicast Backbone)让研究者能在互联网上做多播实验,但缺少统一的实时包格式。
| 年代 | 事件 | 意义 |
|---|---|---|
| 1992 | Frederick 发布 RTP/RTCP 草案 | 为多播音视频定义统一容器 |
| 1992 | 手写软件视频压缩(nv 编解码) | MPEG-1 无法实时,催生轻量压缩思路 |
| 1996 | RTP 成为 IETF RFC 1889(后修订为 RFC 3550) | 从实验走向标准 |
| 2000s | 单播 VoIP 取代 MBONE 多播 | RTP 适配单播 + RTCP 反馈 |
| 2011+ | WebRTC 标准化 | SRTP + AVPF + TWCC 叠加在 RTP 之上 |
Frederick 本人承认 RTCP 在简单单播场景显得「过重」——周期性 SR/RR 占用带宽,反馈类型不断膨胀(NACK、PLI、TWCC…)。但 WebRTC 恰恰证明:没有 RTCP 反馈,GCC 拥塞控制和丢包恢复都无法工作。RTP 的 12 字节头保持了极简;复杂性被有意推到了 RTCP 控制面。
本篇术语表
| 术语 | 英文 | 解释 |
|---|---|---|
| RTP | Real-time Transport Protocol | 实时传输协议,RFC 3550 |
| RTCP | RTP Control Protocol | RTP 的控制面,传输统计与反馈 |
| SSRC | Synchronization Source | 32 bit 同步源标识符,标识一个 RTP 流 |
| CSRC | Contributing Source | 混音/混流中的贡献源列表 |
| PT | Payload Type | 7 bit 载荷类型,映射到编解码器 |
| Sequence Number | — | 16 bit 序号,检测丢包与乱序 |
| Timestamp | — | 32 bit 时间戳,播放时序基准 |
| M bit | Marker Bit | 标记位,常标识视频帧边界 |
| SR | Sender Report | RTCP 发送端报告,含发包数与时间戳 |
| RR | Receiver Report | RTCP 接收端报告,含丢包率与 jitter |
| NACK | Negative Acknowledgment | 请求重传丢失的 RTP 包 |
| PLI | Picture Loss Indication | 请求发送关键帧(I 帧) |
| FIR | Full Intra Request | 强制完整帧内编码帧 |
| TWCC | Transport-wide Congestion Control | 逐包延迟反馈,驱动 GCC |
| REMB | Receiver Estimated Maximum Bitrate | 接收端估计最大码率(Legacy) |
| Jitter Buffer | — | 接收端缓冲,平滑网络抖动 |
| FEC | Forward Error Correction | 前向纠错,冗余包恢复丢失数据 |
| RTX | Retransmission | 重传流,独立 SSRC 承载重传包 |
| PLC | Packet Loss Concealment | 丢包隐藏,音频猜测填充 |
| Compound RTCP | — | 多个 RTCP 包合并在一个 UDP 载荷中发送 |
| ROC | Rollover Counter | SRTP 内部序号扩展,见 Ch7 |
| MID | Media Identification | BUNDLE 下区分 m-line 的 RTP 扩展 |
| abs-send-time | — | 绝对发送时间扩展,辅助带宽估计 |
一、RTP 在 WebRTC 媒体路径中的位置
RTP 位于编解码器与 SRTP 之间,是媒体数据的容器格式。它不关心载荷是 Opus 还是 VP8——只负责打上序号、时间戳和 SSRC,让接收端能正确排序、检测丢包、同步播放。
1.1 RTP 的设计目标(RFC 3550)
| 目标 | 实现方式 |
|---|---|
| 实时性 | UDP 传输,无重传阻塞 |
| 多流复用 | SSRC 区分不同源 |
| 时序恢复 | Timestamp + Sequence Number |
| 质量监控 | RTCP 周期性报告 |
| 可扩展 | RTP Header Extension |
1.2 与 RTCP 的分工
RTP 只负责「送数据」,RTCP(RTP Control Protocol) 负责反馈网络质量。WebRTC 中 RTCP 与 RTP 共用 DTLS 通道(a=rtcp-mux),避免额外端口。Ron Frederick 在访谈中也提到 RTCP 复杂度是 RTP 在单播场景被诟病的原因之一——但 WebRTC 恰恰依赖这些反馈实现自适应码率。
二、RTP 包头结构(RFC 3550 Section 5.1)
RTP 固定头至少 12 字节,后跟可选的 CSRC 列表和 Header Extension。
CC > 0 时紧跟 CC × 32 bit 的 CSRC 列表;X=1 时再跟 Header Extension。完整位域定义见下表。
2.1 固定头字段详解
| 字段 | 位数 | 作用 | 调试价值 |
|---|---|---|---|
| V (Version) | 2 | 固定为 2 | — |
| P (Padding) | 1 | 是否有尾部填充 | SRTP 加密后可能填充 |
| X (Extension) | 1 | 是否有 Header Extension | TWCC、abs-send-time 等 |
| CC (CSRC Count) | 4 | CSRC 标识符数量 | 混音场景 |
| M (Marker) | 1 | 标记位 | 视频帧最后一包 M=1 |
| PT (Payload Type) | 7 | 载荷类型 | PT=111 → Opus |
| Sequence Number | 16 | 循环序号 | packetsLost 统计 |
| Timestamp | 32 | 播放时序基准 | 音视频同步 |
| SSRC | 32 | 同步源标识 | Simulcast 每层不同 SSRC |
2.2 Timestamp 时钟频率
| 媒体 | 时钟频率 | 示例 |
|---|---|---|
| 音频 Opus | 48,000 Hz | 20ms 帧 = timestamp += 960 |
| 视频 VP8/H264 | 90,000 Hz | 30fps 帧 = timestamp += 3000 |
| RTX 重传 | 与主 PT 相同 | 复用主时钟 |
Timestamp 的绝对值无意义,接收端只关心增量。音视频同步通过 RTCP SR 中的 NTP 时间戳与 RTP 时间戳的映射实现。
2.3 Sequence Number 与丢包检测
Seq 16 bit,循环 0→65535→0。接收端维护 expected_seq:
if (received_seq == expected_seq):
正常,expected_seq++
elif (received_seq > expected_seq):
检测到丢包,gap = received_seq - expected_seq
packetsLost += gap
expected_seq = received_seq + 1
else:
乱序或重传,放入 Jitter Buffer 重排
2.4 SSRC 与 Simulcast
每个 RTP 流有唯一 SSRC。Simulcast 三档(h/m/l)各自分配不同 SSRC,SFU 按 SSRC 区分层。Ch9 详解 rid 与 SSRC 的映射。
2.5 RTP Header Extension
WebRTC 中常见的扩展(通过 SDP a=extmap 协商):
| extmap ID | URI | 用途 |
|---|---|---|
| 1 | ssrc-audio-level | 音频电平,用于说话人检测 |
| 2 | abs-send-time | 绝对发送时间,带宽估计 |
| 3 | transport-wide-cc | TWCC 序列号 |
| 4 | mid | 媒体 ID,BUNDLE 复用区分 |
| 5 | rtp-stream-id | Simulcast rid |
2.6 BUNDLE、MID 与 SSRC 映射
Ch4 SDP 中 a=group:BUNDLE 将 audio/video 复用到同一 UDP 五元组。此时仅靠 PT 无法区分流——需要 MID Header Extension:
a=group:BUNDLE 0 1
a=mid:0
a=mid:1
a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:mid
Simulcast 场景下,同一 MID 内可能有 3 个 SSRC(h/m/l),通过 rtp-stream-id 扩展区分。SFU 路由逻辑:MID 选 m-line → rid/SSRC 选 Simulcast 层。
2.7 Marker Bit 与视频帧边界
视频编解码器将一帧拆成多个 RTP 包时,最后一包的 M bit = 1,接收端据此判断帧边界。Jitter Buffer 和解码器依赖 M bit 触发解码——若发送端错误设置 M bit,会出现「解码器等待永不结束」或「提前解码半帧」的花屏。
三、RTCP:RTP 的控制面
RTCP 包类型在 200-207 范围(与 RTP PT 0-127 区分)。所有 RTCP 包共享 32 bit 公共头(RFC 3550 §6.1):
WebRTC 主要使用以下 RTCP 类型:
3.1 Sender Report (SR) — PT 200
发送端周期性(通常 1 秒)发送 SR,包含:
| 字段 | 含义 |
|---|---|
| NTP Timestamp | 发送时的 NTP 绝对时间 |
| RTP Timestamp | 对应的 RTP 时间戳 |
| Sender's Packet Count | 累计发送 RTP 包数 |
| Sender's Octet Count | 累计发送字节数 |
SR 使接收端能将 RTP timestamp 映射到 wall-clock time,是音视频同步的基础。
3.2 Receiver Report (RR) — PT 201
接收端周期性发送 RR,每个被接收的 SSRC 一条报告块(24 字节 / SSRC):
| 字段 | 含义 |
|---|---|
| Fraction Lost | 自上次报告以来的丢包率(8 bit 分数) |
| Cumulative Packets Lost | 累计丢包数(24 bit 有符号) |
| Extended Highest Seq | 收到的最高序号 |
| Interarrival Jitter | 到达间隔抖动估计 |
| Last SR Timestamp (LSR) | 最近收到的 SR 的 NTP 中间 32 bit |
| Delay Since Last SR (DLSR) | 自收到 SR 以来的延迟 |
3.3 反馈消息(RFC 4585 AVPF)
WebRTC 使用 AVPF(Audio-Visual Profile with Feedback)profile,允许 RTCP 反馈消息在检测到事件后立即发送(而非等下一个 SR/RR 周期)。
| 反馈类型 | SDP 中的 rtcp-fb | FMT | 作用 |
|---|---|---|---|
| NACK | nack | 1 | 请求重传丢失的 RTP 包 |
| PLI | nack pli | 1 | Picture Loss Indication,请求关键帧 |
| FIR | ccm fir | 4 | Full Intra Request,强制 I 帧 |
| REMB | goog-remb | — | 接收端估计最大码率(Legacy) |
| TWCC | transport-cc | — | Transport-wide Congestion Control(现代主流) |
这些反馈驱动 Ch10 GCC 拥塞控制。
3.4 NACK 重传机制
NACK 包体(RFC 4585 Generic NACK FCI):
发送端维护一个 RTX 缓冲区(通常 1-3 秒),收到 NACK 后从缓冲区取出对应包,通过 RTX SSRC 重传。SDP 中 RTX 映射:
a=rtpmap:97 rtx/90000
a=fmtp:97 apt=96
PT 97 是 RTX 流,关联主 PT 96(VP8)。
3.5 PLI 与 FIR
当 NACK 重传来不及(丢包过多或延迟过大),接收端发送 PLI 请求发送端立即编码一个关键帧(I 帧/IDR)。关键帧不依赖前序帧,可独立解码。
| 场景 | 触发 |
|---|---|
| 新订阅者加入 SFU | SFU 向发送端发 PLI |
| 连续丢包超过阈值 | 接收端发 PLI |
| 解码器报错 | 接收端发 PLI |
FIR 类似 PLI 但语义更强——强制完整帧内刷新,常用于切换分辨率层。
3.6 TWCC(Transport-wide Congestion Control)
TWCC 是现代 WebRTC 拥塞控制的核心反馈机制。与 per-SSRC 的 RR 不同,TWCC 在 RTP Header Extension 中为每个包打上 transport-wide sequence number,接收端回传每个包的到达时间。
发送端维护一个延迟梯度估计器(GCC 算法),根据 TWCC 反馈调整目标码率。详见 Ch10。
3.7 RTCP 带宽与 Compound Packet
RFC 3550 规定 RTCP 占用会话带宽的 5%(至少 16 kbps)。发送端按以下比例分配:
| RTCP 类型 | 带宽占比 |
|---|---|
| SR/RR | 25% |
| SDES(源描述) | 5% |
| BYE / APP | 剩余 |
WebRTC 中多个 RTCP 消息常合并为 Compound Packet 一次发送——例如 RR + NACK + TWCC feedback 在同一 UDP 载荷中。Wireshark 解析时需展开 Compound 内的子包。
3.8 REMB 与 abs-send-time(Legacy)
在 TWCC 普及之前,Chrome 使用 REMB(Receiver Estimated Maximum Bitrate)和 abs-send-time 扩展做带宽估计:
a=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time
a=rtcp-fb:96 goog-remb
| 机制 | 粒度 | 现状 |
|---|---|---|
| REMB | 整会话码率建议 | Legacy,部分 SFU 仍解析 |
| abs-send-time | 每包发送时间 | 辅助 GCC,逐渐被 TWCC 取代 |
| TWCC | 每包到达时间反馈 | 现代主流 |
新项目应只依赖 TWCC;对接旧 SFU 时需确认是否仍发送 REMB。
3.9 音视频同步(A/V Sync)
SR 中的 NTP Timestamp 与 RTP Timestamp 映射是 lip-sync 的基础:
接收端分别维护 audio/video 的 RTP→wall-clock 映射,在 Jitter Buffer 输出时对齐。getStats() 中 totalInterFrameDelay 和 jitterBufferDelay 可间接反映 sync 质量——若视频 JB 持续大于音频,会出现「口型不同步」。
四、Jitter Buffer 工作原理
网络抖动导致 RTP 包到达时间不均匀。Jitter Buffer 在播放前缓冲并重新排序:
4.1 Jitter 计算(RFC 3550 Appendix A.8)
J(i) = J(i-1) + (|D(i-1,i)| - J(i-1)) / 16
D(i-1,i) = (arrival_time_i - arrival_time_{i-1}) - (timestamp_i - timestamp_{i-1})
这是一个指数加权移动平均,反映在 RR 报告的 jitter 字段中。
4.2 自适应 Jitter Buffer
WebRTC 的 Jitter Buffer 是自适应的:
| 网络状况 | Buffer 行为 |
|---|---|
| 低抖动 | 缩小缓冲 → 降低延迟 |
| 高抖动 | 扩大缓冲 → 平滑播放 |
| 持续丢包 | 触发 PLC / 请求关键帧 |
4.3 延迟权衡
端到端延迟 = 采集延迟 + 编码延迟 + 网络延迟 + Jitter Buffer + 解码延迟 + 渲染延迟
Buffer 越大越平滑,但端到端延迟越高——这是实时通信的经典权衡。音频典型缓冲 20-60ms,视频 0-30ms(视频 Jitter Buffer 通常更激进以降低延迟)。
4.4 NetEQ(音频)
WebRTC 音频使用 NetEQ 作为 Jitter Buffer 实现,集成了:
- 自适应缓冲
- 时间拉伸(加速/减速播放)
- PLC(丢包隐藏)
- 舒适噪声生成(DTX 期间)
4.5 音频 stats:concealedSamples 与 silentConcealedSamples
getStats() 中 inbound-rtp(audio)特有字段:
stats.forEach((r) => {
if (r.type === "inbound-rtp" && r.kind === "audio") {
const concealed = r.concealedSamples ?? 0;
const total = r.totalSamplesReceived ?? 1;
const plcRate = (concealed / total * 100).toFixed(2);
console.log(`PLC rate: ${plcRate}%, jitter: ${(r.jitter * 1000).toFixed(1)}ms`);
}
});
| 字段 | 含义 |
|---|---|
concealedSamples | NetEQ 通过 PLC 合成的样本数 |
silentConcealedSamples | 静音段被隐藏的样本 |
fecPacketsReceived | 收到的 FEC 包数 |
insertedSamplesForDeceleration | 减速播放插入的样本(JB 膨胀) |
removedSamplesForAcceleration | 加速播放移除的样本(JB 收缩) |
concealedSamples 持续上升说明 FEC/NACK 未能恢复,NetEQ 在「猜」音频——主观音质发闷或断续。
五、丢包补偿策略
| 机制 | 原理 | 适用 | 延迟代价 |
|---|---|---|---|
| NACK + Retransmission | RTCP NACK 触发重传 | 视频,丢包率 < 5% | 1 RTT |
| FEC | 发送冗余包(XOR / Reed-Solomon) | 高丢包网络(> 5%) | 0 RTT,+带宽 |
| Opus In-band FEC | 音频帧内嵌前向纠错 | 音频抗丢包 | 0 RTT,+码率 |
| PLC | 丢包隐藏(猜测填充) | 音频最后防线 | 0,质量下降 |
| ULPFEC | 通用 FEC 框架 | 视频冗余 | 0 RTT,+带宽 |
5.1 Opus In-band FEC
SDP 中通过 a=fmtp:111 useinbandfec=1 启用。Opus 在编码时将前一帧的部分信息冗余编码进当前帧。丢包时解码器尝试从 FEC 数据恢复,无需重传。
// 检查 Opus FEC 是否启用
const sdp = pc.localDescription.sdp;
const fecEnabled = sdp.includes("useinbandfec=1");
console.log("Opus FEC:", fecEnabled);
5.2 视频 FEC(ULPFEC / FlexFEC)
WebRTC 支持 RFC 5109 ULPFEC 和 FlexFEC:
FEC 牺牲带宽换取零延迟恢复,适合高丢包但延迟敏感的链路(如移动网络)。
5.3 RTX 重传流
RTX 使用独立 SSRC 和 PT,避免重传包与原始包混淆:
原始: SSRC=12345, PT=96, seq=100
重传: SSRC=67890, PT=97, seq=50 (RTX seq 独立计数)
OSN=100 (Original Sequence Number, 在 RTX 载荷头中)
5.4 FlexFEC 与 ULPFEC 选型
| 方案 | RFC | 特点 | WebRTC 支持 |
|---|---|---|---|
| ULPFEC | RFC 5109 | XOR 冗余,实现简单 | 广泛 |
| FlexFEC | RFC 8627 | Reed-Solomon,可配置保护组 | Chrome 部分场景 |
| Opus in-band FEC | RFC 7587 | 音频帧内冗余 | 默认推荐 |
视频 FEC 牺牲约 10–30% 额外带宽。移动网络丢包 5–15% 时,FEC 比 NACK 更稳(零 RTT 恢复);有线网络低丢包时 NACK+RTX 更省带宽。
六、getStats() API 深度解读
RTCPeerConnection.getStats() 是诊断 RTP/RTCP 问题的核心工具。
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach((report) => {
if (report.type === "inbound-rtp" && report.kind === "video") {
console.log({
packetsLost: report.packetsLost,
packetsReceived: report.packetsReceived,
jitter: report.jitter,
framesDecoded: report.framesDecoded,
framesDropped: report.framesDropped,
frameWidth: report.frameWidth,
frameHeight: report.frameHeight,
decoderImplementation: report.decoderImplementation,
pliCount: report.pliCount,
nackCount: report.nackCount,
fecPacketsReceived: report.fecPacketsReceived,
fecPacketsDiscarded: report.fecPacketsDiscarded,
});
}
if (report.type === "outbound-rtp" && report.kind === "video") {
console.log({
targetBitrate: report.targetBitrate,
framesEncoded: report.framesEncoded,
qualityLimitationReason: report.qualityLimitationReason,
qualityLimitationDurations: report.qualityLimitationDurations,
nackCount: report.nackCount,
retransmittedPacketsSent: report.retransmittedPacketsSent,
});
}
if (report.type === "remote-inbound-rtp") {
console.log({
roundTripTime: report.roundTripTime,
fractionLost: report.fractionLost,
jitter: report.jitter,
});
}
if (report.type === "candidate-pair" && report.state === "succeeded") {
console.log({
currentRoundTripTime: report.currentRoundTripTime,
availableOutgoingBitrate: report.availableOutgoingBitrate,
bytesSent: report.bytesSent,
bytesReceived: report.bytesReceived,
});
}
});
}, 2000);
6.1 关键 stats 字段
| 字段 | 类型 | 含义 | 告警阈值 |
|---|---|---|---|
packetsLost | inbound-rtp | 累计丢包 | > 1% 需关注 |
jitter | inbound-rtp | 到达抖动(秒) | > 0.03s 需关注 |
jitterBufferDelay | inbound-rtp | JB 累计延迟 | 持续增长 → 网络恶化 |
framesDropped | inbound-rtp | 解码前丢弃帧数 | 持续增长 → 性能瓶颈 |
pliCount | inbound-rtp | 收到的 PLI 次数 | 频繁 → 网络差或订阅切换 |
nackCount | inbound/outbound | NACK 次数 | 频繁 → 丢包严重 |
qualityLimitationReason | outbound-rtp | 画质限制原因 | bandwidth/cpu |
targetBitrate | outbound-rtp | GCC 目标码率 | 持续下降 → 拥塞 |
availableOutgoingBitrate | candidate-pair | 可用出站带宽估计 | — |
6.2 qualityLimitationReason 解读
| 值 | 含义 | 对策 |
|---|---|---|
none | 无限制 | 正常 |
cpu | CPU 编码能力不足 | 降分辨率/换硬编 |
bandwidth | 带宽不足 | 检查网络/TURN |
other | 其他原因 | 查 webrtc-internals |
6.3 Stats 报告关联
通过 report.id 和 report.transportId / report.codecId 关联不同报告。Ch13 将完整讲解 stats 字段与 Prometheus 指标化。
七、常见问题与排查
| 问题 | 可能原因 | 排查 |
|---|---|---|
| 视频马赛克/花屏 | 丢包 + NACK 来不及 | 查 packetsLost、nackCount |
| 音频断续 | Jitter 过大或 PLC 频繁触发 | 查 jitter、concealedSamples |
| 单向音视频 | SSRC 不匹配 / 只收到 RR 无 RTP | 查 inbound-rtp 是否存在 |
| 延迟持续增长 | Jitter Buffer 膨胀 | 查 jitterBufferDelay 趋势 |
| 画质突然下降 | PLI 触发关键帧等待 | 查 pliCount 突增 |
| 码率不达预期 | GCC 限制或 CPU 瓶颈 | 查 qualityLimitationReason |
| Simulcast 层收不到 | SFU 未转发对应 SSRC | 查多个 inbound-rtp 报告 |
7.1 丢包率计算
function calcLossRate(report) {
const total = report.packetsReceived + report.packetsLost;
if (total === 0) return 0;
return (report.packetsLost / total * 100).toFixed(2) + "%";
}
7.2 模拟弱网
# Linux: 5% 丢包 + 100ms 延迟
sudo tc qdisc add dev eth0 root netem loss 5% delay 100ms
# macOS: 使用 Network Link Conditioner(Xcode Additional Tools)
# Chrome DevTools: Network → Throttling → Custom → 500 kbps
八、实战 Lab
Lab 1:观察 RTP 包头
- 建立 P2P 通话,Wireshark 过滤
rtp - 展开一个 RTP 包,记录 SSRC、Seq、Timestamp、PT、M bit
- 对比音频包(PT=111)与视频包(PT=96)的 Timestamp 增量
Lab 2:丢包与 NACK
sudo tc qdisc add dev eth0 root netem loss 3%getStats()观察packetsLost和nackCount上升- 移除 netem:
sudo tc qdisc del dev eth0 root - 观察 PLI 是否触发(
pliCount突增)
Lab 3:Jitter Buffer 延迟
setInterval(async () => {
const stats = await pc.getStats();
stats.forEach((r) => {
if (r.type === "inbound-rtp" && r.kind === "audio") {
const avgJBDelay = r.jitterBufferDelay / r.jitterBufferEmittedCount;
console.log(`JB avg delay: ${(avgJBDelay * 1000).toFixed(1)}ms, jitter: ${(r.jitter * 1000).toFixed(1)}ms`);
}
});
}, 2000);
逐步增加 netem delay,观察 JB 延迟如何自适应增长。
Lab 4:TWCC 与码率
- Chrome DevTools 限速 500 kbps
- 观察
outbound-rtp.targetBitrate下降 - 观察
qualityLimitationReason变为bandwidth - 恢复带宽,观察码率回升
Lab 5:RTCP 包分析
- Wireshark 过滤
rtcp - 区分 SR(PT=200)和 RR(PT=201)
- 找到 NACK/PLI 反馈包(PT=205,FMT=1)
- 记录 RR 中的
fraction lost和jitter字段
Lab 6:FEC 效果对比
- 在 Opus SDP 中确认
useinbandfec=1 - 10% 丢包下对比开启/关闭 FEC 的
concealedSamples差异 - 记录主观音频质量差异
九、本章小结
| 要点 | 内容 |
|---|---|
| RTP 角色 | 实时媒体容器:序号 + 时间戳 + SSRC |
| RTCP 角色 | 质量反馈:SR/RR 统计 + NACK/PLI/TWCC 事件 |
| 丢包策略 | 音频 FEC+PLC,视频 NACK+RTX+PLI |
| 延迟来源 | Jitter Buffer 是主要可调因素 |
| 诊断工具 | getStats() + Wireshark + netem |
Ron Frederick 1992 年在 Xerox PARC 创造 RTP 时,大概不会想到它会成为三十年后全球视频会议的基础协议。RTP 的美在于极简——12 字节头 + 载荷——把复杂性留给上层(编解码、加密、拥塞控制)。WebRTC 正是这些上层复杂性的集大成者。
下一篇(Ch9):编解码与 Simulcast——RTP 载荷中的 Opus/VP8 帧如何编码,以及 Simulcast 如何用多个 SSRC 实现多档发布。
系列导航
章节 主题 状态 0 架构全景与协议栈地图 ✅ 已发布 1 浏览器媒体 API 与设备管理 ✅ 已发布 2 第一个 P2P 视频通话 ✅ 已发布 3 信令服务器设计与会话状态机 ✅ 已发布 4 SDP 解剖与媒体协商 ✅ 已发布 5 ICE、STUN、TURN 与 NAT 穿透 ✅ 已发布 6 Data Channel 与 SCTP over DTLS ✅ 已发布 7 DTLS 握手与 SRTP 加密体系 ✅ 已发布 8 RTP/RTCP 媒体传输与 QoS ✅ 已发布 9 音视频编解码与 Simulcast 入门 ✅ 已发布 10 带宽估计与拥塞控制 GCC ✅ 已发布 11 Simulcast、SVC 与选择性订阅 ✅ 已发布 12 SFU/MCU/Mesh 架构与 Pion 实战 ✅ 已发布 13 调试工具链与可观测性 ✅ 已发布 14 TURN 集群部署与多区域扩展 ✅ 已发布 15 Capstone 生产级视频会议系统 ✅ 已发布
References
- RFC 3550 — RTP: A Transport Protocol for Real-Time Applications
- RFC 4585 — Extended RTP Profile for RTCP-Based Feedback (AVPF)
- RFC 5109 — RTP Payload Format for Generic FEC
- RFC 4588 — RTP Retransmission Payload Format (RTX)
- draft-holmer-rmcat-transport-wide-cc — TWCC
- RFC 8627 — RTP FlexFEC
- WebRTC for the Curious — RTP 历史访谈
- MDN — RTCPeerConnection.getStats()
- webrtcH4cKS — Demystifying WebRTC RTCP