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