音视频技术应用(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 = &rect;
    }
    
    
    // 拷贝材质到渲染器
    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;
}

<完>

posted @ 2021-11-23 00:20  夜行过客  阅读(285)  评论(0编辑  收藏  举报