Qt 基于FFmpeg的视频播放器 - QtFFmpegPlayer

引言

在这里插入图片描述

  • 本文基于FFmpeg,使用Qt制作了一个极简的视频播放器. 如上所示:
  • FFmpeg版本信息:ffmpeg-n7.0-latest-win64-lgpl-shared-7.0

FFmpeg是一个开源的跨平台音视频处理工具,它提供了音视频编解码、格式转换、流媒体处理等功能。FFmpeg可以在命令行中使用,也可以通过API集成到其他应用程序中使用。FFmpeg支持众多音视频编码格式,如MP3、AAC、AC3、H.264、MPEG-4等。它可以将不同格式的音视频文件转换为其他格式,从而满足不同设备和平台的需求。除了转换格式,FFmpeg还可以进行音视频的剪切、合并、裁剪、旋转等操作。它可以提取音频或视频流,并且支持添加字幕、水印等特效。在流媒体处理方面,FFmpeg可以通过RTMP、HLS、UDP等协议进行直播推流和播放。它可以将本地音视频流推送到流媒体服务器,也可以从流媒体服务器拉取音视频流进行播放。FFmpeg是一个功能强大且灵活的音视频处理工具,被广泛应用于视频编辑、媒体转换、流媒体服务等领域。它的开源特性使得开发者可以自由定制和扩展功能,同时也极大地方便了用户在不同平台上进行音视频处理。

一、设计思路

参考以下博客:
【Qt+FFmpeg】解码播放本地视频(一):https://blog.csdn.net/logani/article/details/127233337
版本信息如下:

1.1 FFmpegVideo类

FFmpeg视频类,主要针对视频文件 (暂不包含音频):设计用来调用FFmpeg视频相关API,编解码以及各式转换等

  • 设计一些变量存储视频信息,主要涉及视频的读取、播放等
  • 设计函数进行视频处理 (解码、转换等) - 封装FFmpeg API

1.2 视频打开与播放函数 (本文重点)

1. 视频打开(获取相关信息):

  • 首先获取AVFormatContext,视频的上下文格式,里面包含视频的各种信息
  • 通过AVFormatContext获取视频流编号和编码器idcodec_id
  • 根据编码器id获取相应的解码器AVCodec,初始化编解码器AVCodecContext
  • 通过编解码器AVCodecContext获取视频帧率 m_fps

2. 视频播放 (循环解码即可):

  • 变量以及缓冲区初始化
  • 使用av_read_frame顺序读取AVFormatContext中一帧数据AVPacket
  • 如果是视频流即用avcodec_send_packet解码,avcodec_receive_frame获取解码输出
  • 使用sws_scale进行将一帧图片转换为RGB格式,赋值到QImage
  • 发送赋值后的QImage到相应的显示窗口 (信号和槽)
  • 根据视频帧率m_fps,使用QThread::msleep延时 (或使用QTimer等实现延时效果)

/todo 视频格式的转换等

1.3 播放线程

在主窗体里创建播放线程:QThread * m_PlayThread;

  • 初始化播放线程,将FFmpegVideo移动到线程中
  • 绑定线程的开始信号FFmpegVideo播放槽函数
  • 启动线程即可播放视频 (需要先读取视频文件)

还可以直接将FFmpegVideo设计为继承QThead,将视频播放的代码放到run()函数即可

1.4 播放窗口

新建一个窗口,继承QWidget

  • 创建接收图片的槽函数,保存接收的图片,然后刷新界面update()
  • 重新实现paintEvent窗体重绘函数,将槽函数接收到的图片画在界面上drawImage

需要绑定FFmpeg发送图片的信号 和 界面中接收图片的槽函数

二、核心源码

2.1 FFmpegVideo类

  • ffmpegvideo.h
#ifndef FFMPEGVIDEO_H
#define FFMPEGVIDEO_H

#include <QString>
#include <QObject>
#include <QImage>

// FFmpeg头文件
extern "C"{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
#include "libswresample/swresample.h"
}

class FFmpegVideo : public QObject
{
    Q_OBJECT

public:
    /**
     * @brief FFmpegVideo构造函数
     */
    FFmpegVideo();

    ~FFmpegVideo();


public:  // 值
    QString m_filename;  ///< 文件名

    AVFormatContext* avformat_context;  ///< 视频文件上下文格式
    AVCodecContext* avcodec_context;    ///< 编解码器上下文格式
    int av_stream_index;                ///< 保存视频流的索引
    int m_fps;           ///< 帧率
    int m_frame_id;      ///< 当前帧id
    bool m_stop;         ///< 是否暂停

public:  // 函数
    /**
     * @brief  加载视频
     * @param filename
     * @return
     */
    bool loadVideoFile(QString filename);

public slots:
    /**
     * @brief 播放视频
     */
    void play();

signals:
    void sig_SendOneFrame(QImage image);  ///< 发送一帧
};

#endif // FFMPEGVIDEO_H

  • ffmpegvideo.cpp
#include "ffmpegvideo.h"
#include <QDebug>
#include <QThread>

FFmpegVideo::FFmpegVideo(){

}

FFmpegVideo::~FFmpegVideo(){

}

bool FFmpegVideo::loadVideoFile(QString filename){

    this->m_filename = filename;
    char* error_info = new char[32];     //异常信息

    // 获取视频文件格式
    avformat_context = avformat_alloc_context();  // 开辟空间
    int avformat_open_result = avformat_open_input(&avformat_context,
                                                   filename.toStdString().c_str(), nullptr, nullptr);
    if (avformat_open_result != 0){
        av_strerror(avformat_open_result, error_info, 100);
        qDebug()<<QString("获取视频文件格式失败 %1").arg(error_info);
        return false;
    };

    // 获取视频流
    int avformat_find_stream_info_result = avformat_find_stream_info(avformat_context, nullptr);
    if (avformat_find_stream_info_result < 0){
        av_strerror(avformat_find_stream_info_result, error_info, 100);
        qDebug()<<QString("获取音视频流失败 %1").arg(error_info);
    }
    av_stream_index = -1;
    for (quint8 i = 0; i < avformat_context->nb_streams; i++){  //循环遍历每一流找到视频流
        if (avformat_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){

            av_stream_index = i;  // 则streams[av_stream_index]是视频流
            break;
        }
    }
    if (av_stream_index == -1){
        qDebug()<<QString("没有找到视频流");
        return false;
    }

    // 获取解码器
    const AVCodec* avcodec = avcodec_find_decoder(avformat_context->streams[av_stream_index]->codecpar->codec_id);
    if (avcodec == nullptr){
        qDebug()<<QString("没有找到视频解码器");
        return false;
    }
    avcodec_context = avcodec_alloc_context3(avcodec);
    if (avcodec_parameters_to_context(avcodec_context, avformat_context->streams[av_stream_index]->codecpar) < 0){
        qDebug()<<"解码器拷贝参数失败";
        return false;
    }
    int avcodec_open2_result = avcodec_open2(avcodec_context, avcodec, nullptr);
    if (avcodec_open2_result != 0){
        av_strerror(avformat_find_stream_info_result, error_info, 100);
        qDebug()<<QString("打开解码器失败 %1").arg(error_info);
        return false;
    }

    /// 视频信息输出
    AVRational framerate = avcodec_context->framerate;
    this->m_fps = framerate.num / framerate.den;
    qDebug()<<"帧率"<<this->m_fps;
    qDebug()<<"视频详细信息输出";
    qDebug()<<"视频时长/s"<<avformat_context->duration/1000000;
    qDebug()<<QString("视频分辨率%1x%2").arg(avcodec_context->width).arg(avcodec_context->height);

    return true;
}

void FFmpegVideo::play(){

    // 初始化临时变量
    AVPacket* av_packet = static_cast<AVPacket*>(av_malloc(sizeof(AVPacket)));
    AVFrame *pFramein = av_frame_alloc();   //输入和输出的帧数据
    AVFrame *pFrameRGB = av_frame_alloc();
    uint8_t * pOutbuffer = static_cast<uint8_t *>(av_malloc(      //缓冲区分配内存
                           static_cast<quint64>(
                           av_image_get_buffer_size(AV_PIX_FMT_RGB32,
                                                    avcodec_context->width,
                                                    avcodec_context->height,
                                                    1))));
    // 初始化缓冲区
    av_image_fill_arrays(pFrameRGB->data,
                         pFrameRGB->linesize,
                         pOutbuffer,
                         AV_PIX_FMT_RGB32,
                         avcodec_context->width, avcodec_context->height, 1);

    // 格式转换
    SwsContext* pSwsContext = sws_getContext(avcodec_context->width,    // 输入宽
                                             avcodec_context->height,   // 输入高
                                             avcodec_context->pix_fmt,  // 输入格式
                                             avcodec_context->width,    // 输出宽
                                             avcodec_context->height,   // 输出高
                                             AV_PIX_FMT_RGB32,          // 输出格式
                                             SWS_BICUBIC,               ///todo
                                             nullptr,
                                             nullptr,
                                             nullptr);

    int ret=0;
    this->m_stop = false;
    this->m_frame_id = 0;

    // 开始循环
    while (m_stop == false){
        //从视频文件上下文中读取包--- 有数据就一直读取
        if (av_read_frame(avformat_context, av_packet) >= 0){
            //解码什么类型流(视频流、音频流、字幕流等等...)
            if (av_packet->stream_index == av_stream_index){
                avcodec_send_packet(avcodec_context, av_packet);       // 解码
                ret = avcodec_receive_frame(avcodec_context,pFramein); // 获取解码输出
                if (ret == 0){
                    sws_scale(pSwsContext,  //图片格式的转换
                              static_cast<const uint8_t* const*>(pFramein->data),
                              pFramein->linesize, 0, avcodec_context->height,
                              pFrameRGB->data,  pFrameRGB->linesize);

                    QImage  *tmpImg  = new QImage(static_cast<uchar *>(pOutbuffer),
                                                  avcodec_context->width,
                                                  avcodec_context->height,
                                                  QImage::Format_RGB32);
                    QImage image = tmpImg->copy();
                    this->m_frame_id++;            //计数第几帧
                    emit sig_SendOneFrame(image);  //发送图片信号
                    QThread::msleep(quint32(1000/this->m_fps));  //延时操作  1秒显示fps帧
                    qDebug()<<QString("当前遍历第 %1 帧").arg(m_frame_id);
                }
            }
        }
        else{
            qDebug()<<"播放完毕";
            this->m_frame_id = 0;
            m_stop = true;
        }
        av_packet_unref(av_packet);
    }
}

2.2 播放窗口

  • qwidget_playvideo.h
#ifndef QWIDGET_PLAYVIDEO_H
#define QWIDGET_PLAYVIDEO_H

#include <QWidget>

class QWidget_PlayVideo : public QWidget
{
    Q_OBJECT
public:
    explicit QWidget_PlayVideo(QWidget *parent = nullptr);
    QImage image;

    void paintEvent(QPaintEvent *); ///< 窗体重绘

signals:

public slots:
    void slot_RecvOneFrame(QImage im); ///< 接收一张图片

};

#endif // QWIDGET_PLAYVIDEO_H

  • qwidget_playvideo.cpp
#include "qwidget_playvideo.h"
#include <QPainter>
#include <QDebug>

QWidget_PlayVideo::QWidget_PlayVideo(QWidget *parent) : QWidget(parent)
{

}

void QWidget_PlayVideo::slot_RecvOneFrame(QImage im){
    //qDebug()<<"slot_RecvOneFrame";
    this->image = im;
    this->update();  //刷新界面
}


void QWidget_PlayVideo::paintEvent(QPaintEvent *)
{
    QPainter painter(this);
    if(!this->image.isNull()){ //不为空则刷新
        painter.drawImage(QRect(0, 0, this->width(), this->height()), this->image);
    }
}

2.3 mainwindow.cpp中的调用

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QPainter>
#include <QDebug>
#include <QFileDialog>
#include <QString>

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    // 1. 初始化界面相关
    m_Widget_PlayVideo = new QWidget_PlayVideo(this);
    this->setCentralWidget(m_Widget_PlayVideo);
    connect(ui->action_open, &QAction::triggered, this, &MainWindow::openVideo);

    // 2. 初始化视频
    m_FFmpegVideo = new FFmpegVideo();
    connect(m_FFmpegVideo, SIGNAL(sig_SendOneFrame(QImage)),      // 视频发送 绑定 播放界面的接收
            m_Widget_PlayVideo, SLOT(slot_RecvOneFrame(QImage)),
            Qt::AutoConnection);

    // 3. 初始化播放线程
    m_PlayThread = new QThread(this);
    m_FFmpegVideo->moveToThread(m_PlayThread);  // 移动到线程中
    connect(m_PlayThread, SIGNAL(started()),    // 播放线程的开始信号 绑定 播放函数 实现异步
            m_FFmpegVideo, SLOT(play()),
            Qt::AutoConnection);
}

MainWindow::~MainWindow()
{
    if(m_PlayThread->isRunning()){
        m_PlayThread->quit();
        m_PlayThread->wait();
    }
    delete m_PlayThread;
    delete ui;
}

void MainWindow::openVideo()
{
    QString filePath = QFileDialog::getOpenFileName(this, QObject::tr("Open File"),
                                                    nullptr,  // QDir::homePath(),
                                                    QObject::tr("mp4 (*.mp4) ;; All Files (*)"));
    if (!filePath.isEmpty()) {
        m_FFmpegVideo->loadVideoFile(filePath);
        m_PlayThread->start();
    }
}

三、其它参考链接

Qt实现 基于ffmpeg拉流播放视频:https://blog.csdn.net/c_shell_python/article/details/109699033
FFmpeg音视频编解码 (详细分析以及Qt代码示例):https://blog.csdn.net/m0_61745661/category_11741735.html
基于Microsoft Visual Studio2019环境编写ffmpeg视频解码代码:https://blog.csdn.net/CHYabc123456hh/article/details/125274785

  • 26
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

大米粥哥哥

感谢认可!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值