ffplay源码分析


***【在线视频教程】***

好文章,来自【福优学苑@音视频+流媒体】

main()函数解析

FFplay的主要流程

调用了如下函数


  • av_register_all():注册所有编码器和解码器。


  • show_banner():打印输出FFmpeg版本信息(编译时间,编译选项,类库信息等)。


  • parse_options():解析输入的命令。


  •  SDL_Init():SDL初始化。


  •  stream_open ():打开输入媒体。


  •  event_loop():处理各种消息,不停地循环下去。


 


 


FFplay的代码总体结构

 image.png


 


 image.png


 


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;//显示视频
  }
}





好文章,来自【福优学苑@音视频+流媒体】
***【在线视频教程】***