ffplay源码分析
好文章,来自【福优学苑@音视频+流媒体】
main()函数解析
FFplay的主要流程
调用了如下函数
av_register_all():注册所有编码器和解码器。
show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。
parse_options():解析输入的命令。
SDL_Init():SDL初始化。
stream_open ():打开输入媒体。
event_loop():处理各种消息,不停地循环下去。
FFplay的代码总体结构
parse_options()
parse_options() 解析全部输入选项。
即将输入命令“ffplay -f h264 test.264”中的“-f”这样的命令解析出来。
需要注意的是,FFplay(ffplay.c)的 parse_options()和FFmpeg(ffmpeg.c)中的parse_options()实际上是一样的。
Ffmepg -i aa.mp4 -acodec aac -vcodec libx264 -ss 0 -t 20 -f flv
SDL_Init()
SDL_Init()用于初始化SDL。
FFplay中视频的显示和声音的播放都用到了SDL。
stream_open()
stream_open()的作用是打开输入的媒体。
这个函数还是比较复杂的,包含了FFplay中各种线程的创建。
stream_open()调用了如下函数:
packet_queue_init():初始化各个PacketQueue(视频/音频/字幕)
read_thread():读取媒体信息线程。
read_thread()
read_thread()调用了如下函数:
Ø avformat_open_input():打开媒体。
Ø avformat_find_stream_info():获得媒体信息。
Ø av_dump_format():输出媒体信息到控制台。
Ø stream_component_open():分别打开视频/音频/字幕解码线程。
Ø refresh_thread():视频刷新线程。
Ø av_read_frame():获取一帧压缩编码数据(即一个AVPacket)。
Ø packet_queue_put():根据压缩编码数据类型的不同(视频/音频/字幕),放到不同的PacketQueue中。
refresh_thread()
refresh_thread()调用了如下函数:
Ø SDL_PushEvent(FF_REFRESH_EVENT):发送FF_REFRESH_EVENT的SDL_Event
Ø av_usleep():每两次发送之间,间隔一段时间。
stream_component_open()
stream_component_open()用于打开视频/音频/字幕解码的线程。
其函数调用关系如下所示。
stream_component_open()调用了如下函数:
Ø avcodec_find_decoder():获得解码器。
Ø avcodec_open2():打开解码器。
Ø audio_open():打开音频解码。
Ø SDL_PauseAudio(0):SDL中播放音频的函数。
Ø video_thread():创建视频解码线程。
Ø subtitle_thread():创建字幕解码线程。
Ø packet_queue_start():初始化PacketQueue。
decoder_start()
开启解码器线程,非常重要的一个函数
audio_open()
audio_open()调用了如下函数
SDL_OpenAudio():SDL 中打开音频设备的函数。
注意它是根据SDL_AudioSpec参数打开音频设备。
SDL_AudioSpec中的callback字段指定了音频播放的 回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。
下面来看一下SDL_AudioSpec中指定的回调函数sdl_audio_callback()。
sdl_audio_callback()调用了如下函数
Ø audio_decode_frame():解码音频数据。
Ø update_sample_display():当不显示视频图像,而是显示音频波形的时候,调用此函数。
audio_decode_frame()调用了如下函数
Ø packet_queue_get():获取音频压缩编码数据(一个AVPacket)。
Ø avcodec_decode_audio4():解码音频压缩编码数据(得到一个AVFrame)。
Ø swr_init():初始化libswresample中的SwrContext。libswresample用于音频采样采样数据(PCM)的转换。
Ø swr_convert():转换音频采样率到适合系统播放的格式。
Ø swr_free():释放SwrContext。
video_thread()
video_thread()调用了如下函数
Ø avcodec_alloc_frame():初始化一个AVFrame。
Ø get_video_frame():获取一个存储解码后数据的AVFrame。
Ø queue_picture():
get_video_frame()调用了如下函数
Ø packet_queue_get():获取视频压缩编码数据(一个AVPacket)。
Ø avcodec_decode_video2():解码视频压缩编码数据(得到一个AVFrame)。
queue_picture()调用了如下函数
Ø SDL_LockYUVOverlay():锁定一个SDL_Overlay。
Ø sws_getCachedContext(): 初始化libswscale中的SwsContext。Libswscale用于图像的Raw格式数据(YUV,RGB)之间的转换。注意 sws_getCachedContext()和sws_getContext()功能是一致的。
Ø sws_scale():转换图像数据到适合系统播放的格式。
Ø SDL_UnlockYUVOverlay():解锁一个SDL_Overlay。
subtitle_thread()调用了如下函数
Ø packet_queue_get():获取字幕压缩编码数据(一个AVPacket)。
Ø avcodec_decode_subtitle2():解码字幕压缩编码数据。
event_loop()
FFplay再打开媒体之后,便会进入event_loop()函数,永远不停的循环下去。
该函数用于接收并处理各种各样的消息。
有点像Windows的消息循环机制。
PS:该循环确实是无止尽的,其形式为如下
SDL_Event event; for (;;) { SDL_WaitEvent(&event); switch (event.type) { case SDLK_ESCAPE: case SDLK_q: do_exit(cur_stream); break; case SDLK_f: … … } }
event_loop()函数调用关系:
根据event_loop()中SDL_WaitEvent()接收到的SDL_Event类型的不同,会调用不同的函数进行处理(从编程的角度来说就是一个switch()语法)。
仅仅列举了几个例子:
SDLK_ESCAPE(按下“ESC”键):do_exit()。退出程序。
SDLK_f(按下“f”键):toggle_full_screen()。切换全屏显示。
SDLK_SPACE(按下“空格”键):toggle_pause()。切换“暂停”。
SDLK_DOWN(按下鼠标键):stream_seek()。跳转到指定的时间点播放。
SDL_VIDEORESIZE(窗口大小发生变化):SDL_SetVideoMode()。重新设置宽高。
FF_REFRESH_EVENT(视频刷新事件(自定义事件)):video_refresh()。刷新视频。
do_exit()
下面分析一下do_exit()函数。该函数用于退出程序。
函数的调用关系如下所示。
Ø do_exit()函数调用了以下函数
Ø stream_close():关闭打开的媒体。
Ø SDL_Quit():关闭SDL。
stream_close()
stream_close()函数调用了以下函数
Ø packet_queue_destroy():释放PacketQueue。
Ø SDL_FreeYUVOverlay():释放SDL_Overlay。
Ø sws_freeContext():释放SwsContext。
video_refresh()
下面重点分析video_refresh()函数。
该函数用于将图像显示到显示器上。
函数的调用关系如下图所示。
Ø video_refresh()函数调用了以下函数
Ø video_display():显示像素数据到屏幕上。
Ø show_status:这算不上是一个函数,但是是一个独立的功能模块,因此列了出来。该部分打印输出播放的状态至屏幕上。如下图所示。
video_display()函数调用了以下函数
Ø video_open():初始化的时候调用,打开播放窗口。
Ø video_audio_display():显示音频波形图(或者频谱图)的时候调用。里面包含了不少画图操作。
Ø video_image_display():显示视频画面的时候调用。
video_open()函数调用了以下函数
Ø SDL_SetVideoMode():设置SDL_Surface(即SDL最基础的黑色的框)的大小等信息。
Ø SDL_WM_SetCaption():设置SDL_Surface对应窗口的标题文字。
音视频同步
视频帧的播放时间其实依赖pts字段的,音频和视频都有自己单独的pts。
但pts究竟是如何生成的呢,假如音视频不同步时,pts是否需要动态调整,以保证音视频的同步?
下面先来分析,如何控制视频帧的显示时间的:
static void video_refresh(void *opaque){ //根据索引获取当前需要显示的VideoPicture VideoPicture *vp = &is->pictq[is->pictq_rindex]; if (is->paused) goto display; //只有在paused的情况下,才播放图像 // 将当前帧的pts减去上一帧的pts,得到中间时间差 last_duration = vp->pts - is->frame_last_pts; //检查差值是否在合理范围内,因为两个连续帧pts的时间差,不应该太大或太小 if (last_duration > 0 && last_duration < 10.0) { /* if duration of the last frame was sane, update last_duration in video state */ is->frame_last_duration = last_duration; } //既然要音视频同步,肯定要以视频或音频为参考标准,然后控制延时来保证音视频的同步, //这个函数就做这个事情了,下面会有分析,具体是如何做到的。 delay = compute_target_delay(is->frame_last_duration, is); //获取当前时间 time= av_gettime()/1000000.0; //假如当前时间小于frame_timer + delay,也就是这帧改显示的时间超前,还没到,就直接返回 if (time < is->frame_timer + delay) return; //根据音频时钟,只要需要延时,即delay大于0,就需要更新累加到frame_timer当中。 if (delay > 0) //更新frame_timer,frame_time是delay的累加值 is->frame_timer += delay * FFMAX(1, floor((time-is->frame_timer) / delay)); SDL_LockMutex(is->pictq_mutex); //更新is当中当前帧的pts,比如video_current_pts、video_current_pos 等变量 update_video_pts(is, vp->pts, vp->pos); SDL_UnlockMutex(is->pictq_mutex); display: /* display picture */ if (!display_disable) video_display(is); }
函数compute_target_delay根据音频的时钟信号,重新计算了延时,从而达到了根据音频来调整视频的显示时间,从而实现音视频同步的效果。
static double compute_target_delay(double delay, VideoState *is) { double sync_threshold, diff; //因为音频是采样数据,有固定的采用周期并且依赖于主系统时钟,要调整音频的延时播放较难控制。所以实际场合中视频同步音频相比音频同步视频实现起来更容易。 if (((is->av_sync_type == AV_SYNC_AUDIO_MASTER && is->audio_st) || is->av_sync_type == AV_SYNC_EXTERNAL_CLOCK)) { //获取当前视频帧播放的时间,与系统主时钟时间相减得到差值 diff = get_video_clock(is) - get_master_clock(is); sync_threshold = FFMAX(AV_SYNC_THRESHOLD, delay); //假如当前帧的播放时间,也就是pts,滞后于主时钟 if (fabs(diff) < AV_NOSYNC_THRESHOLD) { if (diff <= -sync_threshold) delay = 0; //假如当前帧的播放时间,也就是pts,超前于主时钟,那就需要加大延时 else if (diff >= sync_threshold) delay = 2 * delay; } } return delay; }
如何控制视频的播放和暂停?
static void stream_toggle_pause(VideoState *is) { if (is->paused) { //由于frame_timer记下来视频从开始播放到当前帧播放的时间,所以暂停后,必须要将暂停的时间( is->video_current_pts_drift - is->video_current_pts)一起累加起来,并加上drift时间。 is->frame_timer += av_gettime() / 1000000.0 + is->video_current_pts_drift - is->video_current_pts; if (is->read_pause_return != AVERROR(ENOSYS)) { //并更新video_current_pts is->video_current_pts = is->video_current_pts_drift + av_gettime() / 1000000.0; } //drift其实就是当前帧的pts和当前时间的时间差 is->video_current_pts_drift = is->video_current_pts - av_gettime() / 1000000.0; } //paused取反,paused标志位也会控制到图像帧的展示,按一次空格键实现暂停,再按一次就实现播放了。 is->paused = !is->paused; }
特别说明:paused标志位控制着视频是否播放,当需要继续播放的时候,一定要重新更新当前所需要播放帧的pts时间,因为这里面要加上已经暂停的时间。
逐帧播放是如何做的?
在视频解码线程中,不断通过stream_toggle_paused,控制对视频的暂停和显示,从而实现逐帧播放:
static void step_to_next_frame(VideoState *is) { //逐帧播放时,一定要先继续播放,然后再设置step变量,控制逐帧播放 if (is->paused) stream_toggle_pause(is);//会不断将paused进行取反 is->step = 1; }
其原理就是不断的播放,然后暂停,从而实现逐帧播放:
static int video_thread(void *arg) { if (is->step) stream_toggle_pause(is); …………………… if (is->paused) goto display;//显示视频 } }
好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***