zoukankan      html  css  js  c++  java
  • (转译)用FFmpeg和SDL写播放器05同步视频

    指导5:同步视频
     
    如何同步视频
     
    前面整个的一段时间,我们有了一个几乎无用的视频播放器。当然,它能播放视频,也能播放音频,但是它还不能被称为一部电影。那么我们还要做什么呢?
    PTS 和 DTS
       幸运的是,音频和视频流都有一些关于以多快速度和什么时间来播放它们的信息。音频流有采样,视频流有每秒的帧率。然而,如果我们只是简单的通过
    计算帧和乘以帧率的方式来同步视频,那么就很有可能会失去同步。于是作为一种补充,在流中的包有种叫做 DTS(解码时间戳)和 PTS(显示时间戳)的机制。
    想理解这两个参数,你需要了解视频存放的方式。像 MPEG 等格式,使用叫做 B帧(B 表示双向 bidrectional)的方式。另外两种帧被叫做 I 帧和 P 帧 (I 表
    示关键帧,P 表示预测帧)。I 帧包含了某个特定的完整图像。P 帧依赖于前面的 I 帧和 P 帧,并且使用比较或者差分的方式来编码。B 帧与 P 帧有点类似,但是
    它是依赖于前面和后面的帧的信息的。这也就解释了为什么我们可能在调用avcodec_decode_video2() 以后会得不到一帧图像。
      所以对于一个视频,帧是这样来显示的:I B B P。现在我们需要在显示 B 帧之前知道 P 帧中的信息。因此,帧可能会按照这样的方式来存储:I P B B。这就是为
    什么我们会有一个解码时间戳和一个显示时间戳的原因。解码时间戳告诉我们什么时候需要解码,显示时间戳告诉我们什么时候需要显示。所以,在这种情况下,我们的
    流可以是这样的:
         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_video2()函数中得到的帧只是一个 AVFrame,其中并没有包含有用的 PTS 值(注意:AVFrame 并
    没有包含时间戳信息,但当我们等到帧的时候并不是我们想要的样子)。然而,ffmpeg 重新排序包以便于被 avcodec_decode_video2()函数处理,DTS包 可以总是
    与PTS返回的帧相同。但是,另外的一个警告是:我们也并不是总能得到这个信息。
      不用担心,因为有另外一种办法可以找到PTS帧,我们可以让程序自己来重新排序包。我们保存一帧的第一个PTS的包:这将作为完成帧的PTS。我们可以通过函数
    avcodec_decode_video2()来计算出哪个包是一帧的第一个包。怎样实现呢?任何时候当一个包开始一帧的时候,avcodec_decode_video2()将调用一个函数来为一帧
    申请一个缓冲。当然,ffmpeg 允许我们重新定义那个分配内存的函数。所以我们制作了一个新的函数来保存一个包的时间戳。 
      当然,尽管现在,我们可能还是得不到一个正确的时间戳。我们将在后面处理这个问题。
     
    同步 
      现在,知道了什么时候来显示一个视频帧真好,但是我们怎样来实际操作呢?这里有个主意:当我们显示了一帧以后,我们计算出下一帧显示的时间。然后我们
    简单的设置一个新的定时器来。你可能会想,我们检查下一帧的 PTS 值而不是系统时钟,来看经历的时间。可以这么做,但是有两种情况要处理。
      首先,要知道下一个 PTS 是什么。你可能会想,把视频速率添加到我们的 PTS 中--太对了!然而,有些视频帧需要重复多次。这意味着我们重复播放当前的帧。
    这将导致程序显示下一帧过快。所以我们需要计算它们。
      第二,正如程序现在的样子,视频和音频播放很欢快,一点也不受同步的影响。如果一切都工作得很好的话,我们不必担心。但是,你的电脑并不是最好的,很多
    视频文件也不是完好的。所以,我们有三种选择:同步音频到视频,同步视频到音频,或者都同步到外部时钟(例如你的电脑时钟)。从现在开始,我们将同步视频到
    音频。
     
    写代码:获得帧的时间戳 
      现在让我们到代码中来做这些事情。我们将需要为我们的大结构体添加一些成员,但是我们会根据需要来做。首先,让我们看一下视频线程。记住,在这里我
    们得到 了解码线程输出到队列中的包。这里我们需要的是从avcodec_decode_video2 函数中得到帧的时间戳。我们讨论的第一种方式是从上次处理的包 中得到
    DTS,这是很容易的:
     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 如何去做:

     
    codecCtx->get_buffer=our_get_buffer;
    codecCtx->release_buffer=our_release_buffer;
     
     
    现在我们必需添加代码来保存 PTS 到全局变量中,然后在需要的时候来使用它。我们的代码现在看起来应该是这样子:
     
    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);
      技术提示:你可能已经注意到我们使用 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) {
        /* 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;
          }
        }
      对于 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; /* 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 毫秒。事实上

    这里我们应该跳过这一帧,但是我们不想为 此而烦恼。

       我们给大结构体添加了很多的变量,所以不要忘记检查一下代码。同时也不要忘记在函数 streame_component_open 中初始化帧时间 frame_timer 和前面的帧延迟framedelay:
     
        is->frame_timer = (double)av_gettime() / 1000000.0;
        is->frame_last_delay = 40e-3;
    同步:声音时钟
      现在让我们看一下怎样来得到声音时钟。我们可以在声音解码函数audio_decode_frame 中更新时钟时间。现在,请记住我们并不是每次调用这个函数的时候都在处理新的包,所以
    我们要在两个地方更新时钟。第一个地方是我们得到新的包的时候:我们简单的设置声音时钟为这个包的时间戳。然后,如果一 个包里有许多帧,我们通过样本数和采样率来计算,所以
    当我们得到包的时候:
    /* 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);
      一点细节:临时函数被改成包含 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; /* 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/
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

  • 相关阅读:
    记通过身份证号计算是否成年
    mysql出现which is not functionally dependent on columns in GROUP BY clause报错
    Git操作之 git add 撤销、git commit 撤销
    laraval ueditor 上传图片失败
    jquery调用百度api连接实现网页实时刷新汇率
    Laravel框架使用maatwebsite/excel 导出表格样式自定义
    数字千分位格式化
    laravel 5.4 解决使用Excel::load()导入的excel文件中日期格式变为数字
    layui select可以使用jQuery的change事件
    Linux系统通过命令修改BT宝塔面板的默认8888端口
  • 原文地址:https://www.cnblogs.com/xdao/p/ffmpeg_tutor05.html
Copyright © 2011-2022 走看看