實現了使用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 = ▭ } // 拷貝材質到渲染器 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 = ▭ } // 拷貝材質到渲染器 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; }