热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Ffmpeg和SDL如何同步视频

ong>  PTS和DTS 幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有

ong> 



PTS和DTS



幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息在里面。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过数帧和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做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。这就是为什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的流可以是这样的:


PTS: 1 4 2 3


DTS: 1 2 3 4


Stream: I P B B


通常PTS和DTS只有在流中有B帧的时候会不同。


 


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


 


不用担心,因为有另外一种办法可以找到帧的PTS,我们可以让程序自己来重新排序包。我们保存一帧的第一个包的PTS:这将作为整个这一帧的 PTS。我们可以通过函数avcodec_decode_video()来计算出哪个包是一帧的第一个包。怎样实现呢?任何时候当一个包开始一帧的时候,avcodec_decode_video()将调用一个函数来为一帧申请一个缓冲。当然,ffmpeg允许我们重新定义那个分配内存的函数。所以我们制作了一个新的函数来保存一个包的时间戳。


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



 




同步



现在,知道了什么时候来显示一个视频帧真好,但是我们怎样来实际操作呢?这里有个主意:当我们显示了一帧以后,我们计算出下一帧显示的时间。然后我们简单的设置一个新的定时器来。你可能会想,我们检查下一帧的PTS值而不是系统时钟来看超时是否会到。这种方式可以工作,但是有两种情况要处理。


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


第二,正如程序现在这样,视频和音频播放很欢快,一点也不受同步的影响。如果一切都工作得很好的话,我们不必担心。但是,你的电脑并不是最好的,很多视频文件也不是完好的。所以,我们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从现在开始,我们将同步视频到音频。



 




写代码:获得帧的时间戳



现在让我们到代码中来做这些事情。我们将需要为我们的大结构体添加一些成员,但是我们会根据需要来做。首先,让我们看一下视频线程。记住,在这里我们得到了解码线程输出到


队列中的包。这里我们需要的是从avcodec_decode_video函数中得到帧的时间戳。我们讨论的第一种方式是从上次处理的包中得到DTS,这是很容易的:

 

 





[cpp]


view plain copy print ?







  1. double


     pts;  




  2. for


    (;;) {  




  3. if


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



  4. // means we quit getting packets

      




  5. break


    ;  


  6. }  


  7. pts = 0;  



  8. // Decode video frame

      


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


  10. pFrame, &frameFinished,  


  11. packet->data, packet->size);  




  12. if


    (packet->dts != AV_NOPTS_VALUE) {  


  13. pts = packet->dts;  





  14. else


     {  


  15. pts = 0;  


  16. }  


  17. 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结构体难理解的变量中去。所以一开始,这就是我们的函数:

 

 





[cpp]


view plain copy print ?





  1. uint64_t global_video_pkt_pts = AV_NOPTS_VALUE;  




  2. int


     our_get_buffer(


    struct


     AVCodecContext *c, AVFrame *pic) {  




  3. int


     ret = avcodec_default_get_buffer(c, pic);  


  4. uint64_t *pts = av_malloc(


    sizeof


    (uint64_t));  


  5. *pts = global_video_pkt_pts;  


  6. pic->opaque = pts;  




  7. return


     ret;  


  8. }  




  9. void


     our_release_buffer(


    struct


     AVCodecContext *c, AVFrame *pic) {  




  10. if


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


  11. avcodec_default_release_buffer(c, pic);  


  12. }  


 

 


函数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到全局变量中,然后在需要的时候来使用它。我们的代码现在看起来应该是这样子:

 

 





[cpp]


view plain copy print ?







  1. for


    (;;) {  




  2. if


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



  3. // means we quit getting packets

      




  4. break


    ;  


  5. }  


  6. pts = 0;  



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

      


  8. global_video_pkt_pts = packet->pts;  



  9. // Decode video frame

      


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


  11. packet->data, packet->size);  




  12. if


    (packet->dts == AV_NOPTS_VALUE  


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


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





  15. else

     

    if


    (packet->dts != AV_NOPTS_VALUE) {  


  16. pts = packet->dts;  





  17. else


     {  


  18. pts = 0;  


  19. }  


  20. 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,它可以很好的自我注释:

 

 





[cpp]


view plain copy print ?







  1. double


     synchronize_video(VideoState *is, AVFrame *src_frame, 


    double


     pts) {  




  2. double


     frame_delay;  




  3. if


    (pts != 0) {  


  4. is->video_clock = pts;  





  5. else


     {  


  6. pts = is->video_clock;  


  7. }  


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


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


  10. is->video_clock += frame_delay;  




  11. return


     pts;  


  12. }  


 

 


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


现在让我们得到正确的PTS并且使用queue_picture来队列化帧,添加一个新的时间戳参数

 

 





[cpp]


view plain copy print ?





  1. pts:  



  2. // Did we get a video frame?

      




  3. if


    (frameFinished) {  


  4. pts = synchronize_video(is, pFrame, pts);  




  5. if


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




  6. break


    ;  


  7. }  


  8. }  


 

 


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

 

 





[cpp]


view plain copy print ?







  1. typedef

     

    struct


     VideoPicture {  


  2. ...  




  3. double


     pts;  


  4. }  




  5. int


     queue_picture(VideoState *is, AVFrame *pFrame, 


    double


     pts) {  


  6. ... stuff ...  




  7. if


    (vp->bmp) {  


  8. ... convert picture ...  


  9. vp->pts = pts;  


  10. ... alert queue ...  


  11. }  


 

 


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


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


 


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

 

 





[cpp]


view plain copy print ?







  1. void


     video_refresh_timer(


    void


     *userdata) {  


  2. VideoState *is = (VideoState *)userdata;  


  3. VideoPicture *vp;  




  4. double


     actual_delay, delay, sync_threshold, ref_clock, diff;  




  5. if


    (is->video_st) {  




  6. if


    (is->pictq_size == 0) {  


  7. schedule_refresh(is, 1);  





  8. else


     {  


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


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




  11. if


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


  12. delay = is->frame_last_delay;  


  13. }  


  14. is->frame_last_delay = delay;  


  15. is->frame_last_pts = vp->pts;  


  16. ref_clock = get_audio_clock(is);  


  17. diff = vp->pts - ref_clock;  


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




  19. if


    (fabs(diff) < AV_NOSYNC_THRESHOLD) {  




  20. if


    (diff <= -sync_threshold) {  


  21. delay = 0;  





  22. else

     

    if


    (diff >= sync_threshold) {  


  23. delay = 2 * delay;  


  24. }  


  25. }  


  26. is->frame_timer += delay;  


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




  28. if


    (actual_delay < 0.010) {  


  29. actual_delay = 0.010;  


  30. }  


  31. schedule_refresh(is, (


    int


    )(actual_delay * 1000 + 0.5));  


  32. video_display(is);  




  33. if


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


  34. is->pictq_rindex = 0;  


  35. }  


  36. SDL_LockMutex(is->pictq_mutex);  


  37. is->pictq_size--;  


  38. SDL_CondSignal(is->pictq_cond);  


  39. SDL_UnlockMutex(is->pictq_mutex);  


  40. }  





  41. else


     {  


  42. schedule_refresh(is, 100);  


  43. }  


  44. }  


 

 


我们在这里做了很多检查:首先,我们保证现在的时间戳和上一个时间戳之间的处以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;

}

然后当我们处理这个包的时候:

 

 





[cpp]


view plain copy print ?





  1. pts = is->audio_clock;   


  2. *pts_ptr = pts;   


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


  4. is->audio_clock += (


    double


    )data_size /   


  5. (


    double


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


 

 

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

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

 

 





[cpp]


view plain copy print ?







  1. double


     get_audio_clock(VideoState *is) {   




  2. double


     pts;   




  3. int


     hw_buf_size, bytes_per_sec, n;   


  4. pts = is->audio_clock;   


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


  6. bytes_per_sec = 0;   


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




  8. if


    (is->audio_st) {   


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


  10. }   




  11. if


    (bytes_per_sec) {   


  12. pts -= (


    double


    )hw_buf_size / bytes_per_sec;   


  13. }   




  14. return


     pts;   


  15. }   


 

 

你应该知道为什么这个函数可以正常工作了;)

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

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

最后,你可以使用我们自己的电影播放器来看电影了。下次我们将看一下声音同步,然后接下来的指导我们会讨论查询。


Ffmpeg和SDL如何同步视频




推荐阅读
  • LeetCode笔记:剑指Offer 41. 数据流中的中位数(Java、堆、优先队列、知识点)
    本文介绍了LeetCode剑指Offer 41题的解题思路和代码实现,主要涉及了Java中的优先队列和堆排序的知识点。优先队列是Queue接口的实现,可以对其中的元素进行排序,采用小顶堆的方式进行排序。本文还介绍了Java中queue的offer、poll、add、remove、element、peek等方法的区别和用法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • 本文介绍了Codeforces Round #321 (Div. 2)比赛中的问题Kefa and Dishes,通过状压和spfa算法解决了这个问题。给定一个有向图,求在不超过m步的情况下,能获得的最大权值和。点不能重复走。文章详细介绍了问题的题意、解题思路和代码实现。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • 本文介绍了一道经典的状态压缩题目——关灯问题2,并提供了解决该问题的算法思路。通过使用二进制表示灯的状态,并枚举所有可能的状态,可以求解出最少按按钮的次数,从而将所有灯关掉。本文还对状压和位运算进行了解释,并指出了该方法的适用性和局限性。 ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
  • C++ STL复习(13)容器适配器
    STL提供了3种容器适配器,分别为stack栈适配器、queue队列适配器以及priority_queue优先权队列适配器。不同场景下,由于不同的序列式 ... [详细]
  • [转载]从零开始学习OpenGL ES之四 – 光效
    继续我们的iPhoneOpenGLES之旅,我们将讨论光效。目前,我们没有加入任何光效。幸运的是,OpenGL在没有设置光效的情况下仍然可 ... [详细]
author-avatar
手机用户2502875921
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有