跳到主要内容
铁匠铺
lili6507a书摘与小说

MPV源码阅读笔记

历史档案 · 保留原帖文字 · 23 条回复

mpv是一个经典的播放器,几乎是纯c编写的,结构精巧,准备学习一下
mpv在2013年8月发布了0.10版本,源码量从5.7M增加到现在的8M,计划学习2018年版本,该版本被在 https://github.com/ShiftMediaProject/mpv/commits/master 项目支持msvc编译为静态库,方便调试 0.2 增加osc作为控制交互方式 0.3 使用waf编译 Hardware decoding (vdpau, vda, vaapi) can now be used with vo_opengl. Win32: you can drop files on the mpv window. 支持了vaapi了, intel硬件加速 该版本使用intel集成显卡, cpu占有率只有 3-4%
VS2015编译FFmpeg3.3.1源码 site: https://exp.newsmth.net/topic/83de041dd3994cea7e540f38fede08e9
元旦放假又想起这个小玩具了
首先是编译, 系统是ubuntu16.04, 因为系统有自带的libav库, 版本已经是最新的4.0, mpv0.1.0 要求版本是ffmpeg2.0即可 因为ffmpeg版本差异太大, 不向下兼容, 所以要想编译成功mpv0.1.0需要静态链接到ffmpeg2里面提供的库
下载FFMPEG-n2.0.3 https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n2.0.3.tar.gz ./configure --enable-shared --preifx="/home/lq/mpv/mpv-build-ffmpeg2-libav9.8/FFmpeg-n2.0.3" make make install 首先先安装libav的系列库到本地, 将本地的so库去除, 以方便mpv链接到静态库(偷天换日, 邪恶啊)
下载mpv0.10 https://github.com/mpv-player/mpv/archive/refs/tags/v0.1.0.tar.gz ./configure --disable-liass 字幕神马的, 不管它先 -- 补, 半静态编译, libass是最新的 0.15.2 config.mak EXTRALIBS 添加 -L/home/lq/mpv/mpv-build/libass/libass/.libs/ -lass -lfontconfig -lfreetype -lharfbuzz -lfribidi 因为系统里有ffmpeg4.0, 所以检测版本没有问题 编译ffmpeg4时, 注意将 pkg-config 目录的 *.pc 拷贝到 /usr/lib/pkgconfig, 顺便检查下里面写的 so, include的路径是否正确, 这个pc文件是声明编译用的so, include 的, 举个栗子 pkg-config --libs --cflags mpv -I/usr/local/include -L/usr/local/lib -lmpv 修改 config.mak 的 -I, 以及 -L 到ffmpeg, 即可静态链接 .a 并使用上面的ffmpeg里的库 make 一下就生成mpv静态文件了, 不包含libav的动态库, 文件59Mb, strip后仅11Mb 玩具初步组装完毕
有几个问题想弄明白: 1 窗口是怎么生成的(窗口阴影) --补, 窗口阴影是你系统compiz统一渲染的,所有窗口都有(: 2 是如何将快捷键传递下来的 3 自己定义一个快捷键, 将屏幕N分屏 4 如何捕获鼠标位置, 定位分屏, 并传递参数 5 硬件解码是什么流程 -- 硬件解码, 你要看mpv-0.3.0了, 集成显卡解码vaapi, 可以渲染16个视频,cpu 40%, 是我的cpu太好了?? 6 lafi-complex如何实现多视频play_start不一致
想要调试, 打开日志 控制日志打印的函数在这里: mp_msg_test +- lev <= (mp_msg_levels[mod] == -2 ? mp_msg_level_all + verbose : mp_msg_levels[mod]) 不如先强制返回ok好了, 太邪恶了
可以打印出一篇篇日志了, 能否也打印出日志所在的行号呢 void mp_msg(int mod, int lev, const char *format, ...) { va_list va; va_start(va, format); mp_msg_va(mod, lev, format, va); va_end(va); } 修改一下 #define do {} while()
player_files -> play_current_files -> stream_open -> open_stream_full -> auto_open_streams -> stream_info_file 取文件大小, 注册了 write_buff, file_buff , seek 函数
解复用 play_current_files -> demux_open -> open_given_type, 尝试遍历 demuxer_list (edl, cue, rawaudio, rawvideo, tv, mkv, laf, mp4) 其中 lafi : demux_open_lavf -> lavf_check_file 最终找到MP4封装格式, 它使用的函数 demux_seek_lavf, demux_lavf_fill_buffer
init_best_video_out -> reinit_video_chain -> init_best_video_out -> 遍历 vo_list (video_out_drivers) 找到 opengl 视频输出
vo_opengl.c ,resize -> vo_get_src_dst_rects -> print_video_rect vf_open -> vo_reconfig -> determine_window_geometry, 然后 vo->driver->config 指定窗口位置
linux 平台的窗口绘制 mp_play_files -> play_current_file -> reinit_video_chain -> init_best_video_out -> vo_create -> preinit -> config_window -> config_window_x11 -> vo_x11_config_vo_window -> vo_x11_create_window 响应键盘命令 f run_playloop -> handle_input_and_seek_coalesce -> run_command -> mp_property_do -> mp_property_do -> do_action -> mp_property_fullscreen -> control -> vo_x11_control -> vo_x11_fullscreen
mpv使用分层处理播放资源 首先准备为 stream层(处理文件, 网络视频流), 解封装 demux 层(处理mkv, lafi封装), 解码层decode(解析x264等具体视频编码格式), output 层(将视频解码, 输出到设备, 比如win32, x11等) 数据在这几层是如何传递的呢
几个重要的数据结构, 对应上面的几层结构: typedef struct MPContext { struct stream *stream; struct demuxer *demuxer; struct dec_video *d_video; struct dec_audio *d_audio; struct ao *ao; struct vo *video_out; } 初始化: play_current_file(struct MPContext *mpctx) { mpctx->stream = stream_open(stream_filename, mpctx->global); mpctx->demuxer = demux_open(mpctx->stream, opts->demuxer_name, NULL, mpctx->global); reinit_video_chain(mpctx); +- init_demux_stream(mpctx, track); // 这个函数链接 stream -- demux +- mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input, mpctx->encode_lavc_ctx); +- video_init_best_codec(d_video, opts->video_decoders) } 值此, 几个结构初始化完毕
播放主函数: void run_playloop(struct MPContext *mpctx) +- update_video() +- load_next_vo_frame(mpctx, false) +- demux_read_packet(d_video->header); +- video_decode(d_video, pkt, framedrop_type); 似乎是通过几个队列进行链接的, 后续继续看
窗口大小设定 run_playloop -> filter_video -> reconfig_video -> vo_reconfig -> video_execute_format_change -> determine_window_geometry vo->dwidth, dheight决定窗口大小
记录 mpv0.9.2 vo_calc_window_geometry 控制窗口位置和大小 mpv0.5.0 以及之后的版本用户默认配置为 ~/.mpv/mpv.conf 而之前的版本配置为 ~/.mpv/config, 旧版本使用新配置会导致 geometry=50%:50% 这个配置没有加载, 窗口位置不在屏幕中间, 具体版本修改在2014年的7月27日的第一个提交 8bb7d44 -> cb250d4 修改了user的默认配置文件path mpv0.3.0 reisze: 4 play_current_file -> run_playloop -> handle_input_and_seek_coalesce -> run_command -> mp_property_do -> m_property_do -> do_action -> mp_property_window_scale -> control -> vo_x11_control -> vo_x11_highlevel_resize 保存窗口位置的数据结构 mpctx->mconfig(configfiles.c) m_config 也就是m_config_t 实际上是一个数组, 保存500多种配置项, 见数组 const m_option_t mp_opts[] = { ... OPT_GEOMETRY("geometry", vo.geometry, 0) ...} typedef struct MPOpts { mp_vo_opts vo; +- struct m_geometry geometry; } 数据结构: typedef struct MPContext { struct m_config *mconfig; } typedef struct m_config { struct m_config_option *opts; struct m_config_option { const char *name; const struct m_option *opt; void *data; // 数据保存在这里 } } } 调用关系: mpv_main -> mp_parse_cfgfiles (解析自定义配置文件) -> m_config_parse_config_file -> m_config_set_option_ext 那么是如何赋值给vo->opts的呢 vo_create: *vo = { opts: &global->opts->vo; } mpctx->video_out->opts MPContext -> vo -> mp_vo_opts -> m_geometry parse_geometry 解析配置文件中的 geometry=50%:50% 解析后的值保存在哪里呢 首先看这里mpv_main mpctx->mconfig = m_config_new(mpctx, sizeof(struct MPOpts), &mp_default_opts, mp_opts, NULL); mpctx->opts = mpctx->mconfig->optstruct; global->opts 是一个指针, 指向了 mpctx->opts struct MPOpts *opts = mpctx->opts; mpctx->global->opts = opts; vo_create初始化的 vo.opts 被赋值为 &global->opts->vo, 实际上来自 optstruct 这里是名场面: m_config_add_option void *optstruct = config->optstruct; co->data = arg->new ? (char *)optstruct + arg->offset : arg->p; co->data是需要赋值的地方, 通过哪里呢 这里: parse_geometry *((struct m_geometry *)dst) = gm; dst就是这个 co->data, 也就是上面的optstruct, 也就是再上面的 return m_option_parse(co->opt, name, param, set ? co->data : NULL); 最终通过 determine_window_geometry的 vo->x, y, dwidth, dheight决定窗口位置 断函数vo_x11_map_window: mpv_main -> play_files -> play_current_file -> run_playloop -> update_video -> decode_video -> decode_with_fallback -> decode -> init_vo(vd_lavc.c:471) -> mpcodecs_reconfig_vo -> vf_reconfig_wrapper -> vo_reconfig -> reconfig (vo_opengl.c:173) -> config_window (vo_opengl.c:157) -> mpgl_config_window -> config_window_x11 -> vo_x11_config_vo_window -> XCreateWindow 怎么这么乱!!! ------ 视频数据读取: demux_open_lavf 注册 avio_alloc_context(buffer, lavfdopts->buffersize, 0, demuxer, mp_read, NULL, mp_seek) 主循环 run_playloop -> update_video -> demux_read_packet -> ds_get_packets -> demux_fill_buffer -> demux_lavf_fill_buffer 继续 demux_lavf_fill_buffer -- 通过libavformat -- 调用 mp_read -> stream_read -> stream_read_partial -> stream_read_unbuffered -> fill_buffer (stream/stream_file.c:45) -> read函数读取数据 视频解码: run_playloop -> update_video -> video_decode(dec_video.c:306) -> decode_with_fallback (vd_lavc.c:798) -> decode(vd_lavc.c:733) -> avcodec_decode_video2, image_from_decoder, ctx->hwdec->process_image avcodec_decode_video2 -- 通过 libavcodec -- get_buffer2_hwdec(vd_lavc.c:617) -> get_surface_hwdec -> allocate_image -> create_decoder -> preallocate_surfaces -> va_surface_pool_reserve -> va_surface_alloc
X11创建显示窗口 vo_x11_config_vo_window vo_x11_create_window -> XCreateWindow vo_x11_map_window -> XMapWindow, 这里显示窗口 x11的简单例子 // gcc -o hello x11test.c -lX11 #include <X11/Xlib.h> #include <X11/Xutil.h> #include <X11/Xresource.h> #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char ** argv){ int screen_num, width, height; unsigned long background, border; Window win; XEvent ev; Display *dpy; /* First connect to the display server */ dpy = XOpenDisplay(NULL); if (!dpy) {fprintf(stderr, "unable to connect to displayn");return 7;} /* these are macros that pull useful data out of the display object */ /* we use these bits of info enough to want them in their own variables */ screen_num = DefaultScreen(dpy); background = BlackPixel(dpy, screen_num); border = WhitePixel(dpy, screen_num); width = 1200; /* start with a small window */ height = 720; win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), /* display, parent */ 0,0, /* x, y: the window manager will place the window elsewhere */ width, height, /* width, height */ 2, border, /* border width & colour, unless you have a window manager */ background); /* background colour */ /* tell the display server what kind of events we would like to see */ XSelectInput(dpy, win, ButtonPressMask|StructureNotifyMask|KeyPressMask|KeyReleaseMask|KeymapStateMask); /* !!! okay, put the window on the screen, please */ XMapWindow(dpy, win); /* as each event that we asked about occurs, we respond. In this * case we note if the window's shape changed, and exit if a button * is pressed inside the window */ while(1){ XNextEvent(dpy, &ev); switch(ev.type){ case KeymapNotify: XRefreshKeyboardMapping(&ev.xmapping); break; case KeyPress: break; /* ignore these */ case KeyRelease: { char string[25]; int len; KeySym keysym; len = XLookupString(&ev.xkey, string, 25, &keysym, NULL); if (len < 0){ if (string[0] == 'r'){ string[0] = 'n'; } fputs(string, stdout); } } break; case ConfigureNotify: if (width != ev.xconfigure.width || height != ev.xconfigure.height) { width = ev.xconfigure.width; height = ev.xconfigure.height; printf("Size changed to: %d by %dn", width, height); } break; case ButtonPress: XCloseDisplay(dpy); return 0; } } }
mpv使用了talloc内存管理, 它可以维护一棵内存申请树, 一次性释放内存, 减少了手动管理内存, 导致各种bug的情况, 赞一个! talloc 本身不处理线程.它是线程安全的(假设底层的malloc是)需要每个线程使用不同的内存上下文 talloc 是samba开发的内存管理模块, 据作者介绍, 比malloc慢4%, 但使得代码复杂度极大的降低, 很超值! 举个栗子: struct foo *X = talloc (mem_ctx, struct foo); X->name = talloc_strdup (X, "foo" ); 指针X->name 将是talloc 上下文"X"的"子",它本身是mem_ctx 的子 如果执行talloc_free(mem_ctx) 那么它会全部被销毁, 如果执行talloc_free(X) 那么只有X 和X->name 被销毁, 如果执行talloc_free(X->name) 那么只是名称元素X 被销毁
设置开始时间 ./mpv --start 00:20 1.mpv 参数传递: static void play_current_file(struct MPContext *mpctx) { struct MPOpts *opts = mpctx->opts; double startpos = rel_time_to_abs(mpctx, opts->play_start, -1); 此时play_start的值为 float 20.0, 类型为REL_TIME_ABSOLUTE 调用queue_seek 设定 mpctx->seek 的amount mplayer.c:2977 使用上面设置的该值 double demuxer_amount = seek.amount; 调用栈, 断 demux_seek_lavf mpv_main -> play_files -> play_current_file -> execute_queued_seek(mplayer.c:4402) -> execute_queued_seek (mplayer.c:3086) -> seek (mplayer.c:2977) -> demux_seek -> demux_seek_lavf 调用av_seek_frame传入 20*AV_TIME_BASE priv->last_pts += rel_seek_secs * AV_TIME_BASE av_seek_frame(priv->avfc, -1, priv->last_pts, avsflags); 那么mpctx->opts->play_start是在哪里设定的呢? 数据结构才是程序的灵魂啊, 见下图
0.1.0 版本的no_border模式无法响应f键全屏, vo_x11_decoration 函数增加 vo_MotifWmHints.functions = MWM_FUNC_MOVE | MWM_FUNC_RESIZE;

登录后分享你的想法