上一篇文章把
mp模块比作脚本的"操作系统",C 层的mp.get_property、mp.wait_event等函数就是"系统调用"。但系统调用本身怎么实现的?为什么mp.get_property("volume")能拿到播放器的音量?答案藏在一个关键数据结构里:
mpv_handle *client——每个脚本独立持有的、通往 mpv 核心的"通信管道"。
文章目录
1. 问题的核心:脚本怎么和播放器核心通信?
先想象一下架构全景:
每个脚本运行在独立的 Lua 虚拟机中(独立线程、独立 lua_State)。它们不能直接访问核心的内存——必须通过 mpv_handle 这个"通信管道"来和核心对话。
这就是 mpv client API 的设计目的:让外部组件(包括内置的 Lua 脚本!)以安全、隔离的方式操控播放器。
2. 通信管道:script_ctx 和 mpv_handle
每个脚本启动时,lua.c 创建一个 script_ctx,其中最关键的字段是 client:
// 每个脚本持有一个独立的上下文(lua.c)
struct script_ctx {
const char *name; // 脚本名称(如 "osc")
const char *filename; // 脚本文件路径(如 "@osc.lua")
lua_State *state; // 独立的 Lua 虚拟机
struct mpv_handle *client; // ★ 通往 mpv 核心的通信管道
struct mp_log *log; // 日志输出通道
struct MPContext *mpctx; // 播放器全局上下文(部分函数需要)
};
// 在 load_lua() 中初始化:
static int load_lua(struct mp_script_args *args) {
struct script_ctx *ctx = talloc_ptrtype(NULL, ctx);
*ctx = (struct script_ctx) {
.client = args->client, // ← 由 scripting.c 预先创建
// ...
};
}
args->client 是由 scripting.c 的 mp_load_script 在启动脚本线程之前调用 mp_new_client() 创建的:
// scripting.c: mp_load_script()
arg->client = mp_new_client(mpctx->clients, script_name);
// ↑ 向 mpv 核心注册一个新的客户端,
// 返回一个 mpv_handle,作为后续所有通信的句柄
关键:每个脚本拿到的是不同的 mpv_handle(不同的内存地址),虽然它们连接的是同一个 mpv 核心。这保证了隔离——一个脚本的操作不会影响另一个脚本的通信状态。
3. 系统调用的通用模式
打开任意一个 C 层"系统调用"的实现,你会发现它们遵循完全相同的模式:
// 通用模板
static int script_xxx(lua_State *L) {
// ① 获取脚本上下文 → 取出 client 管道
struct script_ctx *ctx = get_ctx(L);
// ② 从 Lua 栈读取参数
const char *arg1 = luaL_checkstring(L, 1);
// ③ 通过 client 调用 mpv 核心 API
int err = mpv_xxx(ctx->client, arg1, ...);
// ④ 把结果压回 Lua 栈
lua_pushstring(L, result);
return 1;
}
get_ctx 怎么拿到上下文?它利用了 Lua 的注册表:
// 在 run_lua() 初始化时,ctx 指针被藏在 Lua 注册表里:
static int run_lua(lua_State *L) {
// ...
lua_pushlightuserdata(L, ctx); // 压入 ctx 指针
lua_setfield(L, LUA_REGISTRYINDEX, "ctx"); // registry["ctx"] = ctx
// ...
}
// 任何 C 函数都能通过注册表取回:
static struct script_ctx *get_ctx(lua_State *L) {
lua_getfield(L, LUA_REGISTRYINDEX, "ctx"); // 从注册表取
struct script_ctx *ctx = lua_touserdata(L, -1);
lua_pop(L, 1);
return ctx;
}
LUA_REGISTRYINDEX是 Lua 提供的一个隐藏表——普通 Lua 代码无法访问它,但 C 代码可以往里存任何东西。mpv 用它存储script_ctx指针,相当于"全局变量但对 Lua 不可见"。
4. 详解三类系统调用的实现
4.1 属性读写:mp.get_property("volume")
static int script_get_property(lua_State *L, void *tmp) // tmp 是 trampoline 给的 talloc 上下文
{
struct script_ctx *ctx = get_ctx(L); // ① 取 client
const char *name = luaL_checkstring(L, 1); // ② 读参数 "volume"
char *result = NULL;
int err = mpv_get_property(ctx->client, // ③ 核心调用
name,
MPV_FORMAT_STRING, // 期望返回字符串格式
&result); // 结果写到这里
if (err >= 0) {
add_af_mpv_alloc(tmp, result); // 把结果内存托管给 talloc
lua_pushstring(L, result); // ④ 压入 Lua 栈
return 1;
} else {
lua_pushvalue(L, 2); // 返回默认值(如果有)
lua_pushstring(L, mpv_error_string(err)); // 返回错误信息
return 2;
}
}
发生了什么:
mp.get_property("volume")
→ mysql_ctx 通过注册表找到 ctx → 取出 ctx->client
→ mpv_get_property(client, "volume", STRING, &result)
→ mpv 核心查找属性表,找到 "volume" = 75
→ 格式化为字符串 "75"
→ 分配内存,result 指向 "75"
← result = "75"
→ lua_pushstring(L, "75")
→ Lua 收到 "75"
写入同理:
static int script_set_property(lua_State *L) {
struct script_ctx *ctx = get_ctx(L);
const char *p = luaL_checkstring(L, 1); // "volume"
const char *v = luaL_checkstring(L, 2); // "50"
return check_error(L, mpv_set_property_string(ctx->client, p, v));
// ↑ 统一的错误处理:成功返回 true,失败返回 nil + 错误信息
}
4.2 事件等待:mp.wait_event(timeout)
static int script_wait_event(lua_State *L, void *tmp)
{
struct script_ctx *ctx = get_ctx(L);
// luaL_optnumber(L, 1, 1e20): 如果没传参数,默认等"无限久"
mpv_event *event = mpv_wait_event(ctx->client, luaL_optnumber(L, 1, 1e20));
// ↑ 阻塞在这里,直到有事件或超时
// 把 C 的 mpv_event 结构体转换为 Lua table
struct mpv_node rn;
mpv_event_to_node(&rn, event);
steal_node_allocations(tmp, &rn); // 内存托管给 talloc
pushnode(L, &rn); // 压入 Lua 栈(返回给 Lua 侧)
return 1;
}
mpv_wait_event 的工作方式:
每个 client 维护一个事件队列:
client->event_queue = [event1, event2, event3, ...]
mpv_wait_event(client, timeout):
if 队列非空:
return 队首事件(立即返回,不阻塞)
else:
阻塞等待,直到:
(a) 有新事件入队 → 返回该事件
(b) timeout 到期 → 返回 MPV_EVENT_NONE
事件的来源:mpv 核心在播放过程中产生各种事件(文件开始、属性变化、关机等),核心会把事件推入所有关心这个事件的 client 的队列中。
这就是为什么每个脚本都有自己的 client——不同脚本订阅的事件不同,核心只把事件推给订阅了的 client。
4.3 事件订阅:mp.request_event("start-file", true)
static int script_request_event(lua_State *L)
{
struct script_ctx *ctx = get_ctx(L);
const char *event = luaL_checkstring(L, 1); // "start-file"
bool enable = lua_toboolean(L, 2); // true
// 把事件名转成数字 id(遍历 mpv_event_name(0~255) 匹配)
int event_id = -1;
for (int n = 0; n < 256; n++) {
const char *name = mpv_event_name(n);
if (name && strcmp(name, event) == 0) {
event_id = n;
break;
}
}
return check_error(L, mpv_request_event(ctx->client, event_id, enable));
// ↑ "以后有这个事件时,请推到我的队列里"
}
4.4 属性观察(底层):mp.raw_observe_property(id, "volume", "number")
static int script_raw_observe_property(lua_State *L)
{
struct script_ctx *ctx = get_ctx(L);
uint64_t id = luaL_checknumber(L, 1); // 自增 id(由 defaults.lua 分配)
const char *name = luaL_checkstring(L, 2); // "volume"
mpv_format format = check_property_format(L, 3); // MPV_FORMAT_DOUBLE
return check_error(L, mpv_observe_property(ctx->client, id, name, format));
// ↑ "当 volume 变化时,发一个 property-change 事件给我,
// 事件里带 id 字段方便我找回对应的回调"
}
当 volume 从 75 变成 50 时:
核心检测到属性变化
→ 查找所有 observe 了 "volume" 的 client
→ 向每个 client 的事件队列推入:
{ event = "property-change", id = 42, name = "volume", data = 50 }
→ defaults.lua 的 property_change 处理器:
properties[42] → 这是之前 mp.observe_property 注册的回调
→ 调用 callback("volume", 50)
4.5 命令执行:mp.command("playlist-next")
static int script_command(lua_State *L)
{
struct script_ctx *ctx = get_ctx(L);
const char *s = luaL_checkstring(L, 1);
return check_error(L, mpv_command_string(ctx->client, s));
// ↑ 像 input.conf 那样执行命令字符串
}
5. 一张图总结通信全貌
6. 为什么这个设计好?
6.1 天然隔离
每个脚本有独立的 mpv_handle,互不干扰。OSC 崩溃不会影响 stats 面板——它们连 Lua 虚拟机都是独立的。
6.2 统一接口
不管是 C 写的 Lua 绑定、外部 C 程序、还是 Python 脚本(通过 libmpv),都用同一套 mpv_xxx() API。你学会看 mpv_get_property 的签名,就能理解三类程序怎么和 mpv 通信。
6.3 异步安全
mpv_wait_event 是线程安全的阻塞操作。脚本线程在等待时不会消耗 CPU,核心有事件时主动唤醒——这是生产者-消费者模型的经典实现。
7. 总结:从 Lua 代码到核心的全路径
你的 Lua 代码: mp.get_property("volume")
↓
C 函数: script_get_property (通过 af_pushcclosure 包装,自动管理资源)
↓
取上下文: get_ctx(L) → 从 Lua 注册表取出 script_ctx
↓
取管道: ctx->client → mpv_handle 指针
↓
核心调用: mpv_get_property(client, "volume", ...)
↓
mpv 核心: 查找属性、格式化、返回
五个关键概念:
| 概念 | 是什么 | 在代码里 |
|---|---|---|
script_ctx | 每个脚本的"进程控制块" | struct script_ctx { ... client ... } |
mpv_handle *client | 通往核心的通信管道 | ctx->client |
LUA_REGISTRYINDEX | C 侧隐藏存储,Lua 不可见 | registry["ctx"] = ctx |
mpv_get_property 等 | 真正的系统调用 | mpv/client.h 定义的 API |
mpv_wait_event | 阻塞等待,事件队列模型 | 消费者端 |
搞懂这一层,你就理解了 mpv 脚本系统最底层的通信机制——之后不管是看 Lua 绑定、JS 绑定、还是 C plugin 绑定,全是一样的模式。


被折叠的 条评论
为什么被折叠?



