FFmpeg入门详解之52: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;//显示视频
  }
}

你可能感兴趣的:(音视频/流媒体,音视频,ffplay)