Qt+FFmpeg 极简播放器示例【音视频方向简单讲解】

一、核心概念与架构

  1. Qt: 跨平台的 C++ 应用程序框架,负责提供用户界面(窗口、按钮等)、事件处理、线程管理以及最终的图像显示。
  2. FFmpeg : 强大的开源多媒体框架,负责处理音视频文件的 解封装Demuxing)、解码Decoding)工作。
  3. 协作流程 :
    • Qt 提供界面和显示窗口。
    • FFmpeg 读取媒体文件,解析出音视频流。
    • FFmpeg 将压缩的音视频数据解码成原始的像素数据(如 YUV)和音频采样数据(如 PCM)。
    • Qt 将解码后的视频帧渲染到窗口上(通常需要转换格式,如 YUVRGB)。
    • Qt 将解码后的音频数据送入系统的音频设备播放(通常使用 QAudioOutput)。
    • 需要协调视频帧的显示时间和音频播放时间以实现同步。

二、关键流程分解

  1. 初始化 FFmpeg:

    cpp 复制代码
    // 注册所有编解码器和格式(简化起见,实际可按需注册)
    av_register_all(); // FFmpeg < 4.0, 新版使用 avformat_network_init() 等
    avformat_network_init(); // 如果需要网络协议
    avdevice_register_all(); // 如果需要设备输入
  2. 打开媒体文件 & 查找流信息:

    cpp 复制代码
    AVFormatContext *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;
        }
    }
  3. 打开视频解码器:

    cpp 复制代码
    AVCodecParameters *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) {
        // 错误处理
    }
  4. 打开音频解码器 (类似视频):

    cpp 复制代码
    // ... (与视频类似,找到音频流和对应的解码器并打开)
  5. 准备 Qt 视频渲染:

    • 创建一个 QWidget 或其子类作为视频显示的窗口。
    • 通常会重写 paintEvent 方法,在其中使用 QPainter 将解码后的图像绘制到窗口上。
    • 解码后的原始帧(AVFrame)通常需要转换成 RGB 格式才能被 Qt 直接绘制。可以使用 FFmpegsws_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()) };
  6. 准备 Qt 音频播放 (简化):

    • 使用 QAudioFormat 设置音频参数(采样率、通道数、样本格式)。
    • 创建 QAudioOutput 对象。
    • 创建一个 QIODevice(如 QBuffer)或自定义类来接收解码后的 PCM 数据并写入 QAudioOutputstart() 返回的设备。
    cpp 复制代码
    QAudioFormat 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(); // 准备写入数据的设备
  7. 解码循环 (核心):

    • 在一个循环(通常在单独的线程中)中不断读取 AVPacket
    • AVPacket 发送给对应的解码器(avcodec_send_packet)。
    • 从解码器接收解码完成的 AVFrameavcodec_receive_frame)。
    • 视频帧处理 :
      • 使用 sws_scaleAVFrame 转换为 RGB 格式,填充到 QImage 中。
      • 计算该帧应该显示的时间点(基于 pts 和时基 time_base)。
      • QImage 发送到 UI 线程(使用信号槽 Qt::QueuedConnection),请求重绘窗口。同时传递时间戳用于同步。
    • 音频帧处理 :
      • 将解码后的 PCM 数据(在 AVFramedata 中)写入 audioIO 设备。
      • 音频播放时间自然流逝,可作为同步基准。
    • 同步 : 简单策略是让视频同步到音频。在 UI 线程绘制视频帧前,检查当前音频播放位置(可通过 QAudioOutput 获取或估算)。如果视频帧的 pts 早于当前音频时间,则丢弃或快速显示;如果晚了,则等待到合适时间再显示(使用 QTimerusleep)。
    cpp 复制代码
    AVPacket *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);
    }
  8. UI 线程绘制视频帧 (槽函数):

    cpp 复制代码
    void 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);
    }
  9. 资源清理:

    • 关闭解码器 (avcodec_close, avcodec_free_context)。
    • 关闭输入文件 (avformat_close_input)。
    • 释放 SwsContext (sws_freeContext)。
    • 释放 AVPacketAVFrame (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();
}

重要说明:

  1. 简化: 上述代码框架极度简化,省略了大量错误处理、资源释放、音视频同步细节、音频处理、线程同步、状态管理、暂停/继续、进度条等功能。一个健壮的播放器需要处理这些。
  2. 线程 : 解码循环应运行在独立线程,避免阻塞 UI。使用 QThreadmoveToThreadQt 推荐的方式。UI 更新必须发生在主线程。
  3. 同步: 示例中直接显示了视频帧,未做同步。实际应用中,需要根据音频播放时间或系统时钟来精确控制视频帧的显示时机。
  4. 内存管理 : FFmpegAVPacketAVFrame 需要手动分配和释放 (av_packet_alloc/av_packet_free, av_frame_alloc/av_frame_free)。av_packet_unref 用于重置 packet 内容以便复用。
  5. 像素格式转换 : 确保 sws_getContext 的源和目标的像素格式设置正确。AV_PIX_FMT_RGB32 对应 QImage::Format_RGB32
  6. FFmpeg 版本 : 不同版本 FFmpegAPI 可能有差异(如 av_register_all 在较新版本中被废弃)。请根据你使用的 FFmpeg 版本调整代码。
  7. 库配置 : 项目需要正确链接 FFmpeg 相关的库文件 (avformat, avcodec, swscale, avutil 等) 并包含头文件路径。

四、总结

使用 QtFFmpeg 构建播放器的核心在于理解两者分工:FFmpeg 负责底层解码,Qt 负责上层界面和渲染。关键步骤包括媒体文件解析、解码器初始化、解码循环、像素格式转换、跨线程图像传递以及音视频同步。这个极简框架提供了一个起点,实际开发中需要在此基础上完善错误处理、用户控制、性能优化和功能扩展。

相关推荐
没有余地 EliasJie2 小时前
FFmpeg介绍与ESP32资源受限下的视频流传输优化策略
单片机·物联网·ffmpeg
纠结哥_Shrek2 小时前
AI视频生成提示词工程完全指南
人工智能·音视频
Alonse_沃虎电子2 小时前
沃虎电子:音频变压器5大痛点剖析与厂家定制化解决方案
网络·音视频·信息与通信·产品·介绍·电子元器件
FuckPatience2 小时前
QT 不允许使用不完整的类型
qt
顾道长生'2 小时前
(Arxiv-2026)Helios:真正的实时长视频生成模型
音视频·自回归·长视频生成
Alonse_沃虎电子3 小时前
沃虎电子VOOHU音频变压器如何定义高保真音质
物联网·音视频·产品·方案·电子元器件·设计策略
顾道长生'3 小时前
(Arxiv-2026)HiAR:基于分层去噪的高效自回归长视频生成
回归·kotlin·音视频·长视频生成
四维碎片3 小时前
【Qt】 无边框窗口方案
开发语言·qt
sycmancia3 小时前
QT——Qt Creator工程介绍
开发语言·qt