mpv libmpv 嵌入指南C API 架构、事件循环与 C 插件开发附源码级实现解析【免费下载链接】mpv Command line media player项目地址: https://gitcode.com/GitHub_Trending/mp/mpvmpv 除了作为命令行播放器运行还可以作为音视频播放后端嵌入到其他程序中。官方文档DOCS/man/libmpv.rst给出了两条主要路径通过libmpv C API定义于include/mpv/client.h将 mpv 嵌入你自己的应用以及编写C 插件直接扩展 mpv 本体的功能。本文完整继承该文档的全部要点并结合当前仓库源码补充了插件加载器、符号表初始化、编译选项等实现细节帮助你掌握 libmpv 的 API 设计模型命令/属性/事件以及mpv_open_cplugin插件入口的完整生命周期。libmpv嵌入 mpv 的推荐方式mpv 可以被嵌入到其他程序中充当视频/音频播放后端官方推荐的途径就是使用 libmpv。其 C API 完整定义在 include/mpv/client.h 中该头文件同时就是 API 的完整文档注释直接写在头文件里其他语言的绑定如 Lua、Python 等都建立在这套 C 接口之上。需要特别注意的一点是libmpv 本身只是提供了控制 mpv 底层机制的入口真正的功能细节分散在几处文档中。原文档列出的参考资料对应到本仓库即选项见 DOCS/man/options.rst即mpv_set_option*()系列函数所操作的选项体系输入命令见 DOCS/man/commands.rst对应mpv_command*()系列函数属性见 DOCS/man/mpv.rst 中的 Properties 部分对应mpv_get_property()/mpv_set_property()/mpv_observe_property()。换句话说libmpv 的交互模型可以概括为一句话用 options 配置行为用 commands 下发动作用 properties 读写状态用 events 接收异步反馈。从 client.h 头部的注释可以确认API 的两种使用场景是1mpv 内部使用如 Lua 脚本机制2通过mpv_create()把 mpv 作为库嵌入到其他应用中。事件循环mpv 客户端的生命线client.h中对事件循环的说明是理解 libmpv 的关键。API 使用者应该在某个线程中运行事件循环循环中调用mpv_wait_event()——它在新事件到达时返回。如果宿主程序已有自己的事件循环例如 GUI 工具包可以用mpv_set_wakeup_callback()注册唤醒回调然后以 0 超时轮询mpv_wait_event(ctx, 0)。有几个行为细节值得记住均来自client.h的官方注释事件循环与播放核心是解耦的不调用mpv_wait_event()不会停止播放但事件队列最终会拥塞API 整体是线程安全的但由于播放核心内部用单一锁串行化所有访问使用多个线程访问 client API 并没有实际收益同步调用可能阻塞无界时间例如网络缓慢时对响应性要求高的场景应优先使用带_async后缀的异步函数它们立即入队结果以后续事件返回并通过 64 位reply_userdata字段关联请求与回复。运行环境要求与兼容性client.h专门用一节 Basic environment requirements 列出了以库方式使用 mpv 时的 C 环境硬性约束这些都是从长期实践中沉淀的坑LC_NUMERIClocale 必须为C若调用setlocale(LC_ALL, ...)必须把LC_NUMERIC重置回默认X11 VO 会安装进程级 xlib 错误处理程序可能干扰同进程内的 GUI 工具包mpv 依赖的某些库Fribidi/libass、ALSA、FFmpeg 等本身不是线程安全的FPU 精度至少需要 double 精度内存耗尽时 mpv 会直接终止进程若进程内任何地方注册信号处理器必须设置SA_RESTART标志否则信号会导致随机故障。关于版本兼容client.h说明 API 使用MPV_CLIENT_API_VERSION进行版本化MPV_MAKE_VERSION(major, minor)将高 16 位作为主版本、低 16 位作为次版本。每次 API 变更都记录在 DOCS/client-api-changes.rst 中——例如当前仓库中记录的 2.0 版本mpv 0.35.0移除了废弃的mpv_detach_destroy、旧opengl_cbAPI 等2.5 版本mpv 0.40.0弃用了MPV_RENDER_PARAM_AMBIENT_LIGHT。原文档还特别提示C 函数本身可能长期保持兼容但其暴露的功能选项名、命令、属性取值变化更快防御性编程的做法是优先使用MPV_FORMAT_STRING来与底层类型解耦。C 插件使用 libmpv API 但不链接 libmpvC 插件是 mpv 提供的另一种扩展方式插件代码使用 libmpv 的 API但并不需要链接 libmpv 库本身。这是它与嵌入式用法最本质的区别。启用条件与编译要求C 插件默认在满足条件时启用。对照 meson.build 中的构建逻辑约 L378-L385cplugins get_option(cplugins).require( win32 or (features[libdl] and cc.has_link_argument(-rdynamic)), error_message: cplugins not supported by the os or compiler!, ) if features[cplugins] and not win32 link_flags -rdynamic endif也就是说在Linux/BSD上要求编译器支持-rdynamic链接标志且能找到dl库libdl满足则默认启用并会自动给 mpv 可执行文件追加-rdynamic链接参数——这个标志的作用正是把 mpv 宿主二进制的符号动态导出供插件调用在Windows上始终启用该特性在 meson.options 中定义为option(cplugins, type: feature, value: auto, ...)因此也可以用-Dcpluginsenabled/disabled显式控制。C 插件的存放位置与加载方式C 插件与 Lua 脚本放在同一目录mpv 配置目录下的scripts/子目录Linux/BSD 标准路径为~/.config/mpv/scripts/见 DOCS/man/mpv.rst 的 FILES 一节该目录中的脚本按字母序加载路径也可由XDG_CONFIG_HOME、MPV_HOME等环境变量重定向。插件文件必须具有平台对应的扩展名——Linux 为.soWindows 为.dll——也可以不依赖目录扫描直接用--script选项显式加载指定路径的插件。从源码看目录扫描与类型分派在 player/scripting.c 中完成加载器遍历所有配置目录中的scripts子目录并逐一调用mp_load_script()而 C 插件的类型注册表mp_scripting_cplugin将扩展名与加载函数绑定const struct mp_scripting mp_scripting_cplugin { .name cplugin, #ifdef _WIN32 .file_ext dll, #else .file_ext so, #endif .load load_cplugin, };实际的load_cplugin()实现player/scripting.c只有不到 20 行核心逻辑static int load_cplugin(struct mp_script_args *args) { void *lib dlopen(args-filename, RTLD_NOW | RTLD_LOCAL); if (!lib) goto error; // Note: once loaded, we never unload, as unloading the libraries linked to // the plugin can cause random serious problems. mpv_open_cplugin sym (mpv_open_cplugin)dlsym(lib, MPV_DLOPEN_FN); if (!sym) goto error; init_sym_table(args, lib); return sym(args-client) ? -1 : 0; error: ; char *err dlerror(); if (err) MP_ERR(args, C plugin error: %s\n, err); return -1; }这里有三个与原文档描述一一对应的实现事实加载函数签名被定义为typedef int (*mpv_open_cplugin)(mpv_handle *handle)符号名硬编码为#define MPV_DLOPEN_FN mpv_open_cplugin——这正是插件必须导出的入口函数插件一旦加载永远不会被卸载源码注释明确指出卸载与插件关联的库会引起严重问题这解释了后文插件函数返回后 handle 被释放的约束dlopen使用RTLD_NOW | RTLD_LOCAL立即解析所有符号缺符号直接失败且符号仅在该插件内可见。插件入口函数mpv_open_cplugin的契约按照原文档的规范一个 C 插件必须导出以下函数int mpv_open_cplugin(mpv_handle *handle)其完整契约如下每一条都有源码或文档依据调用时机加载时由宿主调用且在独立线程中执行。插件函数在整个插件生命周期内不得返回一旦返回handle随即被释放这与永不卸载的设计一致返回等于自杀返回值语义0表示成功-1表示失败播放器打印一条笼统的加载失败错误见上面load_cplugin()中sym(args-client) ? -1 : 0的判断其他返回值被保留会触发未定义行为可用 API插件内可以调用任意 libmpv API 函数。传入的handle由宿主通过mpv_create_client()或其内部等价物创建并归属插件所有可以调用mpv_wait_event()等待事件等禁止操作绝对不要对该 handle 调用mpv_destroy()或mpv_terminate_destroy()——handle 的生命周期由宿主管理阻塞语义重要玩家会阻塞直到插件第一次调用mpv_wait_event()。这给了插件一个窗口在播放开始前完成初始 hook 安装等准备工作。原文档最后提到C 插件的这些细节与 Lua 脚本相当相似同样是独立线程 事件等待 hook模型熟悉 Lua 脚本的开发者可以平滑迁移。符号来源插件不链接 libmpv原文档 Linkage to libmpv 一节强调了一个容易踩坑的事实当前实现要求插件不要链接 libmpv 库。插件使用的不是 libmpv 二进制中的符号而是mpv 宿主二进制中的符号——这正是 Linux 侧需要-rdynamic的原因。Windows 的特殊处理MPV_CPLUGIN_DYNAMIC_SYM。在 Windows 上加载器没有全局符号的概念把 cplugin 加载进 mpv 进程并不会让插件自动调用到 mpv.exe 或其他模块中的符号插件必须显式链接到某个具体 PE 二进制libmpv-2.dll、mpv.exe 或任何静态链接了 mpv 的二进制。这严重限制了可移植性——每个目标 PE 二进制都要单独编译一份插件实践中不可行。解决方案在 include/mpv/client.h 中实现编译插件时定义MPV_CPLUGIN_DYNAMIC_SYM宏头文件会宏替换所有mpv_*入口为函数指针并在加载时由宿主初始化这些指针#define MPV_DEFINE_SYM_PTR(name) \ MPV_SELECTANY MPV_EXPORT \ MPV_DECLTYPE(name) *pfn_##name; MPV_DEFINE_SYM_PTR(mpv_client_api_version) #define mpv_client_api_version pfn_mpv_client_api_version ...注意两个细节MPV_EXPORT让pfn_*指针从插件中被导出MPV_SELECTANYWindows 下为__declspec(selectany)保证即使多次定义链接器也只保留单一实例。由于函数名通过#define重定向到指针插件源码无需任何改动只需多定义一个宏。宿主侧如何填表player/scripting.c 中的init_sym_table()使用INIT_SYM宏对每个 API 函数执行dlsym(lib, pfn_ #name)——注意它从插件库本身里查找pfn_前缀的符号即插件导出的那些函数指针然后把 mpv 内部真实函数的地址写入其中#define INIT_SYM(name) \ { \ void **sym (void **)dlsym(lib, pfn_ #name); \ if (sym) { \ if (*sym *sym ! name) \ MP_ERR(args, Overriding already set function #name \n); \ *sym name; \ } \ }宏覆盖的符号集包括完整的 client APImpv_create、mpv_command_node、mpv_wait_event、mpv_observe_property、mpv_hook_add等、render APImpv_render_context_create系列以及mpv_stream_cb_add_ro。由于init_sym_table()在sym(args-client)即mpv_open_cplugin之前被调用此时所有指针已指向宿主真实实现——与client.h注释中在调用 mpv_open_cplugin 之前初始化的说明一致。一个最小可用的 C 插件骨架综合原文档契约与上述源码事实一个符合规范的最小插件骨架如下Linux 侧插件放入~/.config/mpv/scripts/并以.so命名或用mpv --script ./myplugin.so video.mp4显式加载#include mpv/client.h // 宿主加载时会调用本函数且在独立线程中执行。 // 必须一直运行不返回直到插件退出。 int mpv_open_cplugin(mpv_handle *handle) { // 此时播放尚未开始可以在此安装 hook、观察属性。 // 例如观察 time-pos 属性 mpv_observe_property(handle, 1, time-pos, MPV_FORMAT_DOUBLE); // 注册我们关心的事件 mpv_request_event(handle, MPV_EVENT_END_FILE, 1); for (;;) { mpv_event *ev mpv_wait_event(handle, -1); // 无限期等待 // 注意首次 mpv_wait_event() 调用才会解除宿主的阻塞 // 在此之前播放器保持阻塞状态等待本线程完成初始化。 if (ev-event_id MPV_EVENT_PROPERTY_OBSERVER) { // 处理属性变化 ... } else if (ev-event_id MPV_EVENT_END_FILE) { // 处理文件结束 ... } } // 永远走不到这里返回后 handle 会被释放插件终止。 // 返回 0 表示成功、-1 表示失败其他值是未定义行为。 return 0; }编译时的要点Linuxgcc -shared -fPIC myplugin.c -o myplugin.so -Impv源码/include不要链接libmpvWindows加-DMPV_CPLUGIN_DYNAMIC_SYM同样不链接 libmpv让pfn_*函数指针机制接管符号解析。总结DOCS/man/libmpv.rst虽然篇幅不长却定义了 mpv 的两类可编程扩展面libmpv 嵌入——通过mpv_create()获得 handle以 options/commands/properties/events 四元组模型控制播放核心事件循环mpv_wait_event 可选的唤醒回调是客户端的骨架环境约束locale、信号、线程安全与版本兼容策略MPV_CLIENT_API_VERSION及 DOCS/client-api-changes.rst是长期维护必须面对的议题C 插件——导出mpv_open_cplugin(mpv_handle *)单入口运行于独立线程、以永不返回 首次mpv_wait_event()解除宿主阻塞的生命周期工作Linux 依赖-rdynamic导出宿主符号Windows 依赖MPV_CPLUGIN_DYNAMIC_SYM函数指针表由 player/scripting.c 中init_sym_table()填充pfn_*指针。理解这些机制后读者既可以安全地把 mpv 嵌入自研应用也可以编写与 Lua 脚本同级的 C 插件扩展 mpv 本体且所有结论均可在当前仓库的include/mpv/client.h、player/scripting.c、meson.build与DOCS/文档中逐一验证。【免费下载链接】mpv Command line media player项目地址: https://gitcode.com/GitHub_Trending/mp/mpv创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考