一、绪论
这是一个基于Qt框架 和FFmpeg多媒体库开发的轻量级视频播放器应用程序。项目实现了基本的视频文件解码、播放控制功能,具有跨平台特性,支持多种常见视频格式。下面我将以功能实现为分界,介绍这个项目,比较适合Qt音视频方向的初学者。(有问题可以私信我,可以带敲和视频讲解,欢迎交流学习)
二、环境配置
Qt6.0:这个大家因该都有我主要是介绍一下在Qt上配置FFmpeg
FFmpeg:
2.1.下载
Releases · BtbN/FFmpeg-Builds
https://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;
- 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播放器的基本功能但效果只能满足简单使用。
当前代码问题分析
-
没有音视频同步机制 - 音频和视频各自独立播放
-
固定帧率 - 使用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;
}
效果:
