问题引入
前文结束时,demux 线程已经启动、轨道已选好、decoder 已经就位------一切准备就绪。从这一刻起,事情变得有趣了:
你点击播放,demux 线程开始往队列里塞 packet。塞多快?塞多少?什么时候该停下来?这些问题没有一个"标准答案"------每个播放器的答案都不一样,而 mpv 的答案是所有开源播放器里最精打细算的一个。
本文拆解它的两招:线程状态机 (什么时候干活、先干哪样)和水位反馈回路 (干到什么程度就停)。 主线是:生产-消费管道怎么被调度、怎么决定读多少 。§1-§6 看"骨架"------线程模型、demux_thread 主循环、thread_work 状态机的优先级,以及那条 demux_queue;§7 看"水位"------reading 反馈回路怎么让 demux 线程"读一点、停一下";§8-§9 收尾并让你亲手验证。
1. 运行时架构全景
#mermaid-svg-86O54ekIL7SjAa2a{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-86O54ekIL7SjAa2a .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-86O54ekIL7SjAa2a .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-86O54ekIL7SjAa2a .error-icon{fill:#552222;}#mermaid-svg-86O54ekIL7SjAa2a .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-86O54ekIL7SjAa2a .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-86O54ekIL7SjAa2a .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-86O54ekIL7SjAa2a .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-86O54ekIL7SjAa2a .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-86O54ekIL7SjAa2a .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-86O54ekIL7SjAa2a .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-86O54ekIL7SjAa2a .marker{fill:#333333;stroke:#333333;}#mermaid-svg-86O54ekIL7SjAa2a .marker.cross{stroke:#333333;}#mermaid-svg-86O54ekIL7SjAa2a svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-86O54ekIL7SjAa2a p{margin:0;}#mermaid-svg-86O54ekIL7SjAa2a .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-86O54ekIL7SjAa2a .cluster-label text{fill:#333;}#mermaid-svg-86O54ekIL7SjAa2a .cluster-label span{color:#333;}#mermaid-svg-86O54ekIL7SjAa2a .cluster-label span p{background-color:transparent;}#mermaid-svg-86O54ekIL7SjAa2a .label text,#mermaid-svg-86O54ekIL7SjAa2a span{fill:#333;color:#333;}#mermaid-svg-86O54ekIL7SjAa2a .node rect,#mermaid-svg-86O54ekIL7SjAa2a .node circle,#mermaid-svg-86O54ekIL7SjAa2a .node ellipse,#mermaid-svg-86O54ekIL7SjAa2a .node polygon,#mermaid-svg-86O54ekIL7SjAa2a .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-86O54ekIL7SjAa2a .rough-node .label text,#mermaid-svg-86O54ekIL7SjAa2a .node .label text,#mermaid-svg-86O54ekIL7SjAa2a .image-shape .label,#mermaid-svg-86O54ekIL7SjAa2a .icon-shape .label{text-anchor:middle;}#mermaid-svg-86O54ekIL7SjAa2a .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-86O54ekIL7SjAa2a .rough-node .label,#mermaid-svg-86O54ekIL7SjAa2a .node .label,#mermaid-svg-86O54ekIL7SjAa2a .image-shape .label,#mermaid-svg-86O54ekIL7SjAa2a .icon-shape .label{text-align:center;}#mermaid-svg-86O54ekIL7SjAa2a .node.clickable{cursor:pointer;}#mermaid-svg-86O54ekIL7SjAa2a .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-86O54ekIL7SjAa2a .arrowheadPath{fill:#333333;}#mermaid-svg-86O54ekIL7SjAa2a .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-86O54ekIL7SjAa2a .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-86O54ekIL7SjAa2a .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-86O54ekIL7SjAa2a .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-86O54ekIL7SjAa2a .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-86O54ekIL7SjAa2a .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-86O54ekIL7SjAa2a .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-86O54ekIL7SjAa2a .cluster text{fill:#333;}#mermaid-svg-86O54ekIL7SjAa2a .cluster span{color:#333;}#mermaid-svg-86O54ekIL7SjAa2a div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-86O54ekIL7SjAa2a .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-86O54ekIL7SjAa2a rect.text{fill:none;stroke-width:0;}#mermaid-svg-86O54ekIL7SjAa2a .icon-shape,#mermaid-svg-86O54ekIL7SjAa2a .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-86O54ekIL7SjAa2a .icon-shape p,#mermaid-svg-86O54ekIL7SjAa2a .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-86O54ekIL7SjAa2a .icon-shape .label rect,#mermaid-svg-86O54ekIL7SjAa2a .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-86O54ekIL7SjAa2a .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-86O54ekIL7SjAa2a .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-86O54ekIL7SjAa2a :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🧵 主线程
🔒 in->lock 保护
🧵 Demux 线程
seeking
tracks_switched
reading=true + 水位低
desc->read_packet()
add_packet_locked()
demux_read_packet_async()
demux_thread()
thread_work() --- 状态机调度
read_packet()
demux_queue: head→pkt₁→...→pktₙ
demux_stream: reader_head/eof/eager
demux_internal: reading/seeking/eof
demux_cached_range\[\]
f_demux_in filter
f_decoder_wrapper
execute_seek() → 释放锁 → desc->seek() → 重新拿锁
execute_trackswitch() → switched_tracks()
demux_packet
核心设计:demux 线程通过 thread_work 状态机调度所有工作------seeking > track switching > backward seek > read_packet。每次最多完成一项工作,出队时根据优先级重新调度。
上图里
RANGES(demux_cached_range[])是"已缓存的 packet 被复用"的基础,seek 的缓存命中路径 (execute_cache_seek)下一章展开。本文负责生产-消费管道本身:thread_work怎么调度、packet 怎么进出demux_queue、水位怎么决定读多少。
2. demux_thread 主循环
c
// demux/demux.c:2661
static MP_THREAD_VOID demux_thread(void *pctx) {
struct demux_internal *in = pctx;
mp_thread_set_name("demux");
mp_mutex_lock(&in->lock);
while (!in->thread_terminate) {
if (thread_work(in)) // ① 有工作就做
continue; // → 做完立即重入(可能有新状态)
mp_cond_signal(&in->wakeup); // ② 没工作了,通知消费者
mp_cond_timedwait_until( // ③ 休眠直到下次 cache_update 或被唤醒
&in->wakeup, &in->lock, in->next_cache_update);
}
// ④ 异步关闭
if (in->shutdown_async) {
mp_mutex_unlock(&in->lock);
demux_shutdown(in);
mp_mutex_lock(&in->lock);
in->shutdown_async = false;
if (in->wakeup_cb)
in->wakeup_cb(in->wakeup_cb_ctx);
}
mp_mutex_unlock(&in->lock);
MP_THREAD_RETURN();
}
整个循环在持有 in->lock 的状态下运行。 demux_thread 入口处 mp_mutex_lock(&in->lock),此后全程持锁。仅在以下时机临时释放:
mp_cond_timedwait_until--- 休眠时自动释放,唤醒后自动重拿execute_seek()---desc->seek()内部有 I/O(可能阻塞很久),临时释放read_packet()---desc->read_packet()内部有 I/O,临时释放execute_trackswitch()---switched_tracks()内部可能涉及 demuxer 操作,临时释放
3. thread_work 状态机
🖥️ 架构 。
demux_thread的主循环本质上是一个微型任务调度器 ,而thread_work就是它的调度函数。把这个优先级队列映射到 OS 概念,理解会非常自然:
demux 优先级 对应 OS 概念 类比 track switch seek 硬件中断 最高优先级,必须立即响应 backward seek / recheck 异常处理 运行时状态不一致,快速修复 normal seek 高优先系统调用 用户操作(拖进度条),立即响应 read_packet 后台 I/O 轮询 日常任务 cache update 定时器中断 定期维护, cond_timedwait到期自动触发同样地,
in->lock就是这把大内核锁(BKL) :demux 线程全程持锁运行,只在耗时 I/O(desc->seek()、desc->read_packet())时临时释放------等价于"耗时系统调用主动让出 CPU,防止阻塞用户态进程"。
c
// demux/demux.c:2626
static bool thread_work(struct demux_internal *in) {
// opts = demuxer 运行时选项
struct demux_opts *opts = in->d_user->opts;
size_t old_max_bytes = opts->max_bytes; // 向前缓存上限 (max-bytes, 默认 150MB)
size_t old_max_bytes_bw = opts->max_bytes_bw; // 向后缓存上限 (max-back-bytes, 默认 50MB)
// seekable_cache 时生效,seek回看时命中缓存
// ① 选项变更检测(用户运行时改了缓存预算 → 可能需裁剪旧 packet)
if (m_config_cache_update(in->d_user->opts_cache)) {
update_opts(in->d_user); // 把新值复制进 in->max_bytes和in->max_bytes_bw
// 总预算(向前+向后)被调小了 → 旧 packet 可能超出新预算
// → 清空 packet 复用池,强制后续裁剪回收内存
if (opts->max_bytes + opts->max_bytes_bw <
old_max_bytes + old_max_bytes_bw)
demux_packet_pool_clear(in->packet_pool);
}
// ② ★ seek 优先
if (in->tracks_switched) {
execute_trackswitch(in);
return true;
}
if (in->need_back_seek) {
perform_backward_seek(in);
return true;
}
if (in->back_any_need_recheck) {
check_backward_seek(in);
return true;
}
if (in->seeking) {
execute_seek(in);
return true;
}
if (read_packet(in))
return true; // read_packet 可能释放并重新获取锁
// ③ 定期更新缓存统计
if (mp_time_ns() >= in->next_cache_update) {
update_cache(in);
return true;
}
return false; // 无事可做,休眠
}
3.1 优先级排序
track switch seek (tracks_switched) ← 最高优先:轨切换后立即重定位
↓
backward seek (need_back_seek) ← 倒放:需要回跳获取更多 packet
↓
backward recheck (back_any_need_recheck)
↓
normal seek (seeking) ← 用户 seek / 缓存命中后的衔接 seek
↓
read_packet (reading==true) ← 正常预读
↓
cache update (next_cache_update) ← 定期更新 bytes_per_second
↓
idle → cond_timedwait ← 无事可做,释放锁休眠
3.2 execute_seek --- 低层 seek 的执行
🧭 这里的
execute_seek是 demux 线程侧的低层重定位 (desc->seek()真 I/O)。"seek 目标时间是否已在缓存里"的判断与execute_cache_seek在下章。
c
// demux/demux.c:2524
static void execute_seek(struct demux_internal *in) {
// ⚠️ 调用者 demux_thread() 已经持有 in->lock
int flags = in->seek_flags;
double pts = in->seek_pts;
in->eof = false;
in->seeking = false;
in->seeking_in_progress = pts;
in->low_level_seeks += 1;
in->after_seek = true;
in->after_seek_to_start = !(flags & (SEEK_FORWARD | SEEK_FACTOR))
&& pts <= in->d_thread->start_time;
// ★ 临时释放锁:desc->seek() 内部有 I/O(可能阻塞很久)
mp_mutex_unlock(&in->lock);
if (in->d_thread->desc->seek)
in->d_thread->desc->seek(in->d_thread, pts, flags);
mp_mutex_lock(&in->lock); // 重新拿回锁
in->seeking_in_progress = MP_NOPTS_VALUE;
}
为什么临时释放? demux_thread 在入口处 mp_mutex_lock(&in->lock) 后全程持锁(仅在 cond_timedwait 和 I/O 点时释放)。desc->seek() 可能触发 HTTP Range 请求等待响应、本地 lseek + 重新 read 等耗时操作。如果持锁执行,主线程在 demux_read_packet_async → mp_mutex_lock(&in->lock) 处会一直被阻塞,表现为播放器"卡死"直到 seek 完成。
4. 数据结构:demux_stream 与 demux_queue
进"怎么读、读多少"之前,先认识管道里那两件容器。它们是 cached_range 的地基,但这里只讲生产-消费需要的部分。
4.1 demux_stream --- 一条轨道的消费状态
c
// demux/demux.c:379
struct demux_stream {
struct demux_internal *in;
struct sh_stream *sh; // ds->sh->ds == ds
enum stream_type type; // 等于 sh->type
int index; // 等于 sh->index
// --- 以下字段均由 in->lock 保护 ---
void (*wakeup_cb)(void *ctx);
void *wakeup_cb_ctx;
// demuxer 状态
bool selected; // 用户选了这条轨?
bool eager; // ★ 激进预读?音视频=true,字幕=false
bool still_image; // 流由多个稀疏静止图像组成
bool refreshing; // 轨切换后正在找回旧位置
bool eof; // 已到流末尾?
// 当前队列 ------ 读和写都用它(从不为空)
struct demux_queue *queue;
// 读者(解码器)状态
double base_ts; // 上次返回给解码器的 packet 时间戳
double bitrate;
struct demux_packet *reader_head; // ★ 指向当前解码位置
bool skip_to_keyframe;
bool need_wakeup; // 下次 reader_head 状态变化时调用 wakeup_cb
...
};
4.2 demux_queue --- 一条流的 packet 链表
c
// demux/demux.c:345
struct demux_queue {
struct demux_stream *ds; // 回指这条流
struct demux_cached_range *range; // ★ 归属哪个区间
struct demux_packet *head, *tail; // 单向链表,尾部追加
uint64_t tail_cum_pos; // 累计大小(含尾部包)
bool correct_dts; // 包 DTS 严格单调递增
bool correct_pos; // 包 pos 严格单调递增
double last_ts; // 加入队列的最后一个包的时间戳
// 用于增量维护 seek PTS 窗口
struct demux_packet *keyframe_latest, *keyframe_first;
double seek_start, seek_end;
bool is_bof, is_eof;
// 完整索引(会跳条目以降低密度)
struct index_entry *index; // 环形缓冲
size_t index_size, index0, num_index;
};
4.3 关系与不变量
demux_stream(一条轨道一个) demux_queue(队列)
┌────────────────────────┐ ┌─────────────────────────────┐
│ .queue ───────────────┼──► │ .ds ← 回指这条流 │
│ .reader_head(消费游标)│ │ .head → pkt₁ → ... → pktₙ │
│ .base_ts / .eof / │ │ .last_ts │
│ .eager / .selected │ │ .index[] / .seek_start/end │
└────────────────────────┘ └─────────────────────────────┘
│ .range ──► cached_range(02d)
两个不变量,先记住:
ds->queue是"活动指针",不是固定数组 :demux_stream只持有一个指向"当前正在读写的那条队列"的指针。读写同一条队列 ------demux 线程往ds->queue尾追加,消费者从ds->queue里的reader_head前进。reader_head是消费者的游标 :它之前的 packet 已被解码器取走,它自身指向解码器下次要取的那个,之后是未消费的。add_packet_locked在队列空、消费者等待时设置它(可带skip_to_keyframe跳到关键帧)。
🧭 每条队列还归属一个
cached_range(queue->range),区间把各条流的队列捆在一起,支持"seek 回去复用缓存"------下一篇再介绍。本文里你只需知道:生产者和消费者永远读写同一条ds->queue。
5. 入队:add_packet_locked
demux 线程从 desc->read_packet() 拿到一个 demux_packet 后,调用 add_packet_locked 入队。
5.1 核心流程
add_packet_locked(sh, dp):
├─ ① drop 判断: !selected or seeking or attached_picture → 丢弃
├─ ② refreshing 判断: 刷新 seek 后跳过旧位置的重叠 packet
├─ ③ record_packet: 写 disk cache + 写 dump 文件
├─ ④ correct_dts/correct_pos 维护: 检测时间戳/字节偏移单调性
├─ ⑤ ★ 挂入 ds->queue 链表尾
│ queue->tail->next = dp; queue->tail = dp;
├─ ⑥ 设置 reader_head(如果消费者在等)
│ if (!ds->reader_head && (!skip_to_keyframe || dp->keyframe))
│ ds->reader_head = dp;
├─ ⑦ 维护 seek 窗口(adjust_seek_range_on_packet)
├─ ⑧ prune_old_packets: 裁剪超出 max_bytes_bw 的历史 packet
├─ ⑨ 更新 duration(如果 PTS 超出)
└─ ⑩ wakeup_ds(ds): 通知消费者
① 中的 attached_picture:嵌入式封面的特殊路径
bool drop = !ds->selected || in->seeking || ds->sh->attached_picture;
attached_picture 不是普通视频帧,而是嵌入在容器中的静态封面图 (MP4 的 moov/udta/meta/ilst/covr、M4A 音乐封面等)。mpv 在 handle_new_stream()(demux/demux_lavf.c:688)检测到 AV_DISPOSITION_ATTACHED_PIC 后,直接把封面数据存到 sh->attached_picture,不走 packet 队列------所以 add_packet_locked 遇到它直接 drop。消费者取封面时从 sh->attached_picture 拷贝一份返回,只返回一次,第二次就 EOF。没有这个机制,封面会被当成只有一帧的普通视频流,解码后立刻 EOF 可能误触发播放停止。
断点建议 :在
add_packet_locked设断点,观察dp->pts、dp->keyframe、ds->reader_head的变化。
6. 消费:demux_read_packet_async 与 f_demux_in
6.1 消费入口:f_demux_in filter
主线程不直接调 demux_read_packet_async。它通过 filter graph 拉数据:
c
// filters/f_demux_in.c:20
static void demux_process(struct mp_filter *f) {
struct priv *p = f->priv;
if (!mp_pin_in_needs_data(f->ppins[0]))
return; // 下游没在等 → 不拉
struct demux_packet *pkt = NULL;
if (demux_read_packet_async(p->src, &pkt) == 0)
return; // 还没数据,等 wakeup_cb 通知
// 有数据了
struct mp_frame frame = {MP_FRAME_PACKET, pkt};
if (pkt) {
p->eof_returned = false;
} else {
frame.type = MP_FRAME_EOF; // EOF
if (p->eof_returned) return;
p->eof_returned = true;
}
mp_pin_in_write(f->ppins[0], frame); // 推给下游 decoder
}
唤醒链路:
demux 线程 add_packet_locked → wakeup_ds(ds)
│ (门闩:仅当 ds->need_wakeup 为 true 才动作------
│ 消费者(主线程)上次拉pkt包遇空队列时挂号 (demux.c:2795),
│ 通知后立即清 false;读到包则无需通知,因它会马上回来再要)
│
├─ ① 回调链 → 唤醒消费者(主线程)
│ ds->wakeup_cb = f_demux_in 的 wakeup(f)
│ → mp_filter_wakeup(f)
│ → filter graph 的 wakeup_cb (mp_wakeup_core_cb, loadfile.c:1764)
│ → mp_wakeup_core → 唤醒主线程 → demux_process 取包
│ (无 per-stream 回调则回退 in->wakeup_cb;字幕轨走 dec_sub 的 wakeup_demux)
│
└─ ② mp_cond_signal(&in->wakeup) → 唤醒 demux 线程自身
(主线程侧调用 wakeup_ds 时------轨道切换/解阻塞------叫醒休眠的 demux 线程)
🧭 被唤醒 ≠ 被需要:第一次
demux_process为什么data_requested是 false?注意,
data_requested == false只能说明"不是 decoder 在要包"(路径 A 被排除)。它不代表 f_demux_in 是被上面那条唤醒链叫起来的------事实上第一次跑图时 demux 线程可能还没产包(add_packet_locked还没跑),flush_async_notifications里也看不到demux_in。真正的原因更早:f_demux_in 在建图时就被"踢"进 pending 了。filter 进
pending[]有三条路,只有一条会置data_requested:
路径 触发 data_requestedA 需求驱动(同步) decoder 要包: lavc_process撞到EAGAIN→mp_pin_out_read→mp_pin_out_request_data(filter.c:319)置 true (和调度 add_pending_pin是同一函数里相邻两行)B 事件驱动(异步) demux 线程产包: wakeup_ds→ f_demux_in 的wakeup→mp_filter_wakeup→async_pending,下次跑图flush_async_notifications(filter.c:198)翻成 pending不碰 C 建图踢一脚(一次性) 创建/连接 pin: mp_filter_add_pin(filter.c:612)末尾 →init_connection→add_pending(f_demux_in)(filter.c:433)不碰 第一次
demux_process时:demux 还没产包(B 排除)、decoder 还没开口(A 排除,所以data_requested才是 false)------f_demux_in 的 pending 标记是建图时路径 C 留下的,一直没被消费,这次跑图才被 LIFO 循环弹出来。时间线:
① 建图:mp_demux_in_create → mp_filter_add_pin → init_connection → add_pending(f_demux_in) ← ★ 踢一脚,标记留着 ② 选轨接链:mp_pin_connect(decoder 输入, demux_in 输出) → init_connection 再踢一次 ③ 主线程 video_output_image → mp_pin_out_read → 触发第一次跑图 ④ 跑图开头 flush_async_notifications:demux 还没产包 → async_pending 里没有 demux_in ⑤ LIFO 弹出 f_demux_in(① ② 留下的标记)→ demux_process ⑥ mp_pin_in_needs_data 看到 data_requested 还是 false → return("下游没在等→不拉")这正是拉模式的自守卫:"被调度"不等于"被需要" ------连建图的一次性 kick 都算一种"被调度"。
process()只是"可能有事"的通知,读不读包还得等 decoder 开口;等 decoder 真缺包(路径 A),它会再调度一次,那时data_requested == true才真正demux_read_packet_async取包------第一次 return 是正常且省事的,不是 bug。想验证:在mp_filter_add_pin末尾的init_connection(filter.c:433)设断点,能看到 f_demux_in 在建图时 就被add_pending了。
6.2 demux_read_packet_async → dequeue_packet
c
// demux/demux.c:2882
int demux_read_packet_async(struct sh_stream *sh,
struct demux_packet **out_pkt) {
struct demux_stream *ds = sh ? sh->ds : NULL;
struct demux_internal *in = ds->in;
mp_mutex_lock(&in->lock);
int r = -1;
while (1) {
r = dequeue_packet(ds, MP_NOPTS_VALUE, out_pkt);
if (in->threading || in->blocked || r != 0)
break;
thread_work(in); // ★ 无线程模式兼容:主线程自己读
}
mp_mutex_unlock(&in->lock);
return r;
}
dequeue_packet 的核心逻辑:
dequeue_packet(ds):
├─ !ds->selected → return -1 (EOF)
├─ in->blocked → return 0 (等待)
├─ attached_picture → 返回静态图片 packet
├─ !ds->reader_head && ds->eof → return -1 (真 EOF)
├─ !ds->reader_head → return 0 (队列空,等生产者)
└─ advance_reader_head(ds) → return 1 (有 packet)
ds->reader_head = pkt->next; // 消费游标前进
ds->last_ret_pos = pkt->pos; // 记录最后返回的位置
ds->last_ret_dts = pkt->dts; // (用于 refresh seek)
🧭 普通播放靠"消费者下拉"惰性拉起预读 :
dequeue_packet发现!in->reading && !in->eof→ 置reading=true+mp_cond_signal(demux.c:2760)。demux 线程在demux_start_thread后本就休眠,靠这次消费者下拉唤醒。demux_start_prefetch()(demux.c:1241)只被--prefetch和--demuxer-cache-wait使用(见 §7.3 表格)。
6.3 锁的交接:Demux 线程 I/O 时主线程"趁虚而入"
in->lock 是一把粗粒度锁,但它的释放时机精确地创造了并发窗口:
🧵 Demux 线程(生产者) 🔒 in->>lock 🧵 主线程(消费者) 🧵 Demux 线程(生产者) 🔒 in->>lock 🧵 主线程(消费者) #mermaid-svg-sLZwcqb8jODyXDZA{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-sLZwcqb8jODyXDZA .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-sLZwcqb8jODyXDZA .error-icon{fill:#552222;}#mermaid-svg-sLZwcqb8jODyXDZA .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-sLZwcqb8jODyXDZA .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-sLZwcqb8jODyXDZA .marker{fill:#333333;stroke:#333333;}#mermaid-svg-sLZwcqb8jODyXDZA .marker.cross{stroke:#333333;}#mermaid-svg-sLZwcqb8jODyXDZA svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-sLZwcqb8jODyXDZA p{margin:0;}#mermaid-svg-sLZwcqb8jODyXDZA .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sLZwcqb8jODyXDZA text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-sLZwcqb8jODyXDZA .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sLZwcqb8jODyXDZA .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-sLZwcqb8jODyXDZA .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-sLZwcqb8jODyXDZA .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-sLZwcqb8jODyXDZA #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-sLZwcqb8jODyXDZA .sequenceNumber{fill:white;}#mermaid-svg-sLZwcqb8jODyXDZA #sequencenumber{fill:#333;}#mermaid-svg-sLZwcqb8jODyXDZA #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-sLZwcqb8jODyXDZA .messageText{fill:#333;stroke:none;}#mermaid-svg-sLZwcqb8jODyXDZA .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sLZwcqb8jODyXDZA .labelText,#mermaid-svg-sLZwcqb8jODyXDZA .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-sLZwcqb8jODyXDZA .loopText,#mermaid-svg-sLZwcqb8jODyXDZA .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-sLZwcqb8jODyXDZA .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-sLZwcqb8jODyXDZA .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-sLZwcqb8jODyXDZA .noteText,#mermaid-svg-sLZwcqb8jODyXDZA .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-sLZwcqb8jODyXDZA .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sLZwcqb8jODyXDZA .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sLZwcqb8jODyXDZA .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-sLZwcqb8jODyXDZA .actorPopupMenu{position:absolute;}#mermaid-svg-sLZwcqb8jODyXDZA .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-sLZwcqb8jODyXDZA .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-sLZwcqb8jODyXDZA .actor-man circle,#mermaid-svg-sLZwcqb8jODyXDZA line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-sLZwcqb8jODyXDZA :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 全程持锁运行 read_packet(in)mp_mutex_unlock(&in->>lock) ⬅ 释放!desc->>read_packet() ⬅ 阻塞在 I/Odemux_read_packet_async() ⬅ 趁虚而入!mp_mutex_lock(&in->>lock) ✅ 拿到锁!dequeue_packet(ds)advance_reader_head ⬅ 消费 packetmp_mutex_unlock(&in->>lock) ⬅ 还锁read_packet() 返回 ⬅ I/O 完成mp_mutex_lock(&in->>lock) ✅ 重新拿回锁add_packet_locked(ds, pkt) ⬅ 入队新 packetwakeup_cb → mp_wakeup_core() ⬅ 通知主线程
关键洞察 :这把锁的设计哲学是 "持锁但不阻塞"。demux 线程在内存操作(入队、状态检查、二分查找)期间持锁,这部分是 O(1) 或 O(log n),微秒级完成。唯一耗时的 I/O 操作全部在释放锁之后执行------这正是 §3 提到的 BKL 模型:内核在系统调用期间主动让出 CPU。
7. 水位控制:reading 的反馈回路
现在你认识了管道:生产者(add_packet_locked)往 ds->queue 追加,消费者(dequeue_packet)从 reader_head 前进。剩下的问题是------demux 线程什么时候读、读多少? 这就是水位控制,它读的是 reader_head/base_ts/last_ts 这些刚介绍过的字段。
7.1 核心参数
| 参数 | 选项 | 默认值 | 含义 |
|---|---|---|---|
min_secs |
--demuxer-readahead-secs |
1.0s | 预读目标:每个 eager 流至少缓存的时长 |
min_secs_cache |
--cache-secs |
3600s | 网络流时覆盖 min_secs |
hyst_secs |
--demuxer-hysteresis-secs |
0 | 迟滞:降到 min-hyst 才恢复预读 |
hyst_bytes |
--demuxer-hysteresis-bytes |
0 | 字节迟滞 |
max_bytes |
--demuxer-max-bytes |
150MB | 缓冲区硬上限 |
max_bytes_bw |
--demuxer-max-back-bytes |
50MB | 向后缓存上限(seek 后可回看) |
7.2 read_packet 内部的水位判断
c
// demux/demux.c:2262(按真实结构精简:略去 back_demuxing / 惰性流细节)
static bool read_packet(struct demux_internal *in) {
bool was_reading = in->reading;
in->reading = false; // ★ 先关掉,下面重新评估
if (!was_reading || in->blocked || demux_cancel_test(in->d_thread))
return false;
// ★★★ read_more / prefetch_more / refresh_more 是"整个 demuxer"的
// 跨流 OR 累加器:任一 eager 流置 true → 整体继续读;
// 暂停 = 所有流都不需要读。
bool read_more = false, prefetch_more = false, refresh_more = false;
uint64_t total_fw_bytes = 0;
for (int n = 0; n < in->num_streams; n++) { // ← 遍历所有流,不是单流
struct demux_stream *ds = in->streams[n]->ds;
if (ds->eager) {
read_more |= !ds->reader_head; // 这条流被消费者耗空 → 必须读
} else {
// 惰性流(字幕等)不在此抢读:真正需要时才置 read_more(见 7.1 表)
}
refresh_more |= ds->refreshing;
// ★ 迟滞检查在循环内、逐流做;但改的是全 demuxer 共享的 in->hyst_active
if (ds->eager && ds->queue->last_ts >= ds->base_ts && in->min_secs > 0) {
if (ds->queue->last_ts - ds->base_ts <= in->hyst_secs)
in->hyst_active = false; // 任一流掉到迟滞线 → 打开预读
if (!in->hyst_active) // ← 预读被整体迟滞门控
prefetch_more |= ds->queue->last_ts - ds->base_ts < in->min_secs;
// ↑ 这条流还低于目标 → 整体要读
}
total_fw_bytes += get_forward_buffered_bytes(ds);
}
// ★ 硬上限:总向前缓冲超预算就整体停(谁撑爆都算谁的)
if (total_fw_bytes >= in->max_bytes) {
if (!read_more) {
in->hyst_active = in->hyst_secs > 0 || in->hyst_bytes > 0;
return false; // 只是预读撑爆 → 暂停
}
if (!in->warned_queue_overflow) {
in->warned_queue_overflow = true;
MP_WARN(in, "Too many packets in the demuxer packet queues:\n");
// ... 打印每条轨的队列长度
}
return false;
}
// ★★ 暂停条件:没有任何一条流需要读(不饥饿 && 都不在 min_secs 之下 && 不刷新)
if (!read_more && !prefetch_more && !refresh_more) {
in->hyst_active = in->hyst_secs > 0 || in->hyst_bytes > 0;
return false; // 所有 eager 流都达标 → 暂停
}
// ★ 真正读 1 个 packet(释放锁做 I/O)------它属于哪条流由 av_read_frame 交错决定
in->reading = true;
mp_mutex_unlock(&in->lock);
bool eof = !demux->desc->read_packet(demux, &pkt);
mp_mutex_lock(&in->lock);
// ...
}
🧭 这段判断是"demuxer 级"的,不是"每条流各自判停"。 三个
|=是关键:
read_more/prefetch_more/refresh_more都是跨所有流 OR 累加 ------只要任意一条 eager 流 满足"被耗空"(read_more)或"缓冲低于min_secs且迟滞开着"(prefetch_more),整体就继续读;- 于是"暂停"(
!read_more && !prefetch_more && !refresh_more)的真正含义是没有一条流需要读 。单条流先达标不会停整体------它只会被捎带 着继续长大:av_read_frame交错吐包,读出来的包可能属于任何流;- 这正是 §7.4 里 video 缓冲到 ~1.9s 还不暂停的原因:audio 那条还在
prefetch_more,拖着整个 demuxer 读;in->hyst_active是全 demuxer 共享的一个迟滞 latch :任一流缓冲掉到hyst_secs就把 latch 打开(整体恢复预读),直到所有流都 ≥min_secs才再合上------"迟滞"也是整体行为,不是每轨各留一份缓冲。
把上面的判据压成一张图。先看"水位"长什么样 (每个 eager 流各有一根自己的水位柱,刻度是前向时长 last_ts - base_ts):
水位轴(秒): 0 hyst_secs=0.5 min_secs=1.0
│────────────┼───────────────────────┼──────────▶ 更满
· 某流被耗空(reader_head==NULL) → read_more=true ← 柱底 0
· 任一流 ≤ 0.5s → 迟滞打开 hyst_active=false
· 某流 < 1.0s 且迟滞开 → prefetch_more=true ← 落在 [0.5, 1.0)
· 所有流 ≥ 1.0s → 整体暂停(达标)
· 全局绳(独立于时间轴):Σ 各流前向字节 ≥ max_bytes → 整体停
例(对应 §7.4):video ≈1.9s、audio ≈0.6s------audio 落在"要继续读"的区间,所以整体在读,video 即使越过 1.0s 也被捎带涨到 ~1.9s 停不下来。
再看整个 demuxer 怎么用这几个 flag 裁决:
#mermaid-svg-Q9QOEYg98u1Ji2CW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Q9QOEYg98u1Ji2CW .error-icon{fill:#552222;}#mermaid-svg-Q9QOEYg98u1Ji2CW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Q9QOEYg98u1Ji2CW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .marker.cross{stroke:#333333;}#mermaid-svg-Q9QOEYg98u1Ji2CW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Q9QOEYg98u1Ji2CW p{margin:0;}#mermaid-svg-Q9QOEYg98u1Ji2CW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster-label text{fill:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster-label span{color:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster-label span p{background-color:transparent;}#mermaid-svg-Q9QOEYg98u1Ji2CW .label text,#mermaid-svg-Q9QOEYg98u1Ji2CW span{fill:#333;color:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .node rect,#mermaid-svg-Q9QOEYg98u1Ji2CW .node circle,#mermaid-svg-Q9QOEYg98u1Ji2CW .node ellipse,#mermaid-svg-Q9QOEYg98u1Ji2CW .node polygon,#mermaid-svg-Q9QOEYg98u1Ji2CW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .rough-node .label text,#mermaid-svg-Q9QOEYg98u1Ji2CW .node .label text,#mermaid-svg-Q9QOEYg98u1Ji2CW .image-shape .label,#mermaid-svg-Q9QOEYg98u1Ji2CW .icon-shape .label{text-anchor:middle;}#mermaid-svg-Q9QOEYg98u1Ji2CW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .rough-node .label,#mermaid-svg-Q9QOEYg98u1Ji2CW .node .label,#mermaid-svg-Q9QOEYg98u1Ji2CW .image-shape .label,#mermaid-svg-Q9QOEYg98u1Ji2CW .icon-shape .label{text-align:center;}#mermaid-svg-Q9QOEYg98u1Ji2CW .node.clickable{cursor:pointer;}#mermaid-svg-Q9QOEYg98u1Ji2CW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .arrowheadPath{fill:#333333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q9QOEYg98u1Ji2CW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Q9QOEYg98u1Ji2CW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q9QOEYg98u1Ji2CW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster text{fill:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW .cluster span{color:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-Q9QOEYg98u1Ji2CW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Q9QOEYg98u1Ji2CW rect.text{fill:none;stroke-width:0;}#mermaid-svg-Q9QOEYg98u1Ji2CW .icon-shape,#mermaid-svg-Q9QOEYg98u1Ji2CW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Q9QOEYg98u1Ji2CW .icon-shape p,#mermaid-svg-Q9QOEYg98u1Ji2CW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-Q9QOEYg98u1Ji2CW .icon-shape .label rect,#mermaid-svg-Q9QOEYg98u1Ji2CW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Q9QOEYg98u1Ji2CW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-Q9QOEYg98u1Ji2CW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-Q9QOEYg98u1Ji2CW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ③ 汇总门:真的没人要才停
② 字节硬上限:整体预算(谁撑爆都算谁的)
① 逐流循环:每流各自产出一个"要不要"(跨流 OR)
是
是
是
任一 true → 继续
全 false → 达标
是 且 !read_more
否
water = last_ts - base_ts
该流被耗空?
reader_head==NULL
该流 water < min_secs
且迟滞开着
该流 refreshing?
read_more=true
prefetch_more=true
refresh_more=true
total_fw_bytes = Σ 各流前向字节
≥ max_bytes ?
read_more ∨
prefetch_more ∨
refresh_more
读 1 个 packet
(av_read_frame 交错,属于哪条流不定)
整体暂停
hyst_active=true
demux 线程休眠
等 timer / 外部唤醒
7.3 水位状态转移图
所有状态判断在 demux 线程 的 read_packet() 中执行。主线程(消费者)通过消费 packet、demux_start_prefetch()、demux_block_reading() 间接影响。
#mermaid-svg-fHYTJUzoX4azXSLC{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-fHYTJUzoX4azXSLC .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-fHYTJUzoX4azXSLC .error-icon{fill:#552222;}#mermaid-svg-fHYTJUzoX4azXSLC .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-fHYTJUzoX4azXSLC .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-fHYTJUzoX4azXSLC .marker{fill:#333333;stroke:#333333;}#mermaid-svg-fHYTJUzoX4azXSLC .marker.cross{stroke:#333333;}#mermaid-svg-fHYTJUzoX4azXSLC svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-fHYTJUzoX4azXSLC p{margin:0;}#mermaid-svg-fHYTJUzoX4azXSLC .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-fHYTJUzoX4azXSLC .cluster-label text{fill:#333;}#mermaid-svg-fHYTJUzoX4azXSLC .cluster-label span{color:#333;}#mermaid-svg-fHYTJUzoX4azXSLC .cluster-label span p{background-color:transparent;}#mermaid-svg-fHYTJUzoX4azXSLC .label text,#mermaid-svg-fHYTJUzoX4azXSLC span{fill:#333;color:#333;}#mermaid-svg-fHYTJUzoX4azXSLC .node rect,#mermaid-svg-fHYTJUzoX4azXSLC .node circle,#mermaid-svg-fHYTJUzoX4azXSLC .node ellipse,#mermaid-svg-fHYTJUzoX4azXSLC .node polygon,#mermaid-svg-fHYTJUzoX4azXSLC .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-fHYTJUzoX4azXSLC .rough-node .label text,#mermaid-svg-fHYTJUzoX4azXSLC .node .label text,#mermaid-svg-fHYTJUzoX4azXSLC .image-shape .label,#mermaid-svg-fHYTJUzoX4azXSLC .icon-shape .label{text-anchor:middle;}#mermaid-svg-fHYTJUzoX4azXSLC .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-fHYTJUzoX4azXSLC .rough-node .label,#mermaid-svg-fHYTJUzoX4azXSLC .node .label,#mermaid-svg-fHYTJUzoX4azXSLC .image-shape .label,#mermaid-svg-fHYTJUzoX4azXSLC .icon-shape .label{text-align:center;}#mermaid-svg-fHYTJUzoX4azXSLC .node.clickable{cursor:pointer;}#mermaid-svg-fHYTJUzoX4azXSLC .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-fHYTJUzoX4azXSLC .arrowheadPath{fill:#333333;}#mermaid-svg-fHYTJUzoX4azXSLC .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-fHYTJUzoX4azXSLC .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-fHYTJUzoX4azXSLC .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fHYTJUzoX4azXSLC .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-fHYTJUzoX4azXSLC .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fHYTJUzoX4azXSLC .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-fHYTJUzoX4azXSLC .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-fHYTJUzoX4azXSLC .cluster text{fill:#333;}#mermaid-svg-fHYTJUzoX4azXSLC .cluster span{color:#333;}#mermaid-svg-fHYTJUzoX4azXSLC div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-fHYTJUzoX4azXSLC .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-fHYTJUzoX4azXSLC rect.text{fill:none;stroke-width:0;}#mermaid-svg-fHYTJUzoX4azXSLC .icon-shape,#mermaid-svg-fHYTJUzoX4azXSLC .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-fHYTJUzoX4azXSLC .icon-shape p,#mermaid-svg-fHYTJUzoX4azXSLC .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-fHYTJUzoX4azXSLC .icon-shape .label rect,#mermaid-svg-fHYTJUzoX4azXSLC .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-fHYTJUzoX4azXSLC .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-fHYTJUzoX4azXSLC .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-fHYTJUzoX4azXSLC :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 🧵 Demux 线程 --- read_packet() 判断
唤醒 demux 线程的两种方式
醒来 → thread_work →
update_cache → 重新设 next_cache_update
被 cond_signal 唤醒
reader_head 空
必须读
缓冲 < min_secs
预读
是, 且不是必须读
否
hyst_active=否
缓冲 < min_secs
hyst_active=是
缓冲 > hyst_secs
hyst_active=是
缓冲 <= hyst_secs
读到一个 packet
add_packet_locked()
EOF
read_packet 直接 return false
主线程: demux_start_prefetch()
→ mp_cond_signal(&in->wakeup)
主线程: demux_seek()
→ queue_seek() → mp_cond_signal
主线程: demux_block_reading(false)
→ mp_cond_signal
内核: cond_timedwait 超时
(next_cache_update 到期, 约 1s)
休眠: reading=false
cond_timedwait(&in->wakeup)
检查水位
读一个 packet
(释放锁, I/O, 重新拿锁)
hyst_active?
缓冲 >= min_secs?
缓冲 >= max_bytes?
水位足够
reading=false
hyst_active=true
返回 false → 休眠
eof=true
reading=false
seek 携带 SEEK_BLOCK
→ queue_seek() 置 blocked=true
(demux.c:3967)
两种唤醒场景:
| 场景 | 触发者 | 机制 | 唤醒后做什么 |
|---|---|---|---|
| 外部触发 | 主线程调用 demux_start_prefetch / demux_seek / demux_block_reading(false) |
mp_cond_signal(&in->wakeup) 打断 cond_timedwait |
进入 CHECK → 可能读 packet |
| 超时 | 内核(无额外线程) | cond_timedwait 的 abstime 参数到期,内核自动唤醒线程 |
thread_work → update_cache 更新 bytes_per_second,重新设 next_cache_update |
📌 外部唤醒到底何时被调用?
API 作用 触发条件 demux_start_prefetch()(demux.c:1241)置 reading=true+mp_cond_signal------"开始预读"仅两个调用点( player/loadfile.c):①:1247开启--prefetch预读下一个播放列表条目;②:1984开启--demuxer-cache-wait起播前预读并等缓存就绪demux_block_reading(false)(demux.c:4259)置 blocked=false+ 唤醒所有流------"解除阻塞"唯一调用点是 seek 提交后 ( playloop.c:407/411);对应的blocked=true由带SEEK_BLOCK的 seek 在queue_seek里设置(demux.c:3967)⚠️ 普通播放并不调用
demux_start_prefetch()------reading=true是消费者首次拉包时惰性拉起的 :dequeue_packet发现!in->reading && !in->eof→ 置reading=true+ signal(demux.c:2760)。demux 线程在demux_start_thread后本就休眠,靠这次消费者下拉唤醒。
wakeup_ds(demux/demux.c:868)方向相反:demux 线程add_packet_locked后,对"消费者在等"的那条流调wakeup_ds→ 回调 → 唤醒主线程 (消费者)。注意它内部的"两条回调"其实是 if/else 回退,不是两条平行路------判断依据是"这条流有没有注册 per-stream 回调":
走哪条 条件 谁注册 效果 per-stream ds->wakeup_cb这条流注册了(AV/字幕轨都注册) f_demux_in(f_demux_in.c:78)、dec_sub(sub/dec_sub.c:275)指名 : mp_filter_wakeup(f)标记 f_demux_in 为 pending,主线程跑图时去拉它demux 级 in->wakeup_cb没注册 per-stream → 回退 player 注册( player/loadfile.c:65的wakeup_demux)泛泛 :只 mp_wakeup_core打断核心,不指名任何 filter顺带澄清两个易混点:① 字幕也走 per-stream ------
dec_sub注册的那个函数恰好也叫wakeup_demux(sub/dec_sub.c:129),别和 player 侧 demux 级的wakeup_demux(player/loadfile.c:65)搞混;② EOF / 运行时新增流这类"全局事件"根本不经wakeup_ds------它们在 demux 侧直接调in->wakeup_cb(demux.c:1047新流、:2386首次 EOF、:2682线程关闭)。真正的语义差别只有一条:回调是否指名 哪个 filter 在等------per-stream 指名(标记它 pending),demux 级泛泛打断(不指名,等跑图时各 filter 用mp_pin_in_needs_data自判要不要拉)。
7.4 具体数值举例
以 1080p H.264 + AAC 为例,假设 min_secs = 1.0s, hyst_secs = 0.5s, max_bytes = 150MB:
| 事件 | video 缓冲(last_ts-base_ts) |
audio 缓冲 | 判定与结果 |
|---|---|---|---|
| 起播,两轨连读 | 涨到 ~1.0s | 涨到 ~0.6s | video 先达标;audio 仍 < min_secs → 整体继续预读 |
继续读若干包(read_packet 每读 1 包就重判一次) |
涨到 ~1.9s(被 audio "捎带"超调) | 涨到 ~1.0s | 两轨都 ≥ min_secs → !read_more && !prefetch_more → hyst_active=true → 暂停预读 |
| 播放中,consumer 消耗、demux 停 | 1.9s→0.5s | 1.0s→0.5s | 暂停态保持(迟滞 latch);某轨降到 hyst_secs 前不会恢复 |
| 某轨降到 ≤0.5s | ~0.5s | ~0.5s | 缓冲 ≤ hyst_secs → hyst_active=false → 恢复预读,回到第一行循环 |
🧭 为什么 video 能到 ~1.9s 还不停? 水位判断是整个 demuxer 级的 ,不是每条流各自停------
read_packet问的是"还有没有 eager 流需要读"(prefetch_more由所有 eager 流累加),只要 audio 还没到min_secs就继续返回 true;av_read_frame交错吐出的视频包把 video 队列一起喂高("被捎带超调")。只有当最后一条饿的轨也达标 (或 consumer 把某流reader_head耗空、read_more强制读),才真正暂停。若只有单轨,情形完全不同:demux_thread每读 1 包就重入thread_work重新判断,所以它一越过min_secs的下一次检查就停------超调至多一个包。
如此循环,各轨大致在 0.5s1.0s 之间波动(被捎带的轨可能瞬时更高)。
关键:hyst_secs 是绝对阈值,不是相对值。 恢复条件是 缓冲 ≤ hyst_secs(即降到 0.5s)。
迟滞的实际效果是:暂停后,消费者需要消耗 min_secs - hyst_secs 时长的数据后才恢复预读------这段消耗量才是真正的"迟滞区间"。
**
hyst_active = true何时触发?** 在read_packet里有两个独立分支会置它:
- 字节上限 :
total_fw_bytes >= max_bytes且消费者未饥饿(!read_more)→ 暂停并置 true(demux.c:2312);- 时间目标达标 :
!read_more && !prefetch_more && !refresh_more------即缓冲达到min_secs且消费者不饥饿 → 暂停并置 true(demux.c:2345)。本例子中
max_bytes=150MB远未触及,所以走分支 2:缓冲到达 1.0s 那一刻,prefetch_more从 true 翻为 false(1.0 < 1.0不成立),触发置 true。注意置入的值是(hyst_secs > 0 || hyst_bytes > 0)------若两者都为 0(默认值 ),hyst_active恒为 false,迟滞等效关闭。反向清除发生在每轮循环开头:缓冲<= hyst_secs时置 false(demux.c:2294),从而恢复预读。
8. 总结
-
thread_work是 demux 线程的单次工作调度器:seeking > track switch > backward seek > read_packet > cache update。每轮最多做一件事,做完立即重入检查新状态。 -
水位控制是预读的自适应反馈回路 :
min_secs设目标、max_bytes设硬上限、迟滞(hyst_secs/hyst_bytes)防止边界震荡。reading标志是整条回路的控制阀------false时 demux 线程休眠,true时持续读。 -
生产者-消费者共用一条
ds->queue:add_packet_locked尾部追加、dequeue_packet从reader_head前进,in->lock用"持锁但不阻塞"的 BKL 哲学在 I/O 点让出并发窗口。 -
f_demux_infilter 是最薄的消费适配层 :仅 85 行 C。它把demux_read_packet_async的返回值翻译为 filter graph 的MP_FRAME_PACKET/MP_FRAME_EOF,通过wakeup_cb连接 demux 线程的唤醒通知。
🔜 下一讲:已缓存的 packet 怎么被组织成
cached_range,seek 到任意位置为什么能零 I/O 秒回。
9. 动手验证
在你的终端跑一条命令,亲眼观察水位控制。
read_packet的 trace 走的是 demuxer 自身的 logger (MP4 →lavf,因in->log = demuxer->log且demuxer->log以desc->name命名),不是demuxlogger 。所以下面用--msg-level=lavf=trace,而不是demux=trace。
bash
# 观察水位控制:看 demux 线程的读/停节律
mpv --msg-level=lavf=trace --demuxer-readahead-secs=2 --demuxer-hysteresis-secs=0.5 \
--frames=600 --vo=null --ao=null test_clock.mp4 2>&1 | grep "bytes=" | head -40
# 每行是 read_packet 的一次水位判断(demux.c:2308):
# bytes=... 当前向前缓存字节数
# read_more=1 → 消费者已耗尽,必须读
# prefetch_more=1 → 缓冲低于 min_secs,预读中
# prefetch_more=0 → 水位达标 → 暂停 → demux 线程休眠
# 观察循环:开头 read_more=1 连续读 → 缓冲涨到 min_secs 后 prefetch_more=0 暂停
# → 消费者消耗到 hyst 阈值以下又恢复 prefetch_more=1
10. 唠两句
Demux 的"生产-消费管道"到这里就完整了。从 stream_t 的环形缓冲区(02a)到 demuxer_desc_t 的策略模式(02b)再到 thread_work 的水位控制(本文),你会发现 mpv 的设计有个一以贯之的原则:用最简单的数据结构(环形缓冲、单向链表、环形索引)做最复杂的调度决策 。没有无锁队列,没有花哨的并发原语,就是一把 in->lock 走天下。这种"朴素但正确"的工程哲学,很值得学。
下一讲讲这些 packet 怎么被组织成可复用的缓存区间------那是 mpv 零 I/O seek 的秘密。