热门标签 | 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如何同步视频




推荐阅读
  • 服务器虚拟化存储设计,完美规划储存与资源,部署高性能虚拟化桌面
    规划部署虚拟桌面环境前,必须先估算目前所使用实体桌面环境的工作负载与IOPS性能,并慎选储存设备。唯有谨慎估算贴近实际的IOPS性能,才能 ... [详细]
  • Asynchronous JavaScript and XML (AJAX) 的流行很大程度上得益于 Google 在其产品如 Google Suggest 和 Google Maps 中的应用。本文将深入探讨 AJAX 在 .NET 环境下的工作原理及其实现方法。 ... [详细]
  • Hibernate全自动全映射ORM框架,旨在消除sql,是一个持久层的ORM框架1)、基础概念DAO(DataAccessorOb ... [详细]
  • 尽管在WPF中工作了一段时间,但在菜单控件的样式设置上遇到了一些基础问题,特别是关于如何正确配置前景色和背景色。 ... [详细]
  • ASP.NET 进度条实现详解
    本文介绍了如何在ASP.NET中使用HTML和JavaScript创建一个动态更新的进度条,并通过Default.aspx页面进行展示。 ... [详细]
  • 本文探讨了如何利用RxJS库在AngularJS应用中实现对用户单击和拖动操作的精确区分,特别是在调整区域大小的场景下。 ... [详细]
  • 在测试软件或进行系统维护时,有时会遇到电脑蓝屏的情况,即便使用了沙盒环境也无法完全避免。本文将详细介绍常见的蓝屏错误代码及其解决方案,帮助用户快速定位并解决问题。 ... [详细]
  • Zabbix自定义监控与邮件告警配置实践
    本文详细介绍了如何在Zabbix中添加自定义监控项目,配置邮件告警功能,并解决测试告警时遇到的邮件不发送问题。 ... [详细]
  • 深入理解线程池及其基本实现
    本文探讨了线程池的概念、优势及其在Java中的应用。通过实例分析不同类型的线程池,并指导如何构建一个简易的线程池。 ... [详细]
  • 探讨了在HTML表单中使用元素代替进行表单提交的方法。 ... [详细]
  • Ubuntu系统下的GIF动画录制解决方案
    在撰写文章或教程时,GIF动态图能够有效地传达信息。对于Windows用户而言,ScreenToGif是一款非常实用的工具。而在Ubuntu系统中,用户同样拥有多种选择来创建GIF动画,本文将重点介绍两款录屏工具——Byzanz和Peek。 ... [详细]
  • 本文详细介绍了如何在最新版本的Xcode中重命名iOS项目,包括项目名称、应用名称及相关的文件夹和配置文件。通过本文,开发者可以轻松完成项目的重命名工作。 ... [详细]
  • 本文深入探讨了WPF框架下的数据验证机制,包括内置验证规则的使用、自定义验证规则的实现方法、错误信息的有效展示策略以及验证时机的选择,旨在帮助开发者构建更加健壮和用户友好的应用程序。 ... [详细]
  • 本文探讨了如何在PHP与MySQL环境中实现高效的分页查询,包括基本的分页实现、性能优化技巧以及高级的分页策略。 ... [详细]
  • 本文详细介绍了如何在Mac操作系统中为IntelliJ IDEA配置更高的内存限制,以提高开发效率和性能。 ... [详细]
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社区 版权所有