Qt-FFmpeg案例(0基础,包含环境配置)

一、绪论

这是一个基于Qt框架FFmpeg多媒体库开发的轻量级视频播放器应用程序。项目实现了基本的视频文件解码、播放控制功能,具有跨平台特性,支持多种常见视频格式。下面我将以功能实现为分界,介绍这个项目,比较适合Qt音视频方向的初学者。(有问题可以私信我,可以带敲和视频讲解,欢迎交流学习)

二、环境配置

Qt6.0:这个大家因该都有我主要是介绍一下在Qt上配置FFmpeg

FFmpeg:

2.1.下载

Releases · BtbN/FFmpeg-Buildshttps://github.com/BtbN/FFmpeg-Builds/releases

注意这里选择

不要去官网,他哪里需要你自己编译,比较繁琐。

2.2.解压

2.3.配置cmake

这是一个全新的cmake

cpp 复制代码
cmake_minimum_required(VERSION 3.16)

project(test VERSION 0.1 LANGUAGES CXX)

set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

set(PROJECT_SOURCES
        main.cpp
        mainwindow.cpp
        mainwindow.h
        mainwindow.ui
)

if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
    qt_add_executable(test
        MANUAL_FINALIZATION
        ${PROJECT_SOURCES}
    )
# Define target properties for Android with Qt 6 as:
#    set_property(TARGET test APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
#                 ${CMAKE_CURRENT_SOURCE_DIR}/android)
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
else()
    if(ANDROID)
        add_library(test SHARED
            ${PROJECT_SOURCES}
        )
# Define properties for Android with Qt 5 after find_package() calls as:
#    set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
    else()
        add_executable(test
            ${PROJECT_SOURCES}
        )
    endif()
endif()

target_link_libraries(test PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
# If you are developing for iOS or macOS you should consider setting an
# explicit, fixed bundle identifier manually though.
if(${QT_VERSION} VERSION_LESS 6.1.0)
  set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.test)
endif()
set_target_properties(test PROPERTIES
    ${BUNDLE_ID_OPTION}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

include(GNUInstallDirs)
install(TARGETS test
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

if(QT_VERSION_MAJOR EQUAL 6)
    qt_finalize_executable(test)
endif()

下面给出需要添加的代码

find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) 之后添加:

cpp 复制代码
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)

# ========== 添加FFmpeg配置开始 ==========
# FFmpeg 配置
set(FFMPEG_ROOT "D:/z_demo/ffmpeg-master-latest-win64-gpl-shared/ffmpeg-master-latest-win64-gpl-shared")

# 检查 FFmpeg 目录是否存在
if(NOT EXISTS ${FFMPEG_ROOT})
    message(WARNING "FFmpeg directory not found: ${FFMPEG_ROOT}")
endif()

include_directories(${FFMPEG_ROOT}/include)
link_directories(${FFMPEG_ROOT}/lib)
# ========== 添加FFmpeg配置结束 ==========

set(PROJECT_SOURCES
    main.cpp
    mainwindow.cpp
    mainwindow.h
    mainwindow.ui
)

这里注意把

cpp 复制代码
set(FFMPEG_ROOT "D:/z_demo/ffmpeg-master-latest-win64-gpl-shared/ffmpeg-master-latest-win64-gpl-shared")

换成你自己的文件地址。

target_link_libraries(test PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) 之后添加:

cpp 复制代码
target_link_libraries(test PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)

# ========== 添加FFmpeg链接库开始 ==========
# 链接FFmpeg库
target_link_libraries(test PRIVATE
    avcodec
    avformat
    avutil
    swscale
    swresample
)

# Windows 特定库
if(WIN32)
    target_link_libraries(test PRIVATE ws2_32 secur32 bcrypt)
endif()

# Windows DLL 复制
if(WIN32)
    # 使用通配符匹配 DLL 版本,避免硬编码版本号
    file(GLOB FFMPEG_DLLS "${FFMPEG_ROOT}/bin/*.dll")
    foreach(DLL ${FFMPEG_DLLS})
        add_custom_command(TARGET test POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${DLL}"
            $<TARGET_FILE_DIR:test>
        )
    endforeach()
endif()
# ========== 添加FFmpeg链接库结束 ==========

2.4.测试

在mainwindow.h上添加

cpp 复制代码
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
// 其他FFmpeg头文件
}

在mainwindow.cpp的构造函数中添加

cpp 复制代码
    qDebug() << "FFmpeg version:" << avcodec_version();

运行后如果出现类似于

就说明配置成功了

三、基本功能(打开文件和播放)

UI模块 : MainWindow - 处理用户交互和显示

业务模块 : VideoPlayer - 处理核心播放逻辑

解码模块: FFmpeg - 处理底层多媒体解码

3.1UI设计

就一个label(video)和两个按钮(openfile,play)

3.2新增类

添加一个videoplayer继承于QObject,用与专门处理核心播放逻辑。(这里先处理视频)

处理逻辑:

cpp 复制代码
原始视频帧 → 解码 → 像素格式转换(sws_scale) → RGB图像 → 显示

videoplayer.h

cpp 复制代码
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QObject>
#include <QImage>
#include<QTimer>
extern "C" {
#include <libavformat/avformat.h>//媒体容器格式处理
#include <libavcodec/avcodec.h>//音视频编解码
#include <libswscale/swscale.h>//图像缩放和格式转换
}
class videoplayer : public QObject
{
    Q_OBJECT
public:
    explicit videoplayer(QObject *parent = nullptr);

    bool openFile(const QString &filename);//打开媒体文件
    void play();//播放,启动解码定时器,开始播放媒体文件
    void pause();//暂停
    bool isPlaying() const { return m_isPlaying; }//检查是否播放

private slots:
    void decodeFrame();//解码音频帧
signals:
    void playstatechange(bool playing);//播放状态改变
    void videoFrame(const QImage &frame);//将解码出的视频帧传递到界面显示

private:

    QImage convertFrameToImage(AVFrame *frame);// 将FFmpeg帧转换为Qt图像,使用libswscale进行YUV到RGB格式转换


    AVFormatContext *m_formatCtx = nullptr;//媒体格式上下文,管理文件容器
    AVCodecContext *m_videoCodecCtx = nullptr;//视频编解码器上下文,管理视频解码;存储视频流的解码参数
    SwsContext *m_swsCtx = nullptr;//图像缩放转换上下文,YUV转RGB
    
    int m_videoStreamIndex = -1;  //视频流在文件中的索引,-1表示未找到


    bool m_isPlaying = false;//播放状态标志
    bool m_pause=false;

     QTimer *m_timer;  // 解码定时器,控制解码帧率;这个是控制策略,播放、暂停、停止、重新播放都是通过这个控制的
    //原理:每隔固定时间触发timeout()信号,调用decodeFrame()而decodeFrame()函数会解码后生成一帧(就是一张图片)
     int frame_rate=33;
};

#endif // VIDEOPLAYER_H

videoplayer.cpp

cpp 复制代码
#include "videoplayer.h"
#include<qDebug>
videoplayer::videoplayer(QObject *parent)
    : QObject{parent}
{
    m_timer = new QTimer(this);
    connect(m_timer, &QTimer::timeout, this, &videoplayer::decodeFrame);
}
bool videoplayer::openFile(const QString &filename)//打开媒体文件
{
    //avformat_open_input初始化AVFormatContext(m_formatCtx),打开指定的媒体文件
    if(avformat_open_input(&m_formatCtx,filename.toUtf8().constData(),nullptr,nullptr)!=0)
    {
        return false;
    }

    if(avformat_find_stream_info(m_formatCtx,nullptr)<0)
    {
        return false;
    }
    for(int i=0;i<m_formatCtx->nb_streams;i++)
    {
        AVCodecParameters *codecparams=m_formatCtx->streams[i]->codecpar;
        //获取当前流的编解码器参数
        if(codecparams->codec_type==AVMEDIA_TYPE_VIDEO&&m_videoStreamIndex==-1)
        {
            m_videoStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_videoCodecCtx=avcodec_alloc_context3(codec);
                avcodec_parameters_to_context(m_videoCodecCtx,codecparams);
                if(avcodec_open2(m_videoCodecCtx,codec,nullptr)<0)//打开解码器
                {
                    return false;
                }

            }
        }

    }
    return true;

}
void videoplayer::decodeFrame()//解码
{
    if(!m_formatCtx||!m_isPlaying)
    {
        return;
    }
    AVPacket packet;
    AVFrame *frame = av_frame_alloc();

    while (av_read_frame(m_formatCtx, &packet) >= 0) {
        // 处理视频流
        if (packet.stream_index != m_videoStreamIndex) {
            av_packet_unref(&packet);
            continue;
        }
        // 尝试解码
        if (avcodec_send_packet(m_videoCodecCtx, &packet) != 0 ||
            avcodec_receive_frame(m_videoCodecCtx, frame) != 0) {
            av_packet_unref(&packet);
            continue;
        }
        // 解码成功,处理帧
        QImage image = convertFrameToImage(frame);//格式转换QImage VideoPlayer::convertFrameToImage函数
        emit videoFrame(image);

        av_packet_unref(&packet);
        break;
    }
    av_frame_free(&frame);

}
QImage videoplayer::convertFrameToImage(AVFrame *frame)
{
    if(!frame||!frame->data[0])
    {
        return QImage();
    }
    if (!m_swsCtx ||
        frame->width != m_videoCodecCtx->width ||
        frame->height != m_videoCodecCtx->height ||
        frame->format != m_videoCodecCtx->pix_fmt) {//检查转换上下文是否存在以及尺寸是否与视频流标准格式一致,不存在创建

        if(m_swsCtx)
        {
            sws_freeContext(m_swsCtx);//释放旧的上下文
        }
        m_swsCtx=sws_getContext(frame->width,frame->height,(AVPixelFormat)frame->format,
                                  frame->width,frame->height,AV_PIX_FMT_RGB32,SWS_BILINEAR,nullptr,nullptr,nullptr);


    }
    QImage image(frame->width, frame->height, QImage::Format_RGB32);//创建目标图像

    uint8_t *destDate[1]={image.bits()};
    int destLinesize[1]={static_cast<int>(image.bytesPerLine())};//设置转换格式


    //执行转换格式
    sws_scale(m_swsCtx,frame->data,frame->linesize,0,frame->height,destDate,destLinesize);
    return  image;



}
void videoplayer::play()
{
    qDebug() << "videoplayer:开始播放";
    if (!m_formatCtx) {
        qDebug() << "错误:m_formatCtx 为空";
        return;
    }
    m_isPlaying = true;
    m_pause=false;
    m_timer->start(frame_rate);  // 约30fps,33ms一帧
    qDebug() << "定时器已启动";
    emit playstatechange(true);
}
void videoplayer::pause()
{
    qDebug() << "videoplayer:暂停";
    m_pause=!m_pause;//这个要注意不能直接写等于true;否则就无法从暂停状态切换出来
    if(m_pause)
    {
        m_timer->stop();//解码时钟停止
    }
    else
    {
        m_timer->start(frame_rate);
    }
    emit playstatechange(!m_pause);
}

重点讲解:

1.AVFormatContext *m_formatCtx = nullptr;

AVFormatContext 是 FFmpeg 中媒体容器格式的上下文管理器 ,它是处理多媒体文件的总控制中心

这是他存储的流信息

cpp 复制代码
// 存储媒体文件的所有流信息
typedef struct AVFormatContext {
    unsigned int nb_streams;           // 流的数量
    AVStream **streams;                // 流数组指针(视频流、音频流、字幕流等)
    
    // 文件基本信息
    int64_t duration;                  // 媒体总时长(微秒)
    int64_t bit_rate;                  // 比特率
    AVDictionary *metadata;            // 元数据(标题、作者、专辑等)
    
    // 输入输出相关
    AVInputFormat *iformat;            // 输入格式
    AVOutputFormat *oformat;           // 输出格式
    AVIOContext *pb;                   // I/O 上下文
} AVFormatContext;

2.AVCodecContext *m_videoCodecCtx = nullptr;

AVCodecContext 是 FFmpeg 中视频编解码器的上下文管理器,它负责管理特定视频流的解码或编码过程。

存储视频流解码参数

cpp 复制代码
// 包含的关键参数信息:
typedef struct AVCodecContext {
    // 基础信息
    enum AVCodecID codec_id;          // 编解码器ID (H.264, H.265, MPEG-4等)
    int width, height;                // 视频帧的宽度和高度
    AVRational time_base;             // 时间基准(帧率相关)
    
    // 视频格式信息
    enum AVPixelFormat pix_fmt;       // 像素格式 (YUV420P, NV12, RGB24等)
    AVRational sample_aspect_ratio;   // 样本宽高比
    int coded_width, coded_height;    // 编码后的宽高(可能包含padding)
    
    // 码流信息
    int64_t bit_rate;                 // 比特率
    int gop_size;                     // GOP大小(关键帧间隔)
    int max_b_frames;                 // 最大B帧数量
    
    // 解码器状态
    // ... 内部状态管理
} AVCodecContext;
  1. SwsContext *m_swsCtx = nullptr;

SwsContext 是 FFmpeg 中图像缩放和格式转换的上下文,负责在不同像素格式和图像尺寸之间进行转换。

像素格式转换

cpp 复制代码
// 支持的各种像素格式转换
YUV420P → RGB32      // 最常用:YUV转RGB
YUV422P → RGB24      // 其他格式转换
NV12    → RGB32
GRAY8   → RGB32      // 灰度图转彩色
// ... 支持数十种格式转换

图像缩放

cpp 复制代码
// 支持各种缩放算法
SWS_FAST_BILINEAR    // 快速双线性(速度优先)
SWS_BILINEAR         // 双线性插值(质量与速度平衡)
SWS_BICUBIC          // 双三次插值(高质量)
SWS_LANCZOS          // Lanczos 重采样(最高质量)

3.3主窗口

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include"videoplayer.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();


private slots:
    void on_openfile_clicked();
    void on_play_clicked();
    void onStateChanged(bool playing);
private:
    Ui::MainWindow *ui;

    void onVideoFrame(const QImage &frame);
    videoplayer *m_player;
};
#endif // MAINWINDOW_H

mainwindow.cpp

cpp 复制代码
​
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include<QFileDialog>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->play->setEnabled(false);
    m_player=new videoplayer(this);
    connect(m_player,&videoplayer::videoFrame, this, &MainWindow::onVideoFrame);
    connect(m_player,&videoplayer::playstatechange,this,&MainWindow::onStateChanged);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_openfile_clicked()
{
    QString filename = QFileDialog::getOpenFileName(this,
                                                    "打开视频文件", "","视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)");
    if(!filename.isEmpty())
    {
        // 打开视频文件
        if(m_player->openFile(filename)) {
            // 视频文件打开成功,改变播放按钮状态
            ui->play->setEnabled(true);
        } else {
            // 文件打开失败处理
            ui->play->setEnabled(false);
            qDebug() << "打开文件失败:" << filename;
        }
    }
}
void MainWindow::onVideoFrame(const QImage &frame)//显示视频帧
{
    QPixmap pixmap = QPixmap::fromImage(frame);
    ui->video->setPixmap(pixmap.scaled(ui->video->size(), Qt::KeepAspectRatio));
}

void MainWindow::on_play_clicked()
{
    if(m_player->isPlaying())
    {
        m_player->pause();
    }
    else
    {
        m_player->play();
    }
}
void MainWindow::onStateChanged(bool playing)
{
    ui->play->setText(playing ? "暂停" : "播放");
}


​

四、拓展功能

4.1.音频

音频的处理逻辑和视频很像

cpp 复制代码
原始音频数据 → 解码 → 重采样(swr_convert) → 格式转换 → 音频输出

所以不用新增太多函数

4.1.1.ui

增加一个即可

4.1.2.videoplayer.h

新增代码:

cpp 复制代码
  void setVolume(float volume); // 设置音量
  //================================= 处理声音模块==================================
    // 音频相关成员变量
    AVCodecContext *m_audioCodecCtx = nullptr;
    int m_audioStreamIndex = -1;
    SwrContext *m_swrCtx = nullptr;
    float m_volume = 1.0f;
    bool m_audioEnabled = false;
    // Qt6 音频输出
    QAudioSink *m_audioSink = nullptr;
    QIODevice *m_audioDevice = nullptr;
    QAudioFormat m_audioFormat;
    // 音频同步
    int64_t m_audioPts = 0;
    AVRational m_audioTimeBase;
    // 音频缓冲区
    QByteArray m_audioBuffer;

    // 音频相关函数
    bool initAudio(); // 初始化音频
    void decodeAudioFrame(AVFrame *frame);

整体代码:

cpp 复制代码
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QObject>
#include <QImage>
#include<QTimer>

#include <QAudioSink>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
extern "C" {
#include <libavformat/avformat.h>//媒体容器格式处理
#include <libavcodec/avcodec.h>//音视频编解码
#include <libswscale/swscale.h>//图像缩放和格式转换
#include <libswresample/swresample.h>  // 添加音频重采样
#include <libavutil/opt.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/channel_layout.h>  // 声道布局相关
#include <libavutil/error.h>  // 错误处理

}
class videoplayer : public QObject
{
    Q_OBJECT
public:
    explicit videoplayer(QObject *parent = nullptr);
    ~videoplayer();

    bool openFile(const QString &filename);//打开媒体文件
    void play();//播放,启动解码定时器,开始播放媒体文件
    void pause();//暂停
    bool isPlaying() const { return m_isPlaying; }//检查是否播放
    void setVolume(float volume); // 设置音量


private slots:
    void decodeFrame();//解码音视频帧
signals:
    void playstatechange(bool playing);//播放状态改变
    void videoFrame(const QImage &frame);//将解码出的视频帧传递到界面显示

private:
    //================================= 处理视频模块==========================================================
    QImage convertFrameToImage(AVFrame *frame);// 将FFmpeg帧转换为Qt图像,使用libswscale进行YUV到RGB格式转换
    AVFormatContext *m_formatCtx = nullptr;//媒体格式上下文,管理文件容器
    AVCodecContext *m_videoCodecCtx = nullptr;//视频编解码器上下文,管理视频解码;存储视频流的解码参数
    SwsContext *m_swsCtx = nullptr;//图像缩放转换上下文,YUV转RGB

    int m_videoStreamIndex = -1;  //视频流在文件中的索引,-1表示未找到

    bool m_isPlaying = false;//播放状态标志
    bool m_pause=false;
    int frame_rate=33;
    QTimer *m_timer;  // 解码定时器,控制解码帧率;这个是控制策略,播放、暂停、停止、重新播放都是通过这个控制的
    //原理:每隔固定时间触发timeout()信号,调用decodeFrame()而decodeFrame()函数会解码后生成一帧(就是一张图片)



    //================================= 处理声音模块==========================================================
    // 音频相关成员变量
    AVCodecContext *m_audioCodecCtx = nullptr;
    int m_audioStreamIndex = -1;
    SwrContext *m_swrCtx = nullptr;
    float m_volume = 1.0f;
    bool m_audioEnabled = false;
    // Qt6 音频输出
    QAudioSink *m_audioSink = nullptr;
    QIODevice *m_audioDevice = nullptr;
    QAudioFormat m_audioFormat;
    // 音频同步
    int64_t m_audioPts = 0;
    AVRational m_audioTimeBase;
    // 音频缓冲区
    QByteArray m_audioBuffer;

    // 音频相关函数
    bool initAudio(); // 初始化音频
    void decodeAudioFrame(AVFrame *frame);





};

#endif // VIDEOPLAYER_H

4.1.3videoplayer.cpp

新增代码基本上都和视频处理函数重叠了,在

openFile函数中添加了获取音频流的逻辑

新增decodeAudioFrame用于处理音频解码

decodeFrame函数中添加了调用解码音频流的逻辑

setVolume函数处理调节音量

完整代码:

cpp 复制代码
#include "videoplayer.h"
#include <qDebug>

videoplayer::videoplayer(QObject *parent)
    : QObject{parent}
{
    m_timer = new QTimer(this);//创建定时器用于控制视频解码节奏
    connect(m_timer, &QTimer::timeout, this, &videoplayer::decodeFrame);//连接定时器超时信号到解码帧槽函数
}

videoplayer::~videoplayer()
{
    if (m_timer) m_timer->stop();//清理定时器
    if (m_audioSink) {
        m_audioSink->stop();
        delete m_audioSink;//清理音频输出设备
    }
    //释放FFmpeg相关资源
    if (m_swrCtx) swr_free(&m_swrCtx);//音频重采样上下文
    if (m_swsCtx) sws_freeContext(m_swsCtx);//视频像素格式转换上下文
    if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);//音频编解码上下文
    if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);//视频编解码上下文
    if (m_formatCtx) avformat_close_input(&m_formatCtx);//格式上下文
}
//打开媒体文件
bool videoplayer::openFile(const QString &filename)
{
    //如果已经打开了文件,先清理之前的资源
    if (m_formatCtx) {
        if (m_timer) m_timer->stop();
        if (m_audioSink) {
            m_audioSink->stop();
            delete m_audioSink;
            m_audioSink = nullptr;
        }
        if (m_swrCtx) swr_free(&m_swrCtx);
        if (m_swsCtx) sws_freeContext(m_swsCtx);
        if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);
        if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);
        avformat_close_input(&m_formatCtx);
    }

    //打开媒体文件
    if(avformat_open_input(&m_formatCtx,filename.toUtf8().constData(),nullptr,nullptr)!=0) return false;
    //获取流信息
    if(avformat_find_stream_info(m_formatCtx,nullptr)<0) return false;

    for(int i=0;i<m_formatCtx->nb_streams;i++)//遍历所有流,查找视频流和音频流
    {
        AVCodecParameters *codecparams=m_formatCtx->streams[i]->codecpar;
        //处理视频流
        if(codecparams->codec_type==AVMEDIA_TYPE_VIDEO&&m_videoStreamIndex==-1)
        {
            m_videoStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_videoCodecCtx=avcodec_alloc_context3(codec);//分配视频解码上下文
                avcodec_parameters_to_context(m_videoCodecCtx,codecparams);//将编解码参数复制到编解码上下文
                if(avcodec_open2(m_videoCodecCtx,codec,nullptr)<0) return false;//打开视频编解码器
            }
        }
        //处理音频流
        else if(codecparams->codec_type==AVMEDIA_TYPE_AUDIO&&m_audioStreamIndex==-1)
        {
            m_audioStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_audioCodecCtx=avcodec_alloc_context3(codec);//分配音频编解码上下文
                avcodec_parameters_to_context(m_audioCodecCtx,codecparams);
                if(avcodec_open2(m_audioCodecCtx,codec,nullptr)<0)//打开音频编解码器
                {
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                //配置音频重采样器(将音频转换为统一的44.1kHz立体声格式)
                AVChannelLayout in_layout = {}, out_layout = {};
                av_channel_layout_default(&in_layout, m_audioCodecCtx->ch_layout.nb_channels);
                av_channel_layout_from_mask(&out_layout, AV_CH_LAYOUT_STEREO);
                //创建音频重采样上下文
                int ret = swr_alloc_set_opts2(&m_swrCtx,
                                              &out_layout, AV_SAMPLE_FMT_S16, 44100,
                                              &in_layout, m_audioCodecCtx->sample_fmt, m_audioCodecCtx->sample_rate,
                                              0, nullptr);

                av_channel_layout_uninit(&in_layout);
                av_channel_layout_uninit(&out_layout);

                if (ret < 0 || !m_swrCtx) {
                    // 处理错误
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                //初始化音频重采样器
                if(m_swrCtx && swr_init(m_swrCtx) >= 0)
                {
                    //配置音频输出格式
                    m_audioFormat.setSampleRate(44100);//采样率44.1kHz
                    m_audioFormat.setChannelCount(2);//立体声
                    m_audioFormat.setSampleFormat(QAudioFormat::Int16);

                    QAudioDevice device = QMediaDevices::defaultAudioOutput();//获取默认音频输出设备
                    if(!device.isNull())
                    {
                        if(!device.isFormatSupported(m_audioFormat))//检查格式支持,如果不支持则使用设备首选格式
                            m_audioFormat = device.preferredFormat();

                        m_audioSink = new QAudioSink(device, m_audioFormat, this);//创建音频输出设备
                        m_audioSink->setVolume(m_volume);
                        m_audioSink->setBufferSize(44100 * 2 * 2 / 10); // 设置0.1秒的缓冲
                        m_audioDevice = m_audioSink->start();
                        m_audioEnabled = (m_audioDevice != nullptr);
                    }
                }
            }
        }
    }
    return true;
}

void videoplayer::decodeFrame()//解码帧函数:由定时器定期调用
{
    if(!m_formatCtx || !m_isPlaying) return;

    AVPacket packet;
    AVFrame *frame = av_frame_alloc();

    int packet_count = 0;//每次解码最多处理10个数据包
    while (packet_count < 10 && av_read_frame(m_formatCtx, &packet) >= 0) {
        //处理音频包
        if (m_audioEnabled && packet.stream_index == m_audioStreamIndex) {
            if (avcodec_send_packet(m_audioCodecCtx, &packet) == 0) {
                while (avcodec_receive_frame(m_audioCodecCtx, frame) == 0) {
                    decodeAudioFrame(frame);
                }
            }
        }
        //处理视频包
        else if (packet.stream_index == m_videoStreamIndex) {
            if (avcodec_send_packet(m_videoCodecCtx, &packet) == 0) {
                if (avcodec_receive_frame(m_videoCodecCtx, frame) == 0) {
                    QImage image = convertFrameToImage(frame);
                    emit videoFrame(image);
                    av_packet_unref(&packet);
                    av_frame_free(&frame);
                    return;//每次只解码一帧视频
                }
            }
        }
        av_packet_unref(&packet);
        packet_count++;
    }
    av_frame_free(&frame);
}

QImage videoplayer::convertFrameToImage(AVFrame *frame)//将AVFrame转换为QImage
{
    if(!frame||!frame->data[0]) return QImage();
    //检查是否需要重新创建像素格式转换上下文
    if (!m_swsCtx || frame->width != m_videoCodecCtx->width ||
        frame->height != m_videoCodecCtx->height ||
        frame->format != m_videoCodecCtx->pix_fmt) {

        if(m_swsCtx) sws_freeContext(m_swsCtx);
        //创建像素格式转换上下文(转换为RGB32格式)
        m_swsCtx=sws_getContext(frame->width,frame->height,(AVPixelFormat)frame->format,
                                  frame->width,frame->height,AV_PIX_FMT_RGB32,SWS_BILINEAR,nullptr,nullptr,nullptr);
    }

    QImage image(frame->width, frame->height, QImage::Format_RGB32);//创建QImage用于存储转换后的图像
    uint8_t *destDate[1] = {image.bits()};
    int destLinesize[1] = {static_cast<int>(image.bytesPerLine())};
    //执行像素格式转换
    sws_scale(m_swsCtx, frame->data, frame->linesize, 0, frame->height, destDate, destLinesize);
    return image;
}

void videoplayer::play()
{
    if (!m_formatCtx) return;
    m_isPlaying = true;
    m_pause = false;
    //处理音频输出状态
    if (m_audioEnabled && m_audioSink) {
        if (m_audioSink->state() == QAudio::SuspendedState) {
            m_audioSink->resume();//从暂停状态恢复
        } else if (m_audioSink->state() == QAudio::StoppedState) {
            m_audioDevice = m_audioSink->start();//重新启动
        }
    }

    m_timer->start(frame_rate);//启动定时器开始解码帧
    emit playstatechange(true);//发射播放状态改变信号
}

void videoplayer::pause()
{
    m_pause = !m_pause;

    if(m_pause)
    {
        m_timer->stop();//停止定时器
        if (m_audioEnabled && m_audioSink) m_audioSink->suspend();//暂停音频
    }
    else
    {
        m_timer->start(frame_rate);//重新启动定时器
        if (m_audioEnabled && m_audioSink) m_audioSink->resume();//恢复音频
    }
    emit playstatechange(!m_pause);//发射播放状态改变信号
}

void videoplayer::decodeAudioFrame(AVFrame *frame)//解码音频帧
{
    if (!frame || !m_audioSink || !m_audioDevice || !m_swrCtx) return;
    //计算重采样后的样本数
    int out_samples = av_rescale_rnd(swr_get_delay(m_swrCtx, m_audioCodecCtx->sample_rate) + frame->nb_samples,
                                     44100, m_audioCodecCtx->sample_rate, AV_ROUND_UP);

    uint8_t **resampled_data = nullptr;
    int linesize;
    //分配重采样输出缓冲区
    if (av_samples_alloc_array_and_samples(&resampled_data, &linesize, 2, out_samples, AV_SAMPLE_FMT_S16, 0) < 0) return;
    //执行音频重采样
    int samples_converted = swr_convert(m_swrCtx, resampled_data, out_samples, (const uint8_t**)frame->data, frame->nb_samples);

    if (samples_converted > 0) {
        int data_size = samples_converted * 2 * 2;//计算数据大小
        //应用音量调节
        if (m_volume != 1.0f) {
            int16_t *samples = (int16_t*)resampled_data[0];
            for (int i = 0; i < samples_converted * 2; i++) samples[i] = (int16_t)(samples[i] * m_volume);
        }
        //将音频数据写入音频设备
        if (m_audioDevice && m_audioDevice->isOpen()) m_audioDevice->write((const char*)resampled_data[0], data_size);
    }
    //释放重采样数据缓冲区
    if (resampled_data) {
        if (resampled_data[0]) av_freep(&resampled_data[0]);
        av_freep(&resampled_data);
    }
}

void videoplayer::setVolume(float volume)
{
    m_volume = qBound(0.0f, volume, 1.0f);
    if (m_audioSink) m_audioSink->setVolume(m_volume);
}

4.1.4mainwindow

这个都没啥改动很少就直接给了:

mainwindow.h

cpp 复制代码
#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include"videoplayer.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class MainWindow;
}
QT_END_NAMESPACE

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();


private slots:
    void on_openfile_clicked();
    void on_play_clicked();
    void onStateChanged(bool playing);
    void on_audio_actionTriggered(int action);

private:
    Ui::MainWindow *ui;

    void onVideoFrame(const QImage &frame);
    videoplayer *m_player;
};
#endif // MAINWINDOW_H

mainwindow.cpp

cpp 复制代码
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include<QFileDialog>
MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->play->setEnabled(false);
    m_player=new videoplayer(this);
    connect(m_player,&videoplayer::videoFrame, this, &MainWindow::onVideoFrame);
    connect(m_player,&videoplayer::playstatechange,this,&MainWindow::onStateChanged);
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_openfile_clicked()
{
    QString filename = QFileDialog::getOpenFileName(this,
                                                    "打开视频文件", "","视频文件 (*.mp4 *.avi *.mkv *.mov *.flv *.wmv);;所有文件 (*.*)");
    if(!filename.isEmpty())
    {
        // 打开视频文件
        if(m_player->openFile(filename)) {
            // 视频文件打开成功,改变播放按钮状态
            ui->play->setEnabled(true);
        } else {
            // 文件打开失败处理
            ui->play->setEnabled(false);
            qDebug() << "打开文件失败:" << filename;
        }
    }
}
void MainWindow::onVideoFrame(const QImage &frame)//显示视频帧
{
    QPixmap pixmap = QPixmap::fromImage(frame);
    ui->video->setPixmap(pixmap.scaled(ui->video->size(), Qt::KeepAspectRatio));
}

void MainWindow::on_play_clicked()
{
    if(m_player->isPlaying())
    {
        m_player->pause();
    }
    else
    {
        m_player->play();
    }
}
void MainWindow::onStateChanged(bool playing)
{
    ui->play->setText(playing ? "暂停" : "播放");
}


void MainWindow::on_audio_actionTriggered(int action)
{
    float volume = action/ 100.0f; // 转换为0.0-1.0范围
    m_player->setVolume(volume);
}

写到这里githup地址: https://github.com/orpheanlive/Qt_FFmpeg.git

4.2.问题

上面虽然实现了ffmpeg播放器的基本功能但效果只能满足简单使用。

当前代码问题分析

  1. 没有音视频同步机制 - 音频和视频各自独立播放

  2. 固定帧率 - 使用33ms固定间隔,不匹配视频实际帧率

4.3.优化

4.3.1添加音视频同步机制

videoplayer.h

cpp 复制代码
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QObject>
#include <QImage>
#include<QTimer>
#include <QAudioSink>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include<QThread>
extern "C" {
#include <libavformat/avformat.h>//媒体容器格式处理
#include <libavcodec/avcodec.h>//音视频编解码
#include <libswscale/swscale.h>//图像缩放和格式转换
#include <libswresample/swresample.h>  // 添加音频重采样
#include <libavutil/opt.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/channel_layout.h>  // 声道布局相关
#include <libavutil/error.h>  // 错误处理

}
class videoplayer : public QObject
{
    Q_OBJECT
public:
    explicit videoplayer(QObject *parent = nullptr);
    ~videoplayer();

    bool openFile(const QString &filename);//打开媒体文件
    void play();//播放,启动解码定时器,开始播放媒体文件
    void pause();//暂停
    bool isPlaying() const { return m_isPlaying; }//检查是否播放
    void setVolume(float volume); // 设置音量


private slots:
    void decodeFrame();//解码音视频帧
signals:
    void playstatechange(bool playing);//播放状态改变
    void videoFrame(const QImage &frame);//将解码出的视频帧传递到界面显示

private:
    //================================= 处理视频模块==========================================================
    QImage convertFrameToImage(AVFrame *frame);// 将FFmpeg帧转换为Qt图像,使用libswscale进行YUV到RGB格式转换
    AVFormatContext *m_formatCtx = nullptr;//媒体格式上下文,管理文件容器
    AVCodecContext *m_videoCodecCtx = nullptr;//视频编解码器上下文,管理视频解码;存储视频流的解码参数
    SwsContext *m_swsCtx = nullptr;//图像缩放转换上下文,YUV转RGB

    int m_videoStreamIndex = -1;  //视频流在文件中的索引,-1表示未找到

    bool m_isPlaying = false;//播放状态标志
    bool m_pause=false;
    int frame_rate=33;
    QTimer *m_timer;  // 解码定时器,控制解码帧率;这个是控制策略,播放、暂停、停止、重新播放都是通过这个控制的
    //原理:每隔固定时间触发timeout()信号,调用decodeFrame()而decodeFrame()函数会解码后生成一帧(就是一张图片)



    //================================= 处理声音模块==========================================================
    // 音频相关成员变量
    AVCodecContext *m_audioCodecCtx = nullptr;
    int m_audioStreamIndex = -1;
    SwrContext *m_swrCtx = nullptr;
    float m_volume = 1.0f;
    bool m_audioEnabled = false;
    // Qt6 音频输出
    QAudioSink *m_audioSink = nullptr;
    QIODevice *m_audioDevice = nullptr;
    QAudioFormat m_audioFormat;
    // 音频同步
    int64_t m_audioPts = 0;
    AVRational m_audioTimeBase;
    // 音频缓冲区
    QByteArray m_audioBuffer;

    // 音频相关函数
    bool initAudio(); // 初始化音频
    void decodeAudioFrame(AVFrame *frame);


    // ========== 音视频同步核心成员 ==========
    double m_audioClock = 0.0;              // 音频时钟(基于PTS)
    double m_videoClock = 0.0;              // 视频时钟
    AVRational m_videoTimeBase;             // 视频时间基准
    int64_t m_startPts = AV_NOPTS_VALUE;    // 起始PTS,用于计算相对时间
    double m_videoFps = 0.0;                // 视频实际帧率
    double m_syncThreshold = 0.03;          // 同步阈值:30ms
    double m_maxFrameDelay = 0.1;           // 最大帧延迟:100ms

    // 音频缓冲区相关
    int64_t m_audioBytesWritten = 0;        // 已写入音频设备的字节数

    // 同步函数声明
    double getAudioClock();                  // 获取音频时钟
    double getVideoClock(int64_t pts);       // 获取视频时钟
    double calculateFrameDelay(double video_pts, double audio_pts); // 计算帧延迟
    int64_t m_audioStartPts = 0;  // 音频起始PTS

};

#endif // VIDEOPLAYER_H

videoplayer.cpp

cpp 复制代码
#include "videoplayer.h"
#include <qDebug>

videoplayer::videoplayer(QObject *parent)
    : QObject{parent}
{
    m_timer = new QTimer(this);//创建定时器用于控制视频解码节奏
    connect(m_timer, &QTimer::timeout, this, &videoplayer::decodeFrame);//连接定时器超时信号到解码帧槽函数
}

videoplayer::~videoplayer()
{
    if (m_timer) m_timer->stop();//清理定时器
    if (m_audioSink) {
        m_audioSink->stop();
        delete m_audioSink;//清理音频输出设备
    }
    //释放FFmpeg相关资源
    if (m_swrCtx) swr_free(&m_swrCtx);//音频重采样上下文
    if (m_swsCtx) sws_freeContext(m_swsCtx);//视频像素格式转换上下文
    if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);//音频编解码上下文
    if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);//视频编解码上下文
    if (m_formatCtx) avformat_close_input(&m_formatCtx);//格式上下文
}
//打开媒体文件
bool videoplayer::openFile(const QString &filename)
{
    //如果已经打开了文件,先清理之前的资源
    if (m_formatCtx) {
        if (m_timer) m_timer->stop();
        if (m_audioSink) {
            m_audioSink->stop();
            delete m_audioSink;
            m_audioSink = nullptr;
        }
        if (m_swrCtx) swr_free(&m_swrCtx);
        if (m_swsCtx) sws_freeContext(m_swsCtx);
        if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);
        if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);
        avformat_close_input(&m_formatCtx);
    }

    //打开媒体文件
    if(avformat_open_input(&m_formatCtx,filename.toUtf8().constData(),nullptr,nullptr)!=0) return false;
    //获取流信息
    if(avformat_find_stream_info(m_formatCtx,nullptr)<0) return false;

    for(int i=0;i<m_formatCtx->nb_streams;i++)//遍历所有流,查找视频流和音频流
    {
        AVCodecParameters *codecparams=m_formatCtx->streams[i]->codecpar;//描述编解码器的参数和特性
        //处理视频流
        if(codecparams->codec_type==AVMEDIA_TYPE_VIDEO&&m_videoStreamIndex==-1)
        {
            m_videoStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);//通编解码器id查找编解码器
            if(codec)
            {
                m_videoCodecCtx=avcodec_alloc_context3(codec);//分配视频解码上下文
                avcodec_parameters_to_context(m_videoCodecCtx,codecparams);//将编解码参数复制到编解码上下文
                if(avcodec_open2(m_videoCodecCtx,codec,nullptr)<0) return false;//打开视频编解码器

                // ========== 获取视频时间基准和帧率 ==========
                m_videoTimeBase = m_formatCtx->streams[m_videoStreamIndex]->time_base;

                // 计算视频实际帧率
                AVRational avg_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->avg_frame_rate;
                if (avg_frame_rate.num > 0 && avg_frame_rate.den > 0) {
                    m_videoFps = av_q2d(avg_frame_rate);
                } else {
                    // 备用方法:从编解码器获取
                    AVRational r_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->r_frame_rate;
                    if (r_frame_rate.num > 0 && r_frame_rate.den > 0) {
                        m_videoFps = av_q2d(r_frame_rate);
                    } else {
                        m_videoFps = 30.0; // 默认30fps
                    }
                }

            }
        }
        //处理音频流
        else if(codecparams->codec_type==AVMEDIA_TYPE_AUDIO&&m_audioStreamIndex==-1)
        {
            m_audioStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_audioCodecCtx=avcodec_alloc_context3(codec);//分配音频编解码上下文
                avcodec_parameters_to_context(m_audioCodecCtx,codecparams);
                if(avcodec_open2(m_audioCodecCtx,codec,nullptr)<0)//打开音频编解码器
                {
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                m_audioTimeBase = m_formatCtx->streams[m_audioStreamIndex]->time_base;
                //配置音频重采样器(将音频转换为统一的44.1kHz立体声格式)
                AVChannelLayout in_layout = {}, out_layout = {};
                av_channel_layout_default(&in_layout, m_audioCodecCtx->ch_layout.nb_channels);
                av_channel_layout_from_mask(&out_layout, AV_CH_LAYOUT_STEREO);
                //创建音频重采样上下文
                int ret = swr_alloc_set_opts2(&m_swrCtx,
                                              &out_layout, AV_SAMPLE_FMT_S16, 44100,
                                              &in_layout, m_audioCodecCtx->sample_fmt, m_audioCodecCtx->sample_rate,
                                              0, nullptr);

                av_channel_layout_uninit(&in_layout);
                av_channel_layout_uninit(&out_layout);

                if (ret < 0 || !m_swrCtx) {
                    // 处理错误
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                //初始化音频重采样器
                if(m_swrCtx && swr_init(m_swrCtx) >= 0)
                {
                    //配置音频输出格式
                    m_audioFormat.setSampleRate(44100);//采样率44.1kHz
                    m_audioFormat.setChannelCount(2);//立体声
                    m_audioFormat.setSampleFormat(QAudioFormat::Int16);
                    // ========== 初始化音频写入字节数 ==========
                    m_audioBytesWritten= 0;  // 初始化音频写入字节数
                    QAudioDevice device = QMediaDevices::defaultAudioOutput();//获取默认音频输出设备
                    if(!device.isNull())
                    {
                        if(!device.isFormatSupported(m_audioFormat))//检查格式支持,如果不支持则使用设备首选格式
                            m_audioFormat = device.preferredFormat();

                        m_audioSink = new QAudioSink(device, m_audioFormat, this);//创建音频输出设备
                        m_audioSink->setVolume(m_volume);
                        m_audioSink->setBufferSize(44100 * 2 * 2 / 10); // 设置0.1秒的缓冲
                        m_audioDevice = m_audioSink->start();
                        m_audioEnabled = (m_audioDevice != nullptr);
                    }
                }
            }
        }
    }
    return true;
}

void videoplayer::decodeFrame()//解码帧函数:由定时器定期调用
{
    if(!m_formatCtx || !m_isPlaying) return;

    AVPacket packet;//从媒体文件中读取的原始数据包,存储压缩的媒体数据(编码后的数据)
    AVFrame *frame = av_frame_alloc();//包含一帧完整的未压缩数据,存储解码后的原始媒体数据

    int packet_count = 0;//每次解码最多处理10个数据包
    while (packet_count < 10 && av_read_frame(m_formatCtx, &packet) >= 0) {
        //处理音频包
        if (m_audioEnabled && packet.stream_index == m_audioStreamIndex) {
            if (avcodec_send_packet(m_audioCodecCtx, &packet) == 0) {
                while (avcodec_receive_frame(m_audioCodecCtx, frame) == 0) {
                    decodeAudioFrame(frame);
                }
            }
        }
        //处理视频包
        else if (packet.stream_index == m_videoStreamIndex) {
            if (avcodec_send_packet(m_videoCodecCtx, &packet) == 0) {
                if (avcodec_receive_frame(m_videoCodecCtx, frame) == 0) {
                    // ========== 音视频同步处理开始 ==========
                    // 计算视频帧PTS
                    int64_t pts = frame->pts;
                    if (pts == AV_NOPTS_VALUE) {
                        pts = frame->pkt_dts;  // 如果PTS无效,使用DTS
                    }

                    // 记录起始PTS(第一帧)
                    if (m_startPts == AV_NOPTS_VALUE) {
                        m_startPts = pts;
                    }

                    // 计算视频时钟(相对于起始PTS的时间,单位:秒)
                    double video_pts = getVideoClock(pts);
                    // 获取音频时钟(当前音频播放位置,单位:秒)
                    double audio_pts = getAudioClock();
                    // 计算基础帧延迟(根据实际帧率)
                    double frame_delay = (m_videoFps > 0) ? (1.0 / m_videoFps) : 0.033;
                    double delay = frame_delay;
                    // 音视频同步:如果音频可用,调整视频帧延迟
                    if (m_audioEnabled) {
                        delay = calculateFrameDelay(video_pts, audio_pts);
                        // 调试输出
                        static int debug_count = 0;
                        if (debug_count++ % 50 == 0) {
                            qDebug() << "音视频同步 - 视频:" << (video_pts * 1000) << "ms, 音频:"
                                     << (audio_pts * 1000) << "ms, 差异:" << ((video_pts - audio_pts) * 1000)
                                     << "ms, 延迟:" << (delay * 1000) << "ms";
                        }
                    }

                    // 更新视频时钟
                    m_videoClock = video_pts + delay;

                    QImage image = convertFrameToImage(frame);

                    // 执行同步延迟
                    if (delay > 0 && delay < 0.1) {
                        int delay_ms = (int)(delay * 1000);
                        QThread::msleep(delay_ms);
                    }

                    emit videoFrame(image);
                    // ========== 音视频同步处理结束 ==========

                    av_packet_unref(&packet);

                    return;//每次只解码一帧视频
                }
            }
        }
        packet_count++;
    }

    av_frame_free(&frame);
}
QImage videoplayer::convertFrameToImage(AVFrame *frame)//将AVFrame转换为QImage
{
    if(!frame||!frame->data[0]) return QImage();
    //检查是否需要重新创建像素格式转换上下文
    if (!m_swsCtx || frame->width != m_videoCodecCtx->width ||
        frame->height != m_videoCodecCtx->height ||
        frame->format != m_videoCodecCtx->pix_fmt) {

        if(m_swsCtx) sws_freeContext(m_swsCtx);
        //创建像素格式转换上下文(转换为RGB32格式)
        m_swsCtx=sws_getContext(frame->width,frame->height,(AVPixelFormat)frame->format,
                                  frame->width,frame->height,AV_PIX_FMT_RGB32,SWS_BILINEAR,nullptr,nullptr,nullptr);
    }

    QImage image(frame->width, frame->height, QImage::Format_RGB32);//创建QImage用于存储转换后的图像
    uint8_t *destDate[1] = {image.bits()};
    int destLinesize[1] = {static_cast<int>(image.bytesPerLine())};
    //执行像素格式转换
    sws_scale(m_swsCtx, frame->data, frame->linesize, 0, frame->height, destDate, destLinesize);
    return image;
}

void videoplayer::play()
{
    // ========== 重置音视频同步状态 ==========
    m_audioClock = 0.0;
    m_videoClock = 0.0;
    m_startPts = AV_NOPTS_VALUE;
    m_audioBytesWritten = 0;
    m_audioPts = 0;

    if (!m_formatCtx) return;
    m_isPlaying = true;
    m_pause = false;

    // 处理音频输出状态
    if (m_audioEnabled && m_audioSink) {
        if (m_audioSink->state() == QAudio::SuspendedState) {
            m_audioSink->resume();
        } else if (m_audioSink->state() == QAudio::StoppedState) {
            m_audioDevice = m_audioSink->start();
            // ========== 修复:重置音频写入计数 ==========
            m_audioBytesWritten = 0;
        }
    }

    m_timer->start(frame_rate);
    emit playstatechange(true);
}

void videoplayer::pause()
{
    m_pause = !m_pause;

    if(m_pause)
    {
        m_timer->stop();//停止定时器
        if (m_audioEnabled && m_audioSink) m_audioSink->suspend();//暂停音频
    }
    else
    {
        m_timer->start(frame_rate);//重新启动定时器
        if (m_audioEnabled && m_audioSink) m_audioSink->resume();//恢复音频
    }
    emit playstatechange(!m_pause);//发射播放状态改变信号
}

void videoplayer::decodeAudioFrame(AVFrame *frame)//解码音频帧
{
    // ========== 更新音频PTS ==========
    if (frame->pts != AV_NOPTS_VALUE) {
        m_audioPts
            = frame->pts;
    } else if (frame->pkt_dts != AV_NOPTS_VALUE) {
        m_audioPts
            = frame->pkt_dts;
    }
    if (!frame || !m_audioSink || !m_audioDevice || !m_swrCtx) return;
    //计算重采样后的样本数
    int out_samples = av_rescale_rnd(swr_get_delay(m_swrCtx, m_audioCodecCtx->sample_rate) + frame->nb_samples,
                                     44100, m_audioCodecCtx->sample_rate, AV_ROUND_UP);

    uint8_t **resampled_data = nullptr;
    int linesize;
    //分配重采样输出缓冲区
    if (av_samples_alloc_array_and_samples(&resampled_data, &linesize, 2, out_samples, AV_SAMPLE_FMT_S16, 0) < 0) return;
    //执行音频重采样
    int samples_converted = swr_convert(m_swrCtx, resampled_data, out_samples, (const uint8_t**)frame->data, frame->nb_samples);

    if (samples_converted > 0) {
        int data_size = samples_converted * 2 * 2;//计算数据大小
        //应用音量调节
        if (m_volume != 1.0f) {
            int16_t *samples = (int16_t*)resampled_data[0];
            for (int i = 0; i < samples_converted * 2; i++) samples[i] = (int16_t)(samples[i] * m_volume);
        }
        //将音频数据写入音频设备
        if (m_audioDevice && m_audioDevice->isOpen()) {
            qint64 written = m_audioDevice->write((const char*)resampled_data[0], data_size);

            // ========== 更新音频时钟(基于写入字节数) ==========
            if (written > 0) {
                m_audioBytesWritten += written;
            }
        }
    }
    //释放重采样数据缓冲区
    if (resampled_data) {
        if (resampled_data[0]) av_freep(&resampled_data[0]);
        av_freep(&resampled_data);
    }
}

void videoplayer::setVolume(float volume)
{
    m_volume = qBound(0.0f, volume, 1.0f);
    if (m_audioSink) m_audioSink->setVolume(m_volume);
}
// ========== 音视频同步函数实现 ==========

double videoplayer::getAudioClock()
{
    if (!m_audioEnabled || !m_audioCodecCtx) {
        return 0.0;
    }

    // 方法1:基于PTS的精确计算
    if (m_audioPts != 0 && m_audioTimeBase.num > 0) {
        double audio_time = m_audioPts * av_q2d(m_audioTimeBase);
        return audio_time;
    }

    // 方法2:基于已播放音频数据的估算
    if (m_audioBytesWritten > 0 && m_audioFormat.isValid()) {
        // 计算已播放的时间(秒)= 已写入字节数 / (采样率 * 通道数 * 样本大小)
        double seconds_played = (double)m_audioBytesWritten /
                                (m_audioFormat.sampleRate() *
                                 m_audioFormat.channelCount() *
                                 (m_audioFormat.bytesPerSample()));
        return seconds_played;
    }

    return 0.0;
}

double videoplayer::getVideoClock(int64_t pts)
{
    if (pts == AV_NOPTS_VALUE) {
        return 0.0;
    }

    // 将PTS转换为秒:PTS * 时间基准
    double pts_time = pts * av_q2d(m_videoTimeBase);
    return pts_time;
}

double videoplayer::calculateFrameDelay(double video_pts, double audio_pts)
{
    // 计算基础帧延迟(根据实际帧率)
    double frame_delay = (m_videoFps > 0) ? (1.0 / m_videoFps) : 0.033;
    double diff = video_pts - audio_pts;
    double actual_delay = frame_delay;

    // 分级同步策略
    if (fabs(diff) < m_syncThreshold) {
        // 小差异:微小调整避免累积误差
        actual_delay = frame_delay + diff * 0.3;
    }
    else if (diff > 0) {
        // 视频超前音频:增加延迟等待音频
        actual_delay = frame_delay + diff * 0.7;
    }
    else {
        // 视频落后音频:减少延迟追赶音频
        actual_delay = frame_delay + diff; // diff为负值
        if (actual_delay < 0) actual_delay = 0;
    }

    // 限制延迟范围
    if (actual_delay > m_maxFrameDelay) {
        actual_delay = m_maxFrameDelay;
    }
    if (actual_delay < 0) {
        actual_delay = 0;
    }

    static int frameCount = 0;
    if (frameCount++ % 50 == 0) {
        qDebug() << "音视频同步信息 - 视频时钟:" << video_pts * 1000 << "ms, 音频时钟:"
                 << audio_pts * 1000 << "ms, 差异:" << (video_pts - audio_pts) * 1000
                 << "ms, 计算延迟:" << actual_delay * 1000 << "ms";
    }

    return actual_delay;
}

结果:延迟小于100ms

4.3.2.使用33ms固定间隔,不匹配视频实际帧率

我们可以在openfile函数中替换原本的30帧确定值:

cpp 复制代码
  // ========== 替换原有的帧率计算代码 ==========
                // 获取视频时间基准
                m_videoTimeBase = m_formatCtx->streams[m_videoStreamIndex]->time_base;

                // 计算视频实际帧率(多种方法尝试)
                double calculated_fps = 0.0;

                // // 方法1:使用平均帧率
                // AVRational avg_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->avg_frame_rate;
                // if (avg_frame_rate.num > 0 && avg_frame_rate.den > 0) {
                //     calculated_fps = av_q2d(avg_frame_rate);
                //     qDebug() << "使用平均帧率:" << calculated_fps << "fps";
                // }

                // 方法2:如果平均帧率无效,使用实时帧率
                if (calculated_fps <= 0) {
                    AVRational r_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->r_frame_rate;
                    if (r_frame_rate.num > 0 && r_frame_rate.den > 0) {
                        calculated_fps = av_q2d(r_frame_rate);
                        qDebug() << "使用实时帧率:" << calculated_fps << "fps";
                    }
                }

                // // 方法3:如果前两种方法都无效,根据码流信息估算
                // if (calculated_fps <= 0) {
                //     // 估算帧率:总帧数 / 持续时间
                //     int64_t duration = m_formatCtx->streams[m_videoStreamIndex]->duration;
                //     int64_t nb_frames = m_formatCtx->streams[m_videoStreamIndex]->nb_frames;

                //     if (duration > 0 && nb_frames > 0) {
                //         double stream_duration = duration * av_q2d(m_videoTimeBase);
                //         if (stream_duration > 0) {
                //             calculated_fps = nb_frames / stream_duration;
                //             qDebug() << "使用估算帧率:" << calculated_fps << "fps, 帧数:" << nb_frames << "时长:" << stream_duration;
                //         }
                //     }
                // }

                // // 方法4:如果所有方法都失败,使用默认值
                // if (calculated_fps <= 0) {
                //     calculated_fps = 30.0;
                //     qDebug() << "使用默认帧率: 30fps";
                // }

这是4种方法,可以通过注释一点一点调试。

videoplayer.h

cpp 复制代码
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H

#include <QObject>
#include <QImage>
#include<QTimer>
#include <QAudioSink>
#include <QMediaDevices>
#include <QAudioDevice>
#include <QAudioFormat>
#include<QThread>
extern "C" {
#include <libavformat/avformat.h>//媒体容器格式处理
#include <libavcodec/avcodec.h>//音视频编解码
#include <libswscale/swscale.h>//图像缩放和格式转换
#include <libswresample/swresample.h>  // 添加音频重采样
#include <libavutil/opt.h>
#include <libavutil/audio_fifo.h>
#include <libavutil/channel_layout.h>  // 声道布局相关
#include <libavutil/error.h>  // 错误处理

}
class videoplayer : public QObject
{
    Q_OBJECT
public:
    explicit videoplayer(QObject *parent = nullptr);
    ~videoplayer();

    bool openFile(const QString &filename);//打开媒体文件
    void play();//播放,启动解码定时器,开始播放媒体文件
    void pause();//暂停
    bool isPlaying() const { return m_isPlaying; }//检查是否播放
    void setVolume(float volume); // 设置音量


private slots:
    void decodeFrame();//解码音视频帧
signals:
    void playstatechange(bool playing);//播放状态改变
    void videoFrame(const QImage &frame);//将解码出的视频帧传递到界面显示

private:
    //================================= 处理视频模块==========================================================
    QImage convertFrameToImage(AVFrame *frame);// 将FFmpeg帧转换为Qt图像,使用libswscale进行YUV到RGB格式转换
    AVFormatContext *m_formatCtx = nullptr;//媒体格式上下文,管理文件容器
    AVCodecContext *m_videoCodecCtx = nullptr;//视频编解码器上下文,管理视频解码;存储视频流的解码参数
    SwsContext *m_swsCtx = nullptr;//图像缩放转换上下文,YUV转RGB

    int m_videoStreamIndex = -1;  //视频流在文件中的索引,-1表示未找到

    bool m_isPlaying = false;//播放状态标志
    bool m_pause=false;
    //int frame_rate=33;
    QTimer *m_timer;  // 解码定时器,控制解码帧率;这个是控制策略,播放、暂停、停止、重新播放都是通过这个控制的
    //原理:每隔固定时间触发timeout()信号,调用decodeFrame()而decodeFrame()函数会解码后生成一帧(就是一张图片)



    //================================= 处理声音模块==========================================================
    // 音频相关成员变量
    AVCodecContext *m_audioCodecCtx = nullptr;
    int m_audioStreamIndex = -1;
    SwrContext *m_swrCtx = nullptr;
    float m_volume = 1.0f;
    bool m_audioEnabled = false;
    // Qt6 音频输出
    QAudioSink *m_audioSink = nullptr;
    QIODevice *m_audioDevice = nullptr;
    QAudioFormat m_audioFormat;
    // 音频同步
    int64_t m_audioPts = 0;
    AVRational m_audioTimeBase;
    // 音频缓冲区
    QByteArray m_audioBuffer;

    // 音频相关函数
    bool initAudio(); // 初始化音频
    void decodeAudioFrame(AVFrame *frame);


    // ========== 音视频同步核心成员 ==========
    double m_audioClock = 0.0;              // 音频时钟(基于PTS)
    double m_videoClock = 0.0;              // 视频时钟
    AVRational m_videoTimeBase;             // 视频时间基准
    int64_t m_startPts = AV_NOPTS_VALUE;    // 起始PTS,用于计算相对时间
    double m_videoFps = 0.0;                // 视频实际帧率
    double m_syncThreshold = 0.03;          // 同步阈值:30ms
    double m_maxFrameDelay = 0.1;           // 最大帧延迟:100ms

    // 音频缓冲区相关
    int64_t m_audioBytesWritten = 0;        // 已写入音频设备的字节数

    // 同步函数声明
    double getAudioClock();                  // 获取音频时钟
    double getVideoClock(int64_t pts);       // 获取视频时钟
    double calculateFrameDelay(double video_pts, double audio_pts); // 计算帧延迟
    int64_t m_audioStartPts = 0;  // 音频起始PTS


      int m_timerInterval = 33;  // 定时器间隔,根据实际帧率调整

};

#endif // VIDEOPLAYER_H

videoplayer.cpp

cpp 复制代码
#include "videoplayer.h"
#include <qDebug>

videoplayer::videoplayer(QObject *parent)
    : QObject{parent}
{
    m_timer = new QTimer(this);//创建定时器用于控制视频解码节奏
    connect(m_timer, &QTimer::timeout, this, &videoplayer::decodeFrame);//连接定时器超时信号到解码帧槽函数
}

videoplayer::~videoplayer()
{
    if (m_timer) m_timer->stop();//清理定时器
    if (m_audioSink) {
        m_audioSink->stop();
        delete m_audioSink;//清理音频输出设备
    }
    //释放FFmpeg相关资源
    if (m_swrCtx) swr_free(&m_swrCtx);//音频重采样上下文
    if (m_swsCtx) sws_freeContext(m_swsCtx);//视频像素格式转换上下文
    if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);//音频编解码上下文
    if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);//视频编解码上下文
    if (m_formatCtx) avformat_close_input(&m_formatCtx);//格式上下文
}
//打开媒体文件
bool videoplayer::openFile(const QString &filename)
{
    //如果已经打开了文件,先清理之前的资源
    if (m_formatCtx) {
        if (m_timer) m_timer->stop();
        if (m_audioSink) {
            m_audioSink->stop();
            delete m_audioSink;
            m_audioSink = nullptr;
        }
        if (m_swrCtx) swr_free(&m_swrCtx);
        if (m_swsCtx) sws_freeContext(m_swsCtx);
        if (m_audioCodecCtx) avcodec_free_context(&m_audioCodecCtx);
        if (m_videoCodecCtx) avcodec_free_context(&m_videoCodecCtx);
        avformat_close_input(&m_formatCtx);
    }

    //打开媒体文件
    if(avformat_open_input(&m_formatCtx,filename.toUtf8().constData(),nullptr,nullptr)!=0) return false;
    //获取流信息
    if(avformat_find_stream_info(m_formatCtx,nullptr)<0) return false;

    for(int i=0;i<m_formatCtx->nb_streams;i++)//遍历所有流,查找视频流和音频流
    {
        AVCodecParameters *codecparams=m_formatCtx->streams[i]->codecpar;//描述编解码器的参数和特性
        //处理视频流
        // 处理视频流
        if(codecparams->codec_type==AVMEDIA_TYPE_VIDEO&&m_videoStreamIndex==-1)
        {
            m_videoStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_videoCodecCtx=avcodec_alloc_context3(codec);
                avcodec_parameters_to_context(m_videoCodecCtx,codecparams);
                if(avcodec_open2(m_videoCodecCtx,codec,nullptr)<0) return false;

                // ========== 替换原有的帧率计算代码 ==========
                // 获取视频时间基准
                m_videoTimeBase = m_formatCtx->streams[m_videoStreamIndex]->time_base;

                // 计算视频实际帧率(多种方法尝试)
                double calculated_fps = 0.0;

                // 方法1:使用平均帧率
                AVRational avg_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->avg_frame_rate;
                if (avg_frame_rate.num > 0 && avg_frame_rate.den > 0) {
                    calculated_fps = av_q2d(avg_frame_rate);
                    qDebug() << "使用平均帧率:" << calculated_fps << "fps";
                }

                // 方法2:如果平均帧率无效,使用实时帧率
                if (calculated_fps <= 0) {
                    AVRational r_frame_rate = m_formatCtx->streams[m_videoStreamIndex]->r_frame_rate;
                    if (r_frame_rate.num > 0 && r_frame_rate.den > 0) {
                        calculated_fps = av_q2d(r_frame_rate);
                        qDebug() << "使用实时帧率:" << calculated_fps << "fps";
                    }
                }

                // 方法3:如果前两种方法都无效,根据码流信息估算
                if (calculated_fps <= 0) {
                    // 估算帧率:总帧数 / 持续时间
                    int64_t duration = m_formatCtx->streams[m_videoStreamIndex]->duration;
                    int64_t nb_frames = m_formatCtx->streams[m_videoStreamIndex]->nb_frames;

                    if (duration > 0 && nb_frames > 0) {
                        double stream_duration = duration * av_q2d(m_videoTimeBase);
                        if (stream_duration > 0) {
                            calculated_fps = nb_frames / stream_duration;
                            qDebug() << "使用估算帧率:" << calculated_fps << "fps, 帧数:" << nb_frames << "时长:" << stream_duration;
                        }
                    }
                }

                // 方法4:如果所有方法都失败,使用默认值
                if (calculated_fps <= 0) {
                    calculated_fps = 30.0;
                    qDebug() << "使用默认帧率: 30fps";
                }

                m_videoFps = calculated_fps;

                // 计算定时器间隔,限制在合理范围内
                m_timerInterval = (int)(1000.0 / m_videoFps);
                if (m_timerInterval < 10) m_timerInterval = 10;  // 最小10ms (100fps)
                if (m_timerInterval > 100) m_timerInterval = 100; // 最大100ms (10fps)

                qDebug() << "最终帧率:" << m_videoFps << "fps, 定时器间隔:" << m_timerInterval << "ms";
                // ========== 帧率计算结束 ==========
            }
        }
        //处理音频流
        else if(codecparams->codec_type==AVMEDIA_TYPE_AUDIO&&m_audioStreamIndex==-1)
        {
            m_audioStreamIndex=i;
            const AVCodec *codec=avcodec_find_decoder(codecparams->codec_id);
            if(codec)
            {
                m_audioCodecCtx=avcodec_alloc_context3(codec);//分配音频编解码上下文
                avcodec_parameters_to_context(m_audioCodecCtx,codecparams);
                if(avcodec_open2(m_audioCodecCtx,codec,nullptr)<0)//打开音频编解码器
                {
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                m_audioTimeBase = m_formatCtx->streams[m_audioStreamIndex]->time_base;
                //配置音频重采样器(将音频转换为统一的44.1kHz立体声格式)
                AVChannelLayout in_layout = {}, out_layout = {};
                av_channel_layout_default(&in_layout, m_audioCodecCtx->ch_layout.nb_channels);
                av_channel_layout_from_mask(&out_layout, AV_CH_LAYOUT_STEREO);
                //创建音频重采样上下文
                int ret = swr_alloc_set_opts2(&m_swrCtx,
                                              &out_layout, AV_SAMPLE_FMT_S16, 44100,
                                              &in_layout, m_audioCodecCtx->sample_fmt, m_audioCodecCtx->sample_rate,
                                              0, nullptr);

                av_channel_layout_uninit(&in_layout);
                av_channel_layout_uninit(&out_layout);

                if (ret < 0 || !m_swrCtx) {
                    // 处理错误
                    avcodec_free_context(&m_audioCodecCtx);
                    m_audioCodecCtx = nullptr;
                    m_audioStreamIndex = -1;
                    continue;
                }
                //初始化音频重采样器
                if(m_swrCtx && swr_init(m_swrCtx) >= 0)
                {
                    //配置音频输出格式
                    m_audioFormat.setSampleRate(44100);//采样率44.1kHz
                    m_audioFormat.setChannelCount(2);//立体声
                    m_audioFormat.setSampleFormat(QAudioFormat::Int16);
                    // ========== 初始化音频写入字节数 ==========
                    m_audioBytesWritten= 0;  // 初始化音频写入字节数
                    QAudioDevice device = QMediaDevices::defaultAudioOutput();//获取默认音频输出设备
                    if(!device.isNull())
                    {
                        if(!device.isFormatSupported(m_audioFormat))//检查格式支持,如果不支持则使用设备首选格式
                            m_audioFormat = device.preferredFormat();

                        m_audioSink = new QAudioSink(device, m_audioFormat, this);//创建音频输出设备
                        m_audioSink->setVolume(m_volume);
                        m_audioSink->setBufferSize(44100 * 2 * 2 / 10); // 设置0.1秒的缓冲
                        m_audioDevice = m_audioSink->start();
                        m_audioEnabled = (m_audioDevice != nullptr);
                    }
                }
            }
        }
    }
    return true;
}

void videoplayer::decodeFrame()//解码帧函数:由定时器定期调用
{
    if(!m_formatCtx || !m_isPlaying) return;

    AVPacket packet;//从媒体文件中读取的原始数据包,存储压缩的媒体数据(编码后的数据)
    AVFrame *frame = av_frame_alloc();//包含一帧完整的未压缩数据,存储解码后的原始媒体数据

    int packet_count = 0;//每次解码最多处理10个数据包
    while (packet_count < 10 && av_read_frame(m_formatCtx, &packet) >= 0) {
        //处理音频包
        if (m_audioEnabled && packet.stream_index == m_audioStreamIndex) {
            if (avcodec_send_packet(m_audioCodecCtx, &packet) == 0) {
                while (avcodec_receive_frame(m_audioCodecCtx, frame) == 0) {
                    decodeAudioFrame(frame);
                }
            }
        }
        //处理视频包
        else if (packet.stream_index == m_videoStreamIndex) {
            if (avcodec_send_packet(m_videoCodecCtx, &packet) == 0) {
                if (avcodec_receive_frame(m_videoCodecCtx, frame) == 0) {
                    // ========== 音视频同步处理开始 ==========
                    // 计算视频帧PTS
                    int64_t pts = frame->pts;
                    if (pts == AV_NOPTS_VALUE) {
                        pts = frame->pkt_dts;  // 如果PTS无效,使用DTS
                    }

                    // 记录起始PTS(第一帧)
                    if (m_startPts == AV_NOPTS_VALUE) {
                        m_startPts = pts;
                    }

                    // 计算视频时钟(相对于起始PTS的时间,单位:秒)
                    double video_pts = getVideoClock(pts);
                    // 获取音频时钟(当前音频播放位置,单位:秒)
                    double audio_pts = getAudioClock();
                    // 计算基础帧延迟(根据实际帧率)
                    double frame_delay = (m_videoFps > 0) ? (1.0 / m_videoFps) : 0.033;
                    double delay = frame_delay;
                    // 音视频同步:如果音频可用,调整视频帧延迟
                    if (m_audioEnabled) {
                        delay = calculateFrameDelay(video_pts, audio_pts);
                        // 调试输出
                        static int debug_count = 0;
                        if (debug_count++ % 50 == 0) {
                            qDebug() << "音视频同步 - 视频:" << (video_pts * 1000) << "ms, 音频:"
                                     << (audio_pts * 1000) << "ms, 差异:" << ((video_pts - audio_pts) * 1000)
                                     << "ms, 延迟:" << (delay * 1000) << "ms";
                        }
                    }

                    // 更新视频时钟
                    m_videoClock = video_pts + delay;

                    QImage image = convertFrameToImage(frame);

                    // 执行同步延迟
                    if (delay > 0 && delay < 0.1) {
                        int delay_ms = (int)(delay * 1000);
                        QThread::msleep(delay_ms);
                    }

                    emit videoFrame(image);
                    // ========== 音视频同步处理结束 ==========

                    av_packet_unref(&packet);

                    return;//每次只解码一帧视频
                }
            }
        }
        packet_count++;
    }

    av_frame_free(&frame);
}
QImage videoplayer::convertFrameToImage(AVFrame *frame)//将AVFrame转换为QImage
{
    if(!frame||!frame->data[0]) return QImage();
    //检查是否需要重新创建像素格式转换上下文
    if (!m_swsCtx || frame->width != m_videoCodecCtx->width ||
        frame->height != m_videoCodecCtx->height ||
        frame->format != m_videoCodecCtx->pix_fmt) {

        if(m_swsCtx) sws_freeContext(m_swsCtx);
        //创建像素格式转换上下文(转换为RGB32格式)
        m_swsCtx=sws_getContext(frame->width,frame->height,(AVPixelFormat)frame->format,
                                  frame->width,frame->height,AV_PIX_FMT_RGB32,SWS_BILINEAR,nullptr,nullptr,nullptr);
    }

    QImage image(frame->width, frame->height, QImage::Format_RGB32);//创建QImage用于存储转换后的图像
    uint8_t *destDate[1] = {image.bits()};
    int destLinesize[1] = {static_cast<int>(image.bytesPerLine())};
    //执行像素格式转换
    sws_scale(m_swsCtx, frame->data, frame->linesize, 0, frame->height, destDate, destLinesize);
    return image;
}

void videoplayer::play()
{
    // ========== 重置音视频同步状态 ==========
    m_audioClock = 0.0;
    m_videoClock = 0.0;
    m_startPts = AV_NOPTS_VALUE;
    m_audioBytesWritten = 0;
    m_audioPts = 0;

    if (!m_formatCtx) return;
    m_isPlaying = true;
    m_pause = false;

    // 处理音频输出状态
    if (m_audioEnabled && m_audioSink) {
        if (m_audioSink->state() == QAudio::SuspendedState) {
            m_audioSink->resume();
        } else if (m_audioSink->state() == QAudio::StoppedState) {
            m_audioDevice = m_audioSink->start();
            m_audioBytesWritten = 0;
        }
    }

    // ========== 使用动态计算的定时器间隔 ==========
    m_timer->start(m_timerInterval);
    qDebug() << "启动定时器,间隔:" << m_timerInterval << "ms";
    emit playstatechange(true);
}
void videoplayer::pause()
{
    m_pause = !m_pause;

    if(m_pause)
    {
        m_timer->stop();
        if (m_audioEnabled && m_audioSink) m_audioSink->suspend();
    }
    else
    {
        // ========== 恢复时使用动态间隔 ==========
        m_timer->start(m_timerInterval);
        if (m_audioEnabled && m_audioSink) m_audioSink->resume();
    }
    emit playstatechange(!m_pause);
}

void videoplayer::decodeAudioFrame(AVFrame *frame)//解码音频帧
{
    // ========== 更新音频PTS ==========
    if (frame->pts != AV_NOPTS_VALUE) {
        m_audioPts
            = frame->pts;
    } else if (frame->pkt_dts != AV_NOPTS_VALUE) {
        m_audioPts
            = frame->pkt_dts;
    }
    if (!frame || !m_audioSink || !m_audioDevice || !m_swrCtx) return;
    //计算重采样后的样本数
    int out_samples = av_rescale_rnd(swr_get_delay(m_swrCtx, m_audioCodecCtx->sample_rate) + frame->nb_samples,
                                     44100, m_audioCodecCtx->sample_rate, AV_ROUND_UP);

    uint8_t **resampled_data = nullptr;
    int linesize;
    //分配重采样输出缓冲区
    if (av_samples_alloc_array_and_samples(&resampled_data, &linesize, 2, out_samples, AV_SAMPLE_FMT_S16, 0) < 0) return;
    //执行音频重采样
    int samples_converted = swr_convert(m_swrCtx, resampled_data, out_samples, (const uint8_t**)frame->data, frame->nb_samples);

    if (samples_converted > 0) {
        int data_size = samples_converted * 2 * 2;//计算数据大小
        //应用音量调节
        if (m_volume != 1.0f) {
            int16_t *samples = (int16_t*)resampled_data[0];
            for (int i = 0; i < samples_converted * 2; i++) samples[i] = (int16_t)(samples[i] * m_volume);
        }
        //将音频数据写入音频设备
        if (m_audioDevice && m_audioDevice->isOpen()) {
            qint64 written = m_audioDevice->write((const char*)resampled_data[0], data_size);

            // ========== 更新音频时钟(基于写入字节数) ==========
            if (written > 0) {
                m_audioBytesWritten += written;
            }
        }
    }
    //释放重采样数据缓冲区
    if (resampled_data) {
        if (resampled_data[0]) av_freep(&resampled_data[0]);
        av_freep(&resampled_data);
    }
}

void videoplayer::setVolume(float volume)
{
    m_volume = qBound(0.0f, volume, 1.0f);
    if (m_audioSink) m_audioSink->setVolume(m_volume);
}
// ========== 音视频同步函数实现 ==========

double videoplayer::getAudioClock()
{
    if (!m_audioEnabled || !m_audioCodecCtx) {
        return 0.0;
    }

    // 方法1:基于PTS的精确计算
    if (m_audioPts != 0 && m_audioTimeBase.num > 0) {
        double audio_time = m_audioPts * av_q2d(m_audioTimeBase);
        return audio_time;
    }

    // 方法2:基于已播放音频数据的估算
    if (m_audioBytesWritten > 0 && m_audioFormat.isValid()) {
        // 计算已播放的时间(秒)= 已写入字节数 / (采样率 * 通道数 * 样本大小)
        double seconds_played = (double)m_audioBytesWritten /
                                (m_audioFormat.sampleRate() *
                                 m_audioFormat.channelCount() *
                                 (m_audioFormat.bytesPerSample()));
        return seconds_played;
    }

    return 0.0;
}

double videoplayer::getVideoClock(int64_t pts)
{
    if (pts == AV_NOPTS_VALUE) {
        return 0.0;
    }

    // 将PTS转换为秒:PTS * 时间基准
    double pts_time = pts * av_q2d(m_videoTimeBase);
    return pts_time;
}

double videoplayer::calculateFrameDelay(double video_pts, double audio_pts)
{
    // ========== 使用定时器间隔作为基础帧延迟 ==========
    double frame_delay = m_timerInterval / 1000.0; // 转换为秒
    double diff = video_pts - audio_pts;
    double actual_delay = frame_delay;

    // 如果差异过大,使用更激进的同步策略
    if (fabs(diff) > 0.5) { // 差异超过500ms
        qDebug() << "音视频差异过大,激进同步,差异:" << diff * 1000 << "ms";
        actual_delay = frame_delay + diff * 0.8;
    }
    else if (fabs(diff) > m_syncThreshold) {
        // 中等差异:适度调整
        actual_delay = frame_delay + diff * 0.3;
    }
    else {
        // 小差异:微小调整避免抖动
        actual_delay = frame_delay + diff * 0.1;
    }

    // 限制延迟范围
    if (actual_delay > m_maxFrameDelay) {
        actual_delay = m_maxFrameDelay;
    }
    if (actual_delay < 0) {
        actual_delay = 0;
    }

    return actual_delay;
}

效果:

相关推荐
tanxiaomi2 小时前
Spring、Spring MVC 和 Spring Boot ,mybatis 相关面试题
java·开发语言·mybatis
浮尘笔记2 小时前
Go并发编程核心:Mutex和RWMutex的用法
开发语言·后端·golang
散峰而望2 小时前
C++数组(一)(算法竞赛)
c语言·开发语言·c++·算法·github
wjs20242 小时前
C++ 指针
开发语言
20岁30年经验的码农3 小时前
Java Sentinel流量控制与熔断降级框架详解
java·开发语言·sentinel
二川bro3 小时前
特征工程完全手册:2025 Python实战技巧
开发语言·python
p***h6434 小时前
JavaScript图像处理开发
开发语言·javascript·图像处理
2501_941148154 小时前
高并发搜索引擎Elasticsearch与Solr深度优化在互联网实践分享
java·开发语言·前端
专家大圣5 小时前
告别局域网束缚!飞牛云 NAS+cpolar 让远程管理更简单
开发语言·网络·内网穿透·cpolar