FFmpeg音视频同步思路
文章目录
- 使用音频pts为基准同步音视频的好处
- 拿到音频PTS
- 同步PTS的计算
- 重新计算音频PTS
- 拿到视频PTS
- B帧对解码的影响
- 帧分割/合并,解码延迟/缓冲的影响
- 进行音视频间的同步处理
使用音频pts为基准同步音视频的好处
- 符合人类感知特性
听觉对延迟更敏,这种 “音频为主,视频跟随” 的策略,能在保证音质的前提下,通过最小化视觉干扰实现同步 - 感知优先级
优先保证听觉体验,避免 “声画脱节” 带来的违和感。 - 调整成本
视频可灵活丢帧 / 重复帧,调整成本远低于音频变速
拿到音频PTS
通过FFMpeg的api接口拿到音频帧
av_get_bytes_per_sample((AVSampleFormat)frame->format)
从音频帧中可以拿到frame->pts
然后在音频初始化的时候拿到音频的时间基AVRational* time_base
由于SDL对音频的渲染是基于回调函数
SDL_AudioSpec sdl_spec;
//回调的频率和时机由音频设备的参数(采样率、缓冲区大小等)决定,无需手动干预。
sdl_spec.callback = AudioCallback;
if (SDL_OpenAudio(&sdl_spec, nullptr) < 0)
{cerr << SDL_GetError() << endl;return false;
}
//开始播放 调用 SDL_PauseAudio(0) 后,
// SDL 内部会自动调用通过 SDL_AudioSpec.callback 设置的音频回调函数
SDL_PauseAudio(0);
我们需要在回调函数里将音频从数据包队列拿出并放到SDL给定的Stream缓冲区,SDL每次渲染一段Stream缓冲区里的音频,
//将音频数据复制到SDL提供的缓冲区(即回调函数的stream参数指向的缓冲区)中,
// SDL就会自动播放这些数据
SDL_MixAudio(stream + mixed_size, // 目标音频缓冲区//vector::data()返回一个指向容器首元素的指针buf.data.data() + buf.offset,// 源音频缓冲区size, volume_);//缓冲区长度 , 源音频的音量
即一帧音频frame从音频队列拿出被放到SDL给定的Stream缓冲区以后才算开始渲染,一段缓冲区可能有很多帧音频Frame,而音视频的同步又不可能在SDL的回调函数里完成,所以我们先只选择这段音频包的第一帧的pts
这一段点很关键!!!
我们在此时再记录一下渲染该帧音频的那一刻的时间!!我们记为Last_time
Last_time = clock() / (CLOCKS_PER_SEC / 1000)
这样在我们计算当前音频播放到哪里的时候就非常方便了!!即使这次一次处理的一段音频帧,而我们只有开头的一帧的pts
同步PTS的计算
在新开的专门用于做音视频同步的线程里,基于上面拿到的音频帧来进行计算得到视频帧此时对应的pts
通过FFMpeg的API接口
av_rescale_q(audio_pts, *src_time_base, *des_time_base)
- audio_pts 要转换的值
- src_time_base 值 a 所使用的时间基(源时间基)
- des_time_base 转换到的目标时间基
- 返回 des_time_base
重新计算音频PTS
但是!!!
我们这里传入的audio_pts需要重新计算!!
这时就有一个好办法
就是不管这一帧的pts是什么
我们在计算同步pts的时候在拿到当前时间
cur_time = clock() / (CLOCKS_PER_SEC / 1000)
那么距离上次通过回调函数写入SDL的给定stream缓冲区的时间的增量就是
已播放时长增量(ms) =当前时间(cur_time) - 数据包开始时间(Last_time )
然后再将这段时间转换成pts的时间基
increment_pts = ms / (double)1000 / (double)audio_time_base_
最后audio_pts + increment_pts 就是当前音频播放到的位置的pts!!
再把这个pts带入av_rescale_q()的第一个参数
这样就能得到在该音频时间基下的音频帧的pts转换到视频时间基下的pts了
得到的这个pts就是用于同步的syn_pts!!
拿到视频PTS
在 avcodec_receive_frame();之后拿到解码完成的一帧 Avframe
此时可以拿到在编码阶段时写入的pts
注意!!
不能使用av_read_frame()解封装出来的 AVPacket 的pts!!!
大多数情况下,对于简单的、没有 B 帧的视频流,解码器通常会直接将对应的输入 AVPacket.pts 复制到输出 AVFrame.pts,此时两者值相同
但是!!!
存在一些复杂情况会导致 AVFrame.pts 与源 AVPacket.pts 不同或不直接对应
B帧对解码的影响
为了保证解码器能顺利工作,编码器 在输出 AVPacket 时,需要重新排序,按照 解码顺序 而不是 呈现顺序 排列帧
B帧依赖于其后(未来)的帧才能解码。在编码和封装时,帧是按呈现顺序存储的:
pts (显示时间戳)
I P P B B
1 2 3 4 5
然而在 传输和解码 时,为了能解出 B 帧,它们需要被按解码顺序传输:
dts(解码时间戳)
I P B B P
1 2 4 5 3
(一个 P 帧的解码顺序可能在其应该先显示的 B 帧之前)。
AVPacket.dts 就表示解码顺序
所以在有B帧的视频流中,有可能靠后的pts的帧会比靠前的pts的帧先到解码器!
那这个错误的pts的顺序当然不能被用来做音视频同步!
而解码器输出 AVFrame 的顺序是 显示顺序 (pts 顺序)
解码器负责管理依赖关系和缓冲,确保当它输出一帧时,所有依赖的帧都已准备好。因此,输出的帧严格按照其 pts 从低到高的顺序排列
帧分割/合并,解码延迟/缓冲的影响
-
帧分割与合并:
一些编码格式(如 H.264/AVC, H.265/HEVC)允许 将一帧视频分割成多个 AVPacket(例如多个 NAL Units)。同样,某些音频编码或打包方式也可能导致多个 AVPacket 最终解码成一个 AVFrame。解码器负责将这些相关的数据包组装成一个完整的帧。
在这个过程中,只有主要或最后一个 AVPacket 的 pts(或者根据规则计算出的一个 pts)会被用来设置输出 AVFrame.pts。直接使用其中一个 AVPacket.pts 可能不准确。 -
解码延迟/缓冲:
现代编解码器通常存在解码延迟。例如在解码 H.264 流时,发送第一个包含 SPS/PPS 的 AVPacket 并不会立即产生一个可显示的 AVFrame。解码器可能需要接收多个 AVPacket 后才会输出第一帧。它输出的第一帧 AVFrame.pts 对应于它完成解码所需的那个关键 AVPacket 的 pts(并考虑顺序调整)。在队列中的其他 AVPacket 的 pts 暂时未对应任何输出帧
同步发生在呈现阶段而非传输阶段,因此必须使用解码后帧的时间信息!!!
进行音视频间的同步处理
在avcodec_send_packet()拿到AVPacke之前可以进行音视频间的同步
即直接进行视频pts和syn_pts_之间的比较
然后再决定要不要继续解码
while (!is_exit_)
{//同步if (syn_pts_ >= 0 && cur_pts_ > syn_pts_){MSleep(1);continue;}break;
}
如果视频超前于音频那么就一直阻塞等待
如果视频落后于音频则全力渲染追赶音频
所以只要音频不是特别快(几十倍数速率播放),都不会出现视频赶不上音频的情况