[mpv脚本系统] (四) 脚本加载与事件循环系统

想象一下:你在 mpv 里写了一个 Lua 脚本,只需要 mp.register_event("start-file", fn),播放新文件时 fn 就自动被调用。你不知道是谁在监听事件、不知道回调是怎么被调度的、甚至不需要写主循环------但它就是能工作。

这背后,是 mp 模块提供的一整套运行时环境在默默运转。本文用"操作系统"类比,从设计层面讲清楚这套环境做了什么、怎么实现的。


文章目录

    • [1. 把 mp 模块理解为一个"操作系统"](#1. 把 mp 模块理解为一个"操作系统")
    • [2. "内核"(C 层 mp 模块)提供了什么?](#2. "内核"(C 层 mp 模块)提供了什么?)
      • [2.1 读取/写入播放器状态](#2.1 读取/写入播放器状态)
      • [2.2 接收播放器事件](#2.2 接收播放器事件)
      • [2.3 发送命令](#2.3 发送命令)
      • [2.4 观察属性变化(底层)](#2.4 观察属性变化(底层))
      • [2.5 日志输出](#2.5 日志输出)
      • [2.6 五类系统调用一览](#2.6 五类系统调用一览)
    • [3. "操作系统"(defaults.lua)在系统调用之上做了什么?](#3. "操作系统"(defaults.lua)在系统调用之上做了什么?)
      • [3.1 服务一:事件分发系统](#3.1 服务一:事件分发系统)
      • [3.2 服务二:属性观察系统](#3.2 服务二:属性观察系统)
      • [3.3 服务三:纯 Lua 实现的定时器系统](#3.3 服务三:纯 Lua 实现的定时器系统)
      • [3.4 服务四:主事件循环](#3.4 服务四:主事件循环)
    • [4. 一个脚本的完整生命周期](#4. 一个脚本的完整生命周期)
      • [4.1 启动阶段](#4.1 启动阶段)
      • [4.2 运行阶段------具体例子](#4.2 运行阶段——具体例子)
    • [5. 为什么叫"操作系统"而不是"框架"?](#5. 为什么叫"操作系统"而不是"框架"?)
    • [6. 串联四篇:从 C 函数到界面渲染](#6. 串联四篇:从 C 函数到界面渲染)
    • [7. 总结](#7. 总结)

1. 把 mp 模块理解为一个"操作系统"

如果你写过嵌入式程序或者 Node.js,这个类比应该很亲切:
#mermaid-svg-satusgy0xjWmh4p0{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-satusgy0xjWmh4p0 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-satusgy0xjWmh4p0 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-satusgy0xjWmh4p0 .error-icon{fill:#552222;}#mermaid-svg-satusgy0xjWmh4p0 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-satusgy0xjWmh4p0 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-satusgy0xjWmh4p0 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-satusgy0xjWmh4p0 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-satusgy0xjWmh4p0 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-satusgy0xjWmh4p0 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-satusgy0xjWmh4p0 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-satusgy0xjWmh4p0 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-satusgy0xjWmh4p0 .marker.cross{stroke:#333333;}#mermaid-svg-satusgy0xjWmh4p0 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-satusgy0xjWmh4p0 p{margin:0;}#mermaid-svg-satusgy0xjWmh4p0 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-satusgy0xjWmh4p0 .cluster-label text{fill:#333;}#mermaid-svg-satusgy0xjWmh4p0 .cluster-label span{color:#333;}#mermaid-svg-satusgy0xjWmh4p0 .cluster-label span p{background-color:transparent;}#mermaid-svg-satusgy0xjWmh4p0 .label text,#mermaid-svg-satusgy0xjWmh4p0 span{fill:#333;color:#333;}#mermaid-svg-satusgy0xjWmh4p0 .node rect,#mermaid-svg-satusgy0xjWmh4p0 .node circle,#mermaid-svg-satusgy0xjWmh4p0 .node ellipse,#mermaid-svg-satusgy0xjWmh4p0 .node polygon,#mermaid-svg-satusgy0xjWmh4p0 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-satusgy0xjWmh4p0 .rough-node .label text,#mermaid-svg-satusgy0xjWmh4p0 .node .label text,#mermaid-svg-satusgy0xjWmh4p0 .image-shape .label,#mermaid-svg-satusgy0xjWmh4p0 .icon-shape .label{text-anchor:middle;}#mermaid-svg-satusgy0xjWmh4p0 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-satusgy0xjWmh4p0 .rough-node .label,#mermaid-svg-satusgy0xjWmh4p0 .node .label,#mermaid-svg-satusgy0xjWmh4p0 .image-shape .label,#mermaid-svg-satusgy0xjWmh4p0 .icon-shape .label{text-align:center;}#mermaid-svg-satusgy0xjWmh4p0 .node.clickable{cursor:pointer;}#mermaid-svg-satusgy0xjWmh4p0 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-satusgy0xjWmh4p0 .arrowheadPath{fill:#333333;}#mermaid-svg-satusgy0xjWmh4p0 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-satusgy0xjWmh4p0 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-satusgy0xjWmh4p0 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-satusgy0xjWmh4p0 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-satusgy0xjWmh4p0 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-satusgy0xjWmh4p0 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-satusgy0xjWmh4p0 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-satusgy0xjWmh4p0 .cluster text{fill:#333;}#mermaid-svg-satusgy0xjWmh4p0 .cluster span{color:#333;}#mermaid-svg-satusgy0xjWmh4p0 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-satusgy0xjWmh4p0 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-satusgy0xjWmh4p0 rect.text{fill:none;stroke-width:0;}#mermaid-svg-satusgy0xjWmh4p0 .icon-shape,#mermaid-svg-satusgy0xjWmh4p0 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-satusgy0xjWmh4p0 .icon-shape p,#mermaid-svg-satusgy0xjWmh4p0 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-satusgy0xjWmh4p0 .icon-shape .label rect,#mermaid-svg-satusgy0xjWmh4p0 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-satusgy0xjWmh4p0 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-satusgy0xjWmh4p0 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-satusgy0xjWmh4p0 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 硬件(mpv 核心)
内核(C 层 mp 模块表)
操作系统服务(defaults.lua)
用户程序(Lua 脚本)
osc.lua

屏幕控制器
stats.lua

性能面板
ytdl_hook.lua

YouTube 加载器
~/.config/mpv/scripts/

用户自定义脚本
事件循环

dispatch_events()
定时器系统

add_timeout / add_periodic_timer
事件分发

register_event → call_event_handlers
属性观察

observe_property
底层系统调用

raw_observe_property / request_event

wait_event / add_timeout 的 C 实现
播放引擎

demux / decode / vo / ao

对应 mpv 实际组件 职责
用户程序 所有 Lua 脚本(OSC、stats、你的脚本...) 写业务逻辑,调用 API
操作系统 defaults.lua(~600 行 Lua 代码) 事件循环、定时器、回调分发、属性观察
内核 C 层的 mp 模块表(main_fns 数组中的函数) 底层系统调用:读写属性、等待事件、请求通知
硬件 mpv 核心(player/core.c 等) 解码、渲染、播放控制

关键洞察 :你写的 Lua 脚本不是在"裸机"上运行------mp 模块的 C 函数是系统调用,defaults.lua 用这些系统调用搭了一套"应用框架",你只需要注册回调就行了。


2. "内核"(C 层 mp 模块)提供了什么?

run_lua 初始化之后,每个脚本的 Lua 虚拟机里,mp 这个全局表有近 30 个 C 函数可用。按功能分为五类:

2.1 读取/写入播放器状态

lua 复制代码
-- 读当前音量
local vol = mp.get_property_number("volume")      -- → 75

-- 设置属性
mp.set_property("volume", 50)                     -- 音量设为 50%
mp.set_property_bool("pause", true)               -- 暂停

-- 读取原生(native)格式------返回 Lua table 而非字符串
local tracks = mp.get_property_native("track-list")
-- tracks = {
--   {type="video", selected=true, ...},
--   {type="audio", selected=true, lang="eng", ...},
-- }

C 侧实现:script_get_property_numberscript_set_property 等函数,通过 mpv_get_property / mpv_set_property 与核心通信。

2.2 接收播放器事件

lua 复制代码
-- 告诉 mpv:"我对 start-file 事件感兴趣"
mp.request_event("start-file", true)

-- 阻塞等待下一个事件(通常在事件循环中调用)
local event = mp.wait_event(1.0)   -- 最多等 1 秒
-- event = { event = "start-file", ... }

C 侧实现:script_request_eventmpv_request_eventscript_wait_eventmpv_wait_event。这是事件驱动模型的基础。

2.3 发送命令

lua 复制代码
mp.command("playlist-next")                        -- 跳到下一个文件
mp.commandv("seek", "30", "absolute")              -- 跳转到 30 秒

2.4 观察属性变化(底层)

lua 复制代码
-- raw_ 前缀表示这是底层 API,通常不直接使用
-- 高层封装在 defaults.lua 的 mp.observe_property 中
local id = 1
mp.raw_observe_property(id, "pause", "bool")       -- "当 pause 变化时通知我"

C 侧实现:script_raw_observe_propertympv_observe_property。当属性变化时,mpv 核心会发送 property-change 事件,携带 id 字段来标识是哪个观察。

2.5 日志输出

lua 复制代码
mp.log("info", "当前音量: " .. vol)
mp.msg.warn("文件即将结束")                        -- defaults.lua 封装的高层 API

2.6 五类系统调用一览

复制代码
mp 模块表的 C 函数(~30个)
├── 属性读写 ─── get_property, set_property, get_property_native, ...
├── 事件机制 ─── wait_event, request_event, ...
├── 命令执行 ─── command, commandv, command_native, ...
├── 属性观察 ─── raw_observe_property, raw_unobserve_property
├── 日志输出 ─── log, enable_messages
├── 定时器 ─── (无------定时器纯 Lua 实现,见下文)
└── 工具函数 ─── get_time, format_time, find_config_file, ...

关键 :C 层没有提供 add_timeoutregister_eventobserve_property------这些是 defaults.lua 用底层 API 拼装出来的。


3. "操作系统"(defaults.lua)在系统调用之上做了什么?

defaults.lua 读起来像一个小型操作系统内核。它在 C 层"系统调用"之上,构建了四个核心服务:

3.1 服务一:事件分发系统

问题 :C 层 mp.wait_event() 返回一个原始事件,脚本需要自己判断事件类型、自己写 if/elseif 分发。每个脚本都写一遍这些分支?

defaults.lua 的解决方案:一个全局注册表 + 回调分发。

lua 复制代码
-- 全局注册表:事件名 → 回调函数列表
local event_handlers = {
    ["start-file"]    = { osc.request_init, stats.on_new_file, ... },
    ["end-file"]      = { ... },
    ["property-change"] = { defaults.property_change },
    ["shutdown"]      = { defaults.exit },
    ["client-message"] = { defaults.message_dispatch },
}

-- 注册 API:脚本只需要关心"我要什么事件 + 干什么"
function mp.register_event(name, cb)
    local list = event_handlers[name]
    if not list then
        list = {}
        event_handlers[name] = list
    end
    list[#list + 1] = cb
    return mp.request_event(name, true)   -- 告诉 C 层"我需要这个事件"
end

-- 事件循环中收到事件后的分发逻辑
local function call_event_handlers(e)
    local handlers = event_handlers[e.event]
    if handlers then
        for _, handler in ipairs(handlers) do
            handler(e)
        end
    end
end

使用效果

lua 复制代码
-- 你的脚本只需三行,不需要理解事件循环内部怎么运转:
mp.register_event("start-file", function(e)
    mp.msg.info("新文件开始播放!")
end)

3.2 服务二:属性观察系统

问题 :C 层的 raw_observe_property 只做了"告诉核心我要观察"这一步。当属性变化时,核心发来的是 {event="property-change", id=1, data=...}------脚本还得自己维护 id → 回调 的映射。

defaults.lua 的解决方案:自动分配 id、自动映射、对脚本完全透明。

lua 复制代码
local property_id = 0
local properties = {}   -- { [id] = callback }

function mp.observe_property(name, t, cb)
    local id = property_id + 1
    property_id = id
    properties[id] = cb                       -- 记住 id → 回调
    mp.raw_observe_property(id, name, t)      -- 调用 C 层系统调用
end

-- 事件循环中收到 property-change 事件时的自动分发:
mp.register_event("property-change", function(ev)
    local prop = properties[ev.id]
    if prop then
        prop(ev.name, ev.data)    -- 自动找到正确的回调并调用
    end
end)

使用效果

lua 复制代码
-- 一行代码,属性变化时自动通知你
mp.observe_property("volume", "number", function(name, val)
    print("音量变为: " .. val)
end)

3.3 服务三:纯 Lua 实现的定时器系统

问题 :C 层没有 setTimeout。怎么让脚本能"1 秒后执行某操作"?

defaults.lua 的解决方案 :利用 mp.wait_event(timeout) 的阻塞超时机制,在事件循环中插入定时器检查。

lua 复制代码
local timers = {}   -- 所有活跃定时器

function mp.add_timeout(seconds, cb, disabled)
    -- 创建 oneshot 定时器对象,塞入 timers 表
    local t = mp.add_periodic_timer(seconds, cb, disabled)
    t.oneshot = true
    return t
end

function mp.add_periodic_timer(seconds, cb, disabled)
    local t = {
        timeout = seconds,
        cb = cb,
        oneshot = false,
        next_deadline = mp.get_time() + seconds,
    }
    timers[t] = t   -- 注册进全局表
    return t
end

-- 事件循环中每次迭代前执行:
local function process_timers()
    while true do
        local timer = get_next_timer()      -- 找最近到期的
        if not timer then return end

        local wait = timer.next_deadline - mp.get_time()
        if wait > 0 then return wait end    -- 还没到期,返回"要等多久"

        -- 到期了:执行回调
        if timer.oneshot then
            timer:kill()
        else
            timer.next_deadline = mp.get_time() + timer.timeout
        end
        timer.cb()
    end
end

定时器不是操作系统中断------是事件循环中"插空"执行的 。每次进入 dispatch_events 的循环体时,先检查有没有到期的定时器,有就先执行。

使用效果

lua 复制代码
-- 3 秒后弹出提示
mp.add_timeout(3, function()
    mp.osd_message("欢迎使用 mpv!")
end)

-- 每 0.1 秒刷新一次 UI(osc.lua 的核心驱动方式)
local tick_timer = mp.add_periodic_timer(0.1, function()
    render_ui()
end)

3.4 服务四:主事件循环

lua 复制代码
-- 这就是所有脚本共享的主循环:
function mp.dispatch_events(allow_wait)
    while mp.keep_running do
        -- ❶ 检查并执行到期的定时器
        local wait = process_timers() or 1e20

        -- ❷ 阻塞等待 mpv 核心发事件(或定时器超时)
        local e = mp.wait_event(wait)

        -- ❸ 分发给所有注册了此事件的脚本回调
        if e.event ~= "none" then
            call_event_handlers(e)
        end
    end
end

_G.mp_event_loop = function()
    mp.dispatch_events(true)
end

一个循环周期 = 检查定时器 → 等待事件 → 分发回调 。整个过程在 while mp.keep_running 中无限循环,直到收到 shutdown 事件(exit() 设置 mp.keep_running = false)。


4. 一个脚本的完整生命周期

现在我们把前面各篇的知识串起来,看一个脚本从"零"到"运行"的完整过程。

4.1 启动阶段

你的脚本 defaults.lua Lua 虚拟机 lua.c 你的脚本 defaults.lua Lua 虚拟机 lua.c #mermaid-svg-wFjh7YIZoLdr37vo{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-wFjh7YIZoLdr37vo .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wFjh7YIZoLdr37vo .error-icon{fill:#552222;}#mermaid-svg-wFjh7YIZoLdr37vo .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wFjh7YIZoLdr37vo .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wFjh7YIZoLdr37vo .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wFjh7YIZoLdr37vo .marker.cross{stroke:#333333;}#mermaid-svg-wFjh7YIZoLdr37vo svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wFjh7YIZoLdr37vo p{margin:0;}#mermaid-svg-wFjh7YIZoLdr37vo .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFjh7YIZoLdr37vo text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-wFjh7YIZoLdr37vo .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-wFjh7YIZoLdr37vo .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-wFjh7YIZoLdr37vo .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-wFjh7YIZoLdr37vo .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-wFjh7YIZoLdr37vo #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-wFjh7YIZoLdr37vo .sequenceNumber{fill:white;}#mermaid-svg-wFjh7YIZoLdr37vo #sequencenumber{fill:#333;}#mermaid-svg-wFjh7YIZoLdr37vo #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-wFjh7YIZoLdr37vo .messageText{fill:#333;stroke:none;}#mermaid-svg-wFjh7YIZoLdr37vo .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFjh7YIZoLdr37vo .labelText,#mermaid-svg-wFjh7YIZoLdr37vo .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-wFjh7YIZoLdr37vo .loopText,#mermaid-svg-wFjh7YIZoLdr37vo .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-wFjh7YIZoLdr37vo .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-wFjh7YIZoLdr37vo .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-wFjh7YIZoLdr37vo .noteText,#mermaid-svg-wFjh7YIZoLdr37vo .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-wFjh7YIZoLdr37vo .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFjh7YIZoLdr37vo .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFjh7YIZoLdr37vo .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-wFjh7YIZoLdr37vo .actorPopupMenu{position:absolute;}#mermaid-svg-wFjh7YIZoLdr37vo .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-wFjh7YIZoLdr37vo .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-wFjh7YIZoLdr37vo .actor-man circle,#mermaid-svg-wFjh7YIZoLdr37vo line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-wFjh7YIZoLdr37vo :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} run_lua() 启动 load_scripts() 开始加载 定义 event_handlers, properties, timers 定义 3 个注册 API(register_event 等) 注册 3 个默认事件处理器 定义 mp_event_loop = dispatch_events 业务代码开始注册: mp.register_event(...) mp.observe_property(...) mp.add_timeout(...) 进入事件循环 add_functions() 注册 mp.xxx C 函数 _G.mp = mp 模块表 package.preload"your_script" = load_builtin require("mp.defaults") 执行 defaults.lua require("your_script") 执行你的脚本 mp_event_loop()

4.2 运行阶段------具体例子

假设你写了一个脚本,功能是"每次切换文件时打印文件名"。完整过程:

你的脚本(只有 3 行)

lua 复制代码
mp.register_event("start-file", function()
    local path = mp.get_property("path")
    mp.msg.info("正在播放: " .. path)
end)

运行时发生了什么

复制代码
1. 注册阶段
   register_event("start-file", cb)
     → event_handlers["start-file"] = {..., cb}   (塞入全局表)
     → mp.request_event("start-file", true)         (告诉 C 层)

2. 事件循环中
   mp.wait_event(1e20)  阻塞...
     ↓ mpv 核心加载了新文件
   event = { event = "start-file", ... }
     ↓
   call_event_handlers(event)
     → 遍历 event_handlers["start-file"]
     → 调用你的 cb()
       → cb 内部调用 mp.get_property("path")
         → C 函数 script_get_property → mpv_get_property
         → 返回文件路径
       → mp.msg.info(...) 输出日志

3. 无限循环
   回到 while mp.keep_running,等待下一个事件

整个过程中你没有写的代码

  • 事件循环(while 循环)
  • 事件分发(if event == "start-file" then ...
  • 定时器检查
  • 退出处理

全部由 defaults.lua 提供。


5. 为什么叫"操作系统"而不是"框架"?

框架通常需要你遵循它的结构 ------Django 需要你写 urls.py,React 需要你写 render()。但 mpv 的 defaults.lua 更像是操作系统:

  1. 脚本可以不依赖它 。你可以完全不用 register_event,自己写 while true do mp.wait_event() end。就像程序可以不调 libc,直接调 syscall

  2. 它是可替换的 。如果你不喜欢 defaults.lua 的事件循环,你可以在自己的脚本 里重新定义 _G.mp_event_loop------load_scripts 永远取最后定义的版本。

lua 复制代码
-- 你的脚本覆盖默认事件循环
_G.mp_event_loop = function()
    -- 你完全自己控制循环逻辑
    while true do
        local e = mp.wait_event(-1)  -- 永久阻塞
        if e.event == "shutdown" then break end
        my_dispatch(e)
    end
end
  1. 它提供的是"服务"而非"约束" 。你用 register_event 也好、observe_property 也好、add_timeout 也好,都是按需调用,不需要继承任何基类、实现任何接口。

6. 串联四篇:从 C 函数到界面渲染

#mermaid-svg-r4DT7nXvHNz76kWY{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-r4DT7nXvHNz76kWY .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-r4DT7nXvHNz76kWY .error-icon{fill:#552222;}#mermaid-svg-r4DT7nXvHNz76kWY .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-r4DT7nXvHNz76kWY .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-r4DT7nXvHNz76kWY .marker{fill:#333333;stroke:#333333;}#mermaid-svg-r4DT7nXvHNz76kWY .marker.cross{stroke:#333333;}#mermaid-svg-r4DT7nXvHNz76kWY svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-r4DT7nXvHNz76kWY p{margin:0;}#mermaid-svg-r4DT7nXvHNz76kWY .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-r4DT7nXvHNz76kWY .cluster-label text{fill:#333;}#mermaid-svg-r4DT7nXvHNz76kWY .cluster-label span{color:#333;}#mermaid-svg-r4DT7nXvHNz76kWY .cluster-label span p{background-color:transparent;}#mermaid-svg-r4DT7nXvHNz76kWY .label text,#mermaid-svg-r4DT7nXvHNz76kWY span{fill:#333;color:#333;}#mermaid-svg-r4DT7nXvHNz76kWY .node rect,#mermaid-svg-r4DT7nXvHNz76kWY .node circle,#mermaid-svg-r4DT7nXvHNz76kWY .node ellipse,#mermaid-svg-r4DT7nXvHNz76kWY .node polygon,#mermaid-svg-r4DT7nXvHNz76kWY .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-r4DT7nXvHNz76kWY .rough-node .label text,#mermaid-svg-r4DT7nXvHNz76kWY .node .label text,#mermaid-svg-r4DT7nXvHNz76kWY .image-shape .label,#mermaid-svg-r4DT7nXvHNz76kWY .icon-shape .label{text-anchor:middle;}#mermaid-svg-r4DT7nXvHNz76kWY .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-r4DT7nXvHNz76kWY .rough-node .label,#mermaid-svg-r4DT7nXvHNz76kWY .node .label,#mermaid-svg-r4DT7nXvHNz76kWY .image-shape .label,#mermaid-svg-r4DT7nXvHNz76kWY .icon-shape .label{text-align:center;}#mermaid-svg-r4DT7nXvHNz76kWY .node.clickable{cursor:pointer;}#mermaid-svg-r4DT7nXvHNz76kWY .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-r4DT7nXvHNz76kWY .arrowheadPath{fill:#333333;}#mermaid-svg-r4DT7nXvHNz76kWY .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-r4DT7nXvHNz76kWY .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-r4DT7nXvHNz76kWY .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r4DT7nXvHNz76kWY .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-r4DT7nXvHNz76kWY .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r4DT7nXvHNz76kWY .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-r4DT7nXvHNz76kWY .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-r4DT7nXvHNz76kWY .cluster text{fill:#333;}#mermaid-svg-r4DT7nXvHNz76kWY .cluster span{color:#333;}#mermaid-svg-r4DT7nXvHNz76kWY 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-r4DT7nXvHNz76kWY .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-r4DT7nXvHNz76kWY rect.text{fill:none;stroke-width:0;}#mermaid-svg-r4DT7nXvHNz76kWY .icon-shape,#mermaid-svg-r4DT7nXvHNz76kWY .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-r4DT7nXvHNz76kWY .icon-shape p,#mermaid-svg-r4DT7nXvHNz76kWY .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-r4DT7nXvHNz76kWY .icon-shape .label rect,#mermaid-svg-r4DT7nXvHNz76kWY .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-r4DT7nXvHNz76kWY .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-r4DT7nXvHNz76kWY .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-r4DT7nXvHNz76kWY :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ④ 运行时环境(本篇)
③ 模块注册
② autofree 包装
① 闭包原理
lua_pushcclosure(L, fn, n)

栈顶 n 个值 → 上值
af_pushcclosure 三层闭包

trampoline → autofree_call → 业务函数

talloc_free 保证资源安全
push_module_table → package.loaded

register_package_fns → 灌入函数
mp 模块表(C 层)

─── 系统调用层
defaults.lua

─── 操作系统层
你的脚本

─── 用户程序层

一条完整的调用链(以读取属性为例)

复制代码
用户脚本: mp.get_property("volume")
  → C 层: script_get_property(② af_pushcclosure 包装过的闭包)
    → trampoline: talloc_new → pcall → talloc_free(② 自动资源管理)
    → autofree_call: 取出上值中的函数指针
    → 实际执行: mpv_get_property(client, "volume", ...)
  ← 返回值沿调用栈返回

7. 总结

概念 mpv 中的实际组件 提供的核心价值
内核(系统调用) C 层 mp 模块表(~30个函数) 属性读写、事件等待、命令执行、属性观察
操作系统(服务层) defaults.lua(~600行) 事件分发、属性观察封装、定时器系统、主事件循环
进程(用户程序) 每个 Lua 脚本 通过 register_event/observe_property/add_timeout 注册回调
启动器 lua.cload_scripts require("mp.defaults")require(脚本)mp_event_loop()

三个关键设计原则

  1. 分层清晰:C 层只提供原子操作,Lua 层负责编排------改动事件循环不需要重编译 C 代码。
  2. 注册而非继承:脚本通过"注册回调"融入系统,不继承任何基类------灵活且无侵入。
  3. 可替换默认实现_G.mp_event_loop 可以被任何脚本覆盖------满足特殊需求,不强迫所有人接受同一套设计。

系列目录

Lua 闭包与上值 --- 从概念到 C API

mpv 的 af_pushcclosure --- 三层闭包与自动资源管理

mpv 模块注册:C 函数如何变成 Lua 模块

④ 本文 --- mpv 脚本的"操作系统":事件循环与运行时环境

相关推荐
SPC的存折1 小时前
MySQL完整学习手册(视频精华版)
学习·mysql·音视频
草莓熊Lotso1 小时前
【Linux网络】深入理解传输层 UDP 协议:从底层原理到实战应用
linux·运维·服务器·c语言·网络·c++·udp
ACP广源盛139246256731 小时前
GSV2231 三屏显示扩展芯片@ACP#RTX Spark AI 终端多屏协作专属解决方案
大数据·人工智能·分布式·信息可视化·spark·电脑·音视频
wu_ye_m1 小时前
学习c语言第34天 用函数每次输出+1,链式访问,int和void
c语言·学习·算法
凉、介1 小时前
深入理解 ARMv8-A|Application Binary Interface (ABI)
c语言·笔记·学习·嵌入式·arm
zhangfeng11331 小时前
想做自媒体数字人访谈视频,在百度 AI Studio 上安装 OpenAvatarChat,显存要求
人工智能·音视频·transformer·自媒体
2301_789015622 小时前
Linux基础开发工具一:软件包管理器、vim编辑器
linux·服务器·c语言·汇编·c++·编辑器·vim
玖玥拾2 小时前
C/C++ 基础笔记(十)
c语言·c++
开开心心就好2 小时前
新手友好的音视频格式转换工具
linux·服务器·网络·智能手机·pdf·beautifulsoup·音视频