ffplay视频播放原理分析
一、播放器工作流程
解协议:媒体文件在网络上传输时,需要经过流媒体协议将媒体数据分段成若干个数据包,这样就可以满足用户一边下载一边观看的需求,而不需要等整个媒体文件都下载完成才能观看。常见的流媒体协议有 RTMP、HTTP、HLS、MPEG-DASH、MSS、HDS 等。由于流媒体协议中不仅仅包含媒体数据,还包含控制播放的信令数据。因此,解协议是移除协议中的信令数据,输出音视频封装格式数据。
解封装:封装格式也叫容器,就是将已经编码压缩好的视频流和音频流按照一定的格式放到一个文件中,常见的封装格式有 MP4、FLV、MPEG2-TS、AVI、MKV、MOV 等。解封装是将封装格式数据中的音频流压缩编码数据和视频流压缩编码数据分离,方便在解码阶段使用不同的解码器解码。
解码:压缩编码数据是在原始数据基础上采用不同的编码压缩得到的数据,而解码阶段就是编码的逆向操作。常见的视频压缩编码标准有 H.264/H.265 、MPEG-2 、AV1 、V8/9 等,音频压缩编码标准有 AAC 、MP3 等。解压后得到的视频图像数据是 YUV 或 RGB ,音频采样数据是 PCM 。
音视频同步:解码后的视频数据和音频数据是独立的,在送给显卡和声卡播放前,需要将视频和音频同步,避免播放进度不一致。
二、main函数
注:本文 ffplay 源码基于 ffmpeg 4.4。
2.1 环境初始化
init_dynload:调用SetDllDirectory("")删除 动态链接库(DLL)搜索路径中的当前工作目录,是 Windows 平台下的一种安全预防措施。 av_log_set_flag:设置 log 打印的标记为AV_LOG_SKIP_REPEATED,即跳过重复消息。 parse_loglevel:解析 log 的级别,会匹配命令中的-loglevel字段。如果命令中添加-report,会将播放日志输出成文件。 avdevice_register_all:注册特殊设备的封装库。 avformat_network_init:初始化网络资源,可以从网络中拉流。 parse_options:解析命令行参数,示例中的-i input.mp4和-loop 2就是通过这个函数解析的,支持的选项定义在options静态数组中。解析得到的文件名、文件格式分别保存在全局变量input_filename和file_iformat中。
2.2 SDL初始化
SDL_Init:初始化 SDL 库,传入的参数 flags,默认支持视频、音频和定时器,如果命令中配置了-an则禁用音频,配置了-vn则禁用视频。 SDL_CreateWindow:创建播放视频的窗口,该函数可以指定窗口的位置、大小,默认是 640*480 大小。 SDL_CreateRenderer:为指定的窗口创建渲染器上下文,对应的结构体是 SDL_Render 。我们既可以使用渲染器创建纹理,也可以渲染视图。
2.3 解析媒体流
2.4 SDL事件处理
三、read_thread函数
3.1 创建AVFormatContext
ic->interrupt_callback.callback = decode_interrupt_cb;
ic->interrupt_callback.opaque = is;
3.2 打开输入文件
3.3 搜索流信息
3.4 设置窗口大小
3.5 创建解码线程
3.6 解封装处理
四、stream_component_open函数
4.1 创建AVCodecContext
4.2 查找解码器
4.3 解码器初始化
4.4 创建解码线程
switch (avctx->codec_type) {
case AVMEDIA_TYPE_AUDIO: // 音频
...
if ((ret = decoder_init(&is->auddec, avctx, &is->audioq, is->continue_read_thread)) < 0)
goto fail;
...
if ((ret = decoder_start(&is->auddec, audio_thread, "audio_decoder", is)) < 0)
goto out;
...
case AVMEDIA_TYPE_VIDEO: // 视频
...
if ((ret = decoder_init(&is->viddec, avctx, &is->videoq, is->continue_read_thread)) < 0)
goto fail;
if ((ret = decoder_start(&is->viddec, video_thread, "video_decoder", is)) < 0)
goto out;
...
case AVMEDIA_TYPE_SUBTITLE: // 字幕
...
if ((ret = decoder_init(&is->subdec, avctx, &is->subtitleq, is->continue_read_thread)) < 0)
goto fail;
if ((ret = decoder_start(&is->subdec, subtitle_thread, "subtitle_decoder", is)) < 0)
goto out;
...
}
五、video_thread函数
GEEK TALK
5.1 创建AVFrame
5.2 视频解码
拿到解码后的视频帧后,会根据音视频同步的方式和命令行的-framedrop选项,判断是否需要丢弃失去同步的视频帧。
命令行带-framedrop选项,无论哪种音视频同步机制,都会丢弃失去同步的视频帧。
命令行带-noframedrop选项,无论哪种音视频同步机制,都不会丢弃失去同步的视频帧。
命令行不带-framedrop或-noframedrop选项,若音视频同步机制为同步到视频,则不丢弃失去同步的视频帧,否则会丢弃失去同步的视频帧。
5.3 放入FrameQueue
static void frame_queue_push(FrameQueue *f)
{
if (++f->windex == f->max_size)
f->windex = 0;
SDL_LockMutex(f->mutex);
f->size++;
SDL_CondSignal(f->cond);
SDL_UnlockMutex(f->mutex);
}
六、音视频同步
如果视频和音频进度一致,不需要同步;
如果视频落后音频,则丢弃当前帧直接播放下一帧,人眼感觉跳帧了;
如果视频超前音频,则重复显示上一帧,等待音频,人眼感觉视频画面停止了,但是有声音在播放;
6.1 判断播放完成
6.2 播放序列匹配
如果不等,重试视频播放的逻辑;
如果相等,则进入(2)流程判断;
注: serial
是用来区分是不是连续的数据,如果发生了 seek ,会开始一个新的播放序列,
如果不相等,则将frame_timer更新为当前时间;
如果相等,不处理并进入下一流程
6.3 判断是否重复上一帧
注: pts
全称是 Presentation Time Stamp ,显示时间戳,表示解码后得到的帧的显示时间。
last_duration = vp_duration(is, lastvp, vp);
delay = compute_target_delay(last_duration, is);
time= av_gettime_relative()/1000000.0;
if (time < is->frame_timer + delay) {
*remaining_time = FFMIN(is->frame_timer + delay - time, *remaining_time);
goto display;
}
6.4 判断是否丢弃未播放的帧
当前播放模式不是步进模式;
丢帧策略生效:framedrop>0,或者当前音视频同步策略不是音频到视频。
当前帧vp还没有来得及播放,但是下一帧的播放时刻(is->frame_timer + duration)已经小于当前系统时刻(time)了。
if (frame_queue_nb_remaining(&is->pictq) > 1) { Frame *nextvp = frame_queue_peek_next(&is->pictq); duration = vp_duration(is, vp, nextvp); if(!is->step && (framedrop>0 || (framedrop && get_master_sync_type(is) != AV_SYNC_VIDEO_MASTER)) && time > is->frame_timer + duration){ is->frame_drops_late++; frame_queue_next(&is->pictq); goto retry; }}
七、渲染
GEEK TALK
ffplay 最终的图像渲染是由 SDL 完成的,在 video_display 中调用了 SDL_RenderPresent(render) 函数,其中 render 参数是最开始在 main 函数中创建的。在渲染之前,需要将解码得到的视频帧数据转换为 SDL 支持的图像格式。转换过程在 upload_texture 函数中实现,细节不在此处分析。
音频类似,如果解码得到的音频不能被 SDL 支持,需要对音频进行重采样,将音频帧格式转换为 SDL 支持的格式。
八、小结
GEEK TALK
本文从整体播放流程出发,介绍了 ffplay 播放器播放媒体文件的主要流程,不深陷于代码细节。同时,对 FFmpeg 的一些常用函数有了一些了解,对我们自己手写一个简单的播放器有很大的帮助。