音视频技术应用(17)- 开启DXVA2硬件加速, 并使用SDL显示

实现了使用DXVA2 进行硬件加速,并且使用SDL渲染h264格式的视频, 视频大小为400x300。

一. 示例Code

test_decode_view_hw.cpp

#include <iostream>
#include <fstream>
#include <string>
#include "xvideo_view.h"

using namespace std;


extern "C" { // 指定函数是C语言函数,以C语言的方式去编译
#include <libavcodec/avcodec.h>
#include <libavutil/opt.h>
}

// 以预处理指令的方式导入库
#pragma comment(lib, "avcodec.lib")
#pragma comment(lib, "avutil.lib")


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

    // 创建一个XVideoView
    auto view = XVideoView::Create();

    // 分割H264存入AVPacket
    string filename = "test.h264";
    ifstream ifs(filename, ios::binary);
    if (!ifs) return -1;

    unsigned char inbuf[4096] = {0};

    AVCodecID codec_id = AV_CODEC_ID_H264;

    // 1. 查找解码器
    auto codec = avcodec_find_decoder(codec_id);
    
    // 2. 根据解码器创建解码器上下文
    auto c = avcodec_alloc_context3(codec);
    

    //////////////////////////////////////////////////
    /// 硬件加速部分

    auto hw_type = AV_HWDEVICE_TYPE_DXVA2;

    // 打印所有支持的硬件加速方式
    for (int i = 0;; i++)
    {
        auto config = avcodec_get_hw_config(codec, i);
        if (!config) break;
        if (config->device_type)
            cout << av_hwdevice_get_type_name(config->device_type) << endl;
    }

    // 初始化硬件加速上下文
    AVBufferRef* hw_ctx = nullptr;
    av_hwdevice_ctx_create(&hw_ctx, hw_type, nullptr, nullptr, 0);

    // 开启硬件加速
    c->hw_device_ctx = av_buffer_ref(hw_ctx);
    
    // 开启多线程解码
    c->thread_count = 16;

    // 3. 打开解码器
    avcodec_open2(c, nullptr, nullptr);

    // 分割上下文
    auto parser = av_parser_init(codec_id);
    auto pkt = av_packet_alloc();
    auto frame = av_frame_alloc();
    auto hw_frame = av_frame_alloc();               // 硬解码转换用,未来会把显存中的数据转换到此frame中

    auto begin = NowMs();

    int count = 0;                                  // 当前解码的帧数

    bool is_init_window = false;                    // 标志位,用于标识窗口是否已经初始化

    while (!ifs.eof())
    {
        ifs.read((char *)inbuf, sizeof(inbuf));

        if (ifs.eof())
        {
            ifs.clear();
            ifs.seekg(0, ios::beg);
        }

        int data_size = ifs.gcount();               // 读取的字节数
        if (data_size <= 0) break;

        auto data = inbuf;
        
        while (data_size > 0)                       // 一次可能有多帧数据
        {
            // 通过0001 截断输出到AVPacket 返回值代表帧的大小
            int ret = av_parser_parse2(parser, c, 
                &pkt->data, &pkt->size,             // 输出数据
                data, data_size,                   // 输入数据
                AV_NOPTS_VALUE, AV_NOPTS_VALUE, 0
            );

            // 移动指针位置
            data += ret;

            data_size -= ret;                       // 已处理数据

            if (pkt->size)
            {
                // cout << pkt->size << " " << flush;

                ret = avcodec_send_packet(c, pkt);
                if (ret < 0) break;

                // 获取多帧解码数据
                while (ret >= 0)
                {
                    // 每次都会调用av_frame_unref;
                    ret = avcodec_receive_frame(c, frame);
                    if (ret < 0) break;
                   
                    auto pframe = frame;
                    if (c->hw_device_ctx) // 如果设置了 hw_device_ctx 成员,则认为是硬件解码
                    {
                        // 硬解码出来的数据转换 GPU->CPU (从显存复制到内存,不想复制的话可以直接使用DX渲染)
                        av_hwframe_transfer_data(hw_frame, frame, 0);
                        
                        // 注意转换后的数据格式会变成NV12格式 (AV_PIX_FMT_NV12)
                        pframe = hw_frame;
                    }

                    cout << pframe->format << " " << flush;

                    if (!is_init_window)
                    {
                        // 根据第一帧的宽高来初始化窗口,若窗口已初始化,则将此标志位置为true
                        is_init_window = true;
                        view->Init(pframe->width, pframe->height, (XVideoView::Format)pframe->format);
                    }

                    view->DrawFrame(pframe);

                    // 解码成功后累积帧数
                    count++;

                    auto cur = NowMs();
                    if (cur - begin > 1000)                  // 每1秒计算一次
                    {
                        cout << "\nfps = " << count << endl;
                        // 重置起始时间
                        count = 0;
                        begin = cur;
                    }

                }
            }
        }

    }

    // 取出缓存数据,确保所有数据都能够得到解码
    int ret = avcodec_send_packet(c, NULL);

    while (ret >= 0)
    {
        ret = avcodec_receive_frame(c, frame);
        if (ret < 0) break;

        cout << frame->format << "-" << flush;
    }

    av_parser_close(parser);
    avcodec_free_context(&c);
    av_frame_free(&frame);
    av_packet_free(&pkt);

    return 0;
}

xvideo_view.h

#ifndef XVIDEO_VIEW_H
#define XVIDEO_VIEW_H

#include <mutex>
#include <fstream>

/**
 * @brief 自定义休眠函数
 * @param ms 休眠时间
*/
void MSleep(unsigned int ms);

/**
 * @brief 获取当前时间戳(单位是毫秒)
 * @return 当前时间戳
*/
long long NowMs();

struct AVFrame;
class XVideoView
{
public:

    /**
     * @brief 定义所有支持的图像的像素格式
    */
    enum Format                                    // 枚举中的值与FFmpeg中像素格式定义的值保持一致
    {
        YUV420P = 0,
        NV12 = 23,
        ARGB = 25,
        RGBA = 26,
        BGRA = 28
    };

    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) = 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                    渲染图片,这里是对Y/U/V数据分别进行渲染(线程安全)
     * @param y                    Y分量的数据
     * @param y_pitch            Y 的个数
     * @param u                    U分量的数据
     * @param u_pitch            U的个数
     * @param v                    V分量的数据
     * @param v_pitch            V的个数
     * @return                    绘制的结果
    */
    virtual bool Draw(const unsigned char *y, int y_pitch, 
                      const unsigned char *u, int u_pitch,
                      const unsigned char *v, int v_pitch
    ) = 0;

    /**
     * @brief 提供一个接口用于显示缩放
     * @param w 实际显示的宽度
     * @param h 实际显示的高度
    */
    void Scale(int w, int h)
    {
        scale_w_ = w;
        scale_h_ = h;
    }

    /**
     * @brief 绘制AVFrame中的数据
     * @param frame AVFrame对象
     * @return 绘制结果
    */
    bool DrawFrame(AVFrame *frame);

    /**
     * @brief 处理窗口退出事件
     * @return 是否已退出
    */
    virtual bool isExit() = 0;

    /**
     * @brief 返回当前视频帧率
     * @return    当前视频帧率
    */
    int render_fps() { return render_fps_; }

    /**
     * @brief 打开文件
     * @param filepath    文件路径 
     * @return 打开结果
    */
    bool Open(std::string filepath);

    /**
     * @brief 读取一帧数据并返回AVFrame空间
     * @return 新读取的AVFrame数据,读取失败则返回nullptr
    */
    AVFrame* Read();

    /**
     * @brief 设置当前关联的窗口句柄
     * @param win 设置的窗口句柄
    */
    void set_win_id(void* win) { win_id_ = win; }

    // 注意,父类的析构函数必须定义成虚函数,避免父类指向子类对象时, 子类的析构不会调用
    virtual ~XVideoView();

protected:
    void* win_id_ = nullptr;                        // 当前关联的窗口句柄
    int render_fps_ = 0;                            // 当前视频帧率
    int width_ = 0;                                    // 材质的宽度
    int height_ = 0;                                // 材质的高度
    Format fmt_ = RGBA;                                // 像素格式

    std::mutex    mtx_;                                // 用于确保线程安全

    int scale_w_ = 0;                                // 实际显示的宽度
    int scale_h_ = 0;                                // 实际显示的高度

    long long beg_ms_ = 0;                            // 计时的开始时间
    int count_ = 0;                                    // 统计显示次数

private:
    std::ifstream ifs_;                                // 读取文件的流
    AVFrame* frame_ = nullptr;                        // 读取的AVFrame数据
    unsigned char* cache_ = nullptr;                // 复制NV12数据的缓冲
};


#endif 

xvideo_view.cpp

#include "xvideo_view.h"
#include "xsdl.h"

#include <iostream>

using namespace std;

extern "C"
{
#include <libavcodec/avcodec.h>
}

#pragma comment(lib, "avutil.lib")

XVideoView* XVideoView::Create(RenderType type)
{
    switch (type)
    {
    case XVideoView::SDL:
        return new XSDL();
        break;
    default:
        break;
    }


    // 如果不支持的话,就直接返回nullptr
    return nullptr;
}

XVideoView::~XVideoView()
{
    if (cache_)
        delete cache_;
    cache_ = nullptr;
}


bool XVideoView::DrawFrame(AVFrame* frame)
{
    if (!frame)
    {
        cout << "input frame can not be null" << endl;
        return false;
    }

    // 累积显示次数
    count_++;

    if (beg_ms_ <= 0)
    {
        beg_ms_ = clock();                                                // 开始计时
    }
    else if ((clock() - beg_ms_) / (CLOCKS_PER_SEC / 1000) >= 1000)        // 1s 计算一次fps
    {
        render_fps_ = count_;
        count_ = 0;
        beg_ms_ = clock();                                                // 重新计时
    }

    int linesize = 0;
    switch (frame->format)
    {
    case AV_PIX_FMT_YUV420P:
        return Draw(frame->data[0], frame->linesize[0],                // Y
                    frame->data[1], frame->linesize[1],                // U
                    frame->data[2], frame->linesize[2]                // V
        );
    case AV_PIX_FMT_NV12:
        if (!cache_)
        {
            // 若空间尚未分配,则申请一块较大的空间(4K的一幅图像的大小)
            cache_ = new unsigned char[4096 * 2160 * 1.5];
        }

        // PS: 注意!这里面涉及到一个内存对齐的问题!
        // 如果是400x300的YUV420P/NV12格式,它的linesize很可能会被FFmpeg自动进行字节对齐, 即当图像尺寸为400x300时,它的linesize可能
        // 不是400,而是416(假设ffmpeg是以16字节进行对齐)
        // 而在使用SDL进行渲染时,linesize若传递416,很可能导致渲染出现问题,为了避免出现这种情况,下面提供了一种逐行复制的策略。
        
        // 下面的内存拷贝主要目的是为了让数据是连续的
        linesize = frame->width;
        if (frame->linesize[0] == frame->width)
        {
            // ---------------- 若linesize与图像的宽度一致,说明未发生字节对齐 --------------- 
            // 拷贝所有Y分量
            memcpy(cache_, frame->data[0], frame->linesize[0] * frame->height); 
            // 拷贝所有的UV分量
            memcpy(cache_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2);
        }
        else
        {
            // ---------------- 若linesize与图像的宽度一致,说明发生了字节对齐 --------------- 
            // 使用逐行拷贝的方式
            // 拷贝所有的Y分量
            for (int i = 0; i < frame->height; i++)
            {
                memcpy(cache_ + i * frame->width, 
                    frame->data[0] + i * frame->linesize[0], 
                    frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
                );
            }

            // 拷贝所有UV分量
            for (int i = 0; i < frame->height / 2; i++)
            {
                // 将指针定位到所有Y分量数据的结尾处,从所有Y分量数据的结尾处开始拷贝UV分量
                auto p = cache_ + frame->width * frame->height;
                memcpy(p + i * frame->width,
                    frame->data[1] + i * frame->linesize[1],
                    frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
                );
            }
        }

        return Draw(cache_, linesize);
    case AV_PIX_FMT_RGBA:
    case AV_PIX_FMT_BGRA:
    case AV_PIX_FMT_ARGB:
        return Draw(frame->data[0], frame->linesize[0]);
    default:
        break;
    }

    return false;
}

/**
 * @brief 返回当前时间戳
 * @return 当前时间戳
*/
long long NowMs()
{
    // 注意!!! 这里不要直接返回clock(), 这样的话只能在windows中当中使用,因为这个函数在windows当中返回的是毫秒数
    // 但是在linux当中这个值返回的是微秒数,所以需要除以 (CLOCKS_PER_SEC / 1000)
    // 因为在linux当中 CLOCKS_PER_SEC 这个值返回的是1000000, 但是在windows当中这个值返回的是1000.
    // 这样可以最大限度地保证兼容性和跨平台
    return clock() / (CLOCKS_PER_SEC / 1000);
}

AVFrame* XVideoView::Read()
{
    // 如果宽度小于0,高度小于0,且文件未成功打开,则直接返回
    if (width_ <= 0 || height_ <= 0 || !ifs_) return nullptr;

    // 若AVFrame内存空间已申请,但参数发生变化,则需要释放空间
    // 因为用户可能通过界面上的文本输入框重新设定图像大小
    if (frame_)
    {
        if (frame_->width != width_ ||
            frame_->height != height_ ||
            frame_->format != fmt_)
        {
            // 说明用户重新选择了界面参数(宽,高,像素格式),需要释放AVFrame空间
            av_frame_free(&frame_);
        }
    }

    // 若frame为空,则重新申请
    if (!frame_)
    {
        frame_ = av_frame_alloc();
        frame_->width = width_;
        frame_->height = height_;
        frame_->format = fmt_;

        frame_->linesize[0] = width_ * 4;            // 默认像素格式为RGBA
        if (frame_->format == AV_PIX_FMT_YUV420P)
        {
            frame_->linesize[0] = width_;            // Y
            frame_->linesize[1] = width_ / 2;        // U
            frame_->linesize[2] = width_ / 2;        // V
        }

        // 生成AVFrame的buff空间,使用默认对齐方式
        auto re = av_frame_get_buffer(frame_, 0);
        if (re != 0)
        {
            char buf[1024] = { 0 };
            av_strerror(re, buf, sizeof(buf) - 1);
            cout << buf << endl;
            av_frame_free(&frame_);
            return nullptr;
        }
    }

    if (!frame_) return nullptr;

    // 读取一帧数据
    if (frame_->format == AV_PIX_FMT_YUV420P)
    {
        ifs_.read((char*)frame_->data[0], frame_->linesize[0] * height_);            // 读取Y
        ifs_.read((char*)frame_->data[1], frame_->linesize[1] * height_ / 2);        // 读取U
        ifs_.read((char*)frame_->data[2], frame_->linesize[2] * height_ / 2);        // 读取V
    }
    else
    {
        ifs_.read((char *)frame_->data[0], frame_->linesize[0] * height_);
    }

    // 如果读取到文件末尾,则直接返回
    if (ifs_.gcount() == 0)
        return nullptr;

    // 否则返回读取后的AVFrame数据
    return frame_;
}

bool XVideoView::Open(std::string filepath)
{
    if (ifs_.is_open())
    {
        ifs_.close();
    }

    ifs_.open(filepath, ios::binary);

    return ifs_.is_open();
}

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) 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                    渲染图片,这里是对Y/U/V数据分别进行渲染(线程安全)
    * @param y                    Y分量的数据
    * @param y_pitch            Y 的个数
    * @param u                    U分量的数据
    * @param u_pitch            U的个数
    * @param v                    V分量的数据
    * @param v_pitch            V的个数
    * @return                    绘制的结果
    */
    bool Draw(const unsigned char* y, int y_pitch,
        const unsigned char* u, int u_pitch,
        const unsigned char* v, int v_pitch
    ) 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 <thread>

#include <sdl/SDL.h>

#pragma comment(lib, "SDL2.lib")

using namespace std;


void MSleep(unsigned int ms)
{
    auto beg = clock();
    for (int i = 0; i < ms; i++)
    {
        this_thread::sleep_for(1ms);
        if ((clock() - beg) / (CLOCKS_PER_SEC / 1000) >= ms)
            break;
    }
}


/**
 * @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)
{
    // 判断输入的参数是否合法
    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_RGBA32;
        break;
    case XVideoView::BGRA:
        sdl_fmt = SDL_PIXELFORMAT_BGRA32;
        break;
    case XVideoView::ARGB:
        sdl_fmt = SDL_PIXELFORMAT_ARGB32;
        break;
    case XVideoView::NV12:
        sdl_fmt = SDL_PIXELFORMAT_NV12;
        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;
}


bool XSDL::Draw(const unsigned char* y, int y_pitch,
                const unsigned char* u, int u_pitch,
                const unsigned char* v, int v_pitch
)
{
    if (!y || !u || !v)
    {
        cout << "input y, u, v data can not be 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;
    }

    // 复制内存到显存
    auto re = SDL_UpdateYUVTexture(texture_, NULL,
        y, y_pitch,                    // Y
        u, u_pitch,                    // U
        v, v_pitch                    // V
    );
    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;
}

二. 需要注意的地方

1. 开启硬件加速的方法

只需要设置解码器的 hw_device_ctx 成员的值即可,比如我这里将该成员的值设置为:

    // 开启硬件加速
    c->hw_device_ctx = av_buffer_ref(hw_ctx);

2. 硬解码出来的数据可能不是常规的格式

使用DXVA2硬解码出来的数据可能不是常规的YUV420格式,实际测试,硬解码出来的数据格式是: AV_PIX_FMT_DXVA2_VLD 这种格式。比如我在下面的code位置添加了一个断点,可以清楚地看到该数据格式:

注意到解码后的AVFrame中的format的值为53,而53是  AV_PIX_FMT_DXVA2_VLD 这种格式:

3. 从显存复制到内存后,frame的像素格式会变成NV12格式

从显存复制到内存后,像素格式会变成NV12格式:

注意此时的AVFrame中的format的值为23,而23是  AV_PIX_FMT_NV12 格式。

4. 图像的linesize可能在解码后发生改变

图像的linesize在解码后可能会发生改变,这样可能会导致SDL在渲染时发生异常,所以需要额外处理:

switch (frame->format)
    {
    case AV_PIX_FMT_YUV420P:
        return Draw(frame->data[0], frame->linesize[0],                // Y
                    frame->data[1], frame->linesize[1],                // U
                    frame->data[2], frame->linesize[2]                // V
        );
    case AV_PIX_FMT_NV12:
        if (!cache_)
        {
            // 若空间尚未分配,则申请一块较大的空间(4K的一幅图像的大小)
            cache_ = new unsigned char[4096 * 2160 * 1.5];
        }

        // PS: 注意!这里面涉及到一个内存对齐的问题!
        // 如果是400x300的YUV420P/NV12格式,它的linesize很可能会被FFmpeg自动进行字节对齐, 即当图像尺寸为400x300时,它的linesize可能
        // 不是400,而是416(假设ffmpeg是以16字节进行对齐)
        // 而在使用SDL进行渲染时,linesize若传递416,很可能导致渲染出现问题,为了避免出现这种情况,下面提供了一种逐行复制的策略。
        
        // 下面的内存拷贝主要目的是为了让数据是连续的
        linesize = frame->width;
        if (frame->linesize[0] == frame->width)
        {
            // ---------------- 若linesize与图像的宽度一致,说明未发生字节对齐 --------------- 
            // 拷贝所有Y分量
            memcpy(cache_, frame->data[0], frame->linesize[0] * frame->height); 
            // 拷贝所有的UV分量
            memcpy(cache_ + frame->linesize[0] * frame->height, frame->data[1], frame->linesize[1] * frame->height / 2);
        }
        else
        {
            // ---------------- 若linesize与图像的宽度一致,说明发生了字节对齐 --------------- 
            // 使用逐行拷贝的方式
            // 拷贝所有的Y分量
            for (int i = 0; i < frame->height; i++)
            {
                memcpy(cache_ + i * frame->width, 
                    frame->data[0] + i * frame->linesize[0], 
                    frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
                );
            }

            // 拷贝所有UV分量
            for (int i = 0; i < frame->height / 2; i++)
            {
                // 将指针定位到所有Y分量数据的结尾处,从所有Y分量数据的结尾处开始拷贝UV分量
                auto p = cache_ + frame->width * frame->height;
                memcpy(p + i * frame->width,
                    frame->data[1] + i * frame->linesize[1],
                    frame->width // 注意只需要拷贝frame->width个数即可,丢弃因为对齐产生的多余数据
                );
            }
        }

        return Draw(cache_, linesize);
    case AV_PIX_FMT_RGBA:
    case AV_PIX_FMT_BGRA:
    case AV_PIX_FMT_ARGB:
        return Draw(frame->data[0], frame->linesize[0]);
    default:
        break;
    }

    return false;
}

 

posted @ 2021-12-30 01:22  夜行过客  阅读(1124)  评论(0编辑  收藏  举报