← 返回

实时视频(二):RTP 解包、Access Unit 与关键帧等待

系列导航:8 | 9 | 10

RTP 一个包不等于一帧视频。H.264 和 H.265 都支持单 NAL、聚合和分片,网络还会产生乱序、重复、丢包和序列号回绕。播放器下游需要的是带时间戳、关键帧标记、参数集版本和完整性结果的 Access Unit,不能把 RTP 细节继续泄漏到渲染代码。

扩展序列号和乱序窗口

每个 SSRC 维护自己的序列号状态。RTP 序列号是 16 位,扩展序列号需要根据已记录的基准判断回绕方向。实现不应直接用普通整数比较,否则从 655350 会被当成大范围乱序。

乱序缓冲保存包、到达时间和期望序列号。窗口按包数或时间限制,窗口到期就记录缺口并决定丢弃当前 NAL。重复包不再次拼接,但计入重复指标。SSRC 变化时切换独立状态,不能把两路源的分片放进同一缓冲。

flowchart TD Packet[RTP 包] --> Header[校验 RTP 头与 SSRC] Header --> Order[扩展序列号与乱序窗口] Order --> Payload{负载类型} Payload --> Single[单 NAL] Payload --> Aggregate[聚合包拆分] Payload --> Fragment[分片重组] Single --> AU[按时间戳组 Access Unit] Aggregate --> AU Fragment --> AU AU --> Check[完整性与参数集检查] Check --> Key[关键帧等待或交付]

H.264 与 H.265 负载解析

H.264 常见单 NAL、STAP-A 和 FU-A。H.265 使用不同的 NAL 头以及聚合和分片类型,NAL 类型所在的位段不能直接复用 H.264 的掩码。聚合包每个单元带长度字段,解析前检查长度不超过剩余字节;一项越界,整个 RTP 包丢弃。

分片重组保存起始 NAL 头、起始标记、结束标记和累计大小。只有起始分片后按序收到中间片,直到结束标记,才形成完整 NAL。新的起始分片到来时清理未完成状态。累计长度设上限,防止畸形输入让内存无界增长。未支持的载荷模式返回明确错误,不按单 NAL 猜测。

拼成 Annex B 时,在 NAL 前添加起始码;交给长度前缀解码器时只做一次明确转换。转换层记录输入格式和输出格式,避免下游重复添加起始码,造成解码器把长度字段当作 NAL 头。

Access Unit 的边界

RTP 时间戳通常相同的一组 NAL 属于同一展示时刻,但不能只凭时间戳判断完整性。Marker 位能提供帧边界提示,仍需结合当前编码和分片状态。Access Unit 至少携带:

timestamp      媒体时钟时间
decodeOrder    解码提交顺序
nalUnits       完整 NAL 列表
isKey          经过 NAL 类型确认的关键帧标记
parameterSetId 当前 VPS/SPS/PPS 摘要
complete       是否存在丢包或解析错误

视频时钟来自 RTP 负载的时钟频率,常见视频频率为 90 kHz,但实现应读取会话协商。32 位时间戳回绕转换为单调时间,网络到达时间只用于抖动和超时。存在 B 帧时,解码顺序和展示顺序可能不同,解包层不能按到达顺序重写展示时间。

音频拥有独立 SSRC 和时钟频率。若有 RTCP Sender Report,可以建立 RTP 时间戳到发送端时间的映射;没有可靠映射时,只能用首包建立相对时钟,并在诊断中写明精度限制。

参数集和关键帧恢复

参数集可以来自 SDP,也可以在带内出现。解析器按摘要缓存 VPS、SPS、PPS,变更时增加参数集版本。新订阅者先获取适用参数集,再等待关键帧。参数集变化后,旧解码器配置失效,旧队列全部清空。

一旦 Access Unit 缺少分片或含有格式错误,当前单元不能交给解码器。后续预测帧依赖损坏参考帧,继续提交会把错误扩散。播放器进入 waiting-keyframe,等待编码器发送恢复点。若链路支持 PLI/FIR,按节流策略请求关键帧;设备不支持时由会话层决定重拉流或提示 GOP 过长。

关键帧判断基于经过验证的 NAL 类型和编码规则,不能只依赖 Marker 位。H.264 与 H.265 的随机访问类型不同,能力矩阵要写清支持范围。缓存的关键帧还要与当前参数集版本匹配,旧参数集不能拿来初始化新解码器。

Rust/WASM 放在哪里

当前浏览器 demo 的 Rust/WASM 模块负责校验 Annex B 结构、去除 RBSP 防仿真字节,并从 H.264/H.265 SPS 提取宽高、profile、level、色度格式和位深等元数据。TypeScript 通过 rust-h26x-probe.ts 调用它,探测输出随帧统计进入 Worker 消息。

Rust 探测器只处理码流边界和元数据。像素路径由 h264decoder@yume-chan/libde265 以及浏览器原生能力分别实现,解码器升级不会改变探测器的 ABI 契约。

测试样本

测试要覆盖序列号回绕、乱序窗口过期、重复包、STAP-A/FU-A 长度错误、分片缺失、参数集变更、关键帧等待和 SSRC 切换。每个失败样本保存输入包摘要、预期状态和释放后的缓冲大小。模糊测试验证截断头、超长聚合项和任意分片顺序不会崩溃或越界。

解包器能产出完整 Access Unit,不代表后续浏览器一定解码成功。编码 profile、description 格式和浏览器能力还要在 WebCodecs 层验证。

参考资料