ENGINEERING CASE STUDIES
源码级故障复盘
从播放症状追到代码边界,用原始提交说明定位与修复取舍。
01 故障描述
部分 Disney+ HEVC 影片在 Tizen Chromium 上无法播放。排查发现,样本在进入平台安全解密之前,已被 Chromium 的 NAL 头检查拒绝。
02 原因分析
MP4 demux / HEVC 样本转换→NAL 头检查 · 返回错误→尚未进入平台解密
H265NaluParser::AdvanceToNextNALU() 检查第二个头字节中的 nuh_temporal_id_plus1 是否非零。但在这类加密样本中,第二字节仍是密文,不应在解密前按明文语法校验。
① MP4 样本内的 NAL 布局 示意:clear_bytes = 5
② 将两个 NAL 头字节展开为位字段完整字段名:F = forbidden_zero_bit,type = nal_unit_type,layer = nuh_layer_id。
为什么是密文?clear_bytes = 5 只覆盖 4 字节长度和第一个头字节;第二个头字节已进入 cypher_bytes 对应的加密区域。
为什么返回错误?密文低 3 位恰好为 0 时,非零检查返回 kInvalidStream;错误可沿样本转换路径传播,导致播放失败。
不能假定 NAL 头第二字节一定是明文。应先看样本加密信息,再决定是否执行字段检查。
03 解决方案
版本对比确认新增检查后,我在 Tizen TV 分支跳过该项检查,让样本继续进入平台安全播放路径。
#if !BUILDFLAG(IS_TIZEN_TV)
TRUE_OR_RETURN(nalu->nuh_temporal_id_plus1 != 0);
#endif
04 官方补丁对照
官方修复保留明文检查,并增加加密头部与明文头部的区分测试,佐证了“不能按明文语法检查密文字段”的问题机制。
源码追踪:从 MP4 demux 到平台解密
SourceBuffer.appendBuffer()JS 向 MSE 追加媒体分片
ChunkDemuxer / SourceBufferState运行 segment parser loop,调用 MP4StreamParser
MP4StreamParser提取加密样本、时间戳与 DecryptConfig(含 subsamples)
HEVCBitstreamConverter::ConvertAndAnalyzeFrame()长度前缀 → Annex B;分析码流,按需插入参数集
H265NaluParser::AdvanceToNextNALU()AnalyzeAnnexB / InsertParamSetsAnnexB 路径调用 NAL 头解析
TRUE_OR_RETURN(nalu->nuh_temporal_id_plus1 != 0)检查第二个头字节的低 3 位;不满足条件则返回 kInvalidStream
DecryptingDemuxerStream → IEMEDrmBridge::DecryptTizen()样本解析通过并下传后,才进入 Tizen 的解密交接路径
MediaPlayerESPlusPlayerTV::SubmitEsPacket()交付加密样本或 TZ handle,进入平台安全播放路径
固定版本:6bb9a6dec96db35094e37103a070a07157251e58。路径:media/formats/mp4/mp4_stream_parser.cc、media/formats/mp4/hevc.cc、media/parsers/h265_nalu_parser.cc、tizen_src/chromium_impl/media/filters/ieme_drm_bridge.cc。
证据范围与原始记录
时间先后来自公开 Git 记录,说明本人平台补丁早于这条官方修复;不表示上游参考或采纳了本人方案,也不主张最早发现。
图中四字节长度是示例,长度前缀不是 NAL 头的一部分。Annex B 转换需同步调整字节偏移。H.265 标准 ↗ · MP4 加密格式 ↗
官方 media 镜像 SHA:0b4a506f88ed8ee21281cd4a6cfacecf62eabf4e。官方 Review ↗
01 故障描述
三星电视上的 Hotstar HLS 播放场景中,开启 enableWorker 后内存持续抬升,触及约 600 MB 的应用预算,被系统终止。关闭 Worker 或推动 GC 后,历史测试中问题得到缓解。
开启 HLS Worker→播放内存快速增长→触及应用预算 · 系统终止
02 原因分析
V8 external-memory 的回收时机与电视应用预算不匹配。大块分片数据尚未及时回收,与媒体缓冲及平台资源叠加,先触及产品限额。
- XHR 接收SharedBuffer
Append(data, len)
累积分片字节
- 读取响应ArrayBuffer分配响应并复制
解除接收缓冲引用
- Worker 处理输入 / 输出交接transfer 传递所有权
解析、重封装后返回
- V8 回收释放存在时间差解除持有 → GC / 清扫
驻留量不一定立即下降
为什么开启 Worker 才触发?
Worker 将解析、重封装移出主线程,也增加独立 isolate 和消息交接。输入、输出及待回收存储可能在时间上重叠;主线程 GC 不等于同时回收 Worker。transfer 转移所有权,不是立即释放内存。
为什么 GC 没赶上预算?
V8 按各 isolate 的 external-memory 计数动态调整回收阈值,并分阶段回收。这个策略不直接对应电视应用约 600 MB 的总预算;回收触发或完成之前,产品限额可能已先耗尽。
03 解决方案
方案一 / 应用侧关闭 HLS Worker
new Hls({ enableWorker: false });
将解析、重封装移回主线程,减少 Worker 环境与跨线程交接。在播放与交互仍满足要求时,这是受限内存设备上的合理取舍;浏览器原生解码、MSE 播放能力保持不变。
验收重点:长时间播放、Seek、切档及遥控响应,确认内存改善没有换来明显主线程卡顿。
方案二 / V8 平台侧约束 external-memory 触发阈值
void set_limit(int64_t value) {
#if defined(OS_TIZEN_TV_PRODUCT)
if (value > kExternalAllocationHardLimit) {
return;
}
#endif
limit_.store(value, std::memory_order_relaxed);
}
kExternalAllocationHardLimit = 300 * 1024 * 1024。平台补丁限制过高的新阈值,让后续分配更容易进入既有内存压力回收路径。TV 构建中主线程和 Worker 的各个 isolate 均适用;300 MiB 是阈值约束,不是应用内存上限,也不是每次直接 full GC。
源码追踪:XHR → Worker → external-memory 回收
XMLHttpRequest::DidReceiveData()SharedBuffer::Append() 累积原生接收缓冲;ReportMemoryUsageToV8() 报告占用
XMLHttpRequest::ResponseArrayBuffer()CreateUninitializedOrNull() → GetBytes();复制为响应后清空 builder,创建响应并不依赖 Worker 开关
TransmuxerInterface.push() → worker.postMessage()transfer 输入至 Worker;Transmuxer.push() 解析、重封装,再由 emitTransmuxComplete() transfer 音视频输出
Isolate::AdjustAmountOfExternalAllocatedMemory()分配或计账更新作用于所属 isolate;正增量超过 external_memory_limit() 时进入压力处理
Heap::ReportExternalMemoryPressure()根据压力与回收状态启动 / 推进增量标记,或执行完整 GC;不是应用总内存达到固定值就统一回收
ArrayBufferSweeper → backing store 释放解除持有、标记、清扫与分配器归还页面有时间差;仍被消息或解析状态持有的数据不能靠 GC 强行释放
源码入口:core/xmlhttprequest/xml_http_request.cc、core/workers/worker_backing_thread.cc、v8/src/api/api.cc、v8/src/heap/heap.h、v8/src/heap/heap.cc、v8/src/heap/array-buffer-sweeper.cc。
Worker GC 基本原理与官方资料
独立的回收环境。HLS Worker 是执行 JS 解析、重封装的线程。Chromium 为它创建独立 V8 isolate,维护自己的 JS heap 和 external-memory 计数;页面主 isolate 的一次 GC 不等于同时收集 Worker。平台可以向 Worker isolates 发送内存压力通知。
解除持有 · 对象不可达→GC 标记 / 清扫→backing store 释放或重用
先判断能不能回收。GC 从栈、全局对象等根引用追踪存活对象。仍被消息、回调或解析状态持有的 buffer 不能回收;不可达后也需要等待相应标记和清扫。V8 采用分代、增量与并发等机制,避免每处理一段数据就暂停 JS 做完整回收。
大 buffer 另有记账。ArrayBuffer 的底层字节存储参与 external-memory 计账;XHR 接收缓冲也会报告原生占用。正向增量超过所属 isolate 的阈值时进入压力处理,按状态启动 / 推进标记或执行完整 GC。清空引用、清扫存储与分配器归还物理页面是不同阶段,进程内存不会保证同步下降。
原始补丁与复盘依据
6fdac8d146dc7a2e6a965e38b39da6daf592dd13 · [VD] Set hard limit for external memory increment
Author:Jinwoo Kwon · AuthorDate:2024-04-05 · CommitDate:2024-05-20。
工程分工:我负责 WebMedia 侧问题排查与方案对接,将修复方案交由 V8 团队提交。原始 Git 提交作者为 Jinwoo Kwon,保留在补丁记录中。
补丁说明直接记录了 M94 媒体播放中 ArrayBuffer backing store 回收频率不足、PSS 触及产品限额的问题。实现位于 Heap::ExternalMemoryAccounting::set_limit();拒绝超过 300 MiB 的新值,保留旧阈值,各 isolate 独立生效。
原始 Patch ↗ · Chromium M94 XHR ↗ · 对应 V8 压力回收 ↗