QT+ffmpeg学习笔记-自制一个简易播放器(二)

QT+ffmpeg学习笔记-自制一个简易播放器(一)-CSDN博客

在上篇文章中我们通过Qlable来显示视频,在本文章中,我们将通过Qpainter绘制视频播放页面,并增加流播放的方式,且将ffmpeg操作独立存放在一个线程,使得mainwindows只做ui显示。

  1. 创建一个自定义的QWidget来替换当前的QLabel
  2. 将FFmpeg的播放操作移到一个单独的线程中。
  3. 添加不同功能

目录

步骤1:创建自定义的QWidget

创建VideoWidget类

在代码中声明VideoWidget类

使用Qt Designer进行控件替换

videowidget.h

videowidget.cpp

步骤2:修改MainWindow以使用自定义的VideoWidget

修改mainwindow.ui

修改mainwindow.h

修改mainwindow.cpp

步骤3:将FFmpeg播放操作移到单独的线程中

创建FfmpegThread类

ffmpegthread.h

ffmpegthread.cpp

添加一个流播放功能,可以选择rtmp,rtsp,udp方式拉流,且增加一个本地视频播放选取的控件与播放按钮,增加一个网络流播放地址输入框与播放按钮

步骤1:更新UI文件(mainwindow.ui)

mainwindow.ui

步骤2:更新MainWindow类以处理新控件

mainwindow.h

mainwindow.cpp

步骤3:更新FfmpegThread以处理不同的媒体源

ffmpegthread.h

ffmpegthread.cpp

步骤4:编译并运行


步骤1:创建自定义的QWidget

创建VideoWidget

创建两个新文件videowidget.hvideowidget.cpp

在代码中声明VideoWidget

确保你已经按照上面的步骤创建了VideoWidget类,并包含了头文件。接下来,在mainwindow.ui文件中进行替换。

使用Qt Designer进行控件替换
  1. 打开mainwindow.ui文件: 使用Qt Designer打开你的mainwindow.ui文件。

  2. 找到中央窗口小部件: 在UI设计器中,找到中央窗口小部件(通常是一个QWidget)。

  3. 替换中央窗口小部件

    • 右键点击中央窗口小部件,然后选择“提升为...”(Promote to...)。
    • 在弹出的对话框中,填写如下内容:
      • 基类名称(Base class name):QWidget
      • 提升的类名称(Promoted class name):VideoWidget
      • 头文件(Header file):videowidget.h
    • 点击“添加”(Add),然后点击“提升”(Promote)。
  4. 保存并生成代码: 保存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.hffmpegthread.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,添加以下控件:

  1. 本地视频播放按钮和文件选择控件

    • 一个QPushButton,名称为openFileButton,文本为Open Local File
    • 一个QLineEdit,名称为filePathEdit
    • 一个QPushButton,名称为playFileButton,文本为Play Local File
  2. 网络流播放地址输入框和播放按钮

    • 一个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中增加了本地视频播放选择控件与播放按钮,以及网络流播放地址输入框与播放按钮。通过这些控件,用户可以方便地选择本地视频文件或输入流媒体地址进行播放。

经过这些改进,应用程序不仅支持本地视频文件的播放,还能通过多种协议流播放视频,极大地提升了功能的多样性和用户体验。我们实现了一个高效、灵活的视频播放应用,为进一步开发和扩展提供了坚实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值