• 音视频技术应用(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;
    }
  • 相关阅读:
    python 错误之SyntaxError: Missing parentheses in call to 'print'
    C语言之指针
    list中的insert语句
    RSA算法加解密
    关于jQuery
    JDBC连接数据库并将resultset中的文件输出为excel
    下载文件的写法
    随机生成订单号
    如何根据选项来动态的添加自己所需要的表单
    搞笑语句集锦
  • 原文地址:https://www.cnblogs.com/yongdaimi/p/15747294.html
Copyright © 2020-2023  润新知