一、核心概念与架构
- Qt: 跨平台的 C++ 应用程序框架,负责提供用户界面(窗口、按钮等)、事件处理、线程管理以及最终的图像显示。
- FFmpeg : 强大的开源多媒体框架,负责处理音视频文件的 解封装 (
Demuxing)、解码 (Decoding)工作。 - 协作流程 :
Qt提供界面和显示窗口。FFmpeg读取媒体文件,解析出音视频流。FFmpeg将压缩的音视频数据解码成原始的像素数据(如YUV)和音频采样数据(如PCM)。Qt将解码后的视频帧渲染到窗口上(通常需要转换格式,如YUV转RGB)。Qt将解码后的音频数据送入系统的音频设备播放(通常使用QAudioOutput)。- 需要协调视频帧的显示时间和音频播放时间以实现同步。
二、关键流程分解
-
初始化 FFmpeg:
cpp// 注册所有编解码器和格式(简化起见,实际可按需注册) av_register_all(); // FFmpeg < 4.0, 新版使用 avformat_network_init() 等 avformat_network_init(); // 如果需要网络协议 avdevice_register_all(); // 如果需要设备输入 -
打开媒体文件 & 查找流信息:
cppAVFormatContext *pFormatCtx = avformat_alloc_context(); if (avformat_open_input(&pFormatCtx, filepath, nullptr, nullptr) != 0) { // 错误处理 } if (avformat_find_stream_info(pFormatCtx, nullptr) < 0) { // 错误处理 } // 查找视频流和音频流索引 int videoStream = -1, audioStream = -1; for (int i = 0; i < pFormatCtx->nb_streams; i++) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { videoStream = i; } else if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) { audioStream = i; } } -
打开视频解码器:
cppAVCodecParameters *pVideoCodecPar = pFormatCtx->streams[videoStream]->codecpar; const AVCodec *pVideoCodec = avcodec_find_decoder(pVideoCodecPar->codec_id); AVCodecContext *pVideoCodecCtx = avcodec_alloc_context3(pVideoCodec); avcodec_parameters_to_context(pVideoCodecCtx, pVideoCodecPar); if (avcodec_open2(pVideoCodecCtx, pVideoCodec, nullptr) < 0) { // 错误处理 } -
打开音频解码器 (类似视频):
cpp// ... (与视频类似,找到音频流和对应的解码器并打开) -
准备 Qt 视频渲染:
- 创建一个
QWidget或其子类作为视频显示的窗口。 - 通常会重写
paintEvent方法,在其中使用QPainter将解码后的图像绘制到窗口上。 - 解码后的原始帧(
AVFrame)通常需要转换成RGB格式才能被Qt直接绘制。可以使用FFmpeg的sws_scale进行转换。
cpp// 创建 SwsContext 用于 YUV -> RGB 转换 (假设视频帧是 YUV420P) SwsContext *sws_ctx = sws_getContext( pVideoCodecCtx->width, pVideoCodecCtx->height, pVideoCodecCtx->pix_fmt, pVideoCodecCtx->width, pVideoCodecCtx->height, AV_PIX_FMT_RGB32, SWS_BILINEAR, nullptr, nullptr, nullptr ); // 分配转换后的 RGB 帧所需的内存 QImage videoImage(pVideoCodecCtx->width, pVideoCodecCtx->height, QImage::Format_RGB32); uint8_t *dstData[1] = { videoImage.bits() }; int dstLinesize[1] = { static_cast<int>(videoImage.bytesPerLine()) }; - 创建一个
-
准备 Qt 音频播放 (简化):
- 使用
QAudioFormat设置音频参数(采样率、通道数、样本格式)。 - 创建
QAudioOutput对象。 - 创建一个
QIODevice(如QBuffer)或自定义类来接收解码后的PCM数据并写入QAudioOutput的start()返回的设备。
cppQAudioFormat format; format.setSampleRate(pAudioCodecCtx->sample_rate); format.setChannelCount(pAudioCodecCtx->channels); format.setSampleSize(16); // 假设解码成16位PCM format.setSampleType(QAudioFormat::SignedInt); format.setCodec("audio/pcm"); QAudioOutput *audioOutput = new QAudioOutput(format); QIODevice *audioIO = audioOutput->start(); // 准备写入数据的设备 - 使用
-
解码循环 (核心):
- 在一个循环(通常在单独的线程中)中不断读取
AVPacket。 - 将
AVPacket发送给对应的解码器(avcodec_send_packet)。 - 从解码器接收解码完成的
AVFrame(avcodec_receive_frame)。 - 视频帧处理 :
- 使用
sws_scale将AVFrame转换为RGB格式,填充到QImage中。 - 计算该帧应该显示的时间点(基于
pts和时基time_base)。 - 将
QImage发送到UI线程(使用信号槽Qt::QueuedConnection),请求重绘窗口。同时传递时间戳用于同步。
- 使用
- 音频帧处理 :
- 将解码后的
PCM数据(在AVFrame的data中)写入audioIO设备。 - 音频播放时间自然流逝,可作为同步基准。
- 将解码后的
- 同步 : 简单策略是让视频同步到音频。在
UI线程绘制视频帧前,检查当前音频播放位置(可通过QAudioOutput获取或估算)。如果视频帧的pts早于当前音频时间,则丢弃或快速显示;如果晚了,则等待到合适时间再显示(使用QTimer或usleep)。
cppAVPacket *packet = av_packet_alloc(); AVFrame *frame = av_frame_alloc(); while (!stopRequested) { if (av_read_frame(pFormatCtx, packet) < 0) break; // 读包 if (packet->stream_index == videoStream) { avcodec_send_packet(pVideoCodecCtx, packet); while (avcodec_receive_frame(pVideoCodecCtx, frame) == 0) { // 转换 YUV -> RGB sws_scale(sws_ctx, frame->data, frame->linesize, 0, pVideoCodecCtx->height, dstData, dstLinesize); // 计算显示时间戳 (pts) 转换为毫秒或其他单位 double pts_sec = frame->pts * av_q2d(pFormatCtx->streams[videoStream]->time_base); // 发射信号,携带videoImage和pts_sec,通知UI线程更新图像 emit frameDecoded(videoImage.copy(), pts_sec); // 注意拷贝避免竞争 } } else if (packet->stream_index == audioStream) { // 类似地,发送给音频解码器,接收帧,写入audioIO } av_packet_unref(packet); } - 在一个循环(通常在单独的线程中)中不断读取
-
UI 线程绘制视频帧 (槽函数):
cppvoid VideoWidget::onFrameDecoded(QImage image, double pts) { // 简单同步:检查当前音频播放时间 (currentAudioPos) // 如果 pts > currentAudioPos, 计算需要等待的时间 delta = (pts - currentAudioPos) * 1000 -> ms // 使用QTimer::singleShot(delta, this, [this, image](){ ... }) 来延迟显示 // 如果 pts <= currentAudioPos, 直接显示或丢弃 // 更新当前显示的图像 currentImage = image; update(); // 触发 paintEvent } void VideoWidget::paintEvent(QPaintEvent *event) { QPainter painter(this); painter.drawImage(rect(), currentImage); } -
资源清理:
- 关闭解码器 (
avcodec_close,avcodec_free_context)。 - 关闭输入文件 (
avformat_close_input)。 - 释放
SwsContext(sws_freeContext)。 - 释放
AVPacket和AVFrame(av_packet_free,av_frame_free)。 - 停止并删除
QAudioOutput。 - 释放
AVFormatContext(avformat_free_context)。
- 关闭解码器 (
三、极简源码框架示例 (核心部分)
cpp
#include <QtWidgets>
#include <QAudioOutput>
extern "C" {
#include <libavformat/avformat.h>
#include <libavcodec/avcodec.h>
#include <libswscale/swscale.h>
}
class VideoWidget : public QWidget {
Q_OBJECT
public:
VideoWidget(QWidget *parent = nullptr) : QWidget(parent) {}
void setImage(const QImage &img) { currentImage = img; update(); }
protected:
void paintEvent(QPaintEvent*) override {
QPainter p(this);
p.drawImage(rect(), currentImage);
}
private:
QImage currentImage;
};
class Player : public QObject {
Q_OBJECT
public:
Player(VideoWidget *vw) : videoWidget(vw), stopFlag(false) {}
void play(const QString &file);
void stop() { stopFlag = true; }
signals:
void frameDecoded(QImage image, double pts);
private:
VideoWidget *videoWidget;
volatile bool stopFlag;
};
void Player::play(const QString &file) {
// FFmpeg 初始化 (略)
AVFormatContext *fmtCtx = nullptr;
// ... 打开文件,查找流信息 (略)
// 打开视频解码器 (略)
AVCodecContext *videoCtx = ...;
// 创建 SwsContext (YUV->RGB) (略)
SwsContext *swsCtx = ...;
// 音频相关初始化 (简化,略)
QAudioOutput *audioOutput = nullptr;
QIODevice *audioIO = nullptr;
AVPacket *pkt = av_packet_alloc();
AVFrame *frame = av_frame_alloc();
QImage img(videoCtx->width, videoCtx->height, QImage::Format_RGB32);
while (!stopFlag && av_read_frame(fmtCtx, pkt) >= 0) {
if (pkt->stream_index == videoStreamIdx) {
avcodec_send_packet(videoCtx, pkt);
while (avcodec_receive_frame(videoCtx, frame) == 0) {
uint8_t *dst[1] = { img.bits() };
int dstStride[1] = { static_cast<int>(img.bytesPerLine()) };
sws_scale(swsCtx, frame->data, frame->linesize, 0, videoCtx->height, dst, dstStride);
double pts = frame->pts * av_q2d(fmtCtx->streams[videoStreamIdx]->time_base);
emit frameDecoded(img.copy(), pts); // 发送信号给UI线程
}
} else if (pkt->stream_index == audioStreamIdx && audioOutput) {
// 音频解码并写入audioIO (略)
}
av_packet_unref(pkt);
}
// 清理资源 (略)
av_packet_free(&pkt);
av_frame_free(&frame);
// ...
}
// MainWindow 或主类
class MainWindow : public QMainWindow {
Q_OBJECT
public:
MainWindow() {
videoWidget = new VideoWidget(this);
setCentralWidget(videoWidget);
player = new Player(videoWidget);
connect(player, &Player::frameDecoded, this, [this](QImage img, double pts) {
// 简单同步:这里省略复杂的同步逻辑,直接显示
videoWidget->setImage(img);
});
QPushButton *btn = new QPushButton("Play", this);
connect(btn, &QPushButton::clicked, this, [this]() {
QString file = QFileDialog::getOpenFileName(this);
if (!file.isEmpty()) {
QThread *thread = new QThread;
player->moveToThread(thread);
connect(thread, &QThread::started, [this, file]() { player->play(file); });
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
}
});
// ... 布局
}
private:
VideoWidget *videoWidget;
Player *player;
};
int main(int argc, char *argv[]) {
QApplication app(argc, argv);
MainWindow w;
w.show();
return app.exec();
}
重要说明:
- 简化: 上述代码框架极度简化,省略了大量错误处理、资源释放、音视频同步细节、音频处理、线程同步、状态管理、暂停/继续、进度条等功能。一个健壮的播放器需要处理这些。
- 线程 : 解码循环应运行在独立线程,避免阻塞
UI。使用QThread和moveToThread是Qt推荐的方式。UI更新必须发生在主线程。 - 同步: 示例中直接显示了视频帧,未做同步。实际应用中,需要根据音频播放时间或系统时钟来精确控制视频帧的显示时机。
- 内存管理 :
FFmpeg的AVPacket和AVFrame需要手动分配和释放 (av_packet_alloc/av_packet_free,av_frame_alloc/av_frame_free)。av_packet_unref用于重置packet内容以便复用。 - 像素格式转换 : 确保
sws_getContext的源和目标的像素格式设置正确。AV_PIX_FMT_RGB32对应QImage::Format_RGB32。 - FFmpeg 版本 : 不同版本
FFmpeg的API可能有差异(如av_register_all在较新版本中被废弃)。请根据你使用的FFmpeg版本调整代码。 - 库配置 : 项目需要正确链接
FFmpeg相关的库文件 (avformat,avcodec,swscale,avutil等) 并包含头文件路径。
四、总结
使用 Qt 和 FFmpeg 构建播放器的核心在于理解两者分工:FFmpeg 负责底层解码,Qt 负责上层界面和渲染。关键步骤包括媒体文件解析、解码器初始化、解码循环、像素格式转换、跨线程图像传递以及音视频同步。这个极简框架提供了一个起点,实际开发中需要在此基础上完善错误处理、用户控制、性能优化和功能扩展。