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;