用Qt+FFmpeg手写一个视频播放器(附完整代码)

内容分享2个月前发布
0 0 0

最近有粉丝问我:“想自己做个视频播放器,Qt能搞定吗?

答案是肯定的

在Qt中,有多种方式可以实现播放器功能。列如:

  1. 使用QMediaPlayer类:QMediaPlayer是Qt提供的一个高级多媒体播放器框架,支持多种音频和视频格式。通过使用QMediaPlayer,可以轻松地播放、暂停、停止和切换媒体文件。此外,QMediaPlayer还提供了许多其他有用的功能,如获取媒体信息、设置音量、亮度等。
  2. 使用第三方库:除了QMediaPlayer之外,还有许多第三方库可用于在Qt中实现播放器功能。这些库可提供更多的功能和更好的性能,常见的第三方库包括FFmpeg、GStreamer等。
  3. 自定义播放器:如果需要更高级的功能或者想要实现自己的播放器逻辑,可以思考自定义播放器。这一般涉及到使用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

用Qt+FFmpeg手写一个视频播放器(附完整代码)

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交互等核心功能,适合学习多媒体开发的入门。

优化方向主要聚焦在音视频同步硬件加速错误处理功能扩展,通过引入主时钟、硬件解码、完善日志和扩展交互,可进一步提升播放器的性能和用户体验。

© 版权声明

相关文章

暂无评论

none
暂无评论...