QT+ffmpeg学习笔记-自制一个简易播放器(一)-CSDN博客
在上篇文章中我们通过Qlable来显示视频,在本文章中,我们将通过Qpainter绘制视频播放页面,并增加流播放的方式,且将ffmpeg操作独立存放在一个线程,使得mainwindows只做ui显示。
- 创建一个自定义的
QWidget
来替换当前的QLabel
。 - 将FFmpeg的播放操作移到一个单独的线程中。
- 添加不同功能
目录
步骤2:修改MainWindow以使用自定义的VideoWidget
添加一个流播放功能,可以选择rtmp,rtsp,udp方式拉流,且增加一个本地视频播放选取的控件与播放按钮,增加一个网络流播放地址输入框与播放按钮
步骤1:创建自定义的QWidget
创建VideoWidget
类
创建两个新文件videowidget.h
和videowidget.cpp
。
在代码中声明VideoWidget
类
确保你已经按照上面的步骤创建了VideoWidget
类,并包含了头文件。接下来,在mainwindow.ui
文件中进行替换。
使用Qt Designer进行控件替换
-
打开
mainwindow.ui
文件: 使用Qt Designer打开你的mainwindow.ui
文件。 -
找到中央窗口小部件: 在UI设计器中,找到中央窗口小部件(通常是一个
QWidget
)。 -
替换中央窗口小部件:
- 右键点击中央窗口小部件,然后选择“提升为...”(Promote to...)。
- 在弹出的对话框中,填写如下内容:
- 基类名称(Base class name):
QWidget
- 提升的类名称(Promoted class name):
VideoWidget
- 头文件(Header file):
videowidget.h
- 基类名称(Base class name):
- 点击“添加”(Add),然后点击“提升”(Promote)。
-
保存并生成代码: 保存
mainwindow.ui
文件,并重新编译你的项目。
videowidget.h
#ifndef VIDEOWIDGET_H
#define VIDEOWIDGET_H
#include <QWidget>
#include <QImage>
#include <QMutex>
class VideoWidget : public QWidget {
Q_OBJECT
public:
explicit VideoWidget(QWidget *parent = nullptr);
void setFrame(const QImage &frame);
protected:
void paintEvent(QPaintEvent *event) override;
private:
QImage currentFrame;
QMutex mutex;
};
#endif // VIDEOWIDGET_H
videowidget.cpp
#include "videowidget.h"
#include <QPainter>
VideoWidget::VideoWidget(QWidget *parent) : QWidget(parent) {
}
void VideoWidget::setFrame(const QImage &frame) {
QMutexLocker locker(&mutex);
currentFrame = frame;
update();
}
void VideoWidget::paintEvent(QPaintEvent *event) {
Q_UNUSED(event);
QPainter painter(this);
QMutexLocker locker(&mutex);
if (!currentFrame.isNull()) {
painter.drawImage(rect(), currentFrame);
}
}
步骤2:修改MainWindow
以使用自定义的VideoWidget
修改mainwindow.ui
用Qt Designer打开mainwindow.ui
文件,并将中央窗口小部件替换为VideoWidget
。以下是手动修改的XML代码:
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<widget class="QWidget" name="centralWidget">
<widget class="VideoWidget" name="videoWidget"/> <!-- 使用VideoWidget -->
</widget>
</widget>
<resources/>
<connections/>
</ui>
修改mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "videowidget.h"
#include "ffmpegthread.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 on_open_button_clicked();
private:
Ui::MainWindow *ui;
FfmpegThread *ffmpegThread;
};
#endif // MAINWINDOW_H
修改mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
ffmpegThread = new FfmpegThread(this, ui->videoWidget);
connect(ffmpegThread, &FfmpegThread::frameReady, ui->videoWidget, &VideoWidget::setFrame);
}
MainWindow::~MainWindow() {
ffmpegThread->requestInterruption();
ffmpegThread->wait();
delete ui;
}
void MainWindow::on_open_button_clicked() {
ffmpegThread->start();
}
步骤3:将FFmpeg播放操作移到单独的线程中
创建FfmpegThread
类
创建两个新文件ffmpegthread.h
和ffmpegthread.cpp
。
ffmpegthread.h
#ifndef FFMPEGTHREAD_H
#define FFMPEGTHREAD_H
#include <QThread>
#include <QImage>
#include "videowidget.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavformat/version.h>
#include <libavutil/time.h>
#include <libavutil/mathematics.h>
#include <libavutil/imgutils.h>
}
class FfmpegThread : public QThread {
Q_OBJECT
public:
explicit FfmpegThread(QObject *parent = nullptr, VideoWidget *videoWidget = nullptr);
void run() override;
signals:
void frameReady(const QImage &frame);
private:
VideoWidget *videoWidget;
};
#endif // FFMPEGTHREAD_H
ffmpegthread.cpp
#include "ffmpegthread.h"
#include <QDebug>
FfmpegThread::FfmpegThread(QObject *parent, VideoWidget *videoWidget)
: QThread(parent), videoWidget(videoWidget) {
}
void FfmpegThread::run() {
unsigned char* buf;
int isVideo = -1;
int ret;
unsigned int i, streamIndex = 0;
const AVCodec *pCodec;
AVPacket *pAVpkt;
AVCodecContext *pAVctx;
AVFrame *pAVframe, *pAVframeRGB;
AVFormatContext* pFormatCtx;
struct SwsContext* pSwsCtx;
avformat_network_init();
char videoPath[] = "juren-30s.mp4"; // 你的视频路径
// 创建AVFormatContext
pFormatCtx = avformat_alloc_context();
// 初始化pFormatCtx
if (avformat_open_input(&pFormatCtx, videoPath, 0, 0) != 0) {
qDebug("avformat_open_input err.");
avformat_free_context(pFormatCtx);
return;
}
// 获取音视频流数据信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
avformat_close_input(&pFormatCtx);
qDebug("avformat_find_stream_info err.");
return;
}
// 找到视频流的索引
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
streamIndex = i;
isVideo = 0;
break;
}
}
// 没有视频流就退出
if (isVideo == -1) {
avformat_close_input(&pFormatCtx);
qDebug("nb_streams err.");
return;
}
// 获取视频流编码
pAVctx = avcodec_alloc_context3(NULL);
// 查找解码器
avcodec_parameters_to_context(pAVctx, pFormatCtx->streams[streamIndex]->codecpar);
pCodec = avcodec_find_decoder(pAVctx->codec_id);
if (pCodec == NULL) {
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("avcodec_find_decoder err.");
return;
}
// 初始化pAVctx
if (avcodec_open2(pAVctx, pCodec, NULL) < 0) {
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("avcodec_open2 err.");
return;
}
// 初始化pAVpkt
pAVpkt = av_packet_alloc();
if (!pAVpkt) {
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("av_packet_alloc err.");
return;
}
// 初始化数据帧空间
pAVframe = av_frame_alloc();
pAVframeRGB = av_frame_alloc();
if (!pAVframe || !pAVframeRGB) {
av_packet_free(&pAVpkt);
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("av_frame_alloc err.");
return;
}
// 创建图像数据存储buf
buf = (unsigned char *)av_malloc(av_image_get_buffer_size(AV_PIX_FMT_RGB32, pAVctx->width, pAVctx->height, 1));
av_image_fill_arrays(pAVframeRGB->data, pAVframeRGB->linesize, buf, AV_PIX_FMT_RGB32, pAVctx->width, pAVctx->height, 1);
// 初始化pSwsCtx
pSwsCtx = sws_getContext(pAVctx->width, pAVctx->height, pAVctx->pix_fmt, pAVctx->width, pAVctx->height, AV_PIX_FMT_RGB32, SWS_BICUBIC, NULL, NULL, NULL);
// 循环读取视频数据
while (!isInterruptionRequested()) {
if (av_read_frame(pFormatCtx, pAVpkt) >= 0) {
// 如果是视频数据
if (pAVpkt->stream_index == (int)streamIndex) {
// 解码一帧视频数据
ret = avcodec_send_packet(pAVctx, pAVpkt);
if (ret < 0) {
qDebug("Decode Error: avcodec_send_packet");
av_packet_unref(pAVpkt);
continue;
}
ret = avcodec_receive_frame(pAVctx, pAVframe);
if (ret == 0) {
sws_scale(pSwsCtx, (const unsigned char* const*)pAVframe->data, pAVframe->linesize, 0, pAVctx->height, pAVframeRGB->data, pAVframeRGB->linesize);
QImage img((uchar*)pAVframeRGB->data[0], pAVctx->width, pAVctx->height, QImage::Format_RGB32);
emit frameReady(img);
msleep(30); // 控制帧率
} else if (ret != AVERROR(EAGAIN) && ret != AVERROR_EOF) {
qDebug("Decode Error: avcodec_receive_frame");
}
}
av_packet_unref(pAVpkt);
} else {
break;
}
}
// 释放资源
sws_freeContext(pSwsCtx);
av_frame_free(&pAVframeRGB);
av_frame_free(&pAVframe);
av_packet_free(&pAVpkt);
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug() << "play finish!";
}
添加一个流播放功能,可以选择rtmp,rtsp,udp方式拉流,且增加一个本地视频播放选取的控件与播放按钮,增加一个网络流播放地址输入框与播放按钮
步骤1:更新UI文件(mainwindow.ui
)
在Qt Designer中打开mainwindow.ui
,添加以下控件:
-
本地视频播放按钮和文件选择控件:
- 一个
QPushButton
,名称为openFileButton
,文本为Open Local File
。 - 一个
QLineEdit
,名称为filePathEdit
。 - 一个
QPushButton
,名称为playFileButton
,文本为Play Local File
。
- 一个
-
网络流播放地址输入框和播放按钮:
- 一个
QLineEdit
,名称为streamUrlEdit
。 - 一个
QPushButton
,名称为playStreamButton
,文本为Play Stream
。
- 一个
mainwindow.ui
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="VideoWidget" name="videoWidget"/>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutFile">
<item>
<widget class="QPushButton" name="openFileButton">
<property name="text">
<string>Open Local File</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="filePathEdit"/>
</item>
<item>
<widget class="QPushButton" name="playFileButton">
<property name="text">
<string>Play Local File</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayoutStream">
<item>
<widget class="QLineEdit" name="streamUrlEdit"/>
</item>
<item>
<widget class="QPushButton" name="playStreamButton">
<property name="text">
<string>Play Stream</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar"/>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>
步骤2:更新MainWindow
类以处理新控件
mainwindow.h
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include "videowidget.h"
#include "ffmpegthread.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 on_openFileButton_clicked();
void on_playFileButton_clicked();
void on_playStreamButton_clicked();
private:
Ui::MainWindow *ui;
FfmpegThread *ffmpegThread;
QString currentFilePath;
};
#endif // MAINWINDOW_H
mainwindow.cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QFileDialog>
#include <QDebug>
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow) {
ui->setupUi(this);
ffmpegThread = new FfmpegThread(this, ui->videoWidget);
connect(ffmpegThread, &FfmpegThread::frameReady, ui->videoWidget, &VideoWidget::setFrame);
connect(ui->openFileButton, &QPushButton::clicked, this, &MainWindow::on_openFileButton_clicked);
connect(ui->playFileButton, &QPushButton::clicked, this, &MainWindow::on_playFileButton_clicked);
connect(ui->playStreamButton, &QPushButton::clicked, this, &MainWindow::on_playStreamButton_clicked);
}
MainWindow::~MainWindow() {
ffmpegThread->requestInterruption();
ffmpegThread->wait();
delete ui;
}
void MainWindow::on_openFileButton_clicked() {
QString filePath = QFileDialog::getOpenFileName(this, "Open Video File", "", "Video Files (*.mp4 *.avi *.mkv)");
if (!filePath.isEmpty()) {
ui->filePathEdit->setText(filePath);
currentFilePath = filePath;
}
}
void MainWindow::on_playFileButton_clicked() {
QString filePath = ui->filePathEdit->text();
if (!filePath.isEmpty()) {
if (ffmpegThread->isRunning()) {
ffmpegThread->requestInterruption();
ffmpegThread->wait();
}
ffmpegThread->setMediaSource(filePath);
ffmpegThread->start();
} else {
qDebug() << "No file selected!";
}
}
void MainWindow::on_playStreamButton_clicked() {
QString streamUrl = ui->streamUrlEdit->text();
if (!streamUrl.isEmpty()) {
if (ffmpegThread->isRunning()) {
ffmpegThread->requestInterruption();
ffmpegThread->wait();
}
ffmpegThread->setMediaSource(streamUrl);
ffmpegThread->start();
} else {
qDebug() << "No stream URL provided!";
}
}
步骤3:更新FfmpegThread
以处理不同的媒体源
ffmpegthread.h
#ifndef FFMPEGTHREAD_H
#define FFMPEGTHREAD_H
#include <QThread>
#include <QImage>
#include "videowidget.h"
extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavdevice/avdevice.h>
#include <libavformat/version.h>
#include <libavutil/time.h>
#include <libavutil/mathematics.h>
#include <libavutil/imgutils.h>
}
class FfmpegThread : public QThread {
Q_OBJECT
public:
explicit FfmpegThread(QObject *parent = nullptr, VideoWidget *videoWidget = nullptr);
void run() override;
void setMediaSource(const QString &source);
signals:
void frameReady(const QImage &frame);
private:
VideoWidget *videoWidget;
QString mediaSource;
};
#endif // FFMPEGTHREAD_H
ffmpegthread.cpp
#include "ffmpegthread.h"
#include <QDebug>
FfmpegThread::FfmpegThread(QObject *parent, VideoWidget *videoWidget)
: QThread(parent), videoWidget(videoWidget) {
}
void FfmpegThread::setMediaSource(const QString &source) {
mediaSource = source;
}
void FfmpegThread::run() {
unsigned char* buf;
int isVideo = -1;
int ret;
unsigned int i, streamIndex = 0;
const AVCodec *pCodec;
AVPacket *pAVpkt;
AVCodecContext *pAVctx;
AVFrame *pAVframe, *pAVframeRGB;
AVFormatContext* pFormatCtx;
struct SwsContext* pSwsCtx;
avformat_network_init();
const char *videoPath = mediaSource.toUtf8().constData();
// 创建AVFormatContext
pFormatCtx = avformat_alloc_context();
// 初始化pFormatCtx
if (avformat_open_input(&pFormatCtx, videoPath, 0, 0) != 0) {
qDebug("avformat_open_input err.");
avformat_free_context(pFormatCtx);
return;
}
// 获取音视频流数据信息
if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
avformat_close_input(&pFormatCtx);
qDebug("avformat_find_stream_info err.");
return;
}
// 找到视频流的索引
for (i = 0; i < pFormatCtx->nb_streams; i++) {
if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
streamIndex = i;
isVideo = 0;
break;
}
}
// 没有视频流就退出
if (isVideo == -1) {
avformat_close_input(&pFormatCtx);
qDebug("nb_streams err.");
return;
}
// 获取视频流编码
pAVctx = avcodec_alloc_context3(NULL);
// 查找解码器
avcodec_parameters_to_context(pAVctx, pFormatCtx->streams[streamIndex]->codecpar);
pCodec = avcodec_find_decoder(pAVctx->codec_id);
if (pCodec == NULL) {
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("avcodec_find_decoder err.");
return;
}
// 打开解码器
if (avcodec_open2(pAVctx, pCodec, NULL) < 0) {
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
qDebug("avcodec_open2 err.");
return;
}
// 初始化AVPacket,AVFrame
pAVpkt = av_packet_alloc();
pAVframe = av_frame_alloc();
pAVframeRGB = av_frame_alloc();
// 初始化SwsContext
pSwsCtx = sws_getContext(pAVctx->width, pAVctx->height, pAVctx->pix_fmt,
pAVctx->width, pAVctx->height, AV_PIX_FMT_RGB24,
SWS_BICUBIC, NULL, NULL, NULL);
// 分配RGB帧缓冲区
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGB24, pAVctx->width, pAVctx->height, 1);
buf = (unsigned char*)av_malloc(numBytes * sizeof(unsigned char));
av_image_fill_arrays(pAVframeRGB->data, pAVframeRGB->linesize, buf, AV_PIX_FMT_RGB24,
pAVctx->width, pAVctx->height, 1);
// 读取帧
while (!isInterruptionRequested()) {
if (av_read_frame(pFormatCtx, pAVpkt) >= 0) {
if (pAVpkt->stream_index == streamIndex) {
ret = avcodec_send_packet(pAVctx, pAVpkt);
if (ret < 0) {
av_packet_unref(pAVpkt);
continue;
}
ret = avcodec_receive_frame(pAVctx, pAVframe);
if (ret < 0) {
av_packet_unref(pAVpkt);
continue;
}
sws_scale(pSwsCtx, (uint8_t const* const*)pAVframe->data,
pAVframe->linesize, 0, pAVctx->height, pAVframeRGB->data,
pAVframeRGB->linesize);
QImage img((uchar*)pAVframeRGB->data[0], pAVctx->width, pAVctx->height,
QImage::Format_RGB888);
emit frameReady(img);
av_packet_unref(pAVpkt);
}
} else {
break;
}
}
av_packet_free(&pAVpkt);
av_frame_free(&pAVframe);
av_frame_free(&pAVframeRGB);
av_free(buf);
sws_freeContext(pSwsCtx);
avcodec_free_context(&pAVctx);
avformat_close_input(&pFormatCtx);
avformat_network_deinit();
}
步骤4:编译并运行
现在,编译并运行项目,你将看到增加了本地视频播放选取控件与播放按钮、网络流播放地址输入框与播放按钮,并支持RTMP、RTSP、UDP等协议的流播放功能。
在本文中,我们实现了一个通过QPainter绘制视频播放页面的Qt应用程序,并将FFmpeg操作移至单独的线程,确保MainWindow仅负责UI显示。我们首先创建了一个自定义的QWidget类VideoWidget来替换原来的QLabel,用于显示视频帧。接着,我们使用Qt Designer将VideoWidget集成到MainWindow中,替换中央窗口小部件。
为了提高性能和响应速度,我们将FFmpeg的播放操作移至单独的线程中。为此,我们创建了FfmpegThread类,负责处理视频解码和帧绘制。通过信号槽机制,我们确保解码后的帧能够实时更新到VideoWidget上。
此外,我们添加了一个流播放功能,支持RTMP、RTSP和UDP等协议,并在UI中增加了本地视频播放选择控件与播放按钮,以及网络流播放地址输入框与播放按钮。通过这些控件,用户可以方便地选择本地视频文件或输入流媒体地址进行播放。
经过这些改进,应用程序不仅支持本地视频文件的播放,还能通过多种协议流播放视频,极大地提升了功能的多样性和用户体验。我们实现了一个高效、灵活的视频播放应用,为进一步开发和扩展提供了坚实的基础。