ffplay是用ffmpeg代码实现的一个播放器,可以播放本地文件,也可以播放网络资源,与vlc播放器功能类似。把ffmpeg环境配好后,在资源所在的目录,打开控制台输入ffplay [资源名字]
即可播放该媒体资源,ffplay源码只有3800多行,C工程,音视频显示渲染是用的SDL库。下载ffmpeg源码后,找到ffplay.c,这既是ffplay播放器的全部代码。我用的是ffmpeg版本 4.2.1的源码。主要分以下内容剖析ffplay源码,有兴趣的可以看看,有不当之处请指出,我及时回复。
ffplay源码分析(一):代码架构简述
ffplay源码分析(二): 探讨自定义队列
ffplay源码分析(三):read_thread线程
ffplay源码分析(四): ffplay解码线程分析
ffplay源码分析(五):ffplay video显示线程分析
ffplay源码分析(六):ffplay audio输出线程分析
ffplay源码分析(七):ffplay subtitle显示线程分析
ffplay源码分析(八):音视频同步 -- 基础
ffplay源码分析(九):视频同步音频
ffplay源码分析(十):音频同步视频
ffplay源码分析(十一):同步到外部时钟
在视频文件的播过中,一般要涉及到文件读取、解封装、解码、音视频输出、音视频同步等技术。比较完整的流程如下图:
在这个流程中,主要有几个线程:
(1)读线程。读取文件、解封装
(2)音频解码线程。解码音频压缩数据为PCM数据。
(3)视频解码线程。解码视频压缩数据为图像数据。
(4)音频输出线程。基于SDL播放,该线程实际上是SDL的内部线程。
(5)视频输出线程。基于SDL播放,该线程为程序主线程。
由于存在多个线程,所以线程间的数据传递用到了多线程安全的队列,有FrameQueue和PacketQueue。音频和视频各自独立输出的过程不可避免地会出现音视频的不同步现象,所以在输出前会有一些控制策略保证音视频的同步输出。 经过代码分析后,我画了张简图,如下:
需要原图的,可以点此处下载。
ffplay main函数代码如下
//入口函数,初始化 SDL 库,注册 SDL 消息事件,启动文件解析线程,进入消息循环
int main(int argc, char **argv)
{
int flags;
VideoState *is;
//调用windows API设置dll加载
init_dynload();
av_log_set_flags(AV_LOG_SKIP_REPEATED);
parse_loglevel(argc, argv, options);
/* register all codecs, demux and protocols */
#if CONFIG_AVDEVICE
avdevice_register_all();
#endif
avformat_network_init();
init_opts();
//系统信号设置
signal(SIGINT , sigterm_handler); /* Interrupt (ANSI). */
signal(SIGTERM, sigterm_handler); /* Termination (ANSI). */
show_banner(argc, argv, options);
parse_options(NULL, argc, argv, options, opt_input_file);
//检测文件名,如果是空名字,则输出错误
if (!input_filename)
{
show_usage();
av_log(NULL, AV_LOG_FATAL, "An input file must be specified\n");
av_log(NULL, AV_LOG_FATAL, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
exit(1);
}
if (display_disable)
{
video_disable = 1;
}
flags = SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER;
if (audio_disable)
flags &= ~SDL_INIT_AUDIO;
else {
/* Try to work around an occasional ALSA buffer underflow issue when the
* period size is NPOT due to ALSA resampling by forcing the buffer size. */
if (!SDL_getenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE"))
SDL_setenv("SDL_AUDIO_ALSA_SET_BUFFER_SIZE","1", 1);
}
if (display_disable)
flags &= ~SDL_INIT_VIDEO;
//初始化SDL
if (SDL_Init (flags))
{
av_log(NULL, AV_LOG_FATAL, "Could not initialize SDL - %s\n", SDL_GetError());
av_log(NULL, AV_LOG_FATAL, "(Did you set the DISPLAY variable?)\n");
exit(1);
}
//SDL事件设置
SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);
SDL_EventState(SDL_USEREVENT, SDL_IGNORE);
//初始化AVPacket
av_init_packet(&flush_pkt);
flush_pkt.data = (uint8_t *)&flush_pkt;
if (!display_disable)
{
int flags = SDL_WINDOW_HIDDEN;
if (alwaysontop)
#if SDL_VERSION_ATLEAST(2,0,5)
flags |= SDL_WINDOW_ALWAYS_ON_TOP;
#else
av_log(NULL, AV_LOG_WARNING, "Your SDL version doesn't support SDL_WINDOW_ALWAYS_ON_TOP. Feature will be inactive.\n");
#endif
if (borderless)
flags |= SDL_WINDOW_BORDERLESS;
else
flags |= SDL_WINDOW_RESIZABLE;
//创建窗口
window = SDL_CreateWindow(program_name, SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, default_width, default_height, flags);
SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "linear");
if (window)
{
//基于窗口创建渲染器
renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
if (!renderer)
{
av_log(NULL, AV_LOG_WARNING, "Failed to initialize a hardware accelerated renderer: %s\n", SDL_GetError());
renderer = SDL_CreateRenderer(window, -1, 0);
}
if (renderer)
{
if (!SDL_GetRendererInfo(renderer, &renderer_info))
av_log(NULL, AV_LOG_VERBOSE, "Initialized %s renderer.\n", renderer_info.name);
}
}
if (!window || !renderer || !renderer_info.num_texture_formats)
{
av_log(NULL, AV_LOG_FATAL, "Failed to create window or renderer: %s", SDL_GetError());
do_exit(NULL);
}
}
//打开流
is = stream_open(input_filename, file_iformat);
if (!is)
{
av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
do_exit(NULL);
}
//进入事件循环,处理键盘,鼠标的事件
event_loop(is);
/* never returns */
return 0;
}
在初始化,创建SDL窗口,渲染器等做好后,通过读取控制台的命令,进入stream_open函数,在该函数会有资源解封装,解码,数据帧对列操作,颜色空间转换等,这都是的子线程中进行的,同时主线程的event_loop会处理视频显示,键盘按键,鼠标等事件。再用同步互斥等机制,实现音视频同步播放。
main函数前面是ffmpeg, SDL初始化以及相关信息设置,例如,当我们用ffplay播放一个文件时,在控制台如果没有输入名字,则会报错,如下所示:
//检测文件名,如果是空名字,则输出错误
if (!input_filename)
{
show_usage();
av_log(NULL, AV_LOG_FATAL, "An input file must be specified\n");
av_log(NULL, AV_LOG_FATAL, "Use -h to get full help or, even better, run 'man %s'\n", program_name);
exit(1);
}
错误如下图:
ffplay代码还是很有难度的,这是一个完整播放器的设计,涉及到很多内容,不光是代码,还有音视频的一些知识。例如数据帧队列如何设计呢,多线程时队列读写如何保证同步呢,怎样保证最好的性能,毕竟音视频编解码播放性能很重要,不能一个文件播放出现丢帧,卡顿等,这些都在代码里,查阅了很多博客,确实有很多研究ffmpeg的大佬,后续将这些转载过来,方便以后分析,学习,有兴趣的朋友也可以看看,有不当之处请指出,我及时更新。
ffplay源码可以从项目中拿出来直接编译,可以看我编译的方法:VS2019编译ffplay源码