QT、FFmpeg、SDL2视频播放器(一)

8 篇文章 3 订阅
4 篇文章 0 订阅

之前使用ffmpeg和sdl2制作过一个简单的视频播放器,视频的播放暂停完全是sdl event控制,现在希望将sdl嵌入到QT 窗口中,通过一系列qt按钮实现播放、暂停的控制。

1 核心思想

1 QT多线程

因为视频解码过程消耗资源较多,必须单独开启一个线程来负责视频解码,主线程负责渲染以及控制,否则视频播放的过程中UI会卡住,无法响应用户事件。QT实现多线程有两种方法:
第一种是创建一个线程,类继承QThread,并且重写run方法,主线程中创建线程对象,使用start()方法启动线程,该部分网络上资料较多,此处不再赘述。
第二种创建一个类对象,继承QObject,创建与类对象链接的信号槽,通过类对象moveToThread方法将类对象移动到新线程中,然后调用线程对象的start方法,启动线程,调用信号槽,类对象在新线程中处理数据。
本案例中使用了第二种方法,创建了一个demuxworker类来进行解封装和解码,该类是要放入新线程的类,mediawindow类用来接收解码数据并渲染。
mediawindow.cpp 关键代码

MediaWindow::MediaWindow(QWidget *parent) : QWidget(parent)
{

    this->setFixedSize(661,465);
    startButton = new QPushButton("start");
    pauseButton = new QPushButton("pause");
    stopButton = new QPushButton("stop");
    imgLabel = new QLabel(this);
    imgLabel->resize(601,381);
    imgLabel->move(30,20);
    rect = imgLabel->geometry();//记录widget位置,恢复时使用

    demuxThread = new QThread;
    demuxWorker = new DemuxWorker;
    demuxWorker->moveToThread(demuxThread);


    connect(demuxThread, &QThread::finished, demuxWorker, &QObject::deleteLater);

    connect(demuxWorker,SIGNAL(sigGetVideoInfo(int,int)),this,SLOT(initSdl(int ,int)));
    connect(demuxWorker,SIGNAL(sigGetFrame(AVFrame *)),this,SLOT(updateVideo(AVFrame *)));
    connect(this,SIGNAL(sigStartPlay()),demuxWorker,SLOT(slotDoWork()));

    QHBoxLayout *buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(startButton);
    buttonLayout->addWidget(pauseButton);
    buttonLayout->addWidget(stopButton);

    QVBoxLayout *playerLayout = new QVBoxLayout;
    playerLayout->addWidget(imgLabel);
    playerLayout->addLayout(buttonLayout);

    this->setLayout(playerLayout);

    connect(startButton,SIGNAL(clicked(bool)),this,SLOT(startPlay()));
    connect(pauseButton,SIGNAL(clicked(bool)),this,SLOT(pausePlay()));
    connect(stopButton,&QPushButton::clicked,this,&MediaWindow::stopPlay);

}

MediaWindow::~MediaWindow()
{
    freeSdl();
}
void MediaWindow::startPlay(){

    demuxThread->start();
    emit sigStartPlay();
    startButton->setEnabled(false);
}

2 SDL2嵌入到QT窗口

SDL2不嵌入的创建方法:

win = SDL_CreateWindow("Media Player",
                           SDL_WINDOWPOS_UNDEFINED,
                           SDL_WINDOWPOS_UNDEFINED,
                           w_width, w_height,
                           SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);

将SDL2窗口嵌入到QT子窗口的方法:

QLabel *imgLabel;
sdlWindow = SDL_CreateWindowFrom((void *)imgLabel->winId());

3 视频暂停、退出控制

视频的暂停继续通过isPause控制,退出通过isPlay控制,两个变量都在主线程中修改,因为只有一个地方会修改控制变量,所以不需要用锁保护。
解码线程:

for (;;) {
        if (isPause)
        {
            mSleep(1000);
        }
        else
        {
            if(av_read_frame(avFormatContext, packet) < 0){
                goto end;
            }
            if(packet->stream_index == videoStream)
            {
                err = avcodec_send_packet(pVideoCodecContext, packet);
                if(err != 0){
                    if(AVERROR(EAGAIN) == err)
                        continue;
                    qDebug() << "发送视频帧失败!"<<  err;
                }
                //解码
                while(avcodec_receive_frame(pVideoCodecContext, pFrame) == 0){
                    emit sigGetFrame(pFrame);
                    mSleep(50);
                }
            }
            else
            {
                 av_packet_unref(packet); // 注意清理,容易造成内存泄漏
                 continue;
            }
            if (!isPlay)
            {
                goto end;
            }
        }
    }

end:
    avcodec_close(pVideoCodecContext);
    avformat_close_input(&avFormatContext);
    qDebug() << "end of play";

主线程:

void MediaWindow::pausePlay(){
    if(demuxWorker->isPause)
    {
        demuxWorker->isPause = false;
        pauseButton->setText("pause");
    }
    else
    {
        demuxWorker->isPause = true;
        pauseButton ->setText("continue");
    }
}
void MediaWindow::stopPlay(){
    QMessageBox::StandardButton button=QMessageBox::information(this,"提示","退出",QMessageBox::Ok,QMessageBox::Cancel);
    if(button==QMessageBox::Ok){
        demuxWorker->isPlay = false;
        QEventLoop loop;
        QTimer::singleShot(1.5*1000,&loop,SLOT(quit()));
        QTimer::singleShot(2*1000,this,SLOT(close()));
        loop.exec();
    }
    qDebug()<< "stop play";
}

2 最终效果

目前只实现了视频播放,还没将音频部分加进去。
在这里插入图片描述

3 完整代码

demuxworker.h

#ifndef AVDEMUXTHREAD_H
#define AVDEMUXTHREAD_H

extern "C"
{
#include "libavformat/avformat.h"
};


#include <QThread>
#include <QImage>
#include <QLabel>
#include <QObject>
#include <QThread>


class DemuxWorker:public QObject
{
    Q_OBJECT

public:
    DemuxWorker(QObject *parent = nullptr);
    ~DemuxWorker();

public:
    bool isPlay;
    bool isPause;

signals:
    void sigGetFrame(AVFrame *pFrame);
    void sigGetVideoInfo(int mWidth,int mHeight);

public slots:
    void slotDoWork();
};

#endif // AVDEMUXTHREAD_H

demuxworker.cpp

#include "demuxworker.h"
#include <QDebug>
#include <iostream>
#include <QLabel>
#include <QTime>
#include <QCoreApplication>
#include <sys/stat.h>


void mSleep(int msec)
{
    QTime n=QTime::currentTime();
    QTime now;
    do
    {
        now=QTime::currentTime();
    }while (n.msecsTo(now)<=msec);
}

DemuxWorker::DemuxWorker(QObject *parent):QObject (parent)
{
    qDebug()<<"Thread构造函数ID:"<<QThread::currentThreadId();
}

DemuxWorker::~DemuxWorker()
{

    qDebug() << "thread quit";
}


void DemuxWorker::slotDoWork()
{
    AVFormatContext *avFormatContext = nullptr;
    AVPacket *packet = (AVPacket *)malloc(sizeof(AVPacket));
    AVFrame	*pFrame = nullptr;
    int audioStream= -1;
    int videoStream = -1;
    int err = -1;
    char *fileName = "./movie1.mp4";
    isPlay = true;
    isPause = false;
    avFormatContext = avformat_alloc_context();
    if (!avFormatContext) {
        av_log(nullptr, AV_LOG_FATAL, "Could not allocate context.\n");
    }

    if(avformat_open_input(&avFormatContext, fileName, nullptr, nullptr) != 0){
        qDebug() << "Couldn't open file";
    }
    // Retrieve stream information
    if(avformat_find_stream_info(avFormatContext, nullptr)<0){
        qDebug() << "Couldn't find stream information";
        return;
    }

    for(unsigned int i=0; i < avFormatContext->nb_streams; i++){
        if(avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
            audioStream = static_cast<int>(i);
        }
        else if(avFormatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            videoStream = static_cast<int>(i);
        }
    }

    if(audioStream == -1 && videoStream == -1){
        qDebug() << "Didn't find a audio stream";
        return;
    }

    pFrame = av_frame_alloc();

    //视频
    AVCodecContext *pVideoCodecContext = nullptr;
    AVCodec			*pCodecVideo;
    AVCodecParameters *pVideoChannelCodecPara = nullptr;

    if(videoStream != -1)
    {
        //视频
        pVideoChannelCodecPara = avFormatContext->streams[videoStream]->codecpar;
        pVideoCodecContext =  avcodec_alloc_context3(nullptr);
        if (!pVideoCodecContext){
            qDebug() <<  "avcodec_alloc_context3";
            return;
        }

        err = avcodec_parameters_to_context(pVideoCodecContext, pVideoChannelCodecPara);
        if (err < 0){
            qDebug() << "avcodec_parameters_to_context";
            return;
        }

        pCodecVideo = avcodec_find_decoder(pVideoChannelCodecPara->codec_id);
        if(pCodecVideo == nullptr){
            qDebug() <<  "avcodec_find_decoder";
            return;
        }

        qDebug() << "编解码器名:" << pCodecVideo->long_name;

        err = avcodec_open2(pVideoCodecContext, pCodecVideo, nullptr);
        if(err){
            qDebug() << "avcodec_open2";
            return;
        }

        emit sigGetVideoInfo(pVideoCodecContext->width, pVideoCodecContext->height);
        qDebug() << "视频宽度:" << pVideoCodecContext->width << "高度:" << pVideoCodecContext->height;
    }
    for (;;) {
        if (isPause)
        {
            mSleep(1000);
        }
        else
        {
            if(av_read_frame(avFormatContext, packet) < 0){
                goto end;
            }
            if(packet->stream_index == videoStream)
            {
                err = avcodec_send_packet(pVideoCodecContext, packet);
                if(err != 0){
                    if(AVERROR(EAGAIN) == err)
                        continue;
                    qDebug() << "发送视频帧失败!"<<  err;
                }
                //解码
                while(avcodec_receive_frame(pVideoCodecContext, pFrame) == 0){
                    emit sigGetFrame(pFrame);
                    mSleep(50);
                }
            }
            else
            {
                 av_packet_unref(packet); // 注意清理,容易造成内存泄漏
                 continue;
            }
            if (!isPlay)
            {
                goto end;
            }
        }
    }

end:
    avcodec_close(pVideoCodecContext);
    avformat_close_input(&avFormatContext);
    qDebug() << "end of play";
}

mediawindow.h

#ifndef MEDIAWINDOW_H
#define MEDIAWINDOW_H

#include <QWidget>
#include <QLabel>
#include <QDebug>
#include <QPushButton>
#include "demuxworker.h"
extern "C"
{
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/time.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_rect.h>
}

class MediaWindow : public QWidget
{
    Q_OBJECT
public:
    explicit MediaWindow(QWidget *parent = nullptr);
    ~MediaWindow() ;
    QLabel *imgLabel;
    QRect rect;
    QPushButton *startButton;
    QPushButton *pauseButton;
    QPushButton *stopButton;
private:
    QThread *demuxThread;
    DemuxWorker *demuxWorker;
    SDL_Renderer *sdlRenderer;
    SDL_Texture *sdlTexture;
    SDL_Window *sdlWindow;
    int freeSdl();

signals:
    void sigStartPlay();

public slots:
    void startPlay();
    void pausePlay();
    void stopPlay();
    void updateVideo(AVFrame *pFrame);
    int initSdl(int mWidth,int mHeight);

};

#endif // MEDIAWINDOW_H

mediawindow.cpp

#include "mediawindow.h"

#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QMessageBox>
#include <QCloseEvent>
#include <QEventLoop>
#include <QTimer>

MediaWindow::MediaWindow(QWidget *parent) : QWidget(parent)
{

    this->setFixedSize(661,465);
    startButton = new QPushButton("start");
    pauseButton = new QPushButton("pause");
    stopButton = new QPushButton("stop");
    imgLabel = new QLabel(this);
    imgLabel->resize(601,381);
    imgLabel->move(30,20);
    rect = imgLabel->geometry();//记录widget位置,恢复时使用

    demuxThread = new QThread;
    demuxWorker = new DemuxWorker;
    demuxWorker->moveToThread(demuxThread);


    connect(demuxThread, &QThread::finished, demuxWorker, &QObject::deleteLater);

    connect(demuxWorker,SIGNAL(sigGetVideoInfo(int,int)),this,SLOT(initSdl(int ,int)));
    connect(demuxWorker,SIGNAL(sigGetFrame(AVFrame *)),this,SLOT(updateVideo(AVFrame *)));
    connect(this,SIGNAL(sigStartPlay()),demuxWorker,SLOT(slotDoWork()));

    QHBoxLayout *buttonLayout = new QHBoxLayout;
    buttonLayout->addWidget(startButton);
    buttonLayout->addWidget(pauseButton);
    buttonLayout->addWidget(stopButton);

    QVBoxLayout *playerLayout = new QVBoxLayout;
    playerLayout->addWidget(imgLabel);
    playerLayout->addLayout(buttonLayout);

    this->setLayout(playerLayout);

    connect(startButton,SIGNAL(clicked(bool)),this,SLOT(startPlay()));
    connect(pauseButton,SIGNAL(clicked(bool)),this,SLOT(pausePlay()));
    connect(stopButton,&QPushButton::clicked,this,&MediaWindow::stopPlay);

}

MediaWindow::~MediaWindow()
{
    freeSdl();
}
void MediaWindow::startPlay(){

    demuxThread->start();
    emit sigStartPlay();
    startButton->setEnabled(false);
}


void MediaWindow::pausePlay(){
    if(demuxWorker->isPause)
    {
        demuxWorker->isPause = false;
        pauseButton->setText("pause");
    }
    else
    {
        demuxWorker->isPause = true;
        pauseButton ->setText("continue");
    }
}

void MediaWindow::stopPlay(){
    QMessageBox::StandardButton button=QMessageBox::information(this,"提示","退出",QMessageBox::Ok,QMessageBox::Cancel);
    if(button==QMessageBox::Ok){
        demuxWorker->isPlay = false;
        QEventLoop loop;
        QTimer::singleShot(1.5*1000,&loop,SLOT(quit()));
        QTimer::singleShot(2*1000,this,SLOT(close()));
        loop.exec();
    }
    qDebug()<< "stop play";
}

int MediaWindow::initSdl(int mWidth,int mHeight) {

    if (SDL_Init(SDL_INIT_EVERYTHING) < 0)
    {
        return -1;
    }
    // 创建窗体
    sdlWindow = SDL_CreateWindowFrom((void *)imgLabel->winId());

    if (sdlWindow == nullptr)
    {

        return -1;
    }
    // 从窗体创建渲染器
    sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, 0);
    // 创建渲染器纹理
    sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_IYUV, SDL_TEXTUREACCESS_STREAMING, mWidth, mHeight);
}

int MediaWindow::freeSdl() {
    SDL_DestroyRenderer(sdlRenderer);
    SDL_DestroyTexture(sdlTexture);
    SDL_DestroyWindow(sdlWindow);
    SDL_Quit();
    return 0;
}

void MediaWindow::updateVideo(AVFrame *pFrame){
    SDL_UpdateYUVTexture(sdlTexture, NULL, pFrame->data[0], pFrame->linesize[0],
                                  pFrame->data[1], pFrame->linesize[1], pFrame->data[2],
                                  pFrame->linesize[2]);
    SDL_RenderClear(sdlRenderer);
    SDL_RenderCopy(sdlRenderer, sdlTexture, NULL, NULL);
    SDL_RenderPresent(sdlRenderer);
}

main.cpp

#include <QApplication>
#include "mediawindow.h"

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);

    MediaWindow *mediaWindow = new MediaWindow();
    mediaWindow->show();

    int ret = a.exec();
    return ret;
}

pro文件

#-------------------------------------------------
#
# Project created by QtCreator 2021-09-11T14:17:19
#
#-------------------------------------------------

QT       += core gui charts multimedia

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = mediaPlayer
TEMPLATE = app

# The following define makes your compiler emit warnings if you use
# any feature of Qt which has been marked as deprecated (the exact warnings
# depend on your compiler). Please consult the documentation of the
# deprecated API in order# to know how to port your code away from it.
DEFINES += QT_DEPRECATED_WARNINGS

# You can also make your code fail to compile if you use deprecated APIs.
# In order to do so, uncomment the following line.
# You can also select to disable deprecated APIs only up to a certain version of Qt.
#DEFINES += QT_DISABLE_DEPRECATED_BEFORE=0x060000    # disables all the APIs deprecated before Qt 6.0.0

CONFIG += c++11

SOURCES += \
        demuxworker.cpp \
        main.cpp \
        mediawindow.cpp

HEADERS += \
        demuxworker.h \
        mediawindow.h


FORMS += \


# Default rules for deployment.
qnx: target.path = /tmp/$${TARGET}/bin
else: unix:!android: target.path = /opt/$${TARGET}/bin
!isEmpty(target.path): INSTALLS += target

INCLUDEPATH += /home/zhy/code/ffmpeg/source_code/FFmpeg/ffmpeg_install/include/
INCLUDEPATH += /usr/include/SDL2/

LIBS += -L/home/zhy/code/ffmpeg/source_code/FFmpeg/ffmpeg_install/lib -lswresample -lavformat -lswscale -lavutil  -lavcodec  -lavdevice -lavfilter
LIBS += -L/usr/lib/x86_64-linux-gnu/ -lSDL2 -lSDL2main
LIBS += -lpthread

项目已经打包上传到csdn,下载链接

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值