[mpv架构] (4) demux 线程到底在“预读“什么?

问题引入

前文结束时,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。每次最多完成一项工作,出队时根据优先级重新调度。

上图里 RANGESdemux_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_seekdemux 线程侧的低层重定位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_asyncmp_mutex_lock(&in->lock) 处会一直被阻塞,表现为播放器"卡死"直到 seek 完成。


4. 数据结构:demux_streamdemux_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)

两个不变量,先记住:

  1. ds->queue 是"活动指针",不是固定数组demux_stream 只持有一个指向"当前正在读写的那条队列"的指针。读写同一条队列 ------demux 线程往 ds->queue 尾追加,消费者从 ds->queue 里的 reader_head 前进。
  2. reader_head 是消费者的游标 :它之前的 packet 已被解码器取走,它自身指向解码器下次要取的那个,之后是未消费的。add_packet_locked 在队列空、消费者等待时设置它(可带 skip_to_keyframe 跳到关键帧)。

🧭 每条队列还归属一个 cached_rangequeue->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->ptsdp->keyframeds->reader_head 的变化。


6. 消费:demux_read_packet_asyncf_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_requested
A 需求驱动(同步) decoder 要包:lavc_process 撞到 EAGAINmp_pin_out_readmp_pin_out_request_data(filter.c:319) 置 true (和调度 add_pending_pin 是同一函数里相邻两行)
B 事件驱动(异步) demux 线程产包:wakeup_ds → f_demux_in 的 wakeupmp_filter_wakeupasync_pending,下次跑图 flush_async_notifications(filter.c:198)翻成 pending 不碰
C 建图踢一脚(一次性) 创建/连接 pin:mp_filter_add_pin(filter.c:612)末尾 → init_connectionadd_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_asyncdequeue_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_signaldemux.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_timedwaitabstime 参数到期,内核自动唤醒线程 thread_workupdate_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_dsdemux/demux.c:868)方向相反:demux 线程 add_packet_locked 后,对"消费者在等"的那条流调 wakeup_ds → 回调 → 唤醒主线程 (消费者)。注意它内部的"两条回调"其实是 if/else 回退,不是两条平行路------判断依据是"这条流有没有注册 per-stream 回调":

走哪条 条件 谁注册 效果
per-stream ds->wakeup_cb 这条流注册了(AV/字幕轨都注册) f_demux_inf_demux_in.c:78)、dec_subsub/dec_sub.c:275 指名mp_filter_wakeup(f) 标记 f_demux_in 为 pending,主线程跑图时去拉它
demux 级 in->wakeup_cb 没注册 per-stream → 回退 player 注册(player/loadfile.c:65wakeup_demux 泛泛 :只 mp_wakeup_core 打断核心,不指名任何 filter

顺带澄清两个易混点:① 字幕也走 per-stream ------dec_sub 注册的那个函数恰好也叫 wakeup_demuxsub/dec_sub.c:129),别和 player 侧 demux 级的 wakeup_demuxplayer/loadfile.c:65)搞混;② EOF / 运行时新增流这类"全局事件"根本不经 wakeup_ds ------它们在 demux 侧直接调 in->wakeup_cbdemux.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_morehyst_active=true暂停预读
播放中,consumer 消耗、demux 停 1.9s→0.5s 1.0s→0.5s 暂停态保持(迟滞 latch);某轨降到 hyst_secs 前不会恢复
某轨降到 ≤0.5s ~0.5s ~0.5s 缓冲 ≤ hyst_secshyst_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 里有两个独立分支会置它:

  1. 字节上限total_fw_bytes >= max_bytes 且消费者未饥饿(!read_more)→ 暂停并置 true(demux.c:2312);
  2. 时间目标达标!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. 总结

  1. thread_work 是 demux 线程的单次工作调度器:seeking > track switch > backward seek > read_packet > cache update。每轮最多做一件事,做完立即重入检查新状态。

  2. 水位控制是预读的自适应反馈回路min_secs 设目标、max_bytes 设硬上限、迟滞(hyst_secs/hyst_bytes)防止边界震荡。reading 标志是整条回路的控制阀------false 时 demux 线程休眠,true 时持续读。

  3. 生产者-消费者共用一条 ds->queueadd_packet_locked 尾部追加、dequeue_packetreader_head 前进,in->lock 用"持锁但不阻塞"的 BKL 哲学在 I/O 点让出并发窗口。

  4. f_demux_in filter 是最薄的消费适配层 :仅 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->logdemuxer->logdesc->name 命名),不是 demux logger 。所以下面用 --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 的秘密。

相关推荐
evans在进步1 小时前
Tomcat 整体架构与启动流程:从 Server 到 Servlet 加载
servlet·架构·tomcat
西西弗Sisyphus1 小时前
C++ 实现 替代 OpenCV resize INTER_LINEAR 的一种方式
c++·opencv
阡陌数智1 小时前
高可用LLM服务架构:多模型聚合系统并发管控、限流熔断与降级容错实战
架构
weixin199701080161 小时前
[特殊字符]《二手ERP × 闲鱼消息驱动架构:正向+逆向交易消息如何驱动WMS出库与回传》(附Python源码)
开发语言·python·架构
wabs6661 小时前
关于二叉树【力扣107.二叉树的层序遍历II的思考】
数据结构·c++·算法·leetcode·二叉树·层序遍历
Jay Kay1 小时前
深入理解 RDMA 内存管理:海思 HNS RoCE 架构中的 HEM 表与 MTR 表有什么区别?
服务器·网络·架构
却道天凉_好个秋1 小时前
音视频学习(一百零五):Access Unit (AU)和RTP分包
学习·音视频·access unit·rtp分包
galaxy_strive1 小时前
Qt C++插件化编写项目(1)
开发语言·c++·qt
见叶之秋1 小时前
【C++】C++ 核心进阶(一):泛型编程基石 —— 模板初阶与 STL 体系开篇
开发语言·c++