ffplay视频播放原理分析
作者
赵家祝
FFmpeg框架由命令行工具和函数库组成,ffplay是其中的一种命令行工具,提供了播放音视频文件的功能,不仅可以播放本地多媒体文件,还可以播放网络流媒体文件。本文从ffplay的整体播放流程出发,借鉴其设计思路,学习如何设计一款简易的播放器。
一、播放器工作流程
在学习ffplay源码之前,为了方便理解,我们先宏观了解一下播放器在播放媒体文件时的工作流程。
解协议:媒体文件在网络上传输时,需要经过流媒体协议将媒体数据分段成若干个数据包,这样就可以满足用户一边下载一边观看的需求,而不需要等整个媒体文件都下载完成才能观看。常见的流媒体协议有RTMP、HTTP、HLS、MPEG-DASH、MSS、HDS等。由于流媒体协议中不仅仅包含媒体数据,还包含控制播放的信令数据。因此,解协议是移除协议中的信令数据,输出音视频封装格式数据。
解封装:封装格式也叫容器,就是将已经编码压缩好的视频流和音频流按照一定的格式放到一个文件中,常见的封装格式有MP4、FLV、MPEG2-TS、AVI、MKV、MOV等。解封装是将封装格式数据中的音频流压缩编码数据和视频流压缩编码数据分离,方便在解码阶段使用不同的解码器解码。
解码:压缩编码数据是在原始数据基础上采用不同的编码压缩得到的数据,而解码阶段就是编码的逆向操作。常见的视频压缩编码标准有H./H.、MPEG-2、AV1、V8/9等,音频压缩编码标准有AAC、MP3等。解压后得到的视频图像数据是YUV或RGB,音频采样数据是PCM。
音视频同步:解码后的视频数据和音频数据是独立的,在送给显卡和声卡播放前,需要将视频和音频同步,避免播放进度不一致。
二、main函数
ffplay的使用非常简单,以ffplay-iinput.mp4-loop2为例,表示使用ffplay播放器循环播放input.mp4文件两遍。执行该命令时,对应的源码在fftools/ffplay.c中,程序入口函数是main函数。
注:本文ffplay源码基于ffmpeg4.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:解析命令行参数,示例中的-iinput.mp4和-loop2就是通过这个函数解析的,支持的选项定义在options静态数组中。解析得到的文件名、文件格式分别保存在全局变量input_filename和file_iformat中。
2.2SDL初始化
SDL的全称是SimpleDirectMediaLayer,是一个跨平台的多媒体开发库,支持Linux、Windows、MacOS等多个平台,实际上是对DirectX、OpenGL、Xlib再封装,在不同操作系统上提供了相同的函数。ffplay的播放显示是通过SDL实现的。
main函数中主要调用了以下三个SDL函数:
SDL_Init:初始化SDL库,传入的参数flags,默认支持视频、音频和定时器,如果命令中配置了-an则禁用音频,配置了-vn则禁用视频。SDL_CreateWindow:创建播放视频的窗口,该函数可以指定窗口的位置、大小,默认是*大小。SDL_CreateRenderer:为指定的窗口创建渲染器上下文,对应的结构体是SDL_Render。我们既可以使用渲染器创建纹理,也可以渲染视图。
2.3解析媒体流
stream_open函数是ffplay开始播放流程的起点,该函数传入两个参数,分别是文件名input_filename和文件格式file_iformat。下面是函数内部的处理流程:
(1)初始化VideoState:VideoState是ffplay中最大的结构体,所有的视频信息都定义在其中。初始化VideoState时,先定义VideoState结构体指针类型的局部变量is,分配堆内存。然后初始化结构体中的变量,例如视频流、音频流、字幕流的索引,并赋值函数入参filename和iformat。
(2)初始化FrameQueue:FrameQueue是解码后的Frame队列,Frame是解码后的数据,例如视频解码后是YUV或RGB数据,音频解码后是PCM数据。初始化FrameQueue时,会对VideoState中的pictq(视频帧队列)、subpq(字幕帧队列)、sampq(音频帧队列)依次调用frame_queue_init函数进行初始化。FrameQueue内部是通过数组实现了一个先进先出的环形缓冲区,windex是写指针,被解码线程使用;rindex是读指针,被播放线程使用。使用环形缓冲区的好处是,缓冲区内的元素被移除后,其它元素不需要移动位置,适用于事先知道缓冲区最大容量的场景。
(3)初始化PacketQueue:PacketQueue是解码前的Packet队列,用于保存解封装后的数据。初始化PacketQueue时,会对VideoState中的videoq(视频包队列)、audio(音频包队列)、subtitleq(字幕包队列)依次调用packet_queue_init函数进行初始化。不同于FrameQueue,PacketQueue采用链表的方式实现队列。由于解码前的包大小不可控,无法明确缓冲区的最大容量,如果使用环形缓冲区,容易触发缓冲区扩容,需要移动缓冲区内的数据。因此,使用链表实现队列更加合适。
(4)初始化Clock:Clock是时钟,在音视频同步阶段,有三种同步方法:视频同步到音频,音频同步到视频,以及音频和视频同步到外部时钟。初始化Clock时,会对VideoState中的vidclk(视频时钟)、audclk(音频时钟)、extclk(外部时钟)依次调用init_clock函数进行初始化。
(5)限制音量范围:先限制音量范围在0~之间,然后再根据SDL的音量范围作进一步限制。
(6)设置音视频同步方式:ffplay默认采用AV_SYNC_AUDIO_MASTER,即视频同步到音频。
(7)创建读线程:调用SDL_CreateThread创建读线程,同时设置了线程创建成功的回调read_thread函数以及接收参数is(stream_open函数最开始创建的VideoState指针类型的局部变量)。如果线程创建失败,则调用stream_close做销毁逻辑。
(8)返回值:将局部变量is作为函数返回值返回,用于处理下面的各种SDL事件。
2.4SDL事件处理
event_loop函数内部是一个for循环,使用SDL监听用户的键盘按键事件、鼠标点击事件、窗口事件、退出事件等。
三、read_thread函数
read_thread函数的作用是从磁盘或者网络中获取流,包括音频流、视频流和字幕流,然后根据可用性创建对应流的解码线程。因此read_thread所在的线程实际上起到了解协议/解封装的作用。核心处理流程可以分为以下步骤:
3.1创建AVFormatContext
AVFormatContext是封装上下文,描述了媒体文件或媒体流的构成和基本信息。avformat_alloc_context函数用于分配内存创建AVFormatContext对象ic。
拿到AVFormatContext对象后,在调用avformat_open_input函数打开文件前,需要设置中断回调函数,用于检查是否应该中断IO操作。
ic-interrupt_callback.callback=decode_interrupt_cb;ic-interrupt_callback.opaque=is;
decode_interrupt_cb内部返回了一个VideoState的abort_request变量,该变量在调用stream_close函数关闭流时会被置为1。
3.2打开输入文件
在准备好前面的一些赋值操作后,就可以开始根据filename打开文件了。avformat_open_input函数用于打开一个文件,并对文件进行解析。如果文件是一个网络链接,则发起网络请求,在网络数据返回后解析音频流、视频流相关的数据。
3.3搜索流信息
搜索流信息使用avformat_find_stream_info函数,该从媒体文件中读取若干个包,然后从其中搜索流相关的信息,最后将搜索到的流信息放到ic-streams指针数组中,数组的大小为ic-nb_streams。
由于在实际播放过程中,用户可以指定是否禁用音频流、视频流、字幕流。因此在解码要处理的流之前,会判断对应的流是否处于不可用状态,如果是可用状态则调用av_find_best_stream函数查找对应流的索引,并保存在st_index数组中。
3.4设置窗口大小
如果找到了视频流的索引,则需要渲染视频画面。由于窗体的大小一般使用默认值*,这个值和视频帧真正的大小可能是不相等的。为了正确显示承载视频画面的窗体,需要计算视频帧的宽高比。调用av_guess_sample_aspect_ration函数猜测帧样本的宽高比,调用set_default_window_size函数重新设置显示窗口的大小和宽高比。
3.5创建解码线程
根据st_index判断音频流、视频流、字幕流的索引是否找到,如果找到了就依次调用stream_
转载请注明:http://www.abuoumao.com/hykz/1969.html