音视频同步学习总结

基础概念:

帧率: 表示视频一秒显示的帧数

        以帧率为每秒24帧(24fps)来计算播放时长:1000ms / 24 = 41.67ms

采样率: 表示音频一秒播放的样本的个数

        PCM每个声道包含1024个采样点,采样率为44100,那一个Frame的播放时长为:(1024/44100) * 1000ms = 23.22ms

PTS:(Presentation timestamp)

DTS:(Decoding timestamp)

为什么需要有PTS,DTS:

在H264编码中有三种帧:I、B、P帧

I帧:关键帧 ---- 可以直接解码出来,不依赖于其它信息 采用帧内压缩技术

B帧:双向参考帧 --- 采用帧间压缩技术

P帧:前向参考帧 --- 参考前面的I帧或P帧 采用帧间压缩技术

实际帧顺序:I B B P

解码时帧的顺序: I P B B     // FFmpeg 解码器输出的帧是顺序输出(意思就是已经排好序了),所以从代码层面来说是不需要手动排序,解码器内部已经实际了,这也是在FFmpeg解码的时候需要刷空解码器中的队列原因之一

解码时间戳:1 4 2 3

展示时间戳:1 2 3 4

//在没有B帧的情况下,PTS和DTS是一样的

从哪儿获取PTS

AVPacket中的PTS

AVFrame中的PTS

av_frame_get_best_effort_timestamp()

时间基

tbr:帧率 ----------- 可以计算出每帧的duration(播放的时长)

tbn:time base of stream 流的时间基 

tbc: time base of codec 解码后的时间基

实战分享

音视频同步策略
  1. 视频同步到音频  (此次分享的方式) ------- 也是最简单的和最通用的,原因:音频输出是线性的,需要的数据基本上是固定大小,变化幅度极小,前后相差就一两帧,而且人耳对声音的敏感度要强于视频
  2. 音频同步到视频 (还没有弄明白)
  3. 音频和视频都同步到系统时钟 (也没有弄明白)
计算当前帧的PTS、Duration

视频帧的计算:

//估算frame_rate
AVRational frame_rate = av_guess_frame_rate(_formatCtx, _vStream, NULL);

解码后得到AVFrame
......

//开始计算pts
_vFrame->pts = _vFrame->best_effort_timestamp; // 先得到ffmpeg根据stream timebase算出来的pts

double pts = (_vFrame->pts == AV_NOPTS_VALUE) ? NAN : _vFrame->pts * av_q2d(_vStream->time_base);  // 基于stream timebase来计算 单位为秒



// 计算duration  每一帧要显示的时间
double duration = (frame_rate.num && frame_rate.den) ? av_q2d((AVRational){frame_rate.den, frame_rate.num}) : 0; // 秒 

//保存起来

注意点:
// 得到时间刻度
av_q2d(AVRational a) {
    return a.num / (double)a.den;
}

音频帧的计算:


解码后
........


// 计算pts
AVRational tb = (AVRational){1, frame->sample_rate};
 
if (frame->pts != AV_NOPTS_VALUE) {
    // _context = AVCodecContext 
    frame->pts = av_rescale_q(frame->pts, _context->pkt_timebase, tb);
    // av_rescale_q 是用来把时间戳从一个时间基调整到另外一个时间基时候用的
} else if (_next_pts != AV_NOPTS_VALUE) {
    frame->pts = av_rescale_q(_next_pts, _next_pts_tb, tb);
}
_aTimeBase = tb;

// 保存 
if (frame->pts != AV_NOPTS_VALUE) { 
    _next_pts = frame->pts + frame->nb_samples;
    _next_pts_tb = tb;
}

double pts = (_aFrame->pts == AV_NOPTS_VALUE) ? NAN : _aFrame->pts * av_q2d(tb);

/**
_next_pts, _next_pts_tb 是用保存AVFrame中pts和time_base的用途,防止出现frame->pts,timebase为空的时候 无法计算
*/


// 计算 duration

f->duration = av_q2d((AVRational{_aFrame->nb_samples, _aFrame->sample_rate}));

同步

ffplay源码分析:

视频同步到音频

1.从队列获取音频帧

AudioFrame *ParseManager::getAudioFrame() {
    
    // 记录一下时间 
    _audio_callback_time = av_gettime_relative();

    // 从队列中提取出来
    AudioFrame *f = _aFrameQ.peek_readable();
    _aFrameQ.next();
    

    //double _audio_clock 计算出当前音频帧需要播多长时间
    if (!isnan(f->pts))
        _audio_clock = f->pts + (double) f->nb_samples / f->sample_rate;
    else
        _audio_clock = NAN;

    return f;
}

2.获取视频帧

// 获取视频帧
BLVideoFrame *ParseManager::getVidewFrame() {
    double time;
    double remaining_time = 0.01;
    
retry:
    if (_vFrameQ.nb_remaining() == 0) { // _vFrameQ 视频队列为空
        return nullptr;
    } else {
        double last_duration, duration, delay;
        
        // 当前播放的上一帧
        auto lastvp = _vFrameQ.peek_last();
        // 当前帧
        auto vp = _vFrameQ.peek();
        
        // 计算上一帧和当前帧的时间差
        last_duration = vp_duration(lastvp, vp);

        // 根据视频时钟和音频时钟的差值来计算当前的delay值
        delay = compute_target_delay(last_duration);
        
        time = av_gettime_relative()/1000000.0; // 取系统时间
        
        if (time < _frame_timer + delay) { // 如果上一帧显示时长未满,重重显示上一帧
            remaining_time = FFMIN(_frame_timer + delay - time, remaining_time);
            return nullptr;
        }
        
        // 更新为上一帧结束时刻,也是当前帧开始的时刻
        _frame_timer += delay; 
        if (delay > 0 && time - _frame_timer > AV_SYNC_THRESHOLD_MAX) {
            //frame_timer 帧显示时刻
            _frame_timer = time; // 如果与系统时间的偏离太大,则修正为系统时间
        }
        
        if (!isnan(vp->postion)) {
            update_video_pts(vp->postion, vp->pkt_pts, -1);  // 更新时钟时间
        }
        
        // 丢帧策略
        if (_vFrameQ.nb_remaining() > 1) {
            std::shared_ptr<BLVideoFrame> nextvp = _vFrameQ.peek_next();
            if (nextvp) {
                duration = vp_duration(vp, nextvp);
                if (time > _frame_timer + duration) {
                    _vFrameQ.next();
                    goto retry;
                }
            }
        }

        // 从队列中获取帧用来显示
        BLVideoFrame *f = _vFrameQ.peek_readable();
        _vFrameQ.next();
        return f;
    }
    
}

获取当前要显示的video PTS,减去上一帧视频PTS,则得出上一帧视频应该显示的时长delay;

当前video PTS与参考时钟当前audio PTS比较,得出音视频差距diff;

获取同步阈值sync_threshold,为一帧视频差距,范围为10ms-100ms;

diff小于sync_threshold,则认为不需要同步;否则delay+diff值,则是正确纠正delay;

如果超过sync_threshold,且视频落后于音频,那么需要减小delay(FFMAX(0, delay + diff)),让当前帧尽快显示。

如果视频落后超过1秒,且之前10次都快速输出视频帧,那么需要反馈给音频源减慢,同时反馈视频源进行丢帧处理,让视频尽快追上音频。因为这很可能是视频解码跟不上了,再怎么调整delay也没用。

如果超过sync_threshold,且视频快于音频,那么需要加大delay,让当前帧延迟显示。

将delay*2慢慢调整差距,这是为了平缓调整差距,因为直接delay+diff,会让画面画面迟滞。

如果视频前一帧本身显示时间很长,那么直接delay+diff一步调整到位,因为这种情况再慢慢调整也没太大意义。

考虑到渲染的耗时,还需进行调整。frame_timer为一帧显示的系统时间,frame_timer+delay- curr_time,则得出正在需要延迟显示当前帧的时间。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

blazer_luo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值