音視頻技術應用(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;
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM