MPV源码探究:源码结构和调用层次

源码结构和调用层次

源码结构

从 Github 上拉取最新的源码,目录结构大致如下:

H:\MPV          
├─.github
├─audio                    
│  ├─decode                
│  ├─filter                
│  └─out                   
├─ci                       
├─common                   
├─demux                    
├─DOCS                     
│  └─man                   
├─etc                      
├─filters                  
├─input                    
├─libmpv                   
├─misc                     
├─options                  
├─osdep                    
│  ├─android               
│  ├─ar                    
│  ├─macos                 
│  └─win32                 
│      └─include           
├─player                   
│  ├─javascript            
│  └─lua                   
├─stream                   
├─sub                      
├─ta                       
├─test                     
│  └─ref                   
├─TOOLS                    
│  ├─lua                   
│  ├─mpv-osd-symbols.sfdir 
│  └─osxbundle             
│      └─mpv.app           
│          └─Contents      
│              ├─MacOS     
│              │  └─lib    
│              └─Resources 
├─video                    
│  ├─decode                
│  ├─filter                
│  └─out                   
│      ├─cocoa             
│      ├─cocoa-cb          
│      ├─d3d11             
│      ├─gpu               
│      ├─hwdec             
│      ├─opengl            
│      ├─placebo           
│      ├─vulkan            
│      └─win32             
└─waftools                 
    ├─checks               
    ├─detections           
    ├─fragments            
    └─generators           
  • <libmpv>:这个文件夹内放置了作为 libmpv 链接库所暴露的方法(头文件),具体实现都在别的文件夹里。实际上编译到动态链接库的时候,暴露的方法名都定义在了libmpv/mpv.def里面。但是这个 .def 文件不是标准的导出文件
  • <audio>:顾名思义,音频解码相关的源码。
  • <video>:视频解码、分离、渲染相关的文件,分别在 decode, filter, out 文件夹里。
  • <player>:一个具体的播放器实现,内部调用上面几个部分的模块。
  • wscript:编译脚本。新添加的文件要由此加入到编译流程中。

内部调用层次

初始化核心上下文

如果是启动播放器进行播放,则首先会进行一个内部状态的初始化,主要是初始化了MPContext这个结构体。这个结构体是一个大杂烩,所有播放相关的参数、动态变化的属性都绑定到这上面。然后内核进入 idle 状态,等待播放视频。

初始化渲染驱动

打开第一个媒体文件的时候,会开始进行视频/音频播放链路(video_output_chain)初始化,其中就包括初始化解码和渲染模块。渲染模块由结构体 vo_driver 定义,(mpv 内部使用结构体来定义接口),例如 vo_gpu 的定义如下:

const struct vo_driver video_out_gpu = {
    .description = "Shader-based GPU Renderer",
    .name = "gpu",
    .caps = VO_CAP_ROTATE90,
    .preinit = preinit,
    .query_format = query_format,
    .reconfig = reconfig,
    .control = control,
    .get_image = get_image,
    .draw_frame = draw_frame,
    .flip_page = flip_page,
    .get_vsync = get_vsync,
    .wait_events = wait_events,
    .wakeup = wakeup,
    .uninit = uninit,
    .priv_size = sizeof(struct gpu_priv),
    .options = options,
};

接下来我们都以这个 Windows 下最常用的 vo 驱动器——vo_gpu 为例。在 /video/out/vo.c 中,你可以看到所有支持的 vo_driver:

const struct vo_driver *const video_out_drivers[] =
    {
        &video_out_libmpv,
#if HAVE_ANDROID
        &video_out_mediacodec_embed,
#endif
        &video_out_gpu,
#if HAVE_VDPAU
        &video_out_vdpau,
#endif
...省略多个driver

Mpv 会根据系统、编译情况、传入参数决定使用哪个具体的视频输出驱动。之后,调用该驱动的preinit方法。对于 vo_gpu 来说,它的下层还依赖于不同的 render_context,对应了在不同系统环境上的渲染接口。这也是 Mpv 跨平台兼容的关键。所有 gpu 支持的渲染接口定义在 video/out/gpu/context.c

static const struct ra_ctx_fns *contexts[] = {
#if HAVE_D3D11
    &ra_ctx_d3d11,
#endif

// OpenGL contexts:
#if HAVE_EGL_ANDROID
    &ra_ctx_android,
#endif
#if HAVE_RPI
    &ra_ctx_rpi,
#endif
#if HAVE_GL_COCOA
    &ra_ctx_cocoa,
#endif
#if HAVE_EGL_ANGLE_WIN32
    &ra_ctx_angle,
#endif
#if HAVE_GL_WIN32
    &ra_ctx_wgl,
#endif
...省略大量接口
};

每个底层接口都由结构体 ra_ctx_fns 定义。这个结构体暴露了一组用于配置的具体方法:

const struct ra_ctx_fns ra_ctx_d3d11 = {
    .type = "d3d11",
    .name = "d3d11",
    .reconfig = d3d11_reconfig,
    .control = d3d11_control,
    .init = d3d11_init,
    .uninit = d3d11_uninit,
};

因此在 gpu 渲染驱动的 preinit 函数中一大任务就是调用具体渲染接口的 init 方法。

视频播放循环

视频、音频播放驱动初始化完毕后,就开始视频播放。整个播放的流程(render loop)如下伪代码:

for video in Videos {
    while(1) {
        render_frame(video);
        wait for next frame;
    }
}

对的,就是这么简单粗暴。这里有意忽略了时间同步、音视频同步等具体细节,实际上 Mpv 内部大量依赖于锁和信号量进行线程间同步。

TL;DR

总结一下,一个初始化的流程涉及如下接口的调用:

  • MPContext 初始化
  • vo_driver 初始化
  • render_backend 初始化(即特定的、与系统环境相关的底层接口)

下一篇文章,我们顺着官方播放器的具体代码,看看 Mpv 具体初始化了哪些东西,并试图捋清楚 libmpv 又是如何进行初始化的。

上一篇:Qt 使用 MPV 开源播放器


下一篇:MPV源码探究:背景及准备工作