当我们用手机或者电脑打开一个电影视频或者一首音频歌曲的时候,不论是在线流量还是离线本地播放,通常设备上的音视频播放器都可以将音视频文件中的画面和声音给到我们的视觉和听觉器官,这是我们习以为常的东西。但不知你是否有考虑过,播放器底层究竟是如何处理音视频文件的呢?
如果你对音视频有一些基础和了解,应该知道通常播放一段音视频的基本流程是:解协议 → 解封装 → 解码 → 视音频同步这几大步骤。这里的解协议通常对应的是网络流媒体传输音视频,比如RTMP协议,RTSP协议,HTTP协议等。解完协议之后我们得到的是采用一个音视频(字幕)组织在一起的封装音视频文件,例如MP4,MKV,RMVB,TS,FLV,AVI等等。此时我们通过解封装将音频和视频数据分离出来,音频的归音频,视频的归视频。
然后我们分别将视频,音频的压缩编码数据,解码成为非压缩的视频/音频原始数据,如H264视频和PCM音频。音频的压缩编码标准有AAC,MP3,AC-3等等,视频的压缩编码标准有H.264,MPEG2,VC-1等等。
在拿到解码后的音视频数据之后,我们将其各自送到可以识别和处理的硬件设备上进行播放,比如视频播放的显卡,音频播放的声卡。但是在将解码后的音频和视频数据送去播放的时候,有一个同步的关键问题。比如电影播放一定要保证画面中声音和画面动作的匹配和一致,这样才能有比较理想的观看体验。
以上内容可以说是目前市面上所有播放器的基本流程和必有流程。而且,目前多数播放器底层实现均是基于开源软件ffmpeg自带的ffplay播放器模型。下面就谈谈ffplay是如何工作的。
一、视频播放
需要说明的是,从FFmpeg3.x版本开始,视频解码接口avcodec_decode_video2就被废弃了,解码取而代之的是avcodec_send_packet和avcodec_receive_frame。
av_read_frame得到压缩的数据包AVPacket,一般有三种压缩的数据包(视频、音频和字幕),都用AVPacket表示。然后调用avcodec_send_packet 和 avcodec_receive_frame对AVPacket进行解码得到AVFrame。
在 read_thread 函数中,通过 av_read_frame函数读取数据包AVPacket,然后调用packet_queue_put将AVPacket添加到PacketQueue中。
在 video_thread 函数中,通过get_video_frame函数读取数据帧AVFrame, 然后调用queue_picture将AVFrame添加到FrameQueue中。
那么两个队列是怎么联系起来的呢?通过分析read_thread函数可以知晓:
首先,创建解复用和解码所需要的数据结构;
然后分别通过 stream_component_open 函数打开三种数据流;
最后,通过av_read_frame将解复用后的数据包分别添加到对应的PacketQueue中。
stream_component_open函数主要负责解码工作,ffplay中为解码工作专门设置了一个数据结构Decoder,Decoder结构中有一个成员queue,这个queue就是输入的PacketQueue。
通过decoder_init函数来指定PacketQueue,这个工作就是在stream_component_open中执行的。
指定PacketQueue之后通过get_video_frame函数从PacketQueue中解码出AVFrame结构。
最后通过queue_picture函数将解码得到的帧添加到FrameQueue。
video_thread 和 audio_thread的作用把packet缓冲区里的packet解码成frame,装入frame缓冲区FrameQueue。
video_refresh内部工作过程:
1.先获取最早的frame,计算和下一帧的时间(vp_duration)
2.处理音视频同步的。根据同步的方式,获取修正过后的下一帧时间。
3.如果时间没到直接跳过退出video_refresh,进入av_usleep暂停下。代码里有点坑的是,这里写的是goto display;,而display代码块需要is->force_refresh这个为true才真的起作用,所以实际上是结束了,看起来有些误导。
4.确定要显示下一帧之后,才调用frame_queue_next把下一帧推到队列关键位,即索引rindex指定的位置。
5.video_display2最终到了SDL_Vout的display_overlay函数。和前面一样,到了显示层,在这做了解耦处理,SDL_Vout对象是ffp->vout,也是在IJKFFMoviePlayerControllerinit里构建的。
二、音频播放
音频和视频的逻辑不一样,视频是搭建好播放层(OpenGLES的view等)后,把一帧帧的数据 主动 推过去,而音频是开启了音频队列后,音频队列会过来跟你要数据,解码层这边是 被动 的把数据装载进去。
1.启动定时器Timer,计时器40ms刷新一次,利用SDL事件机制,触发从图像帧队列中读取数据,进行渲染显示;
2.stream_componet_open函数中,av_read_frame()读取到AVPacket,随后放入到音频、视频或字幕Packet队列中;
3.video_thread,从视频packet队列中获取AVPacket并进行解码,得到AVFrame图像帧,放到VideoPicture队列中。
4.audio_thread线程,同video_thread,对音频Packet进行解码;
5.subtitle_thread线程,同video_thread,对字幕Packet进行解码。
三、代码解析
3.1 main函数
ffplay的main函数主要步骤有以下几个函数:
SDL_Init
SDL_CreateWindow
SDL_CreateRender
stream_open
event_loop
这里前面3个步骤几乎是SDL的标配,初始化SDL后创建了一个窗口,然后创建Render用于渲染窗口和其他内容。
第4步接着调用stream_open,创建read_thread线程,read_thread会打开文件,解析封装,获取AVStream信息,启动解码器(创建解码线程),并开始读取文件。
第5步,event_loop主要是一个主循环,执行:refresh_loop_wait_event处理SDL事件队列中的事件。比如关闭窗口可以触发do_exit销毁播放现场。
avdevice_register_all();
avformat_network_init();
parse_options
prepare_app_arguments
parse_option
1.SDL_Init
SDL_EventState(SDL_SYSWMEVENT, SDL_IGNORE);
SDL_EventState(SDL_USEREVENT, SDL_IGNORE);
av_init_packet
2.SDL_CreateWindow
SDL_SetHint
3.SDL_CreateRenderer
4.stream_open()//主要函数内部会创建read_thread线程
frame_queue_init()
packet_queue_init()
init_clock()
SDL_CreateThread(read_thread, "read_thread", is)//this thread gets the stream from the disk or the network
avformat_open_input//根据输入的文件,创建AVFormatContext
avformat_find_stream_info
stream_component_open()//启动线程,执行video_thread函数,从视频队列获取视频数据,然后解码和显示。
avcodec_find_decoder()
avcodec_open2()
decoder_init()//decoder_init函数来指定PacketQueue
decoder_start(&is->viddec, video_thread, is)====SDL_CreateThread
get_video_frame()//从PacketQueue中解码出AVFrame结构
decoder_decode_frame()
for(;;)
avcodec_receive_frame():流连续的情况下,不断获取解码后的frame
packet_queue_get():阻塞调用
将packet送入解码器。
queue_picture()//将解码得到的帧添加到FrameQueue
stream_component_open()//启动SDL的线程,执行sdl_audio_callback,从音频队列获取音频数据,编解码和输出。
audio_open()
sdl_audio_callback()
audio_decode_frame();//处理sampq到audio_buf的过程,最多只是执行了重采样
swr_convert()
avcodec_find_decoder()
avcodec_open2()
decoder_init()//decoder_init函数来指定PacketQueue
decoder_start(&is->auddec, audio_thread, is)//音频解码线程
decoder_decode_frame()
5.event_loop()
refresh_loop_wait_event()//视频显示控制函数
video_refresh()//音视频同步
video_display()
video_image_display2()
SDL_VoutDisplayYUVOverlay()
video_refresh的主体流程分为3个步骤:
1.计算上一帧应显示的时长,判断是否继续显示上一帧
2.估算当前帧应显示的时长,判断是否要丢帧
3.调用video_display进行显示
video_display会调用frame_queue_peek_last获取上次显示的frame,并显示。
所以在video_refresh中如果流程直接走到video_display就会显示lastvp,如果先调用frame_queue_next再调用video_display,那么就会显示vp.
3.2视频解码
video_thread
1.调用get_video_frame解码一帧图像
2.计算时长和pts
3.调用queue_picture放入FrameQueue
static int video_thread(void *arg)
{
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
for (;;)
{
ret = get_video_frame(is, frame); //解码获取一帧视频画面
if (ret < 0)//解码结束
goto the_end;
if (!ret)//没有解码得到画面
continue;
duration = (frame_rate.num && frame_rate.den ? av_q2d((AVRational){
frame_rate.den, frame_rate.num})