最近有粉丝问我:“想自己做个视频播放器,Qt能搞定吗?
答案是肯定的
在Qt中,有多种方式可以实现播放器功能。列如:
- 使用QMediaPlayer类:QMediaPlayer是Qt提供的一个高级多媒体播放器框架,支持多种音频和视频格式。通过使用QMediaPlayer,可以轻松地播放、暂停、停止和切换媒体文件。此外,QMediaPlayer还提供了许多其他有用的功能,如获取媒体信息、设置音量、亮度等。
- 使用第三方库:除了QMediaPlayer之外,还有许多第三方库可用于在Qt中实现播放器功能。这些库可提供更多的功能和更好的性能,常见的第三方库包括FFmpeg、GStreamer等。
- 自定义播放器:如果需要更高级的功能或者想要实现自己的播放器逻辑,可以思考自定义播放器。这一般涉及到使用Qt的多媒体框架(如QMediaPlayer、QMediaPlaylist等)以及一些其他的Qt组件(如QWidget、QSlider等)来创建一个自定义的播放器界面和功能。
今天咱们就用Qt+FFmpeg,开发一个视频播放器。
Part1为什么选Qt+FFmpeg?
Qt自带的QMediaPlayer的确 能播视频,但它像封装好的“黑箱”——你点播放它就播,点暂停就暂停,可解码流程、帧处理这些核心操作全藏在内部,学不到东西。
而FFmpeg是开源界的“解码全能王”,几乎能处理所有主流视频格式(MP4、MOV、AVI…),配合Qt的界面能力,既能练技术,又能完全掌控播放器逻辑。
不过要注意:FFmpeg本身不直接渲染界面,得靠Qt的QWidget或QPainter来“画”解码后的画面;音频部分更麻烦,得借SDL2(简单音频库)来帮忙——这三者配合,就是咱们今天的“铁三角”。
Part2环境准备
- •
- Qt:选5.12.12(稳定版,文档全,社区问题多易解决);
- •
- 编译器:MSVC2017 64位(FFmpeg最新版对MinGW支持一般,MSVC更稳);
- •
- FFmpeg:6.1.1(别用太旧的,新API更规范,列如avformat_open_input的参数调整);
- •
- SDL2:2.0.10+(只用来播音频,轻量且跨平台)。
装环境时记得:FFmpeg要编译成动态库(.dll/.so),并把头文件和库路径加到Qt工程里;SDL2同理,编译时链接它的库。
Part3核心功能
|
功能点 |
技术拆解 |
|
支持多格式视频 |
FFmpeg的avformat_open_input能自动识别容器格式(MP4的ISOBMFF、AVI的RIFF),配合解复用器(demuxer)拆分音视频流。 |
|
匀速播放 |
通过控制解码时间戳(PTS)和播放时钟同步,列如用QTimer按帧间隔(如1/30秒)触发解码。 |
|
自适应窗口缩放 |
用QPainter的drawImage配合scaled方法,根据窗口大小动态调整图像分辨率,保持宽高比。 |
|
实时控制(暂停/继续) |
用标志位(如isPaused)控制解码线程的循环,暂停时停止取包和解码,继续时从当前帧继续。 |
|
模块化低耦合 |
解耦成三个独立模块:解复用(拆分包)、视频解码(转RGB)、UI渲染(画画面),用信号槽传数据。 |
Part4实现思路
视频播放是个“多任务协同”的过程:既要解视频,又要解音频,还要渲染画面,单线程肯定卡成PPT。
所以咱们用三条线程+两个队列来分工:
1. 线程1:解复用(音视频分离)
这一步像“拆快递”——把输入的视频文件(如MP4)拆成一个个“小包裹”(AVPacket),再按类型分到两个“快递柜”(视频队列、音频队列)。
- •
- 关键操作:用FFmpeg的av_read_frame循环读取包,判断是视频流(codecpar->codec_type == AVMEDIA_TYPE_VIDEO)还是音频流,然后塞进对应的队列。
- •
- 注意:队列要加锁(QMutex),避免多线程抢数据导致崩溃;队列长度设上限(列如100个包),防止内存爆炸。
2. 线程2:视频解码(从包到帧)
拿到视频队列里的包(AVPacket),下一步是“翻译”——把压缩的视频数据(H.264、H.265等)解成原始图像帧(AVFrame)。
- •
- 关键操作:
- •
- 用avcodec_send_packet发送包到解码器;
- •
- 用avcodec_receive_frame接收解码后的帧;
- •
- 转换像素格式(列如从YUV420P转成RGB24),由于Qt的QPainter只认识RGB。这里用sws_scale(FFmpeg的缩放转换函数)搞定。
- •
- 输出:解码后的RGB图像(QImage或QPixmap),通过信号槽发给UI线程渲染。
3. 线程3:音频解码(SDL帮忙)
音频处理比视频简单些,但Qt没自带好用的音频播放接口,所以借SDL2的力。SDL的音频模块支持回调模式:
- •
- 关键操作:
- •
- 初始化SDL的音频设备(SDL_OpenAudioDevice);
- •
- 设置回调函数,从音频队列里取包(AVPacket),解码(avcodec_send_packet/avcodec_receive_frame),转成PCM数据(原始声音波形);
- •
- SDL会把PCM数据直接输出到声卡,完成播放。
Part5代码实现
完整工程下载地址(下载即可编译运行): https://pan.quark.cn/s/815b31152cdf
模块功能
|
文件名 |
核心作用 |
关键技术点 |
|
condmutex.h/.cpp |
封装 SDL 的互斥锁和条件变量,提供线程安全的队列操作(加锁、等待、唤醒) |
SDL mutex/cond、线程同步 |
|
videoslider.h/.cpp |
自定义滑块组件,支持点击任意位置跳转进度(重写鼠标事件计算进度值) |
Qt 事件重写、UI 交互 |
|
videowidget.h/.cpp |
视频渲染组件,接收解码后的 RGB 数据,通过 QPainter 自适应窗口缩放绘制 |
QImage 渲染、Qt 绘图事件、宽高比适配 |
|
videoplayer.h/.cpp |
播放器核心逻辑,负责音视频解封装、解码、数据包管理,统一调度各子模块 |
FFmpeg 解封装 / 解码、状态管理 |
|
videoplayer_audio.cpp |
音频子模块,处理音频解码、重采样(转 SDL 支持的 PCM 格式)、SDL 音频播放回调 |
FFmpeg swr 重采样、SDL 音频回调 |
|
videoplayer_video.cpp |
视频子模块,处理视频解码、像素格式转换(转 RGB)、解码线程调度 |
FFmpeg sws 格式转换、多线程解码 |
|
mainwindow.h/.cpp |
主界面与交互逻辑,关联播放器核心与 UI 组件(按钮、滑块等),处理用户操作 |
Qt 信号槽、UI 事件绑定、进度 / 状态更新 |
condmutex.h
#ifndef CONDMUTEX_H
#define CONDMUTEX_H
#include "SDL.h"
class CondMutex {
public:
CondMutex();
~CondMutex();
void lock();
void unlock();
void signal();
void broadcast();
void wait();
private:
/** 互斥锁 */
SDL_mutex *_mutex = nullptr;
/** 条件变量 */
SDL_cond *_cond = nullptr;
};
#endif // CONDMUTEX_H
condmutex.cpp
#include "condmutex.h"
CondMutex::CondMutex() {
// 创建互斥锁
_mutex = SDL_CreateMutex();
// 创建条件变量
_cond = SDL_CreateCond();
}
CondMutex::~CondMutex() {
SDL_DestroyMutex(_mutex);
SDL_DestroyCond(_cond);
}
void CondMutex::lock() {
SDL_LockMutex(_mutex);
}
void CondMutex::unlock() {
SDL_UnlockMutex(_mutex);
}
void CondMutex::signal() {
SDL_CondSignal(_cond);
}
void CondMutex::broadcast() {
SDL_CondBroadcast(_cond);
}
void CondMutex::wait() {
SDL_CondWait(_cond, _mutex);
}
videoslider.h
#ifndef VIDEOSLIDER_H
#define VIDEOSLIDER_H
#include <QSlider>
class VideoSlider : public QSlider {
Q_OBJECT
public:
explicit VideoSlider(QWidget *parent = nullptr);
signals:
void clicked(VideoSlider *slider);
private:
void mousePressEvent(QMouseEvent *ev) override;
};
#endif // VIDEOSLIDER_H
videoslider.cpp
#include "videoslider.h"
#include <QMouseEvent>
#include <QStyle>
VideoSlider::VideoSlider(QWidget *parent) : QSlider(parent) {
}
void VideoSlider::mousePressEvent(QMouseEvent *ev) {
// 根据点击位置的x值,计算出对应的value
int value = QStyle::sliderValueFromPosition(minimum(),maximum(),ev->pos().x(),width());
setValue(value);
QSlider::mousePressEvent(ev);
// 发出信号
emit clicked(this);
}
videowidget.h
#ifndef VIDEOWIDGET_H
#define VIDEOWIDGET_H
#include <QWidget>
#include <QImage>
#include "videoplayer.h"
/**
* 显示(渲染)视频
*/
class VideoWidget : public QWidget {
Q_OBJECT
public:
explicit VideoWidget(QWidget *parent = nullptr);
~VideoWidget();
public slots:
void onPlayerFrameDecoded(VideoPlayer *player, uint8_t *data, VideoPlayer::VideoSwsSpec &spec);
void onPlayerStateChanged(VideoPlayer *player);
private:
QImage *_image = nullptr;
QRect _rect;
void paintEvent(QPaintEvent *event) override;
void freeImage();
};
#endif // VIDEOWIDGET_H
videowidget.cpp
#include "videowidget.h"
#include <QPainter>
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
// 设置背景色
setAttribute(Qt::WA_StyledBackground);
setStyleSheet("background: black");
}
VideoWidget::~VideoWidget() {
freeImage();
}
void VideoWidget::onPlayerStateChanged(VideoPlayer *player) {
if (player->getState() != VideoPlayer::Stopped) return;
freeImage();
update();
}
void VideoWidget::onPlayerFrameDecoded(VideoPlayer *player,uint8_t *data, VideoPlayer::VideoSwsSpec &spec) {
if (player->getState() == VideoPlayer::Stopped) return;
// 释放之前的图片
freeImage();
// 创建新的图片
if (data != nullptr) {
_image = new QImage((uchar *) data,spec.width, spec.height,QImage::Format_RGB888);
// 计算最终的尺寸
// 组件的尺寸
int w = width();
int h = height();
// 计算rect
int dx = 0;
int dy = 0;
int dw = spec.width;
int dh = spec.height;
// 计算目标尺寸
if (dw > w || dh > h) { // 缩放
if (dw * h > w * dh) { // 视频的宽高比 > 播放器的宽高比
dh = w * dh / dw;
dw = w;
} else {
dw = h * dw / dh;
dh = h;
}
}
// 居中
dx = (w - dw) >> 1;
dy = (h - dh) >> 1;
_rect = QRect(dx, dy, dw, dh);
}
update();//触发paintEvent方法
}
void VideoWidget::paintEvent(QPaintEvent *event) {
if (!_image) return;
// 将图片绘制到当前组件上
QPainter(this).drawImage(_rect, *_image);
}
void VideoWidget::freeImage() {
if (_image) {
av_free(_image->bits());
delete _image;
_image = nullptr;
}
}
videoplayer.h
#ifndef VIDEOPLAYER_H
#define VIDEOPLAYER_H
#include <QObject>
#include <QDebug>
#include <list>
#include "condmutex.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/avutil.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
}
#define ERROR_BUF
char errbuf[1024];
av_strerror(ret, errbuf, sizeof (errbuf));
#define CODE(func,code)
if (ret < 0) {
ERROR_BUF;
qDebug() << #func << "error" << errbuf;
code;
}
#define END(func) CODE(func,fataError(); return;)
#define RET(func) CODE(func, return ret;)
#define CONTINUE(func) CODE(func, continue;)
#define BREAK(func) CODE(func, break;)
/**
* 预处理视频数据(不负责显示、渲染视频)
*/
class VideoPlayer : public QObject {
Q_OBJECT
public:
// 状态
typedef enum {
Stopped = 0,
Playing,
Paused
} State;
// 音量
typedef enum {
Min = 0,
Max = 100
} Volumn;
// 视频frame参数
typedef struct {
int width;
int height;
AVPixelFormat pixFmt;
int size;
} VideoSwsSpec;
explicit VideoPlayer(QObject *parent = nullptr);
~VideoPlayer();
/** 播放 */
void play();
/** 暂停 */
void pause();
/** 停止 */
void stop();
/** 是否正在播放中 */
bool isPlaying();
/** 获取当前的状态 */
State getState();
/** 设置文件名 */
void setFilename(QString &filename);
/** 获取总时长(单位是妙,1秒=1000毫秒=1000000微妙)*/
int getDuration();
/** 当前的播放时刻(单位是秒) */
int getTime();
/** 设置当前的播放时刻(单位是秒) */
void setTime(int seekTime);
/** 设置音量 */
void setVolumn(int volumn);
int getVolumn();
/** 设置静音 */
void setMute(bool mute);
bool isMute();
signals:
void stateChanged(VideoPlayer *player);
void timeChanged(VideoPlayer *player);
void initFinished(VideoPlayer *player);
void playFailed(VideoPlayer *player);
void frameDecoded(VideoPlayer *player,uint8_t *data,VideoSwsSpec &spec);
private:
/******** 音频相关 ********/
typedef struct {
int sampleRate;
AVSampleFormat sampleFmt;
int chLayout;
int chs;
int bytesPerSampleFrame;
} AudioSwrSpec;
/** 解码上下文 */
AVCodecContext *_aDecodeCtx = nullptr;
/** 流 */
AVStream *_aStream = nullptr;
/** 存放音频包的列表 */
std::list<AVPacket> _aPktList;
/** 音频包列表的锁 */
CondMutex _aMutex;
/** 音频重采样上下文 */
SwrContext *_aSwrCtx = nullptr;
/** 音频重采样输入输出参数 */
AudioSwrSpec _aSwrInSpec;
AudioSwrSpec _aSwrOutSpec;
/** 音频重采样输入输出frame */
AVFrame *_aSwrInFrame = nullptr;
AVFrame *_aSwrOutFrame = nullptr;
/** 音频重采样输出PCM的索引(从哪个位置开始取出PCM数据填充到SDL的音频缓冲区) */
int _aSwrOutIdx = 0;
/** 音频重采样输出PCM的大小 */
int _aSwrOutSize = 0;
/** 音量 */
int _volumn = Max;
/** 静音 */
bool _mute = false;
/** 音频时钟,当前音频包对应的时间值 */
double _aTime = 0;
/** 是否有音频流 */
bool _hasAudio = false;
/** 音频资源是否可以释放 */
bool _aCanFree = false;
/** 外面设置的当前播放时刻(用于完成seek功能) */
int _aSeekTime = -1;
/** 初始化音频信息 */
int initAudioInfo();
/** 初始化SDL */
int initSDL();
/** 添加数据包到音频包列表中 */
void addAudioPkt(AVPacket &pkt);
/** 清空音频包列表 */
void clearAudioPktList();
/** SDL填充缓冲区的回调函数 */
static void sdlAudioCallbackFunc(void *userdata, Uint8 *stream, int len);
/** SDL填充缓冲区的回调函数 */
void sdlAudioCallback(Uint8 *stream, int len);
/** 音频解码 */
int decodeAudio();
/** 初始化音频重采样 */
int initSwr();
/******** 视频相关 ********/
/** 解码上下文 */
AVCodecContext *_vDecodeCtx = nullptr;
/** 流 */
AVStream *_vStream = nullptr;
/** 像素格式转换的输入输出frame */
AVFrame *_vSwsInFrame = nullptr, *_vSwsOutFrame = nullptr;
/** 像素格式转换的上下文 */
SwsContext *_vSwsCtx = nullptr;
/** 像素格式转换的输出frame的参数 */
VideoSwsSpec _vSwsOutSpec;
/** 存放视频包的列表 */
std::list<AVPacket> _vPktList;
/** 视频包列表的锁 */
CondMutex _vMutex;
/** 视频时钟,当前视频包对应的时间值 */
double _vTime = 0;
/** 是否有视频流 */
bool _hasVideo = false;
/** 视频资源是否可以释放 */
bool _vCanFree = false;
/** 外面设置的当前播放时刻(用于完成seek功能) */
int _vSeekTime = -1;
/** 初始化视频信息 */
int initVideoInfo();
/** 初始化视频像素格式转换 */
int initSws();
/** 添加数据包到视频包列表中 */
void addVideoPkt(AVPacket &pkt);
/** 清空视频包列表 */
void clearVideoPktList();
/** 解码视频 */
void decodeVideo();
/******** 其他 ********/
/** 当前的状态 */
State _state = Stopped;
/** fmtCtx是否可以释放 */
bool _fmtCtxCanFree = false;
/** 文件名 */
QString _filename;
// 解封装上下文
AVFormatContext *_fmtCtx = nullptr;
/** 外面设置的当前播放时刻(用于完成seek功能) */
int _seekTime = -1;
/** 初始化解码器和解码上下文 */
int initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type);
/** 改变状态 */
void setState(State state);
/** 读取文件数据 */
void readFile();
/** 释放资源 */
void free();
void freeAudio();
void freeVideo();
/** 严重错误 */
void fataError();
};
#endif // VIDEOPLAYER_H
videoplayer.cpp
#include "videoplayer.h"
#include <thread>
#define AUDIO_MAX_PKT_SIZE 1000
#define VIDEO_MAX_PKT_SIZE 500
VideoPlayer::VideoPlayer(QObject *parent) : QObject(parent) {
// 初始化Audio子系统
if (SDL_Init(SDL_INIT_AUDIO)) {
// 返回值不是0,就代表失败
qDebug() << "SDL_Init error" << SDL_GetError();
emit playFailed(this);
return;
}
}
VideoPlayer::~VideoPlayer() {
// 不再对外发送消息
disconnect();
stop();
SDL_Quit();
}
void VideoPlayer::play() {
if (_state == Playing) return;
// 状态可能是:暂停、停止、正常完毕
if(_state == Stopped){
// 开始线程:读取文件
std::thread([this](){
readFile();
}).detach();// detach 等到readFile方法执行完,这个线程就会销毁
}else{
setState(Playing);
}
}
void VideoPlayer::pause() {
if (_state != Playing) return;
// 状态可能是:正在播放
setState(Paused);
}
void VideoPlayer::stop() {
if (_state == Stopped) return;
// 状态可能是:正在播放、暂停、正常完毕
// 改变状态
_state = Stopped;
// 释放资源
free();
// 通知外界
emit stateChanged(this);
}
bool VideoPlayer::isPlaying() {
return _state == Playing;
}
VideoPlayer::State VideoPlayer::getState() {
return _state;
}
void VideoPlayer::setFilename(QString &filename) {
_filename = filename;
}
int VideoPlayer::getDuration(){
return _fmtCtx ? round(_fmtCtx->duration * av_q2d(AV_TIME_BASE_Q)) : 0;
}
int VideoPlayer::getTime(){
return round(_aTime);
}
void VideoPlayer::setVolumn(int volumn){
_volumn = volumn;
}
void VideoPlayer::setTime(int seekTime){
_seekTime = seekTime;
}
int VideoPlayer::getVolumn(){
return _volumn;
}
void VideoPlayer::setMute(bool mute) {
_mute = mute;
}
bool VideoPlayer::isMute() {
return _mute;
}
void VideoPlayer::readFile(){
int ret = 0;
// 创建解封装上下文、打开文件
ret = avformat_open_input(&_fmtCtx,_filename.toUtf8().data(),nullptr,nullptr);
END(avformat_open_input);
// 检索流信息
ret = avformat_find_stream_info(_fmtCtx,nullptr);
END(avformat_find_stream_info);
// 打印流信息到控制台
av_dump_format(_fmtCtx,0,_filename.toUtf8().data(),0);
fflush(stderr);
// 初始化音频信息
_hasAudio = initAudioInfo() >= 0;
// 初始化视频信息
_hasVideo = initVideoInfo() >= 0;
if (!_hasAudio && !_hasVideo) {
emit playFailed(this);
free();
return;
}
// 到此为止,初始化完毕
emit initFinished(this);
// 改变状态
setState(Playing);
// 音频解码子线程:开始工作
SDL_PauseAudio(0);
// 开启新的线程去解码视频数据
std::thread([this](){
decodeVideo();
}).detach();
// 从输入文件中读取数据
AVPacket pkt;
while (_state != Stopped) {
// 处理seek操作
if (_seekTime >= 0) {
int streamIdx;
if (_hasAudio) { // 优先使用音频流索引
streamIdx = _aStream->index;
} else {
streamIdx = _vStream->index;
}
// 现实时间 -> 时间戳
AVRational timeBase = _fmtCtx->streams[streamIdx]->time_base;
int64_t ts = _seekTime / av_q2d(timeBase);
// ret = av_seek_frame(_fmtCtx, streamIdx, ts, AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME);
ret = avformat_seek_file(_fmtCtx, streamIdx, INT64_MIN, ts, INT64_MAX, 0);
if(ret < 0){// seek失败
qDebug() << "seek失败" << _seekTime << ts << streamIdx;
_seekTime = -1;
}else{// seek成功
qDebug() << "seek成功" << _seekTime << ts << streamIdx;
// 清空之前读取的数据包
clearAudioPktList();
clearVideoPktList();
_vSeekTime = _seekTime;
_aSeekTime = _seekTime;
_seekTime = -1;
// 恢复时钟
_aTime = 0;
_vTime = 0;
}
}
int vSize = _vPktList.size();
int aSize = _aPktList.size();
if (vSize >= VIDEO_MAX_PKT_SIZE || aSize >= AUDIO_MAX_PKT_SIZE) {
SDL_Delay(1);
continue;
}
ret = av_read_frame(_fmtCtx, &pkt);
if (ret == 0) {
if (pkt.stream_index == _aStream->index) { // 读取到的是音频数据
addAudioPkt(pkt);
} else if (pkt.stream_index == _vStream->index) { // 读取到的是视频数据
addVideoPkt(pkt);
}else{// 如果不是音频、视频流,直接释放
av_packet_unref(&pkt);
}
} else if (ret == AVERROR_EOF) { // 读到了文件的尾部
// break;// seek的时候不能用break
if(vSize == 0 && aSize ==0){
// 说明文件正常播放完毕
_fmtCtxCanFree = true;
break;
}
} else {
ERROR_BUF;
qDebug() << "av_read_frame error" << errbuf;
continue;
}
}
if (_fmtCtxCanFree) { // 文件正常播放完毕
stop();
} else {
// 标记一下:_fmtCtx可以释放了
_fmtCtxCanFree = true;
}
}
int VideoPlayer::initDecoder(AVCodecContext **decodeCtx,AVStream **stream,AVMediaType type) {
// 根据type寻找最合适的流信息
// 返回值是流索引
int ret = av_find_best_stream(_fmtCtx, type, -1, -1, nullptr, 0);
RET(av_find_best_stream);
// 检验流
int streamIdx = ret;
*stream = _fmtCtx->streams[streamIdx];
if (!*stream) {
qDebug() << "stream is empty";
return -1;
}
// 为当前流找到合适的解码器
const AVCodec *decoder = avcodec_find_decoder((*stream)->codecpar->codec_id);
if (!decoder) {
qDebug() << "decoder not found" << (*stream)->codecpar->codec_id;
return -1;
}
// 初始化解码上下文
*decodeCtx = avcodec_alloc_context3(decoder);
if (!decodeCtx) {
qDebug() << "avcodec_alloc_context3 error";
return -1;
}
// 从流中拷贝参数到解码上下文中
ret = avcodec_parameters_to_context(*decodeCtx, (*stream)->codecpar);
RET(avcodec_parameters_to_context);
// 打开解码器
ret = avcodec_open2(*decodeCtx, decoder, nullptr);
RET(avcodec_open2);
return 0;
}
void VideoPlayer::setState(State state) {
if (state == _state) return;
_state = state;
emit stateChanged(this);
}
void VideoPlayer::free(){
while (_hasAudio && !_aCanFree);
while (_hasVideo && !_vCanFree);
while (!_fmtCtxCanFree);
avformat_close_input(&_fmtCtx);
_fmtCtxCanFree = false;
_seekTime = -1;
freeAudio();
freeVideo();
}
void VideoPlayer::fataError(){
setState(Stopped);
free();
emit playFailed(this);
}
videoplayer_audio.cpp
#include "videoplayer.h"
// 初始化音频信息
int VideoPlayer::initAudioInfo() {
int ret = initDecoder(&_aDecodeCtx,&_aStream,AVMEDIA_TYPE_AUDIO);
RET(initDecoder);
// 初始化音频重采样
ret = initSwr();
RET(initSwr);
// 初始化SDL
ret = initSDL();
RET(initSDL);
return 0;
}
int VideoPlayer::initSwr() {
// 重采样输入参数
_aSwrInSpec.sampleFmt = _aDecodeCtx->sample_fmt;
_aSwrInSpec.sampleRate = _aDecodeCtx->sample_rate;
_aSwrInSpec.chLayout = _aDecodeCtx->channel_layout;
_aSwrInSpec.chs = _aDecodeCtx->channels;
// 重采样输出参数
_aSwrOutSpec.sampleFmt = AV_SAMPLE_FMT_S16;
_aSwrOutSpec.sampleRate = 44100;
_aSwrOutSpec.chLayout = AV_CH_LAYOUT_STEREO;
_aSwrOutSpec.chs = av_get_channel_layout_nb_channels(_aSwrOutSpec.chLayout);
_aSwrOutSpec.bytesPerSampleFrame = _aSwrOutSpec.chs * av_get_bytes_per_sample(_aSwrOutSpec.sampleFmt);
// 创建重采样上下文
_aSwrCtx = swr_alloc_set_opts(nullptr,
// 输出参数
_aSwrOutSpec.chLayout,
_aSwrOutSpec.sampleFmt,
_aSwrOutSpec.sampleRate,
// 输入参数
_aSwrInSpec.chLayout,
_aSwrInSpec.sampleFmt,
_aSwrInSpec.sampleRate,
0, nullptr);
if (!_aSwrCtx) {
qDebug() << "swr_alloc_set_opts error";
return -1;
}
// 初始化重采样上下文
int ret = swr_init(_aSwrCtx);
RET(swr_init);
// 初始化重采样的输入frame
_aSwrInFrame = av_frame_alloc();
if (!_aSwrInFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
// 初始化重采样的输出frame
_aSwrOutFrame = av_frame_alloc();
if (!_aSwrOutFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
// 初始化重采样的输出frame的data[0]空间
ret = av_samples_alloc(_aSwrOutFrame->data,
_aSwrOutFrame->linesize,
_aSwrOutSpec.chs,
4096, _aSwrOutSpec.sampleFmt, 1);
RET(av_samples_alloc);
return 0;
}
void VideoPlayer::freeAudio(){
_aSwrOutIdx = 0;
_aSwrOutSize =0;
_aTime = 0;
_aCanFree = false;
_aSeekTime = -1;
clearAudioPktList();
avcodec_free_context(&_aDecodeCtx);
swr_free(&_aSwrCtx);
av_frame_free(&_aSwrInFrame);
if(_aSwrOutFrame){
av_freep(&_aSwrOutFrame->data[0]);// 因手动创建了data[0]的空间
av_frame_free(&_aSwrOutFrame);
}
// 停止播放
SDL_PauseAudio(1);
SDL_CloseAudio();
}
void VideoPlayer::sdlAudioCallbackFunc(void *userdata, uint8_t *stream, int len){
VideoPlayer *player = (VideoPlayer *)userdata;
player->sdlAudioCallback(stream,len);
}
int VideoPlayer::initSDL(){
// 音频参数
SDL_AudioSpec spec;
// 采样率
spec.freq = _aSwrOutSpec.sampleRate;
// 采样格式(s16le)
spec.format = AUDIO_S16LSB;
// 声道数
spec.channels = _aSwrOutSpec.chs;
// 音频缓冲区的样本数量(这个值必须是2的幂)
spec.samples = 512;
// 回调
spec.callback = sdlAudioCallbackFunc;
// 传递给回调的参数
spec.userdata = this;
// 打开音频设备
if (SDL_OpenAudio(&spec, nullptr)) {
qDebug() << "SDL_OpenAudio error" << SDL_GetError();
return -1;
}
return 0;
}
void VideoPlayer::addAudioPkt(AVPacket &pkt){
_aMutex.lock();
_aPktList.push_back(pkt);
_aMutex.signal();
_aMutex.unlock();
}
void VideoPlayer::clearAudioPktList(){
_aMutex.lock();
for(AVPacket &pkt : _aPktList){
av_packet_unref(&pkt);
}
_aPktList.clear();
_aMutex.unlock();
}
void VideoPlayer::sdlAudioCallback(Uint8 *stream, int len){
// 清零(静音)
SDL_memset(stream, 0, len);
// len:SDL音频缓冲区剩余的大小(还未填充的大小)
while (len > 0) {
if (_state == Paused) break;
if (_state == Stopped) {
_aCanFree = true;
break;
}
// 说明当前PCM的数据已经全部拷贝到SDL的音频缓冲区了
// 需要解码下一个pkt,获取新的PCM数据
if (_aSwrOutIdx >= _aSwrOutSize) {
// 全新PCM的大小
_aSwrOutSize = decodeAudio();
// 索引清0
_aSwrOutIdx = 0;
// 没有解码出PCM数据,那就静音处理
if (_aSwrOutSize <= 0) {
// 假定PCM的大小
_aSwrOutSize = 1024;
// 给PCM填充0(静音)
memset(_aSwrOutFrame->data[0], 0, _aSwrOutSize);
}
}
// 本次需要填充到stream中的PCM数据大小
int fillLen = _aSwrOutSize - _aSwrOutIdx;
fillLen = std::min(fillLen, len);
// 获取当前音量
int volumn = _mute ? 0 : ((_volumn * 1.0 / Max) * SDL_MIX_MAXVOLUME);
// 填充SDL缓冲区
SDL_MixAudio(stream,
_aSwrOutFrame->data[0] + _aSwrOutIdx,
fillLen, volumn);
// 移动偏移量
len -= fillLen;
stream += fillLen;
_aSwrOutIdx += fillLen;
}
}
/**
* @brief VideoPlayer::decodeAudio
* @return 解码出来的pcm大小
*/
int VideoPlayer::decodeAudio(){
// 加锁
_aMutex.lock();
if (_aPktList.empty() || _state == Stopped) {
_aMutex.unlock();
return 0;
}
// 取出头部的数据包
AVPacket pkt = _aPktList.front();
// 从头部中删除
_aPktList.pop_front();
// 解锁
_aMutex.unlock();
// 保存音频时钟
if (pkt.pts != AV_NOPTS_VALUE) {
_aTime = av_q2d(_aStream->time_base) *pkt.pts;
// 通知外界:播放时间点发生了改变
emit timeChanged(this);
}
// 如果是视频,不能在这个位置判断(不能提前释放pkt,不然会导致B帧、P帧解码失败,画面撕裂)
// 发现音频的时间是早于seekTime的,直接丢弃
if (_aSeekTime >= 0) {
if (_aTime < _aSeekTime) {
// 释放pkt
av_packet_unref(&pkt);
return 0;
} else {
_aSeekTime = -1;
}
}
// 发送压缩数据到解码器
int ret = avcodec_send_packet(_aDecodeCtx, &pkt);
// 释放pkt
av_packet_unref(&pkt);
RET(avcodec_send_packet);
// 获取解码后的数据
ret = avcodec_receive_frame(_aDecodeCtx, _aSwrInFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
return 0;
} else RET(avcodec_receive_frame);
// 重采样输出的样本数
int outSamples = av_rescale_rnd(_aSwrOutSpec.sampleRate,
_aSwrInFrame->nb_samples,
_aSwrInSpec.sampleRate, AV_ROUND_UP);
// 由于解码出来的PCM。跟SDL要求的PCM格式可能不一致,需要进行重采样
ret = swr_convert(_aSwrCtx,
_aSwrOutFrame->data,
outSamples,
(const uint8_t **) _aSwrInFrame->data,
_aSwrInFrame->nb_samples);
RET(swr_convert);
return ret * _aSwrOutSpec.bytesPerSampleFrame;
}
videoplayer_video.cpp
#include "videoplayer.h"
#include <thread>
extern "C" {
#include <libavutil/imgutils.h>
}
// 初始化视频信息
int VideoPlayer::initVideoInfo() {
int ret = initDecoder(&_vDecodeCtx,&_vStream,AVMEDIA_TYPE_VIDEO);
RET(initDecoder);
// 初始化像素格式转换
ret = initSws();
RET(initSws);
return 0;
}
int VideoPlayer::initSws(){
int inW = _vDecodeCtx->width;
int inH = _vDecodeCtx->height;
// 输出frame的参数
_vSwsOutSpec.width = inW >> 4 << 4;// 先除以16在乘以16,保证是16的倍数
_vSwsOutSpec.height = inH >> 4 << 4;
_vSwsOutSpec.pixFmt = AV_PIX_FMT_RGB24;
_vSwsOutSpec.size = av_image_get_buffer_size(_vSwsOutSpec.pixFmt,_vSwsOutSpec.width,_vSwsOutSpec.height, 1);
// 初始化像素格式转换的上下文
_vSwsCtx = sws_getContext(inW,inH,_vDecodeCtx->pix_fmt,
_vSwsOutSpec.width,_vSwsOutSpec.height,_vSwsOutSpec.pixFmt,
SWS_BILINEAR, nullptr, nullptr, nullptr);
if (!_vSwsCtx) {
qDebug() << "sws_getContext error";
return -1;
}
// 初始化像素格式转换的输入frame
_vSwsInFrame = av_frame_alloc();
if (!_vSwsInFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
// 初始化像素格式转换的输出frame
_vSwsOutFrame = av_frame_alloc();
if (!_vSwsOutFrame) {
qDebug() << "av_frame_alloc error";
return -1;
}
// _vSwsOutFrame的data[0]指向的内存空间
int ret = av_image_alloc(_vSwsOutFrame->data,
_vSwsOutFrame->linesize,
_vSwsOutSpec.width,
_vSwsOutSpec.height,
_vSwsOutSpec.pixFmt,
1);
RET(av_image_alloc);
return 0;
}
void VideoPlayer::addVideoPkt(AVPacket &pkt){
_vMutex.lock();
_vPktList.push_back(pkt);
_vMutex.signal();
_vMutex.unlock();
}
void VideoPlayer::clearVideoPktList(){
_vMutex.lock();
for(AVPacket &pkt : _vPktList){
av_packet_unref(&pkt);
}
_vPktList.clear();
_vMutex.unlock();
}
void VideoPlayer::freeVideo(){
clearVideoPktList();
avcodec_free_context(&_vDecodeCtx);
av_frame_free(&_vSwsInFrame);
if (_vSwsOutFrame) {
av_freep(&_vSwsOutFrame->data[0]);
av_frame_free(&_vSwsOutFrame);
}
sws_freeContext(_vSwsCtx);
_vSwsCtx = nullptr;
_vStream = nullptr;
_vTime = 0;
_vCanFree = false;
_vSeekTime = -1;
}
void VideoPlayer::decodeVideo(){
while (true) {
// 如果是暂停,并且没有Seek操作
if (_state == Paused && _vSeekTime == -1) {
continue;
}
if (_state == Stopped) {
_vCanFree = true;
break;
}
_vMutex.lock();
if(_vPktList.empty()){
_vMutex.unlock();
continue;
}
// 取出头部的视频包
AVPacket pkt = _vPktList.front();
_vPktList.pop_front();
_vMutex.unlock();
// 视频时钟
if (pkt.dts != AV_NOPTS_VALUE) {
_vTime = av_q2d(_vStream->time_base) * pkt.dts;
}
// 发送压缩数据到解码器
int ret = avcodec_send_packet(_vDecodeCtx, &pkt);
// 释放pkt
av_packet_unref(&pkt);
CONTINUE(avcodec_send_packet);
while (true) {
ret = avcodec_receive_frame(_vDecodeCtx, _vSwsInFrame);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
break;
} else BREAK(avcodec_receive_frame);
// 必定要在解码成功后,再进行下面的判断
// 发现视频的时间是早于seekTime的,直接丢弃
if(_vSeekTime >= 0){
if (_vTime < _vSeekTime) {
continue;// 丢掉
} else {
_vSeekTime = -1;
}
}
// 像素格式的转换
sws_scale(_vSwsCtx,
_vSwsInFrame->data, _vSwsInFrame->linesize,
0, _vDecodeCtx->height,
_vSwsOutFrame->data, _vSwsOutFrame->linesize);
if(_hasAudio){// 有音频
// 如果视频包过早被解码出来,那就需要等待对应的音频时钟到达
while (_vTime > _aTime && _state == Playing) {
SDL_Delay(1);
}
}
uint8_t *data = (uint8_t *)av_malloc(_vSwsOutSpec.size);
memcpy(data, _vSwsOutFrame->data[0], _vSwsOutSpec.size);
// 发出信号
emit frameDecoded(this,data,_vSwsOutSpec);
qDebug()<< "渲染了一帧"<< _vTime << _aTime;
}
}
}
界面设计mainwindow.ui

mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "videoplayer.h"
#include "videoslider.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 onPlayerStateChanged(VideoPlayer *player);
void onPlayerTimeChanged(VideoPlayer *player);
void onPlayerInitFinished(VideoPlayer *player);
void onPlayerPlayFailed(VideoPlayer *player);
void onSliderClicked(VideoSlider *slider);
void on_stopBtn_clicked();
void on_openFileBtn_clicked();
void on_currentSlider_valueChanged(int value);
void on_volumnSlider_valueChanged(int value);
void on_playBtn_clicked();
void on_muteBtn_clicked();
private:
Ui::MainWindow *ui;
VideoPlayer *_player;
QString getTimeText(int value);
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QMessageBox>
#define FILEPATH "../test/"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow) {
ui->setupUi(this);
// 注册信号的参数类型,保证能够发出信号
qRegisterMetaType<VideoPlayer::VideoSwsSpec>("VideoSwsSpec&");
// 创建播放器
_player = new VideoPlayer();
connect(_player, &VideoPlayer::stateChanged,this, &MainWindow::onPlayerStateChanged);
connect(_player, &VideoPlayer::timeChanged,this, &MainWindow::onPlayerTimeChanged);
connect(_player, &VideoPlayer::initFinished,this, &MainWindow::onPlayerInitFinished);
connect(_player, &VideoPlayer::playFailed,this, &MainWindow::onPlayerPlayFailed);
connect(_player, &VideoPlayer::frameDecoded,ui->videoWidget, &VideoWidget::onPlayerFrameDecoded);
connect(_player, &VideoPlayer::stateChanged,ui->videoWidget, &VideoWidget::onPlayerStateChanged);
// 监听时间滑块的点击
connect(ui->currentSlider, &VideoSlider::clicked,
this, &MainWindow::onSliderClicked);
// 设置音量滑块的范围
ui->volumnSlider->setRange(VideoPlayer::Volumn::Min,VideoPlayer::Volumn::Max);
ui->volumnSlider->setValue(ui->volumnSlider->maximum() >> 2);
}
MainWindow::~MainWindow() {
delete ui;
delete _player;
}
void MainWindow::onSliderClicked(VideoSlider *slider) {
_player->setTime(slider->value());
}
void MainWindow::onPlayerPlayFailed(VideoPlayer *player) {
QMessageBox::critical(nullptr,"提示","播放失败");
}
void MainWindow::onPlayerTimeChanged(VideoPlayer *player) {
ui->currentSlider->setValue(player->getTime());
}
void MainWindow::onPlayerInitFinished(VideoPlayer *player) {
int duration = player->getDuration();
qDebug()<< duration;
// 设置一些slider的范围
ui->currentSlider->setRange(0,duration);
// 设置label的文字
ui->durationLabel->setText(getTimeText(duration));
}
/**
* onPlayerStateChanged方法的发射虽然在子线程中执行(VideoPlayer::readFile()),
* 但是此方法是在主线程执行,由于它的connect是在主线程执行的
*/
void MainWindow::onPlayerStateChanged(VideoPlayer *player) {
VideoPlayer::State state = player->getState();
if (state == VideoPlayer::Playing) {
ui->playBtn->setText("暂停");
} else {
ui->playBtn->setText("播放");
}
if (state == VideoPlayer::Stopped) {
ui->playBtn->setEnabled(false);
ui->stopBtn->setEnabled(false);
ui->currentSlider->setEnabled(false);
ui->volumnSlider->setEnabled(false);
ui->muteBtn->setEnabled(false);
ui->durationLabel->setText(getTimeText(0));
ui->currentSlider->setValue(0);
// 显示打开文件的页面
ui->playWidget->setCurrentWidget(ui->openFilePage);
} else {
ui->playBtn->setEnabled(true);
ui->stopBtn->setEnabled(true);
ui->currentSlider->setEnabled(true);
ui->volumnSlider->setEnabled(true);
ui->muteBtn->setEnabled(true);
// 显示播放视频的页面
ui->playWidget->setCurrentWidget(ui->videoPage);
}
}
void MainWindow::on_stopBtn_clicked() {
_player->stop();
}
void MainWindow::on_openFileBtn_clicked() {
QString filename = QFileDialog::getOpenFileName(nullptr,
"选择多媒体文件",
FILEPATH,
"多媒体文件 (*.mp4 *.avi *.mkv *.mp3 *.aac)");
qDebug() << "打开文件" << filename;
if (filename.isEmpty()) return;
// 开始播放打开的文件
_player->setFilename(filename);
_player->play();
}
void MainWindow::on_currentSlider_valueChanged(int value) {
ui->currentLabel->setText(getTimeText(value));
}
void MainWindow::on_volumnSlider_valueChanged(int value) {
ui->volumnLabel->setText(QString("%1").arg(value));
_player->setVolumn(value);
}
void MainWindow::on_playBtn_clicked() {
VideoPlayer::State state = _player->getState();
if (state == VideoPlayer::Playing) {
_player->pause();
} else {
_player->play();
}
}
QString MainWindow::getTimeText(int value){
QString h = QString("0%1").arg(value / 3600).right(2);
QString m = QString("0%1").arg((value / 60) % 60).right(2);
QString s = QString("0%1").arg(value % 60).right(2);
return QString("%1:%2:%3").arg(h).arg(m).arg(s);
}
void MainWindow::on_muteBtn_clicked()
{
if (_player->isMute()) {
_player->setMute(false);
ui->muteBtn->setText("静音");
} else {
_player->setMute(true);
ui->muteBtn->setText("开音");
}
}
以上代码提供了一个基于Qt+FFmpeg的视频播放器基础框架,涵盖了音视频解码、多线程同步、UI交互等核心功能,适合学习多媒体开发的入门。
优化方向主要聚焦在音视频同步、硬件加速、错误处理和功能扩展,通过引入主时钟、硬件解码、完善日志和扩展交互,可进一步提升播放器的性能和用户体验。


