FFmpeg学习:音视频同步(2)同步策略

at 6个月前  ca FFmpeg  pv 785  by touch  

相关基本

FFmpeg学习:音视频同步(1)相关基础定义

同步方式

目前主要有三种方式实现同步:

  • 将视频和音频同步外部的时钟上,选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。

  • 将音频同步到视频上,就是以视频的播放速度为基准来同步音频。

  • 将视频同步到音频上,就是以音频的播放速度为基准来同步视频。

比较主流的是第三种,将视频同步到音频上。至于为什么不使用前两种,因为一般来说,人对于声音的敏感度更高,如果频繁地去调整音频会产生杂音让人感觉到刺耳不舒服,而人对图像的敏感度就低很多了,所以一般都会采用第三种方式。

Audio_Clock

  • Audio_Clock,就是Audio的播放时长,从开始到当前的时间。获取Audio_Clock:

  1. if (pkt->pts != AV_NOPTS_VALUE) {
  2.     state->audio_clock = av_q2d(state->audio_st->time_base) * pkt->pts;
  3. }

还没有结束,由于一个packet中可以包含多个Frame帧,packet中的PTS比真正的播放的PTS可能会早很多,可以根据Sample Rate 和 Sample Format来计算出该packet中的数据可以播放的时长,再次更新Audio_Clock。

  1. // 每秒钟音频播放的字节数 采样率 * 通道数 * 采样位数 (一个sample占用的字节数)
  2. = 2 * state->audio_ctx->channels;
  3. state->audio_clock += (double) data_size /
  4.                    (double) (* state->audio_ctx->sample_rate

最后还有一步,在我们获取这个Audio_Clock时,很有可能音频缓冲区还有没有播放结束的数据,也就是有一部分数据实际还没有播放,所以就要在Audio_Clock上减去这部分数据的播放时间,才是真正的Audio_Clock。

  1. double get_audio_clock(VideoState *state) {
  2.     double pts;
  3.     int buf_size, bytes_per_sec;
  4.  
  5.     //上一步获取的PTS
  6.     pts = state->audio_clock;
  7.     // 音频缓冲区还没有播放的数据
  8.     buf_size = state->audio_buf_size - state->audio_buf_index; 
  9.     // 每秒钟音频播放的字节数
  10.     bytes_per_sec = state->audio_ctx->sample_rate * state->audio_ctx->channels * 2;
  11.     pts -= (double) buf_size / bytes_per_sec;
  12.     return pts;
  13. }

get_audio_clock中返回的才是我们最终需要的Audio_Clock,当前的音频的播放时长。

Video_Clock

  • Video_Clock,视频播放到当前帧时的已播放的时间长度

  1. avcodec_send_packet(state->video_ctx, packet);
  2. while (avcodec_receive_frame(state->video_ctx, pFrame) == 0) {
  3.     if ((pts = pFrame->best_effort_timestamp) != AV_NOPTS_VALUE) {
  4.     } else {
  5.         pts = 0;
  6.     }
  7.     pts *= av_q2d(state->video_st->time_base); // 时间基换算,单位为秒
  8.  
  9.     pts = synchronize_video(state, pFrame, pts);
  10.     
  11.     av_packet_unref(packet);
  12. }

旧版的FFmpeg使用av_frame_get_best_effort_timestamp函数获取视频的最合适PTS,新版本的则在解码时生成了best_effort_timestamp。但是依然可能会获取不到正确的PTS,所以在synchronize_video中进行处理。

  1. double synchronize_video(VideoState *state, AVFrame *src_frame, double pts) {
  2.  
  3.     double frame_delay;
  4.  
  5.     if (pts != 0) {
  6.         state->video_clock = pts;
  7.     } else {
  8.         pts = state->video_clock;// PTS错误,使用上一次的PTS值
  9.     }
  10.     //根据时间基,计算每一帧的间隔时间
  11.     frame_delay = av_q2d(state->video_ctx->time_base);
  12.     //解码后的帧要延时的时间
  13.     frame_delay += src_frame->repeat_pict * (frame_delay * 0.5);
  14.     state->video_clock += frame_delay;//得到video_clock,实际上也是预测的下一帧视频的时间
  15.     return pts;
  16. }

同步

上面两步获得了Audio_Clock和Video_Clock,这样我们就有了视频流中Frame的显示时间,并且得到了作为基准时间的音频播放时长Audio clock ,可以将视频同步到音频了。

1、用当前帧的PTS - 上一播放帧的PTS得到一个延迟时间
2、用当前帧的PTS和Audio_Clock进行比较,来判断视频的播放速度是快了还是慢了
3、根据2的结果,设置播放下一帧的延迟时间

  1. #define AV_SYNC_THRESHOLD 0.01 // 同步最小阈值
  2. #define AV_NOSYNC_THRESHOLD 10.0 //  不同步阈值
  3. double actual_delay, delay, sync_threshold, ref_clock, diff;
  4.  
  5. // 当前Frame时间减去上一帧的时间,获取两帧间的延时
  6. delay = vp->pts - is->frame_last_pts;
  7. if (delay <= 0 || delay >= 1.0) { 
  8.     // 延时小于0或大于1秒(太长)都是错误的,将延时时间设置为上一次的延时时间
  9.     delay = is->frame_last_delay;
  10. }
  11.  
  12. // 获取音频Audio_Clock
  13. ref_clock = get_audio_clock(is);
  14. // 得到当前PTS和Audio_Clock的差值
  15. diff = vp->pts - ref_clock;
  16.  
  17. sync_threshold = (delay > AV_SYNC_THRESHOLD) ? delay : AV_SYNC_THRESHOLD;
  18.  
  19. // 调整播放下一帧的延迟时间,以实现同步
  20. if (fabs(diff) < AV_NOSYNC_THRESHOLD) {
  21.     if (diff <= -sync_threshold) { // 慢了,delay设为0
  22.         delay = 0;
  23.     } else if (diff >= sync_threshold) { // 快了,加倍delay
  24.         delay = 2 * delay;
  25.     }
  26.  }
  27. is->frame_timer += delay;
  28. // 最终真正要延时的时间
  29. actual_delay = is->frame_timer - (av_gettime() / 1000000.0);
  30. if (actual_delay < 0.010) {
  31.     // 延时时间过小就设置个最小值
  32.     actual_delay = 0.010;
  33. }
  34. // 根据延时时间刷新视频
  35. schedule_refresh(is, (int) (actual_delay * 1000 + 0.5));

参考

音视频同步


版权声明

本文仅代表作者观点,不代表码农殇立场。
本文系作者授权码农殇发表,未经许可,不得转载。

 

扫一扫在手机阅读、分享本文

冀ICP备14009681号-2 Powered By 码农殇 Theme By zb脚本
您是本站第3171名访客 今日有0篇新文章