(转译)用FFmpeg和SDL写播放器--05同步视频
PTS: 1 4 2 3
DTS: 1 2 3 4
Stream: I P B B
|
double pts; pFrame = avcodec_alloc_frame(); 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 avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet); if(packet->dts == AV_NOPTS_VALUE && pFrame->opaque && *(uint64_t*)pFrame->opaque != AV_NOPTS_VALUE) { pts = *(uint64_t *)pFrame->opaque; } elseelse { 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; /* These are called whenever we allocate a frame * buffer. We use this to store the global_pts in * a frame at the time it is allocated. */ 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 如何去做:
double pts; pFrame = avcodec_alloc_frame(); 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 avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet); 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);
double synchronize_video(VideoState *is, AVFrame *src_frame, double pts) { double frame_delay; if(pts != 0) { /* if we have pts, set video clock to it */ is->video_clock = pts; } else { /* if we aren't given a pts, set it to the clock */ pts = is->video_clock; } /* update the video clock */ frame_delay = av_q2d(is->video_st->codec->time_base); /* if we are repeating a frame, adjust clock accordingly */ 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; } }
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; /* the pts from last time */ if(delay <= 0 || delay >= 1.0) { /* if incorrect delay, use previous one */ delay = is->frame_last_delay; } /* save for next time */ is->frame_last_delay = delay; is->frame_last_pts = vp->pts; /* update delay to sync to audio */ ref_clock = get_audio_clock(is); diff = vp->pts - ref_clock; /* Skip or repeat the frame. Take delay into account FFPlay still doesn't "know if this is the best guess." */ 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; /* computer the REAL delay */ actual_delay = is->frame_timer - (av_gettime() / 1000000.0); if(actual_delay < 0.010) { /* Really it should skip the picture instead */ actual_delay = 0.010; } schedule_refresh(is, (int)(actual_delay * 1000 + 0.5)); /* show the picture! */ video_display(is); /* update queue for next picture! */ 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 毫秒。事实上
这里我们应该跳过这一帧,但是我们不想为 此而烦恼。
is->frame_timer = (double)av_gettime() / 1000000.0; is->frame_last_delay = 40e-3;
/* if update, update the audio clock w/pts */ if(pkt->pts != AV_NOPTS_VALUE) { is->audio_clock = av_q2d(is->audio_st->time_base)*pkt->pts; }
/* Keep audio_clock up-to-date */ 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);
double get_audio_clock(VideoState *is) { double pts; int hw_buf_size, bytes_per_sec, n; pts = is->audio_clock; /* maintained in the audio thread */ 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`
作者:半山
出处:http://www.cnblogs.com/xdao/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。