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