目录
学习课题:逐步构建开发播放器【QT5 + FFmpeg6 + SDL2】
步骤
VideoOutPut模块
1、初始化【分配缓存、读取信息】
2、开始线程工作【从队列读帧->缩放->发送渲染信号到窗口】
VideoWidget自定义Widget类
1、定义内部变量
2、如果使用SDL,需要进行初始化
3、接收到信号后需要执行槽函数进行渲染
主要代码
分配缓存
cpp
// 根据格式和视频宽高获取一张图像的字节数据大小
int byte = av_image_get_buffer_size(AV_PIX_FMT_RGB32, videoWidth, videoHeight, 1);
// 分配缓存空间
buffer = (uint8_t *) av_malloc(byte * sizeof(uint8_t));
// 类似于格式化已经申请的内存
av_image_fill_arrays(playFrame->data, playFrame->linesize, buffer, AV_PIX_FMT_RGB32, videoWidth, videoHeight, 1);
// 初始化分配并返回SwsContext
swsContext = sws_getContext(decCtx->width, decCtx->height, pixelFormat, videoWidth, videoHeight, AV_PIX_FMT_RGB32, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
执行缩放
cpp
sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0, decCtx->height, playFrame->data, playFrame->linesize);
创建SDL窗口纹理渲染器
cpp
// 这里必须要先SDL_CreateWindowFrom,然后在设置父类为this,否则会直接把父类当成参数作为背景
sdlWindow = SDL_CreateWindowFrom((void *) winId());
//渲染器
sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
//纹理 大小是视频大小
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, 1920, 1080);
SDL_SetTextureScaleMode(sdlTexture, SDL_ScaleModeLinear);
SDL渲染
cpp
int ret = 1;
ret = SDL_RenderClear(sdlRenderer);
if (ret != 0) {
qDebug() << "SDL_RenderClear error";
}
// 帧数据更新到纹理
ret = SDL_UpdateTexture(sdlTexture, &m_originalSDLRect, frame->data[0], frame->linesize[0]);
// 如果这里的帧数据已经是YUV则需要使用下面的
// ret = SDL_UpdateYUVTexture(sdlTexture, &sdlRect2, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
if (ret != 0) {
qDebug() << "SDL_UpdateYUVTexture error:" << SDL_GetError();
}
// 将纹理绘制到渲染器上
ret = SDL_RenderCopy(sdlRenderer, sdlTexture, &m_originalSDLRect, nullptr);
if (ret != 0) {
qDebug() << "SDL_RenderCopy error";
}
// 刷新渲染器,将内容显示到窗口上
SDL_RenderPresent(sdlRenderer);
SDL窗口渲染在mac系统上好像有点问题,缩放的时候图像会很模糊,暂时不知道是什么情况。望知道的朋友告知原因。
QPainter渲染
使用Qt的QPainter进行渲染可以原画像显示,不会有模糊的情况。
cpp
// 注意这里需要使用RGB格式
QImage image((uchar *) frame->data[0], 1920, 1080, QImage::Format_RGB32);
QPainter painter(this);
painter.drawImage(this->rect(), image);
完整模块
VideoOutPut
1、run函数实现内为什么需要av_usleep(39999):
在run函数中是进行循环读取帧,然后缩放,最后发送信号进行渲染,如果我们不进行sleep,哪么就会在极短的时间内循环读取完所有的帧,渲染过快导致无法看清图像,因此我们需要进行sleep,这个时间对于单个视频流来说,一般是FPS ,就是画面每秒传输帧数,通俗来讲就是指动画或视频的画面数。这个值一般是可以通过视频流的一些参数计算出来的,我们放到同步部分在说。
cpp
// VideoOutPut.h
#include "FFmpegHeader.h"
#include "SDL.h"
#include "queue/AVFrameQueue.h"
#include <QDebug>
#include <QObject>
#include <QtGui>
#include <QtWidgets>
#include <thread>
class VideoOutPut : public QObject {
Q_OBJECT
private:
std::thread *m_thread;
bool isStopped = true; // 是否已经停止 停止时退出线程
bool isPlaying = false;// 是否正在播放
bool isPause = false; // 是否暂停
uint8_t *buffer;//存储解码后图片buffer
//图像缩放、颜色空间转换上下文
SwsContext *swsContext;
AVFrame *playFrame; //转换后的帧对象
AVFrameQueue *frameQueue;//解码后的帧队列
// 解码器上下文
AVCodecContext *decCtx;// 音频解码器上下文
public:
VideoOutPut(AVCodecContext *dec_ctx, AVFrameQueue *frame_queue);
int init();
int start();
void run();
int videoWidth; //视频宽度
int videoHeight;//视频高度
Q_SIGNALS:
void refreshImage(const SDL_Rect &sdlRect, AVFrame *frame);
};
// VideoOutPut.cpp
#include "VideoOutPut.h"
VideoOutPut::VideoOutPut(AVCodecContext *dec_ctx, AVFrameQueue *frame_queue)
: decCtx(dec_ctx), frameQueue(frame_queue) {
}
int VideoOutPut::init() {
//获取分辨率大小
videoWidth = decCtx->width;
videoHeight = decCtx->height;
playFrame = av_frame_alloc();
AVPixelFormat pixelFormat = decCtx->pix_fmt;
qDebug() << av_get_pix_fmt_name(pixelFormat);
int byte = av_image_get_buffer_size(AV_PIX_FMT_RGB32, videoWidth, videoHeight, 1);
buffer = (uint8_t *) av_malloc(byte * sizeof(uint8_t));
av_image_fill_arrays(playFrame->data, playFrame->linesize, buffer, AV_PIX_FMT_RGB32, videoWidth, videoHeight, 1);
swsContext = sws_getContext(decCtx->width, decCtx->height, pixelFormat, videoWidth, videoHeight, AV_PIX_FMT_RGB32, SWS_FAST_BILINEAR, nullptr, nullptr, nullptr);
return 1;
}
int VideoOutPut::start() {
m_thread = new std::thread(&VideoOutPut::run, this);
if (!m_thread->joinable()) {
qDebug() << "VideoOutPut视频帧处理线程创建失败";
return -1;
}
isStopped = false;
isPlaying = true;
return 0;
}
void VideoOutPut::run() {
AVFrame *frame;
while (!isStopped) {
frame = frameQueue->pop(10);
if (frame) {
//图像缩放、颜色空间转换
sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0, decCtx->height, playFrame->data, playFrame->linesize);
av_frame_unref(frame);
//视频区域
SDL_Rect sdlRect;
sdlRect.x = 0;
sdlRect.y = 0;
sdlRect.w = decCtx->width;
sdlRect.h = decCtx->height;
//渲染到sdl窗口
emit refreshImage(sdlRect, playFrame);
av_usleep(39999);
}
}
}
VideoWidget
我们这里使用到QWidget,直接把SDL封装进去,做一个自定义类,之后使用qt creator 提升一下就行了。
注意:在使用时,如果要把VideoWidget嵌入到某个widget内部,需要在new 之后在单独setParent,不然会把整个父类主窗口当作sdl窗口。
cpp
//VideoWidget.h
#include "FFmpegHeader.h"
#include "SDL.h"
#include <QApplication>
#include <QDebug>
#include <QMetaType>
#include <QWidget>
#include <QtGui>
class VideoWidget : public QWidget {
Q_OBJECT
private:
SDL_Rect m_originalSDLRect;
AVFrame *frame = nullptr;
SDL_Window *sdlWindow = nullptr;
SDL_Renderer *sdlRenderer = nullptr;
SDL_Texture *sdlTexture = nullptr;
protected:
void paintEvent(QPaintEvent *event) override;
public:
VideoWidget(QWidget *parent = 0);
~VideoWidget();
void initSDL();
public slots:
void updateImage(const SDL_Rect &sdl_rect, AVFrame *frame);
};
//VideoWidget.cpp
#include "VideoWidget.h"
VideoWidget::VideoWidget(QWidget *parent)
: QWidget(parent) {
// 注册SDL类
qRegisterMetaType<SDL_Rect>("SDL_Rect");
}
VideoWidget::~VideoWidget() {
if (frame)
av_frame_free(&frame);
if (sdlTexture)
SDL_DestroyTexture(sdlTexture);
if (sdlRenderer)
SDL_DestroyRenderer(sdlRenderer);
if (sdlWindow)
SDL_DestroyWindow(sdlWindow);
SDL_Quit();
}
void VideoWidget::initSDL() {
// SDL init
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
qDebug() << "SDL_INIT_VIDEO error";
return;
}
// 这里必须要先SDL_CreateWindowFrom,然后在设置父类为this,否则会直接把父类当成参数作为背景
sdlWindow = SDL_CreateWindowFrom((void *) winId());
//渲染器
sdlRenderer = SDL_CreateRenderer(sdlWindow, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
//纹理 大小是视频大小
sdlTexture = SDL_CreateTexture(sdlRenderer, SDL_PIXELFORMAT_BGRA32, SDL_TEXTUREACCESS_STREAMING, 1920, 1080);
SDL_SetTextureScaleMode(sdlTexture, SDL_ScaleModeLinear);
}
void VideoWidget::updateImage(const SDL_Rect &sdl_rect, AVFrame *frame) {
this->m_originalSDLRect = sdl_rect;
this->frame = frame;
this->update();
}
void VideoWidget::paintEvent(QPaintEvent *event) {
if (!frame) {
return;
}
#if 0//QPainter渲染
QImage image((uchar *) frame->data[0], 1920, 1080, QImage::Format_RGB32);
QPainter painter(this);
painter.drawImage(this->rect(), image);
#endif
#if 1//SDL渲染
int ret = 1;
ret = SDL_RenderClear(sdlRenderer);
if (ret != 0) {
qDebug() << "SDL_RenderClear error";
}
// 创建SDL纹理并从表面创建
ret = SDL_UpdateTexture(sdlTexture, &m_originalSDLRect, frame->data[0], frame->linesize[0]);
// ret = SDL_UpdateYUVTexture(sdlTexture, &sdlRect2, frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
if (ret != 0) {
qDebug() << "SDL_UpdateYUVTexture error:" << SDL_GetError();
}
// 将纹理绘制到渲染器上
ret = SDL_RenderCopy(sdlRenderer, sdlTexture, &m_originalSDLRect, nullptr);
if (ret != 0) {
qDebug() << "SDL_RenderCopy error";
}
// 刷新渲染器,将内容显示到窗口上
SDL_RenderPresent(sdlRenderer);
#endif
}
PlayerMain
因为我们使用到了QT,所以先简单创建一个qt的ui,并且在内调用播放视频的函数,测试是否能够看见画面
cpp
//PlayerMain.h
#include <QWidget>
#include <QtCore>
//-----------
#include "queue/AVFrameQueue.h"
#include "queue/AVPacketQueue.h"
#include "thread/DecodeThread.h"
#include "thread/DemuxThread.h"
#include "widget/VideoWidget.h"
#include "output/VideoOutPut.h"
QT_BEGIN_NAMESPACE
namespace Ui {
class PlayerMain;
}
QT_END_NAMESPACE
class PlayerMain : public QWidget {
Q_OBJECT
public:
explicit PlayerMain(QWidget *parent = nullptr);
~PlayerMain() override;
private:
Ui::PlayerMain *ui;
// 解复用
DemuxThread *demuxThread;
DecodeThread *audioDecodeThread;
DecodeThread *videoDecodeThread;
// 解码-音频
AVPacketQueue audioPacketQueue;
AVFrameQueue audioFrameQueue;
// 解码-视频
AVPacketQueue videoPacketQueue;
AVFrameQueue videoFrameQueue;
VideoOutPut *videoOutPut;
};
//PlayerMain.cpp
#include "PlayerMain.h"
#include "ui_PlayerMain.h"
PlayerMain::PlayerMain(QWidget *parent)
: QWidget(parent), ui(new Ui::PlayerMain) {
ui->setupUi(this);
// 解复用
demuxThread = new DemuxThread(&audioPacketQueue, &videoPacketQueue);
demuxThread->setUrl("/Users/mac/Downloads/0911超前派对:于文文孟佳爆笑猜词 王源欧阳靖脑洞大开.mp4");
// demuxThread->setUrl("/Users/mac/Downloads/23.mp4");
demuxThread->start();
int ret;
// 解码-音频
audioDecodeThread = new DecodeThread(
demuxThread->getCodec(MediaType::Audio),
demuxThread->getCodecParameters(MediaType::Audio),
&audioPacketQueue,
&audioFrameQueue);
audioDecodeThread->init();
audioDecodeThread->start();
// 解码-视频
videoDecodeThread = new DecodeThread(
demuxThread->getCodec(MediaType::Video),
demuxThread->getCodecParameters(MediaType::Video),
&videoPacketQueue,
&videoFrameQueue);
videoDecodeThread->init();
videoDecodeThread->start();
// video output
this->resize(1920/2,1080/2);
videoOutPut = new VideoOutPut(videoDecodeThread->dec_ctx, &videoFrameQueue);
videoOutPut->init();
VideoWidget *videoWidget = new VideoWidget(this);
connect(videoOutPut, &VideoOutPut::refreshImage, videoWidget, &VideoWidget::updateImage);
videoWidget->show();
videoWidget->initSDL();
videoOutPut->start();
// videoWidget->setParent(this);
}
PlayerMain::~PlayerMain() {
delete ui;
}
测试运行结果
播放器开发(五):视频帧处理并用SDL渲染播放 结果