1、音视频同步
音视频为什么会不同步呢?
音视频不同步(Audio-Video Sync Issue)通常指音频和视频在播放过程中不能保持一致,可能出现视频提前或音频提前的情况,导致用户感知上的“不同步”问题。音视频不同步的原因可能是多方面的,涉及到软硬件层面的复杂性。下面总结了几种常见的原因:
1. 解码速度差异
音频和视频是两种不同类型的数据,解码的复杂性也不相同:
-
音频解码:通常比较简单,数据量相对较小,解码速度较快。音频帧是连续的,解码和播放没有太多延迟。
-
视频解码:相比音频,视频解码更为复杂,尤其是在高分辨率、编码复杂的场景下。解码视频帧的时间会明显比音频解码更长。
这种解码速度差异会导致音频播放速度和视频播放速度不同步。如果音频解码较快,可能会出现“声音超前”的现象;反之,视频解码较慢,可能会出现“视频落后”的现象。
2. 缓冲和网络延迟
对于流媒体播放,音视频不同步可能是由于网络传输的抖动或延迟导致的:
-
缓冲区大小不一致:播放器通常会对音视频流进行缓冲,如果音频和视频的缓冲区大小不一致,可能会导致播放时音视频不同步。
-
网络延迟:在流媒体传输中,网络带宽不足或者波动较大可能导致音频和视频数据传输不平衡,可能音频先到达,而视频滞后。
3. 播放设备性能瓶颈
播放设备的性能不足也是常见原因:
-
CPU和GPU负载过高:如果设备的处理能力不足,尤其是在播放高分辨率视频时,CPU或GPU可能会过载,从而无法及时解码视频帧,而音频通常负载较小,导致音频比视频播放得更快。
-
硬盘/存储设备性能:从慢速硬盘读取视频文件时,视频数据的加载速度可能比音频慢,从而引发不同步问题。
4. 帧率差异
视频的帧率和音频的采样率不同:
-
视频帧率通常为24、30或60帧每秒,意味着每秒渲染固定数量的帧。
-
音频采样率为44100Hz或48000Hz,表示每秒播放对应的音频采样点。
如果视频的实际帧率与播放预期的帧率不一致,可能会导致视频播放速度变快或变慢,从而与音频的播放速度产生差异。常见的情况是视频帧率被错误配置或是视频解码器处理速度不均衡。
如何实现音视频同步
为了解决音视频同步问题,引入了时间戳:首先选择一个参考时钟(要求参考时钟上的时间是线性递增的);编码时依据参考时钟上的给每个音视频数据块都打上时间戳;播放时,根据音视频时间戳及参考时钟,来调整播放。所以,视频和音频的同步实际上是一个动态的过程,同步是暂时的,不同步则是常态。以参考时钟为标准,放快了就减慢播放速度;播放快了就加快播放的速度。
1.以音频为基准:以音频的播放速度为基准来同步视频,
2.以视频为基准:以视频的播放速度为基准来同步音频。
3.以外部时钟为基准:选择一个外部时钟为基准,视频和音频的播放速度都以该时钟为标准。
在很多视频播放器和媒体系统中,音频常常作为同步的主基准,原因是人类对于声音的不连续性或延迟更敏感,而视频帧的丢失或插入对感知的影响较小。
音频基准同步的流程
1. 音频作为时间基准
音频播放通常以固定的采样率(如44100 Hz)进行,解码音频数据并将其交给音频输出设备。音频输出硬件通常能精确控制播放的时间,因此音频往往作为同步的基准。
2. 获取音频的播放时间戳
在解码音频帧时,FFmpeg 会为每个解码帧提供时间戳(PTS - Presentation Time Stamp),它表示该帧应该在视频中的什么时刻播放。音频播放时,根据 QAudioSink
的状态可以获取当前已经播放的音频时长。
3. 同步视频帧到音频时间
视频播放则需要根据音频的当前播放时间调整。每个视频帧也有一个时间戳,表示该帧应当在何时显示。视频解码器会不断解码视频帧,系统通过比较视频帧的时间戳与音频播放的当前时间:
- 如果视频帧的时间戳比当前音频的播放时间早,立即显示该视频帧。
- 如果视频帧的时间戳比当前音频播放时间晚,则需要等待一定的时间再显示。
通过这种方式,可以确保视频帧按照与音频一致的节奏进行播放。
4. 处理视频帧和音频的不同步问题
如果视频帧和音频的时间差异较大,通常有以下几种处理方式:
- 跳帧:当视频帧落后于音频过多时,跳过某些视频帧以赶上音频。
- 重复帧:当视频帧播放速度超过音频时,可以重复显示某些帧,确保两者保持同步。
音视频同步代码:
获取下一帧视频时间戳
double VideoInfo::getVideoFrameTime() {
if (videoFrame && videoFrame->pts != AV_NOPTS_VALUE) {
return videoFrame->pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base);
}
return 0;
}
获取当前音频播放时间
double AudioInfo::getAudioCurrentTime() const {
if (audioSink->state() == QAudio::ActiveState) {
return audioSink->elapsedUSecs() / 1000000.0; // 返回秒
}
return 0;
}
音视频同步
void VideoInfo::run()
{
videoFrame = av_frame_alloc();
videoPacket = av_packet_alloc();
QElapsedTimer timer; // 使用 Qt 的计时器
timer.start(); // 开始计时
while (true) {
mutex.lock();
if (!playing) {
mutex.unlock();
continue;
}
mutex.unlock();
if (av_read_frame(formatContext, videoPacket) >= 0) {
if (videoPacket->stream_index == videoStreamIndex) {
int response = avcodec_send_packet(videoCodecContext, videoPacket);
if (response >= 0) {
response = avcodec_receive_frame(videoCodecContext, videoFrame);
if (response >= 0) {
qDebug() << "audioInfo pointer:" << audioInfo;
// 获取帧的显示时间 (PTS)
double framePts = getVideoFrameTime();
// 计算当前时间 (秒)
double currentTime = audioInfo->getAudioCurrentTime();
// 计算需要延迟的时间
double delay = framePts - currentTime;
// 获取当前时间戳
int64_t pts = videoFrame->pts;
double currentSeconds = pts * av_q2d(formatContext->streams[videoStreamIndex]->time_base);
// 发射信号更新进度条
emit updateProgress(static_cast<int>(currentSeconds), totalSeconds);
if (delay > 0) {
// 视频比音频快,暂停一定时间再播放
QThread::msleep(static_cast<unsigned long>(delay * 1000));
} else if (delay < -0.1) {
// 视频比音频慢,丢弃该帧
continue;
}
// 转换视频帧格式
QImage image(videoCodecContext->width, videoCodecContext->height, QImage::Format_RGB888);
uint8_t *dest[4] = { image.bits(), nullptr, nullptr, nullptr };
int destLinesize[4] = { static_cast<int>(image.bytesPerLine()), 0, 0, 0 };
sws_scale(swsContext, videoFrame->data, videoFrame->linesize, 0, videoCodecContext->height,
dest, destLinesize);
emit frameReady(image);
if (renderer) {
renderer->setFrameData(image); // 调用渲染方法
}
}
}
}
av_packet_unref(videoPacket);
} else {
break; // 播放结束
}
}
av_frame_free(&videoFrame);
av_packet_free(&videoPacket);
}
运行结果如下: