基础概念:
帧率: 表示视频一秒显示的帧数
以帧率为每秒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 解码后的时间基
实战分享
音视频同步策略
- 视频同步到音频 (此次分享的方式) ------- 也是最简单的和最通用的,原因:音频输出是线性的,需要的数据基本上是固定大小,变化幅度极小,前后相差就一两帧,而且人耳对声音的敏感度要强于视频
- 音频同步到视频 (还没有弄明白)
- 音频和视频都同步到系统时钟 (也没有弄明白)
计算当前帧的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,则得出正在需要延迟显示当前帧的时间。