指导5:同步视频

如何同步视频ide

 

前面整个的一段时间,咱们有了一个几乎无用的电影播放器。固然,它能播放视频,也能播放音频,可是它还不能被称为一部电影。那么咱们还要作什么呢?函数

 

PTS和DTSui

 

幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,若是咱们只是简单的经过数帧和乘以帧率的方式来同步视频,那么就颇有可能会失去同步。因而做为一种补充,在流中的包有种叫作DTS(解码时间戳)和PTS(显示时间戳)的机制。为了这两个参数,你须要了解电影存放的方式。像MPEG等格式,使用被叫作B帧(B表示双向bidrectional)的方式。另外两种帧被叫作I帧和P帧(I表示关键帧,P表示预测帧)。I帧包含了某个特定的完整图像。P帧依赖于前面的I帧和P帧而且使用比较或者差分的方式来编码。B帧与P帧有点相似,可是它是依赖于前面和后面的帧的信息的。这也就解释了为何咱们可能在调用avcodec_decode_video之后会得不到一帧图像。编码

因此对于一个电影,帧是这样来显示的:I B B P。如今咱们须要在显示B帧以前知道P帧中的信息。所以,帧可能会按照这样的方式来存储:IPBB。这就是为何咱们会有一个解码时间戳和一个显示时间戳的缘由。解码时间戳告诉咱们何时须要解码,显示时间戳告诉咱们何时须要显示。因此,在这种状况下,咱们的流能够是这样的:spa

   PTS: 1 4 2 3线程

   DTS: 1 2 3 4指针

Stream: I P B Bcode

一般PTS和DTS只有在流中有B帧的时候会不一样。component

 

当咱们调用av_read_frame()获得一个包的时候,PTS和DTS的信息也会保存在包中。可是咱们真正想要的PTS是咱们刚刚解码出来的原始帧的PTS,这样咱们才能知道何时来显示它。然而,咱们从avcodec_decode_video()函数中获得的帧只是一个AVFrame,其中并无包含有用的PTS值(注意:AVFrame并无包含时间戳信息,但当咱们等到帧的时候并非咱们想要的样子)。然而,ffmpeg从新排序包以便于被avcodec_decode_video()函数处理的包的DTS能够老是与其返回的PTS相同。可是,另外的一个警告是:咱们也并非总能获得这个信息。orm

不用担忧,由于有另一种办法能够找到帖的PTS,咱们可让程序本身来从新排序包。咱们保存一帧的第一个包的PTS:这将做为整个这一帧的PTS。咱们能够经过函数avcodec_decode_video()来计算出哪一个包是一帧的第一个包。怎样实现呢?任什么时候候当一个包开始一帧的时候,avcodec_decode_video()将调用一个函数来为一帧申请一个缓冲。固然,ffmpeg容许咱们从新定义那个分配内存的函数。因此咱们制做了一个新的函数来保存一个包的时间戳。

固然,尽管那样,咱们可能仍是得不到一个正确的时间戳。咱们将在后面处理这个问题。

 

同步

 

如今,知道了何时来显示一个视频帧真好,可是咱们怎样来实际操做呢?这里有个主意:当咱们显示了一帧之后,咱们计算出下一帧显示的时间。而后咱们简单的设置一个新的定时器来。你可能会想,咱们检查下一帧的PTS值而不是系统时钟来看超时是否会到。这种方式能够工做,可是有两种状况要处理。

首先,要知道下一个PTS是什么。如今咱们能添加视频速率到咱们的PTS中--太对了!然而,有些电影须要帧重复。这意味着咱们重复播放当前的帧。这将致使程序显示下一帧太快了。因此咱们须要计算它们。

第二,正如程序如今这样,视频和音频播放很欢快,一点也不受同步的影响。若是一切都工做得很好的话,咱们没必要担忧。可是,你的电脑并非最好的,不少视频文件也不是无缺的。因此,咱们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从如今开始,咱们将同步视频到音频。

 

写代码:得到帧的时间戳

 

如今让咱们到代码中来作这些事情。咱们将须要为咱们的大结构体添加一些成员,可是咱们会根据须要来作。首先,让咱们看一下视频线程。记住,在这里咱们获得了解码线程输出到队列中的包。这里咱们须要的是从avcodec_decode_video函数中获得帧的时间戳。咱们讨论的第一种方式是从上次处理的包中获得DTS,这是很容易的:

  double pts;

 

  for(;;) {

    if(packet_queue_get(&is->videoq, packet, 1) < 0) {

      // means we quit getting packets

      break;

    }

    pts = 0;

    // Decode video frame

    len1 = avcodec_decode_video(is->video_st->codec,

                                pFrame, &frameFinished,

              packet->data, packet->size);

    if(packet->dts != AV_NOPTS_VALUE) {

      pts = packet->dts;

    } else {

      pts = 0;

    }

    pts *= av_q2d(is->video_st->time_base);

若是咱们得不到PTS就把它设置为0。

好,那是很容易的。可是咱们所说的若是包的DTS不能帮到咱们,咱们须要使用这一帧的第一个包的PTS。咱们经过让ffmpeg使用咱们本身的申请帧程序来实现。下面的是函数的格式:

int get_buffer(struct AVCodecContext *c, AVFrame *pic);

void release_buffer(struct AVCodecContext *c, AVFrame *pic);

申请函数没有告诉咱们关于包的任何事情,因此咱们要本身每次在获得一个包的时候把PTS保存到一个全局变量中去。咱们本身以读到它。而后,咱们把值保存到AVFrame结构体难理解的变量中去。因此一开始,这就是咱们的函数:

uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;

 

 

int our_get_buffer(struct AVCodecContext *c, AVFrame *pic) {

  int ret = avcodec_default_get_buffer(c, pic);

  uint64_t *pts = av_malloc(sizeof(uint64_t));

  *pts = global_video_pkt_pts;

  pic->opaque = pts;

  return ret;

}

void our_release_buffer(struct AVCodecContext *c, AVFrame *pic) {

  if(pic) av_freep(&pic->opaque);

  avcodec_default_release_buffer(c, pic);

}

函数avcodec_default_get_buffer和avcodec_default_release_buffer是ffmpeg中默认的申请缓冲的函数。函数av_freep是一个内存管理函数,它不但把内存释放并且把指针设置为NULL。

如今到了咱们流打开的函数(stream_component_open),咱们添加这几行来告诉ffmpeg如何去作:

    codecCtx->get_buffer = our_get_buffer;

    codecCtx->release_buffer = our_release_buffer;

如今咱们必需添加代码来保存PTS到全局变量中,而后在须要的时候来使用它。咱们的代码如今看起来应该是这样子:

  for(;;) {

    if(packet_queue_get(&is->videoq, packet, 1) < 0) {

      // means we quit getting packets

      break;

    }

    pts = 0;

 

    // Save global pts to be stored in pFrame in first call

    global_video_pkt_pts = packet->pts;

    // Decode video frame

    len1 = avcodec_decode_video(is->video_st->codec, pFrame, &frameFinished,

              packet->data, packet->size);

    if(packet->dts == AV_NOPTS_VALUE

       && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) {

      pts = *(uint64_t *)pFrame->opaque;

    } else if(packet->dts != AV_NOPTS_VALUE) {

      pts = packet->dts;

    } else {

      pts = 0;

    }

    pts *= av_q2d(is->video_st->time_base);

技术提示:你可能已经注意到咱们使用int64来表示PTS。这是由于PTS是以整型来保存的。这个值是一个时间戳至关于时间的度量,用来以流的time_base为单位进行时间度量。例如,若是一个流是24帧每秒,值为42的PTS表示这一帧应该排在第42个帧的位置若是咱们每秒有24帧(这里并不彻底正确)。

咱们能够经过除以帧率来把这个值转化为秒。流中的time_base值表示1/framerate(对于固定帧率来讲),因此获得了以秒为单位的PTS,咱们须要乘以time_base。

 

写代码:使用PTS来同步

 

如今咱们获得了PTS。咱们要注意前面讨论到的两个同步问题。咱们将定义一个函数叫作synchronize_video,它能够更新同步的PTS。这个函数也能最终处理咱们得不到PTS的状况。同时咱们要知道下一帧的时间以便于正确设置刷新速率。咱们可使用内部的反映当前视频已经播放时间的时钟video_clock来完成这个功能。咱们把这些值添加到大结构体中。

typedef struct VideoState {

  double          video_clock; ///

下面的是函数synchronize_video,它能够很好的自我注释:

double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) {

 

  double frame_delay;

 

  if(pts != 0) {

 

    is->video_clock = pts;

  } else {

 

    pts = is->video_clock;

  }

 

  frame_delay = av_q2d(is->video_st->codec->time_base);

 

  frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);

  is->video_clock += frame_delay;

  return pts;

}

你也会注意到咱们也计算了重复的帧。

 

如今让咱们获得正确的PTS而且使用queue_picture来队列化帧,添加一个新的时间戳参数pts:

    // Did we get a video frame?

    if(frameFinished) {

      pts = synchronize_video(is, pFrame, pts);

      if(queue_picture(is, pFrame, pts) < 0) {

    break;

      }

    }

对于queue_picture来讲惟一改变的事情就是咱们把时间戳值pts保存到VideoPicture结构体中,咱们咱们必需添加一个时间戳变量到结构体中而且添加一行代码:

typedef struct VideoPicture {

  ...

  double pts;

}

int queue_picture(VideoState *is, AVFrame *pFrame, double pts) {

  ... stuff ...

  if(vp->bmp) {

    ... convert picture ...

    vp->pts = pts;

    ... alert queue ...

  }

如今咱们的图像队列中的全部图像都有了正确的时间戳值,因此让咱们看一下视频刷新函数。你会记得上次咱们用80ms的刷新时间来欺骗它。那么,如今咱们将会算出实际的值。

咱们的策略是经过简单计算前一帧和如今这一帧的时间戳来预测出下一个时间戳的时间。同时,咱们须要同步视频到音频。咱们将设置一个音频时间audio clock;一个内部值记录了咱们正在播放的音频的位置。就像从任意的mp3播放器中读出来的数字同样。既然咱们把视频同步到音频,视频线程使用这个值来算出是否太快仍是太慢。

咱们将在后面来实现这些代码;如今咱们假设咱们已经有一个能够给咱们音频时间的函数get_audio_clock。一旦咱们有了这个值,咱们在音频和视频失去同步的时候应该作些什么呢?简单而有点笨的办法是试着用跳过正确帧或者其它的方式来解决。做为一种替代的手段,咱们会调整下次刷新的值;若是时间戳太落后于音频时间,咱们加倍计算延迟。若是时间戳太领先于音频时间,咱们将尽量快的刷新。既然咱们有了调整过的时间和延迟,咱们将把它和咱们经过frame_timer计算出来的时间进行比较。这个帧时间frame_timer将会统计出电影播放中全部的延时。换句话说,这个frame_timer就是指咱们何时来显示下一帧。咱们简单的添加新的帧定时器延时,把它和电脑的系统时间进行比较,而后使用那个值来调度下一次刷新。这可能有点难以理解,因此请认真研究代码:

void video_refresh_timer(void *userdata) {

 

  VideoState *is = (VideoState *)userdata;

  VideoPicture *vp;

  double actual_delay, delay, sync_threshold, ref_clock, diff;

 

  if(is->video_st) {

    if(is->pictq_size == 0) {

      schedule_refresh(is, 1);

    } else {

      vp = &is->pictq[is->pictq_rindex];

 

      delay = vp->pts - is->frame_last_pts;

      if(delay <= 0 || delay >= 1.0) {

 

    delay = is->frame_last_delay;

      }

 

      is->frame_last_delay = delay;

      is->frame_last_pts = vp->pts;

 

 

      ref_clock = get_audio_clock(is);

      diff = vp->pts - ref_clock;

 

 

      sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;

      if(fabs(diff) < AV_NOSYNC_THRESHOLD) {

    if(diff <= -sync_threshold) {

      delay = 0;

    } else if(diff >= sync_threshold) {

      delay = 2 * delay;

    }

      }

      is->frame_timer += delay;

 

      actual_delay = is->frame_timer - (av_gettime() / 1000000.0);

      if(actual_delay < 0.010) {

 

    actual_delay = 0.010;

      }

      schedule_refresh(is, (int)(actual_delay * 1000 + 0.5));

 

      video_display(is);

 

 

      if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {

    is->pictq_rindex = 0;

      }

      SDL_LockMutex(is->pictq_mutex);

      is->pictq_size--;

      SDL_CondSignal(is->pictq_cond);

      SDL_UnlockMutex(is->pictq_mutex);

    }

  } else {

    schedule_refresh(is, 100);

  }

}

咱们在这里作了不少检查:首先,咱们保证如今的时间戳和上一个时间戳之间的处以delay是有意义的。若是不是的话,咱们就猜想着用上次的延迟。接着,咱们有一个同步阈值,由于在同步的时候事情并不老是那么完美的。在ffplay中使用0.01做为它的值。咱们也保证阈值不会比时间戳之间的间隔短。最后,咱们把最小的刷新值设置为10毫秒。

(这句不知道应该放在哪里)事实上这里咱们应该跳过这一帧,可是咱们不想为此而烦恼。

咱们给大结构体添加了不少的变量,因此不要忘记检查一下代码。同时也不要忘记在函数streame_component_open中初始化帧时间frame_timer和前面的帧延迟frame delay:

    is->frame_timer = (double)av_gettime() / 1000000.0;

    is->frame_last_delay = 40e-3;

 

同步:声音时钟

 

如今让咱们看一下怎样来获得声音时钟。咱们能够在声音解码函数audio_decode_frame中更新时钟时间。如今,请记住咱们并非每次调用这个函数的时候都在处理新的包,因此有咱们要在两个地方更新时钟。第一个地方是咱们获得新的包的时候:咱们简单的设置声音时钟为这个包的时间戳。而后,若是一个包里有许多帧,咱们经过样本数和采样率来计算,因此当咱们获得包的时候:

 

    if(pkt->pts != AV_NOPTS_VALUE) {

      is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts;

    }

而后当咱们处理这个包的时候:

 

      pts = is->audio_clock;

      *pts_ptr = pts;

      n = 2 * is->audio_st->codec->channels;

      is->audio_clock += (double)data_size /

    (double)(n * is->audio_st->codec->sample_rate);

一点细节:临时函数被改为包含pts_ptr,因此要保证你已经改了那些。这时的pts_ptr是一个用来通知audio_callback函数当前声音包的时间戳的指针。这将在下次用来同步声音和视频。

如今咱们能够最后来实现咱们的get_audio_clock函数。它并不像获得is->audio_clock值那样简单。注意咱们会在每次处理它的时候设置声音时间戳,可是若是你看了audio_callback函数,它花费了时间来把数据从声音包中移到咱们的输出缓冲区中。这意味着咱们声音时钟中记录的时间比实际的要早太多。因此咱们必需要检查一下咱们还有多少没有写入。下面是完整的代码:

double get_audio_clock(VideoState *is) {

  double pts;

  int hw_buf_size, bytes_per_sec, n;

 

  pts = is->audio_clock;

  hw_buf_size = is->audio_buf_size - is->audio_buf_index;

  bytes_per_sec = 0;

  n = is->audio_st->codec->channels * 2;

  if(is->audio_st) {

    bytes_per_sec = is->audio_st->codec->sample_rate * n;

  }

  if(bytes_per_sec) {

    pts -= (double)hw_buf_size / bytes_per_sec;

  }

  return pts;

}

你应该知道为何这个函数能够正常工做了;)

 

这就是了!让咱们编译它:

gcc -o tutorial05 tutorial05.c -lavutil -lavformat -lavcodec -lz -lm`sdl-config --cflags --libs`

最后,你可使用咱们本身的电影播放器来看电影了。下次咱们将看一下声音同步,而后接下来的指导咱们会讨论查询。

相关文章
相关标签/搜索