[mpv脚本系统] (五) C层系统调用的实现: mpv client通信机制

上一篇文章把 mp 模块比作脚本的"操作系统",C 层的 mp.get_propertymp.wait_event 等函数就是"系统调用"。但系统调用本身怎么实现的?为什么 mp.get_property("volume") 能拿到播放器的音量?

答案藏在一个关键数据结构里:mpv_handle *client------每个脚本独立持有的、通往 mpv 核心的"通信管道"。


文章目录

    • [1. 问题的核心:脚本怎么和播放器核心通信?](#1. 问题的核心:脚本怎么和播放器核心通信?)
    • [2. 通信管道:`script_ctx` 和 `mpv_handle`](#2. 通信管道:script_ctxmpv_handle)
    • [3. 系统调用的通用模式](#3. 系统调用的通用模式)
    • [4. 详解三类系统调用的实现](#4. 详解三类系统调用的实现)
      • [4.1 属性读写:`mp.get_property("volume")`](#4.1 属性读写:mp.get_property("volume"))
      • [4.2 事件等待:`mp.wait_event(timeout)`](#4.2 事件等待:mp.wait_event(timeout))
      • [4.3 事件订阅:`mp.request_event("start-file", true)`](#4.3 事件订阅:mp.request_event("start-file", true))
      • [4.4 属性观察(底层):`mp.raw_observe_property(id, "volume", "number")`](#4.4 属性观察(底层):mp.raw_observe_property(id, "volume", "number"))
      • [4.5 命令执行:`mp.command("playlist-next")`](#4.5 命令执行:mp.command("playlist-next"))
    • [5. 一张图总结通信全貌](#5. 一张图总结通信全貌)
    • [6. 为什么这个设计好?](#6. 为什么这个设计好?)
      • [6.1 天然隔离](#6.1 天然隔离)
      • [6.2 统一接口](#6.2 统一接口)
      • [6.3 异步安全](#6.3 异步安全)
    • [7. 总结:从 Lua 代码到核心的全路径](#7. 总结:从 Lua 代码到核心的全路径)

1. 问题的核心:脚本怎么和播放器核心通信?

先想象一下架构全景:
#mermaid-svg-OtxebUry0BdfG6wg{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-OtxebUry0BdfG6wg .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-OtxebUry0BdfG6wg .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-OtxebUry0BdfG6wg .error-icon{fill:#552222;}#mermaid-svg-OtxebUry0BdfG6wg .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-OtxebUry0BdfG6wg .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-OtxebUry0BdfG6wg .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-OtxebUry0BdfG6wg .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-OtxebUry0BdfG6wg .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-OtxebUry0BdfG6wg .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-OtxebUry0BdfG6wg .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-OtxebUry0BdfG6wg .marker{fill:#333333;stroke:#333333;}#mermaid-svg-OtxebUry0BdfG6wg .marker.cross{stroke:#333333;}#mermaid-svg-OtxebUry0BdfG6wg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-OtxebUry0BdfG6wg p{margin:0;}#mermaid-svg-OtxebUry0BdfG6wg .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-OtxebUry0BdfG6wg .cluster-label text{fill:#333;}#mermaid-svg-OtxebUry0BdfG6wg .cluster-label span{color:#333;}#mermaid-svg-OtxebUry0BdfG6wg .cluster-label span p{background-color:transparent;}#mermaid-svg-OtxebUry0BdfG6wg .label text,#mermaid-svg-OtxebUry0BdfG6wg span{fill:#333;color:#333;}#mermaid-svg-OtxebUry0BdfG6wg .node rect,#mermaid-svg-OtxebUry0BdfG6wg .node circle,#mermaid-svg-OtxebUry0BdfG6wg .node ellipse,#mermaid-svg-OtxebUry0BdfG6wg .node polygon,#mermaid-svg-OtxebUry0BdfG6wg .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-OtxebUry0BdfG6wg .rough-node .label text,#mermaid-svg-OtxebUry0BdfG6wg .node .label text,#mermaid-svg-OtxebUry0BdfG6wg .image-shape .label,#mermaid-svg-OtxebUry0BdfG6wg .icon-shape .label{text-anchor:middle;}#mermaid-svg-OtxebUry0BdfG6wg .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-OtxebUry0BdfG6wg .rough-node .label,#mermaid-svg-OtxebUry0BdfG6wg .node .label,#mermaid-svg-OtxebUry0BdfG6wg .image-shape .label,#mermaid-svg-OtxebUry0BdfG6wg .icon-shape .label{text-align:center;}#mermaid-svg-OtxebUry0BdfG6wg .node.clickable{cursor:pointer;}#mermaid-svg-OtxebUry0BdfG6wg .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-OtxebUry0BdfG6wg .arrowheadPath{fill:#333333;}#mermaid-svg-OtxebUry0BdfG6wg .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-OtxebUry0BdfG6wg .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-OtxebUry0BdfG6wg .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OtxebUry0BdfG6wg .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-OtxebUry0BdfG6wg .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OtxebUry0BdfG6wg .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-OtxebUry0BdfG6wg .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-OtxebUry0BdfG6wg .cluster text{fill:#333;}#mermaid-svg-OtxebUry0BdfG6wg .cluster span{color:#333;}#mermaid-svg-OtxebUry0BdfG6wg 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-OtxebUry0BdfG6wg .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-OtxebUry0BdfG6wg rect.text{fill:none;stroke-width:0;}#mermaid-svg-OtxebUry0BdfG6wg .icon-shape,#mermaid-svg-OtxebUry0BdfG6wg .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-OtxebUry0BdfG6wg .icon-shape p,#mermaid-svg-OtxebUry0BdfG6wg .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-OtxebUry0BdfG6wg .icon-shape .label rect,#mermaid-svg-OtxebUry0BdfG6wg .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-OtxebUry0BdfG6wg .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-OtxebUry0BdfG6wg .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-OtxebUry0BdfG6wg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 线程 2: 用户脚本
线程 1: osc.lua
进程 A: mpv 主进程
mpv client API
mpv client API
mpv 核心

播放引擎 / 属性系统 / 事件系统
Lua 虚拟机

script_ctx.client = 0x1234
Lua 虚拟机

script_ctx.client = 0x5678

每个脚本运行在独立的 Lua 虚拟机 中(独立线程、独立 lua_State)。它们不能直接访问核心的内存------必须通过 mpv_handle 这个"通信管道"来和核心对话。

这就是 mpv client API 的设计目的:让外部组件(包括内置的 Lua 脚本!)以安全、隔离的方式操控播放器。


2. 通信管道:script_ctxmpv_handle

每个脚本启动时,lua.c 创建一个 script_ctx,其中最关键的字段是 client

c 复制代码
// 每个脚本持有一个独立的上下文(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.cmp_load_script 在启动脚本线程之前调用 mp_new_client() 创建的:

c 复制代码
// scripting.c: mp_load_script()
arg->client = mp_new_client(mpctx->clients, script_name);
//            ↑ 向 mpv 核心注册一个新的客户端,
//              返回一个 mpv_handle,作为后续所有通信的句柄

关键 :每个脚本拿到的是不同的 mpv_handle(不同的内存地址),虽然它们连接的是同一个 mpv 核心。这保证了隔离------一个脚本的操作不会影响另一个脚本的通信状态。


3. 系统调用的通用模式

打开任意一个 C 层"系统调用"的实现,你会发现它们遵循完全相同的模式:

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 的注册表:

c 复制代码
// 在 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")

c 复制代码
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"

写入同理

c 复制代码
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)

c 复制代码
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)

c 复制代码
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")

c 复制代码
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")

c 复制代码
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. 一张图总结通信全貌

mpv 核心 mpv client API script_ctx C 函数(script_xxx) Lua 脚本 mpv 核心 mpv client API script_ctx C 函数(script_xxx) Lua 脚本 #mermaid-svg-Wnw0Y0Rg3xCRtNC3{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-Wnw0Y0Rg3xCRtNC3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .error-icon{fill:#552222;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .marker.cross{stroke:#333333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 p{margin:0;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .sequenceNumber{fill:white;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 #sequencenumber{fill:#333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .messageText{fill:#333;stroke:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .labelText,#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .loopText,#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .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-Wnw0Y0Rg3xCRtNC3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .noteText,#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .actorPopupMenu{position:absolute;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .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-Wnw0Y0Rg3xCRtNC3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 .actor-man circle,#mermaid-svg-Wnw0Y0Rg3xCRtNC3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Wnw0Y0Rg3xCRtNC3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} ─── 读取属性 ─── ─── 事件等待 ─── alt 队列非空 队列空 ─── 订阅事件 ─── mp.get_property("volume") get_ctx(L) → ctx->>client mpv_get_property(client, "volume", STRING, &result) 查找属性表 "75" result = "75" lua_pushstring("75") mp.wait_event(1.0) get_ctx(L) → ctx->>client mpv_wait_event(client, 1.0) 检查 event_queue 立即返回事件 阻塞... 新事件入队! 返回事件 mpv_event_to_node → Lua table lua_push...(event_table) mp.request_event("start-file", true) get_ctx(L) → ctx->>client mpv_request_event(client, event_id, true) "此 client 关心 start-file 事件" 记录订阅关系


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 绑定,全是一样的模式。

相关推荐
潜创微科技2 小时前
2026年专业创作KVM方案服务商选型指南:技术、场景与服务的全维度评估
嵌入式硬件·音视频
大阳1232 小时前
ARM.9(RGBLCD,PWM)
c语言·开发语言·汇编·单片机·嵌入式硬件·pwm·rgblcd
searchforAI2 小时前
培训视频转文字后怎么做团队复盘?把本地视频整理成AI笔记的实操方案
人工智能·笔记·ai·whisper·音视频·语音识别·腾讯会议
qq_422152572 小时前
音频裁剪工具怎么选?2026 年主流方案对比与使用指南
人工智能·音视频
csdndeyeye2 小时前
从Ctrl+C/V到一键填充:AI投简历工具实测
c语言·开发语言·自动化·秋招·ai助手·网申·ai投简历
开开心心就好2 小时前
清理重复文件释放C盘空间的工具
安全·智能手机·pdf·gitlab·音视频·intellij idea·1024程序员节
hz567892 小时前
实时音视频SDK发展趋势:TRTC、WebRTC与云端音视频服务融合路径
架构·音视频·webrtc·实时音视频
searchforAI2 小时前
利用AI翻译视频做双语笔记,一套视频翻译到知识库沉淀的完整方案
人工智能·笔记·gpt·音视频·语音识别·知识图谱·机器翻译
VOOHU-沃虎2 小时前
工业以太网接口EMC设计:网口、SFP、PoE电源与音频XLR的整机防护实战
音视频