Qt+FFmpeg+OpenGL实现视频播放器(4)

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);
}

运行结果如下:

  • 10
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值