之前使用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,下载链接。