main()函数解析
调用了如下函数
av_register_all():注册所有编码器和解码器。
show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。
parse_options():解析输入的命令。
SDL_Init():SDL初始化。
stream_open ():打开输入媒体。
event_loop():处理各种消息,不停地循环下去。
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。
FFplay中视频的显示和声音的播放都用到了SDL。
stream_open()的作用是打开输入的媒体。
这个函数还是比较复杂的,包含了FFplay中各种线程的创建。
stream_open()调用了如下函数:
packet_queue_init():初始化各个PacketQueue(视频/音频/字幕)
read_thread():读取媒体信息线程。
read_thread()
read_thread()调用了如下函数:
refresh_thread()调用了如下函数:
stream_component_open()用于打开视频/音频/字幕解码的线程。
其函数调用关系如下所示。
stream_component_open()调用了如下函数:
开启解码器线程,非常重要的一个函数
audio_open()调用了如下函数
SDL_OpenAudio():SDL 中打开音频设备的函数。
注意它是根据SDL_AudioSpec参数打开音频设备。
SDL_AudioSpec中的callback字段指定了音频播放的 回调函数sdl_audio_callback()。当音频设备需要更多数据的时候,会调用该回调函数。因此该函数是会被反复调用的。
下面来看一下SDL_AudioSpec中指定的回调函数sdl_audio_callback()。
video_thread()调用了如下函数
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()中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()函数。该函数用于退出程序。
函数的调用关系如下所示。
stream_close()函数调用了以下函数
下面重点分析video_refresh()函数。
该函数用于将图像显示到显示器上。
函数的调用关系如下所示。
video_refresh()函数调用了以下函数
video_display()函数调用了以下函数
video_open()函数调用了以下函数
视频帧的播放时间其实依赖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;//显示视频 } }