mpv JavaScript 脚本开发指南:API 全解、事件循环与 CommonJS 模块系统
发布时间:2026/9/6 22:02:31 锦皓数字建站

mpv JavaScript 脚本开发指南API 全解、事件循环与 CommonJS 模块系统【免费下载链接】mpv Command line media player项目地址: https://gitcode.com/GitHub_Trending/mp/mpvmpv 的 JavaScript 脚本支持与其 Lua 脚本支持几乎完全相同但运行在 MuJS一个最小化的 ES5 解释器之上。本文以官方手册 DOCS/man/javascript.rst 为主体覆盖.js脚本的加载机制、与 Lua 的 API 对应关系、全部脚本 API 清单、定时器、CommonJSrequire模块系统、自定义初始化与事件循环并结合 player/javascript.c 与 player/javascript/defaults.js 的源码实现说明每项机制在底层是如何落地的。读完本文你可以直接编写.js脚本接入 mpv 的属性观察、命令执行、按键绑定、异步命令与模块复用等完整脚本能力。脚本加载与生命周期mpv 对脚本语言的判定很简单当脚本文件扩展名为.js时按 JavaScript 加载。这一判定由 C 端脚本系统导出表完成见 player/javascript.c#L1264-L1268const struct mp_scripting mp_scripting_js { .name js, .file_ext js, .load s_load_javascript, };除了扩展名之外手册中 DOCS/man/lua.rst 描述的 Lua 脚本目录如script-opts、各平台配置目录、--script选项、脚本参数等规则对 JavaScript 文件同样适用——因为两者共享同一套脚本管理框架。脚本初始化与生命周期和 Lua 完全一致。从源码看加载入口s_load_javascriptplayer/javascript.c#L506-L552为每个脚本创建独立的 MuJS 状态js_newstate随后script__run_scriptplayer/javascript.c#L427-L442按固定顺序完成启动加载内置的错误规范化代码注入全部mp.*C 绑定函数add_functions运行内置文件/defaults.js即 player/javascript/defaults.js其内容通过代码生成内嵌为 C 字符串常量见 player/javascript.c#L48-L55运行用户脚本主文件查找并调用全局函数mp_event_loop——源码中若找不到该函数会直接报错no event loop function。这解释了手册中一个关键细节mp_event_loop是 mpv 在脚本加载后试图调用的函数名你既可以使用内置实现也可以在自己的脚本中同名覆盖以接管事件循环后文详述。JavaScript 的构建开关在 meson.build#L680-L683javascript dependency(mujs, version: 1.0.0, required: get_option(javascript))即需要系统安装 MuJS 库版本 1.0.0并启用javascript选项才能编译出 JS 支持。官方示例暂停时退出全屏手册给出的经典示例是播放器暂停时离开全屏function on_pause_change(name, value) { if (value true) mp.set_property(fullscreen, no); } mp.observe_property(pause, bool, on_pause_change);保存为fullscreen-exit.js放入脚本目录或用--scriptfullscreen-exit.js加载即可。回调签名(name, value)与 Lua 版observe_property一致。与 Lua 的相似之处mpv 在模块mp、mp.utils、mp.msg、mp.options、mp.input中暴露的绝大多数 Lua 函数在 JavaScript 中都有 API 完全一致的对应实现——包括执行命令、获取/设置属性、注册事件/按键绑定/钩子等。player/javascript/defaults.js 中大量 JS 侧 API 本身就是薄封装例如mp.utils.getcwd()直接返回mp.get_property(working-directory)mp.utils.getpid()返回mp.get_property_number(pid)player/javascript/defaults.js#L755-L756。与 Lua 的差异无需加载模块。mp、mp.utils、mp.msg、mp.options、mp.input均已预载可以直接写var cwd mp.utils.getcwd();不需要任何前置设置。错误语义不同。Lua API 出错返回nil时JavaScript 对应 API 返回undefinedLua 返回value, error双返回值时JavaScript 只返回value错误通过mp.last_error()获取空串表示成功非空串为失败原因。注意只有部分函数携带这个额外的 error 值——通常是 Lua 侧同样带 error 的那些函数。优先使用标准 API。例如setTimeout、JSON.stringify可用而mp.add_timeout、mp.utils.format_json则不存在见下文对照表。没有标准库。脚本与 mpv 外部世界的交互限于可用 API典型为mp.utils。不过补充了一些文件读写函数并提供了 CommonJSrequire——被加载的模块拥有与普通脚本相同的权限。不支持的 Lua API 及其 JS 替代Lua APIJavaScript 替代mp.add_timeout(seconds, fn)id setTimeout(fn, ms)mp.add_periodic_timer(seconds, fn)id setInterval(fn, ms)utils.parse_json(str [, trail])JSON.parse(str)utils.format_json(v)JSON.stringify(v)utils.to_string(v)使用下文的dumpmp.get_next_timeout()见下文事件循环mp.dispatch_events([allow_wait])见下文事件循环语言特性ECMAScript 5mpv 当前使用的脚本后端是 MuJS——一个兼容的最小化 ES5 解释器。因此语言特性以 ES5 为准例如String.substring有实现而常见的非标准String.substr则没有。编写脚本时请确保代码兼容 ES5避免使用 ES6 语法箭头函数、let/const、模板字符串、Promise等——从 player/javascript/defaults.js 自身的写法全部使用var、function表达式、forEach也可以印证这一点。脚本 API 清单与 Lua 相同以下 API 与 Lua 版语义一致。标记(LE)表示调用后可用mp.last_error()检测成功空字符串或失败非空原因字符串凡 Lua 侧以nil表示错误之处JS 侧均以undefined表示。命令mp.command(string) (LE) mp.commandv(arg1, arg2, ...) (LE) mp.command_native(table [,def]) (LE) id mp.command_native_async(table [,fn]) (LE) mp.abort_async_command(id)mp.command_native_async成功时返回 true-thy 的id错误信息为空字符串。从 player/javascript/defaults.js#L152-L178 看回调在失败时会通过setTimeout(cb, 0, false, undefined, le)异步触发回调签名是(success, value, error)——这对应 Lua 侧async-command-finished语义的 JS 化。属性mp.del_property(name) (LE) mp.get_property(name [,def]) (LE) mp.get_property_osd(name [,def]) (LE) mp.get_property_bool(name [,def]) (LE) mp.get_property_number(name [,def]) (LE) mp.get_property_native(name [,def]) (LE) mp.set_property(name, value) (LE) mp.set_property_bool(name, value) (LE) mp.set_property_number(name, value) (LE) mp.set_property_native(name, value) (LE)这些 C 端实现如script_get_property、script_set_property_native见 player/javascript.c#L641-L753统一经由 libmpv 的节点 API 与播放器内核交换数据pushnode/makenode负责mpv_node与 JS 对象的双向转换player/javascript.c#L1031-L1150。时间、按键、事件、属性观察mp.get_time() mp.add_key_binding(key, name|fn [,fn [,flags]]) mp.add_forced_key_binding(...) mp.remove_key_binding(name) mp.register_event(name, fn) mp.unregister_event(fn) mp.observe_property(name, type, fn) mp.unobserve_property(fn)按键绑定在 JS 端的实现值得注意add_bindingplayer/javascript/defaults.js#L290-L344把用户回调注册为同名 script message再通过define-section/enable-section命令把输入行注入输入系统绑定行形如key script-binding script_name/name因此外部也可以用script-binding命令手动触发。选项、脚本信息、OSD、消息mp.get_opt(key) mp.get_script_name() mp.get_script_directory() mp.osd_message(text [,duration]) mp.get_wakeup_pipe() mp.register_idle(fn) mp.unregister_idle(fn) mp.enable_messages(level) mp.register_script_message(name, fn) mp.unregister_script_message(name) mp.create_osd_overlay(format) mp.get_osd_size() // 返回对象含 width, height, aspectmp.get_opt读取options/script-opts属性即--script-name-keyvalue命令行选项见 player/javascript/defaults.js#L767-L770。mp.create_osd_overlay返回带update()/remove()方法、res_x/res_y默认为 720p 的 overlay 对象player/javascript/defaults.js#L182-L215底层实际执行osd-overlay命令。日志mp.msgmp.msg.log(level, ...) mp.msg.fatal(...) / mp.msg.error(...) / mp.msg.warn(...) mp.msg.info(...) / mp.msg.verbose(...) / mp.msg.debug(...) / mp.msg.trace(...)mp.msg各级别函数在 player/javascript/defaults.js#L8-L11 中通过mp.log.bind(null, level)生成。mp.utilsmp.utils.getcwd() (LE) mp.utils.readdir(path [, filter]) (LE) mp.utils.file_info(path) (LE) mp.utils.split_path(path) mp.utils.join_path(p1, p2) mp.utils.subprocess(t) mp.utils.subprocess_detached(t) mp.utils.get_env_list() mp.utils.getpid() (LE)注意手册的提醒mp.utils.file_info(path)与 Lua 一样不会展开~~/foo之类的 meta 路径其他 JS 文件函数会展开。钩子与输入系统mp.add_hook(type, priority, fn(hook)) mp.options.read_options(obj [, identifier [, on_update]]) // 类型: string/boolean/number mp.input.get(obj) mp.input.select(obj) mp.input.terminate() mp.input.log(message, style) mp.input.set_log(log) exit() // 全局函数mp.options.read_options的实现在 player/javascript/defaults.js#L590-L651它从~~/script-opts/id.conf读配置文件行keyvalue再叠加--id-keyvalue命令行选项script-opts 优先级更高按obj中键声明的typeof做类型转换on_update回调通过观察options/script-opts属性实现动态更新传入的 changelist 只包含本次变化的键。附加工具JS 独有mp.last_error()— 在会更新 last error 的 API 调用后使用成功返回空字符串失败返回非空原因字符串。其 C 端绑定见 player/javascript.c#L156-L160。Error.stack字符串— 在try { ... } catch(e) { ... }中若错误由Error(...)构造器创建e.stack即堆栈跟踪。print全局—mp.msg.info的便捷别名player/javascript/defaults.js#L750。dump全局— 类似print但会递归展开对象与数组。实现见 player/javascript/defaults.js#L795-L823基于JSON.stringify加自定义 replacer对函数/undefined 输出function/undefined并能检测循环引用输出VISITED。mp.utils.getenv(name)— 返回宿主环境变量name的值未定义时返回undefined。mp.utils.get_user_path(path)—expand-path命令的简单包装返回字符串。read_file、write_file、append_file与require内部已自动展开路径可接受~~desktop/foo等 mpv meta 路径。文件读写错误时抛出异常仅限文本内容mp.utils.read_file(fname [,max]) // 返回文件内容字符串max 0 时限制读取 max 字节 mp.utils.write_file(fname, str) // 覆写文件fname 必须以 file:// 开头 mp.utils.append_file(fname, str) // 文件不存在则同 write_file存在则追加write_file要求file://前缀是刻意的防误操作设计例如mp.utils.write_file(file://~/abc.txt, hello world);mp.get_time_ms()— 与mp.get_time()相同但单位是毫秒而非秒。mp.get_script_file()— 返回当前脚本的文件名。mp.utils.compile_js(fname, content_str)— 将content_str作为文件fname编译为 JS 函数并返回不触碰文件系统类似Function构造器但堆栈跟踪中显示为fname。mp.module_paths—require的全局模块搜索路径数组下文详述。定时器全局标准 HTML/Node.js 定时器均可用id setTimeout(fn [,duration [,arg1 [,arg2...]]]) id setTimeout(code_string [,duration]) clearTimeout(id) id setInterval(fn [,duration [,arg1 [,arg2...]]]) id setInterval(code_string [,duration]) clearInterval(id)setTimeout/setInterval返回id在duration毫秒后调用fn或执行code_stringinterval 每duration毫秒重复。duration的最小值与默认值都是 0code_string是作为 JS 代码求值的普通字符串[,arg1 [,arg2...]]提供时作为回调fn的参数传入。clear...(id)取消对应定时器且不可恢复。两个重要的行为保证手册明确说明实现见 player/javascript/defaults.js#L360-L452回调总是异步的setTimeout(fn)绝不会在返回前调用fnfn要么在当前事件循环迭代末尾调用要么在之后某次迭代中调用。interval 同样不会在同一次事件循环迭代中回调两次。定时器在事件队列清空后才被处理因此setTimeout(fn)可以作为一次性 idle 观察者使用。实现上定时器按到期时间排入有序数组insert_sortedprocess_timers()返回到下一个到期定时器的毫秒数可能为 0无挂起定时器时返回 -1interval 回调后允许 20 毫秒的漂移/时钟分辨率容差超出则跳过一次并重新对齐player/javascript/defaults.js#L436-L440。CommonJS 模块与require(id)CommonJS 模块是脚本之间共享函数的标准机制模块是一个向其预置的exports对象添加属性函数等的脚本另一个脚本用require(module-id)加载并拿到它的exports对象对同一模块的后续require直接返回缓存的exports不会重复执行。mpv 的实现遵循 CommonJS 规范、行为与 node.js 大体相似实现见 player/javascript/defaults.js#L454-L585但要注意以下规则.js扩展名总是被追加require(./foo)加载文件./foo.js。以./或../开头的 id 相对于执行require的脚本/模块解析否则视为顶层 idCommonJS 术语。顶层 id 优先按绝对文件系统路径解析如/x/y或~/x解析不了才作为全局模块 id按mp.module_paths数组顺序查找require(x)尝试在数组路径中加载x.jsidfoo/x尝试加载各路径下foo目录内的x.js。全局模块 id 的查找前缀在实现中被规范化为 meta 根~~modules即依次探测各module_paths下的path/rest.jsresolve_module_fileplayer/javascript/defaults.js#L483-L503。mp.module_paths默认为空唯一例外以目录方式加载的脚本会含有一项directory/modules/player/javascript/defaults.js#L474-L476。脚本或自定义 init见下可以更新该数组只影响之后对尚未加载/缓存的全局模块 id 的require。没有global变量但模块顶层词法作用域中的this就是全局对象严格模式下也是。若某模块依赖global可在require之前写this.global this;。模块中声明的函数和变量不会污染全局对象模块运行在独立函数上下文中。由于缺少fs、process等内置模块大多数 node.js 模块无法运行但依赖很少的少数模块可以工作。这面向的是 mpv 模块生态不是node.js 的替代品。模块加载的核心逻辑new_requireplayer/javascript/defaults.js#L558-L583先用resolve_module_id相对化并规范化 id命中req_cache则直接返回否则读取文件、包进function(require, exports, module) { ... }经mp.utils.compile_js编译执行模块抛错时从缓存中删除后再向上抛出。自定义初始化init.jsmpv 初始化某个脚本的 JavaScript 环境之后、加载该脚本之前会尝试运行 mpv 配置根目录下的init.js文件。该文件中的代码可以为所有脚本进一步修改环境。例如mp.module_paths.push(/foo);这会让所有脚本的require在查找全局模块 id 时也搜索/foo。手册特别提醒不要写mp.module_paths [/foo];——整体赋值会清掉已有路径例如以目录方式加载的脚本的script-dir/modules。若 mpv 以--no-config启动自定义 init 文件会被忽略。实现见 player/javascript/defaults.js#L855-L860mp.find_config_file(init.js)找到文件后通过require执行若只找到旧名.init.js则打印警告并忽略。事件循环mpv 的事件循环工作方式是只要事件队列非空就轮询/分发 mpv 事件随后处理定时器然后等待下一个事件如此永远循环。你可以用下面这段代码替换内置事件循环并打印 mpv 发来的每个事件这是手册给出的完整示例function mp_event_loop() { var wait 0; do { var e mp.wait_event(wait); dump(e); // 输出可能非常多... if (e.event ! none) { mp.dispatch_event(e); wait 0; } else { wait mp.process_timers() / 1000; if (wait ! 0) { mp.notify_idle_observers(); wait mp.peek_timers_wait() / 1000; } } } while (mp.keep_running); }mp_event_loop是 mpv 在脚本加载后试图调用的函数名内置实现与上述代码类似只是没有dump对应 player/javascript/defaults.js#L837-L852调用入口在 player/javascript.c#L438-L441。涉及的底层 API 语义e mp.wait_event(wait)— 下一个 mpv 事件到达时返回wait为正数且期间无事件则等待wait秒后返回。wait为 0 时立即返回队列空则e.event none。C 端实现见 player/javascript.c#L1151。mp.dispatch_event(e)— 调用为e.event注册的所有处理者事件处理者、属性观察者、脚本消息等。mp.process_timers()— 回调所有已添加、未取消且已到期的定时器返回到下一个到期定时器的毫秒数可能为 0无挂起定时器时返回 -1。不可递归调用。mp.notify_idle_observers()— 回调所有 idle 观察者。之所以在即将睡眠wait ! 0前调用且调用后要重算wait是因为观察者可能新增定时器或本身耗时不可忽略。mp.peek_timers_wait()— 返回与mp.process_timers()相同的值但什么都不执行在定时器回调中调用会得到无效结果。最后exit()同时被注册为shutdown事件的处理器其实现就是一句mp.keep_running falseplayer/javascript/defaults.js#L828-L830事件循环条件while (mp.keep_running)因此终止脚本随之退出。小结要点说明加载判定.js扩展名走 JavaScript 后端mp_scripting_js.file_ext js运行环境MuJSES5 子集无标准库、无fs/process模块mp/mp.utils/mp.msg/mp.options/mp.input全部预载错误undefined表失败 mp.last_error()取原因仅 LE 函数定时标准setTimeout/setInterval异步回调、队列清空后才处理模块化标准 CommonJSrequiremp.module_paths控制全局搜索环境扩展配置目录init.js--no-config时忽略控制流可覆盖mp_event_loopexit()即mp.keep_running false对于已有 Lua 脚本经验的开发者迁移成本主要在于把mp.add_timeout类 API 换成标准setTimeout、mp.utils.format_json换成JSON.stringify并理解 last-error 语义其余 API 一一对应且所有--script、脚本目录、script-opts的既有配置方式原样可用。【免费下载链接】mpv Command line media player项目地址: https://gitcode.com/GitHub_Trending/mp/mpv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。