原生播放器架构:线程、状态机与资源生命周期
Read/Demux、Decode、Audio、Render Threads 与 Seek/Flush
把输入、解封装、音视频解码、输出、同步和控制组织成可停止、可 Seek、可恢复的播放器状态机。
先明确本课目标
设计有界 Packet/Frame Queue
理解 serial/generation 隔离 Seek 前后数据
可靠执行 stop、flush、EOF 和错误传播
播放器不是一条 while 循环,而是一组可被控制平面协调的数据线程;Seek、停止和错误会沿所有队列传播一个新的“世代”。
怎样防止 Seek 前的旧帧在 Seek 后突然显示?
每次 Seek 增加 generation/serial。Packet、Frame 和时钟锚点都携带该编号;消费者发现编号不是当前世代就立即丢弃。
cppvoid seek(double target) {
const int next = ++generation;
packetQueue.flush();
frameQueue.flush();
demuxer.seek(target);
decoder.flush();
clocks.reset(target, next);
}- 增加播放世代 Seek 前先让旧数据拥有过期编号。
- 清空所有中间状态 Packet/Frame 队列与 Decoder 参考缓存都必须清理。
- 重建时间锚点 新世代从目标附近关键帧开始,时钟同步重新建立。
任何晚到的旧 Packet 或 Frame 都会因 generation 不匹配被拒绝,不会污染新时间线。
OPTIONAL · 按需查阅完整术语表与概念边界遇到陌生术语时再展开,不打断主线实验。
展开⌄
边改边运行
浏览器加载后会激活代码编辑器、独立运行环境与实时输出。
写出播放器 Seek 的状态迁移表和每个组件必须执行的清理动作。
回到原理,解释刚才发生了什么
先用结果建立反馈,再按运行流程、核心原理和知识关系逐层深入。
核心讲解
控制面与数据面分离
数据线程持续搬运 Packet/Frame;控制面处理 play、pause、seek、stop 和错误。状态变化通过原子状态、条件变量和队列控制传播。
Seek 是全管线事务
暂停生产、清队列、定位 Demuxer、flush Decoder、重置时钟,再用新的 serial 标记后续数据,避免旧帧穿越到新时间点。
OPTIONAL · 迁移时查阅Web 与原生实现对照主线实验完成后,再用完整映射处理跨平台或跨 API 迁移。
展开⌄
Web 媒体元素提供成品状态机,原生架构把同一状态变化拆到多个线程和队列。
WebGL
应用定义 PlayerState、线程退出协议、队列 abort/flush 和资源所有权。
WebGPU
HTMLMediaElement 通过 play/pause/currentTime/events 暴露托管状态;MSE/WebCodecs 允许逐层接管。
先定义状态和所有权,再创建线程;多媒体死锁往往来自“谁负责停止谁”没有写清。