想象一下:你在 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_number、script_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_event → mpv_request_event;script_wait_event → mpv_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_property → mpv_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_timeout、register_event、observe_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 更像是操作系统:
-
脚本可以不依赖它 。你可以完全不用
register_event,自己写while true do mp.wait_event() end。就像程序可以不调libc,直接调syscall。 -
它是可替换的 。如果你不喜欢
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
- 它提供的是"服务"而非"约束" 。你用
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.c 的 load_scripts |
require("mp.defaults") → require(脚本) → mp_event_loop() |
三个关键设计原则:
- 分层清晰:C 层只提供原子操作,Lua 层负责编排------改动事件循环不需要重编译 C 代码。
- 注册而非继承:脚本通过"注册回调"融入系统,不继承任何基类------灵活且无侵入。
- 可替换默认实现 :
_G.mp_event_loop可以被任何脚本覆盖------满足特殊需求,不强迫所有人接受同一套设计。
系列目录 :
② mpv 的 af_pushcclosure --- 三层闭包与自动资源管理
④ 本文 --- mpv 脚本的"操作系统":事件循环与运行时环境