音视频技术应用(11)- 基于SDL QT 的一个简易的视频播放器
总结之前的内容:
sdlqtrgb.h
#pragma once #include <QtWidgets/QWidget> #include "ui_sdlqtrgb.h" class SDLQtRGB : public QWidget { Q_OBJECT public: SDLQtRGB(QWidget *parent = Q_NULLPTR); // 重写QT的一个定时器函数,在该函数里执行定时操作 void timerEvent(QTimerEvent *ev) override; // 重写QT的resizeEvent()接口,监听窗口变化 void resizeEvent(QResizeEvent* event) override; private: Ui::SDLQtRGBClass ui; };
sdlqtrgb.cpp
#include "sdlqtrgb.h" #include "xvideo_view.h" #include <iostream> #include <fstream> #include <qmessagebox.h> #include <sdl/SDL.h> #pragma comment(lib, "SDL2.lib") using namespace std; static int sdl_width = 0; static int sdl_height = 0; static SDL_Window* sdl_window = NULL; static SDL_Renderer* sdl_render = NULL; static SDL_Texture* sdl_texture = NULL; static int pixel_size = 2; // 在YUV420p采样格式下,单个像素的大小是1.5个字节,这里使用2个字节,便于容纳,这里的单个像素大小可以大于1.5,但是绝对不能小于1.5 static unsigned char* yuv = NULL; static ifstream yuv_file; static XVideoView* view; SDLQtRGB::SDLQtRGB(QWidget *parent) : QWidget(parent) { ui.setupUi(this); // 打开YUV文件 yuv_file.open("400_300_25.yuv", ios::binary); if (!yuv_file) { QMessageBox::information(this, "", "open yuv failed!"); return; } // 设定窗口和label的宽高, 这个视频的大小是源视频的宽度和高度 sdl_width = 400; sdl_height = 300; // 重新设定label的大小, 因为yuv视频文件的分辨率是400*300, 所以这里更新label的大小,以便能够正常显示YUV图像 ui.label->resize(sdl_width, sdl_height); view = XVideoView::Create(XVideoView::SDL); // view->Init(sdl_width, sdl_height, XVideoView::YUV420P); view->Init(sdl_width, sdl_height, XVideoView::YUV420P, (void*)ui.label->winId()); // 4. 根据label控件的宽高来创建材质 // 这里的像素格式要与YUV的图像格式相对应,之前生成的YUV的图像格式是YUV420p, 所以对应的材质的格式必须是 SDL_PIXELFORMAT_IYUV // 申请一块内存空间用于存放RGB数据 yuv = new unsigned char[sdl_width * sdl_height * pixel_size]; // 每隔10ms调用一次timerEvent函数 startTimer(10); } void SDLQtRGB::timerEvent(QTimerEvent* ev) { if (view->isExit()) { view->Close(); exit(0); return; } // 读取yuv数据信息(一次读取一帧) // 1.5 代表单个YUV像素点的大小 yuv_file.read((char *)yuv, sdl_width * sdl_height * 1.5); view->Draw(yuv, 0); } void SDLQtRGB::resizeEvent(QResizeEvent* event) { ui.label->resize(size()); ui.label->move(0, 0); view->Scale(width(), height()); }
xvideo_view.h
#ifndef XVIDEO_VIEW_H #define XVIDEO_VIEW_H #include <mutex> class XVideoView { public: enum Format { RGBA = 0, ARGB, YUV420P }; enum RenderType { SDL = 0 }; static XVideoView* Create(RenderType type = SDL); /** * @brief 初始化渲染接口(线程安全) 可以多次调用 * @param w 窗口宽度 * @param h 窗口高度 * @param fmt 绘制的像素格式(要绘制的图像的像素格式) * @param win_id 窗口句柄,如果为空,则创建窗口 * @return 是否创建成功 */ virtual bool Init(int w, int h, Format fmt = RGBA, void *win_id = nullptr) = 0; /** * @brief 清理所有申请的资源,包括关闭窗口 线程安全 */ virtual void Close() = 0; /** * @brief 渲染图像(线程安全) * @param data 渲染的二进制数据 * @param linesize 一行数据的字节数,对于YUV420P的数据格式,就是Y一行的字节数, 如果linesize <= 0, 则根据宽度和像素格式自动算出大小 * @return 是否渲染成功 */ virtual bool Draw(const unsigned char *data, int linesize = 0) = 0; /** * @brief 提供一个接口用于显示缩放 * @param w 实际显示的宽度 * @param h 实际显示的高度 */ void Scale(int w, int h) { scale_w_ = w; scale_h_ = h; } /** * @brief 处理窗口退出事件 * @return 是否已退出 */ virtual bool isExit() = 0; protected: int width_ = 0; // 材质的宽度 int height_ = 0; // 材质的高度 Format fmt_ = RGBA; // 像素格式 std::mutex mtx_; // 用于确保线程安全 int scale_w_ = 0; // 实际显示的宽度 int scale_h_ = 0; // 实际显示的高度 }; #endif
xvideo_view.cpp
#include "xvideo_view.h" #include "xsdl.h" XVideoView* XVideoView::Create(RenderType type) { switch (type) { case XVideoView::SDL: return new XSDL(); break; default: break; } // 如果不支持的话,就直接返回nullptr return nullptr; }
xsdl.h
#ifndef XSDL_H #define XSDL_H #include "xvideo_view.h" struct SDL_Window; struct SDL_Renderer; struct SDL_Texture; class XSDL : public XVideoView { public: /** * @brief 初始化渲染接口(线程安全) * @param w 窗口宽度 * @param h 窗口高度 * @param fmt 绘制的像素格式(要绘制的图像的像素格式) * @param win_id 窗口句柄,如果为空,则创建窗口 * @return 是否创建成功 */ bool Init(int w, int h, Format fmt = RGBA, void* win_id = nullptr) override; /** * @brief 清理的接口 */ void Close() override; /** * @brief 渲染图像(线程安全) * @param data 渲染的二进制数据 * @param linesize 一行数据的字节数,对于YUV420P的数据格式,就是Y一行的字节数, 如果linesize <= 0, 则根据宽度和像素格式自动算出大小 * @return 是否渲染成功 */ bool Draw(const unsigned char* data, int linesize = 0) override; /** * @brief 处理窗口退出事件 * @return 是否已退出 */ bool isExit() override; private: SDL_Window* win_ = nullptr; SDL_Renderer* render_ = nullptr; SDL_Texture* texture_ = nullptr; }; #endif
xsdl.cpp
#include "xsdl.h" #include <iostream> #include <sdl/SDL.h> #pragma comment(lib, "SDL2.lib") using namespace std; /** * @brief 初始化视频库 * @return 初始化结果 */ static bool InitVideo() { static bool is_first = true; // 这里使用的是静态变量,表示多次进来使用的是同一个对象 static mutex mux; unique_lock<mutex> sdl_lock(mux); // 表示已经初始化过了 if (!is_first) return true; if (SDL_Init(SDL_INIT_VIDEO)) { cout << SDL_GetError() << endl; return false; } // 设定缩放算法, 解决锯齿问题,这里采用的是线性插值算法 // "0": 临近插值算法 // "1": 线性插值算法 // "2":目前与线性插值算法一致 SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1"); return true; } bool XSDL::Init(int w, int h, Format fmt, void* win_id) { // 判断输入的参数是否合法 if (w <= 0 || h <= 0) { cout << "input width or height is error " << endl; return false; } // 初始化SDL Video 库 if (!InitVideo()) { cout << "init video failed" << endl; return false; } // 确保线程安全 // unique_lock 相当于是在栈中分配了一个对象 sdl_lock, 只要这个对象一出栈, // 就会自动调用析构函数,它在析构函数中会调用 mtx_的 unlock()方法 unique_lock<mutex> sdl_lock(mtx_); width_ = w; height_ = h; fmt_ = fmt; // 如果再次初始化时发现材质和渲染器已存在,则先对其进行销毁 // 这样做的目的是为了保证多次初始化能够成功 if (texture_) { SDL_DestroyTexture(texture_); } if (render_) { SDL_DestroyRenderer(render_); } // 1. 创建SDL窗口 if (!win_) { if (!win_id) { // 如果用户没有给出创建窗口的句柄,那我们就自行创建一个新的窗口 win_ = SDL_CreateWindow("", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, width_, height_, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE ); } else { // 如果用户给定了创建窗口的句柄,则将画面渲染到用户的给定的控件窗口 win_ = SDL_CreateWindowFrom(win_id); } } if (!win_) { cerr << SDL_GetError() << endl; return false; } // 2. 创建渲染器 render_ = SDL_CreateRenderer(win_, -1, SDL_RENDERER_ACCELERATED); if (!render_) { cerr << SDL_GetError() << endl; return false; } // 3. 创建材质 (存在于显存当中) unsigned int sdl_fmt = SDL_PIXELFORMAT_RGBA8888; switch (fmt) { case XVideoView::RGBA: sdl_fmt = SDL_PIXELFORMAT_RGBA8888; break; case XVideoView::ARGB: sdl_fmt = SDL_PIXELFORMAT_ARGB32; break; case XVideoView::YUV420P: sdl_fmt = SDL_PIXELFORMAT_IYUV; break; default: break; } texture_ = SDL_CreateTexture(render_, sdl_fmt, // 像素格式 SDL_TEXTUREACCESS_STREAMING, // 频繁修改的渲染 w, h // 材质的宽高 ); if (!texture_) { cerr << SDL_GetError() << endl; return false; } // cout << "XSDL init success" << endl; return true; } bool XSDL::Draw(const unsigned char* data, int linesize) { if (!data) { cout << "input data is null" << endl; return false; } // 保证线程同步 unique_lock<mutex> sdl_lock(mtx_); if (!texture_ || !render_ || !win_ || width_ <= 0 || height_ <= 0) { cout << "draw failed, param error" << endl; return false; } if (linesize <= 0) { switch (fmt_) { case XVideoView::RGBA: case XVideoView::ARGB: linesize = width_ * height_ * 4; break; case XVideoView::YUV420P: linesize = width_; break; default: break; } } if (linesize <= 0) { cout << "linesize is error" << endl; return false; } // 复制内存到显存 auto re = SDL_UpdateTexture(texture_, NULL, data, linesize); if (re) { cout << "update texture failed" << endl; return false; } // 清理渲染器 SDL_RenderClear(render_); // 如果用户手动设置了缩放,就按照用户设置的大小显示 // 如果用户没有设置,就传递null, 采用默认的窗口大小 SDL_Rect *prect = nullptr; if (scale_w_ > 0 || scale_h_ > 0) { SDL_Rect rect; rect.x = 0; rect.y = 0; rect.w = scale_w_; rect.h = scale_h_; prect = ▭ } // 拷贝材质到渲染器 re = SDL_RenderCopy(render_, texture_, NULL, prect); if (re) { cout << "copy texture failed" << endl; return false; } // 显示 SDL_RenderPresent(render_); return true; } void XSDL::Close() { // 保证线程安全 unique_lock<mutex> sdl_lock(mtx_); // 注意!!! 一定要先清理Texture, 再清理Render, 因为Texture是绑定在Render当中的, // 如果先清理render, 再清理Texture, 可能会有问题 if (texture_) { SDL_DestroyTexture(texture_); texture_ = nullptr; } if (render_) { SDL_DestroyRenderer(render_); render_ = nullptr; } if (win_) { SDL_DestroyWindow(win_); win_ = nullptr; } // cout << "do Close()" << endl; } bool XSDL::isExit() { // 创建一个event用于接收事件 SDL_Event ev; SDL_WaitEventTimeout(&ev, 1); // 等待1ms, 避免阻塞 if (ev.type == SDL_QUIT) { return true; } return false; }
<完>