跳至正文
View Categories

4 min read

本项目实现一个简单的视频播放器,支持打开本地视频文件并播放,以及提供进度条展示播放进度和更改视频进度,支持视频播放/暂停,声音/静音状态切换,以及设置按钮图标。

环境准备 #

下载并安装 LAVFilters 解码器

https://github.com/Nevcairiel/LAVFilters/releases

按照默认设置进行安装。

如果在后续播放视频中出现 doRender: Unknown error 0x80040266 类似错误. 则表示解码库安装失败。

设计方案 #

使用QMediaPlayer进行播放音视频,QMediaPlayer是Qt封装的一个与平台无关的统一音视频操作接口,底层的解码还需要依靠平台完成,所以在项目开始之前我们需要安装 LAVFilters 解码器。

QMediaPlayer提供常用的接口如下

这些接口能够完成播放,暂停,停止,倍速播放,设置音量等基本操作。

但QMediaplayer类只负责多媒体控制,没有在UI显示功能,所以我们需要一个widget来显示视频内容。

Qt提供一个QVideoWidget来支持QMediaplayer的图像输出,我们可以使用QMediaplayer::setVideoOutput函数进行指定用来输出图像的组件,setVideoOutput支持的UI组件有QVideoWidget,QGraphicsVideoItem,QAbstractVideoSurface这三种。

QVideoWidget类虽然使用比较方便,但不支持获取视频帧,所以需要使用QAbstractVideoSurface类来达到这种效果。QAbstractVideoSurface是抽象类,需要使用子类去继承才可以实例化。

QAbstractVideoSurface类成员函数如下。

在用户界面上使用一个QLabel显示视频图像,底下一个进度条进行管理视频进度,最底下是一排控制按钮,包括打开,播放/暂停,静音。

新建项目

取消选中,不成声*.ui文件。

视频播放需要用到Qt的多媒体模块,在pro文件中如下图所示位置加入multimedia multimediawidgets。

在项目上右键,选择Add New。

选择C++类。

Base Class选择Custom设置为QAbstractVideoSurface,类名自定义为VideoSuface。

完成后项目目录如下。

项目代码 #

整个视频播放器代码如下。

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QMediaPlayer>
#include <QVideoWidget>
#include <QMediaPlaylist>
#include <QPushButton>
#include <QToolButton>
#include <QLabel>
#include <QSlider>

#include "videosurface.h"

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    MainWindow(QWidget *parent = nullptr);
    ~MainWindow();

private:
    QMediaPlayer m_player;
    QMediaPlaylist m_player_list;
    QVideoWidget *m_video_widget;
    VideoSurface m_video_surface;
    QLabel m_lb_video;

    // 控制按钮
    QPushButton *m_pb_open;
    QToolButton *m_tb_play;
    QToolButton *m_tb_volume;

    QSlider *m_sd_schedule;

private:
    void init_ui();
    void video_play();

private slots:
    void open_file();
    void show_frame(QVideoFrame frame);
};
#endif // MAINWINDOW_H

mainwindow.cpp

#include "mainwindow.h"
#include <QHBoxLayout>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QStatusBar>
#include <QStyle>

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
{
    init_ui();
}

MainWindow::~MainWindow()
{
}

void MainWindow::init_ui()
{

    m_pb_open = new QPushButton("open");
    m_tb_play = new QToolButton();
    m_tb_volume = new QToolButton();
    m_tb_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
    m_tb_volume->setIcon(style()->standardIcon(QStyle::SP_MediaVolume));

    connect(m_pb_open, SIGNAL(clicked(bool)), this, SLOT(open_file()));
    connect(m_tb_play, &QToolButton::clicked, this, &MainWindow::video_play);
    connect(m_tb_volume, &QToolButton::clicked, [=](){
        m_player.setMuted(!m_player.isMuted());
        if (m_player.isMuted()) {
            m_player.setMuted(true);
            m_tb_volume->setIcon(style()->standardIcon(QStyle::SP_MediaVolumeMuted));
        } else {
            m_player.setMuted(false);
            m_tb_volume->setIcon(style()->standardIcon(QStyle::SP_MediaVolume));
        }
    });

    QHBoxLayout *hl_control = new QHBoxLayout();
    hl_control->addWidget(m_pb_open);
    hl_control->addWidget(m_tb_play);
    hl_control->addWidget(m_tb_volume);

    m_video_widget = new QVideoWidget();
    m_sd_schedule = new QSlider(Qt::Horizontal);
    connect(m_sd_schedule, &QSlider::sliderMoved, [=] () {
        m_player.setPosition(m_sd_schedule->value() * 1000);
    });
    connect(&m_player, &QMediaPlayer::durationChanged, [=](){
        m_sd_schedule->setRange(0, m_player.duration() / 1000);
    });
    connect(&m_player, &QMediaPlayer::positionChanged, [=](){
        m_sd_schedule->setValue(m_player.position() / 1000);
    });

    m_player.setPlaylist(&m_player_list);
    m_player.setVideoOutput(&m_video_surface);

    connect(&m_video_surface, &VideoSurface::frame_ready,
            this, &MainWindow::show_frame);

    QVBoxLayout *vl_main = new QVBoxLayout();
    vl_main->addWidget(&m_lb_video);
    vl_main->addWidget(m_sd_schedule);
    vl_main->addLayout(hl_control);

    QWidget *widget = new QWidget();
    widget->setLayout(vl_main);
    this->setCentralWidget(widget);

}

void MainWindow::open_file()
{
    QString filename;
    filename = QFileDialog::getOpenFileName(this, "Open File", "./" ,"Video(*.mp4 *.avi *.rmvb)");
    if (filename.isEmpty()) {
        this->statusBar()->showMessage("file is empty");
    } else {
        this->setWindowTitle(filename);
        m_player_list.addMedia(QUrl(filename));
        m_player.stop();
        m_player_list.next();
        video_play();
    }
}

void MainWindow::show_frame(QVideoFrame frame)
{
    QVideoFrame frametodraw(frame);
        if(!frametodraw.map(QAbstractVideoBuffer::ReadOnly))
            return;
        QImage img = QImage(frame.bits(),
                    frame.width(),
                    frame.height(),
                    frame.bytesPerLine(),
                    QVideoFrame::imageFormatFromPixelFormat(frametodraw.pixelFormat())
                    //QImage::Format_RGB32
                    );
//        img = img.mirrored(false, true);//图像垂直翻转(图像是倒置的,调用该函数可正置。同时该函数也解决了频繁的内存错误引起的崩溃, 原因不明)
        QPixmap pix = QPixmap::fromImage(img);
        m_lb_video.setPixmap(pix);

        frametodraw.unmap();
}

void MainWindow::video_play()
{
    switch (m_player.state()) {
    case QMediaPlayer::StoppedState:
    case QMediaPlayer::PausedState:
        m_player.play();
        m_tb_play->setIcon(style()->standardIcon(QStyle::SP_MediaPause));
        break;
    case QMediaPlayer::PlayingState:
        m_player.pause();
        m_tb_play->setIcon(style()->standardIcon(QStyle::SP_MediaPlay));
        break;

    }
}

videosurface.h

#ifndef VIDEOSURFACE_H
#define VIDEOSURFACE_H

#include <QAbstractVideoSurface>
#include <QObject>
#include <QVideoFrame>
#include <QVideoSurfaceFormat>

class VideoSurface : public QAbstractVideoSurface
{
    Q_OBJECT
public:
    VideoSurface();

    QList<QVideoFrame::PixelFormat> supportedPixelFormats(
            QAbstractVideoBuffer::HandleType type = QAbstractVideoBuffer::NoHandle) const;
    bool present(const QVideoFrame &frame) ;
    bool start(const QVideoSurfaceFormat &videoformat);
    void stop();
signals:
    void frame_ready(QVideoFrame frame);
};

#endif // VIDEOSURFACE_H

videosurface.cpp

#include "videosurface.h"
#include <QDebug>

VideoSurface::VideoSurface(): QAbstractVideoSurface()
{

}


QList<QVideoFrame::PixelFormat> VideoSurface::supportedPixelFormats(
        QAbstractVideoBuffer::HandleType type) const
{
    if(type == QAbstractVideoBuffer::NoHandle)
       {
           return QList<QVideoFrame::PixelFormat>()
                   << QVideoFrame::Format_RGB32;
       }
       else
           return QList<QVideoFrame::PixelFormat>();
}

bool VideoSurface::present(const QVideoFrame &frame)
{

    if(frame.isValid())
    {
        emit frame_ready(frame);
//        qDebug()<<__func__;
        return true;
    }
    stop();
    return false;
}

bool VideoSurface::start(const QVideoSurfaceFormat &videoformat)
{           //格式是RGB32
    if(QVideoFrame::imageFormatFromPixelFormat(videoformat.pixelFormat()) != QImage::Format_Invalid && !videoformat.frameSize().isEmpty()){
        QAbstractVideoSurface::start(videoformat);
        return true;
    }
    return false;
}

void VideoSurface::stop()
{
    QAbstractVideoSurface::stop();
}

成果展示 #

运行后打开视频播放,窗口标题显示路径。

播放/暂停按钮图标演示。

音量/静音按钮图标演示。