首页 > 其他分享 >利用QT和FFmpeg实现一个简单的视频播放器

利用QT和FFmpeg实现一个简单的视频播放器

时间:2024-08-10 10:24:47浏览次数:17  
标签:播放器 QT int void VideoPlayer player ui include FFmpeg

          在当今的多媒体世界中,视频播放已成为不可或缺的一部分。从简单的媒体播放器到复杂的视频编辑软件,视频解码和显示技术无处不在。本示例使用Qt和FFmpeg构建一个简单的视频播放器。利用ffmpeg解码视频,通过QWidget渲染解码后的图像,支持进度条跳转、进度条显示,总时间显示,视频基本信息显示。特点: 采用软件解码(CPU)、只解码图像数据,主要是演示了ffmpeg的基本使用流程,如何通过ffmpeg完成视频解码,转换图像像素格式,最后完成图像渲染。视频解码采用独立子线程,解码后将得到的图像数据通过信号槽发方式传递给UI界面进行渲染。

一、 环境介绍    

1、QT版本: QT5.12.6

2、编译器:  MSVC2017 64

3、ffmpeg版本: 6.1.1

4、SDL2 音频播放所需

5、完整工程下载地址(下载即可编译运行): https://download.csdn.net/download/u012959478/89626950

二、实现功能
  • 使用ffmpeg音视频库软解码实现视频播放器
  • 支持打开多种本地视频文件(如mp4,mov,avi等)
  • 支持视频匀速播放
  • 采用QPainter进行图像显示,支持自适应窗口缩放
  • 视频播放支持实时开始,暂停,继续播放
  • 采用模块化编程,视频解码,线程控制,图像显示各功能分离,低耦合
  • 多线程编程
三、实现思路  

该视频播放器的主要运行三条线程,需要两条队列:

线程1(音视频数据分离):使用FFMPEG分解视频文件,将视频数据存入到视频队列中,将音频数据存入到音频队列中。

线程2(视频解码):从视频队列中获取一包视频数据,通过FFMPEG解码该包视频数据,解码后再将视频转换为RGB数据,最后通过QT的画图显示将视频画面显示出来。

线程3(音频解码):实际该线程由SDL新建,它是通过回调的方式来从音频队列中获取音频数据,由SDL解码后再进行声音的播放。

四、示例代码  
 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进行音频重采样,并使用fdk-aac进行编码。这个录音软件不仅简单易用,可以帮助我们记录和存储语音信息,是一个非常实用的工具。

五、运行效果

​​​​​​​

        谢谢您的阅读。希望本文能对您有所帮助,并且给您带来了一些新的观点和思考。如果您有任何问题或意见,请随时与我联系。再次感谢您的支持!

 六、相关文章

Windosw下Visual Studio2022编译FFmpeg(支持x264、x265、fdk-acc)-CSDN博客

标签:播放器,QT,int,void,VideoPlayer,player,ui,include,FFmpeg
From: https://blog.csdn.net/u012959478/article/details/141086028

相关文章

  • mqtt订阅和发布
    importpaho.mqtt.clientasmqttimporttimeMQTTHOST="192.168.0.4"MQTTPORT=1883mqttClient=mqtt.Client()#连接MQTT服务器defon_mqtt_connect():mqttClient.connect(MQTTHOST,MQTTPORT,60)mqttClient.loop_start()#publish消息defon_publish(t......
  • 使用Qt实现自定义GraphicsView
    在本文中,我们将介绍如何使用Qt实现一个自定义的GraphicsView,主要是作为笔记使用QGraphicsView框架方面的使用手法、套路,对代码就不做过多的解释了,它具有以下功能:显示图像可拖动的十字标记(CrossMarkItem)可调整大小的ROI(RegionofInterest)矩形FPS和日期时间显示保存和......
  • qt之QTableWidget按列遍历数据
    QObject::connect(ui->tableWidget_4,&QTableWidget::itemSelectionChanged,[=](){QList<QTableWidgetItem*>selectedItems=ui->tableWidget_4->selectedItems();if(!selectedItems.isEmpty()){in......
  • 基于YOLOv10深度学习的交通信号灯检测识别系统【python源码+Pyqt5界面+数据集+训练代
    《博主简介》小伙伴们好,我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。✌更多学习资源,可关注公-仲-hao:【阿旭算法与机器学习】,共同学习交流~......
  • 关于Qt使用msvc时安装了Windows SDK后还显示警告
    如果我们在安装Qt时没有选择SDK或者其他原因,你的套件前面就会有一个黄色的感叹号。但是当我们安装SDK之后,而且电脑重启之后,会发现还是这个鸟样当我点击编译器时,我发现是有的所以我想在kit这里是不是要自己配置呢?然后就大胆进行配置了下点击ok,然后创建一个项目发现......
  • qt 输入一张图片,在图片上绘制后,再另存为图片
    boolDdrawCircleOnImage(constQString&inputImagePath,constQString&outputImagePath,QVector<QPoint>dotData){if(inputImagePath.isEmpty()||outputImagePath.isEmpty()){qWarning("输入图片路径无效!");retur......
  • 实现qt页面cpp
    在Qt中实现一个具体的界面,首先需要确定你的界面需求,包括需要哪些控件(如按钮、文本框、标签等)、布局方式(如垂直布局、水平布局、网格布局等)以及可能的交互逻辑。下面是一个简单的步骤,用于在Qt中实现一个基本的界面:创建Qt项目:使用QtCreator创建一个新的QtWidgetsApplicat......
  • FFmpeg源码:av_realloc、av_reallocp、size_mult、av_realloc_f函数分析
    =================================================================FFmpeg内存管理相关的源码分析:FFmpeg中内存分配和释放相关的源码:av_malloc函数、av_mallocz函数、av_free函数和av_freep函数分析FFmpeg源码:av_realloc、av_reallocp、size_mult、av_realloc_f函数分析FF......
  • FFmpeg存放压缩后的音视频数据的结构体:AVPacket简介
    FFmpeg源码中通过AVPacket存储压缩后的音视频数据。它通常由解复用器(demuxers)输出,然后作为输入传递给解码器,或者从编码器作为输出接收,然后传递给多路复用器(muxers)。对于视频,它通常包含一个压缩帧;对于音频,它可能包含几个压缩帧。编码器允许输出不包含压缩音视频数据、只包含side......
  • vue3(nuxt3)+Aliplayer播放器进行直播播流
    前言:    上一篇讲到使用自定义的一个播放器去进行播流进行观看直播,由于之前都是自己研发的,服务器不是特别好,所以决定使用阿里的推流以及阿里的播放器去进行拉流也更加的适配吧,至少后面出现问题可以有文档看比较完善实践    1.这里的话先把官方文档的地......