ffmpeg使用vaapi解码后的视频如何基于x11或EGL实现0-copy渲染?

技术背景

对于ffmpeg硬解码后渲染常见的做法是解码后通过av_hwframe_transfer_data方法将数据从GPU拷贝到CPU,然后做一些转换处理用opengl渲染,必然涉及到譬如类似glTexImage2D的函数将数据上传到GPU。而这样2次copy就会导致CPU的使用率变高,且GPU没有被充分利用。

基于此,我们可以使用下面的0-copy技术可以将VAAPI解码后的数据直接通过GPU渲染,1080P@60FPS的视频CPU使用率可以降低到5%以内。

知识背景

一、ffmpeg 硬件解码

首先假设读者已经了解或具备使用 ffmpeg 进行硬件解码的相关知识。如果不了解的话建议先学习ffmpeg官方示例:https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/hw_decode.c

二、vaapi 解码流程

//以下代码位于:/usr/include/va/va.h* dpy = vaGetDisplayDRM(fd);* vaInitialize(dpy, ...);** // Create surfaces required for decoding and subsequence encoding* vaCreateSurfaces(dpy, VA_RT_FORMAT_YUV420, width, height, &surfaces[0], ...);** // Set up a queue for the surfaces shared between decode and encode threads* surface_queue = queue_create();** // Create decode_thread* pthread_create(&decode_thread, NULL, decode, ...);** // Create encode_thread* pthread_create(&encode_thread, NULL, encode, ...);** // Decode thread function* decode() {*   // Find the decode entrypoint for H.264*   vaQueryConfigEntrypoints(dpy, h264_profile, entrypoints, ...);**   // Create a config for H.264 decode*   vaCreateConfig(dpy, h264_profile, VAEntrypointVLD, ...);**   // Create a context for decode*   vaCreateContext(dpy, config, width, height, VA_PROGRESSIVE, surfaces,*     num_surfaces, &decode_context);**   // Decode frames in the bitstream*   for (;;) {*     // Parse one frame and decode*     vaBeginPicture(dpy, decode_context, surfaces[surface_index]);*     vaRenderPicture(dpy, decode_context, buf, ...);*     vaEndPicture(dpy, decode_context);*     // Poll the decoding status and enqueue the surface in display order after*     // decoding is complete*     vaQuerySurfaceStatus();*     enqueue(surface_queue, surface_index);*   }* }

如上,vaapi硬解码的基本流程为:

  1. 创建vadisplay.
  2. 查询profile和Entrypoint。
  3. 根据profile创建config和context。
  4. 通过vaBeginPicture、vaRenderPicture、vaEndPicture完成解码(真正的硬解码是发生在vaEndPicture的调用上)。

三、vaapi硬解码中 VADisplay 和 VASurfaceID 的关系

VADisplay:它是一个代表和管理整个VAAPI会话的上下文对象。VADisplay负责与硬件加速器的通信,创建会话,查询硬件加速器的功能,并且是进行各种VAAPI调用的起点。在X Window系统中,VADisplay通常与一个X11 Display连接关联,因为它需要与视频输出设备进行交互,但它也可以在不直接使用X11的情况下使用,比如通过DRM直接与GPU交互。

VASurfaceID: 这是一个标识符,代表特定的解码后的视频帧或者用于编码、处理的图像表面。VASurfaceID实质上是内存中存放图像数据的缓冲区,这些缓冲区是硬件加速的,并且对应于VAAPI内部的图像资源。

彼此之间的关系:

  • VADisplay用于创建和管理VASurfaceID。在解码视频流的过程中,首先需要通过VADisplay来创建一系列的VASurfaceID,这些VASurface将用于存储解码出来的视频帧。
  • 解码视频时,解码器会将视频帧解码到由VASurfaceID代表的表面上。这些表面被管理在VADisplay的上下文中,以便能够让解码器知道将解码数据放置在哪里。
  • 在视频渲染或播放阶段,VADisplay能够利用关联的输出系统(比如X11或者Wayland)来显示VASurfaceID所代表的视频帧。

总的来说,VADisplay是管理和执行解码会话的接口,而VASurfaceID则是在这一过程中实际存储解码数据的缓冲区的标识。两者配合使用,实现了硬件加速视频的解码和显示过程。

完整流程图

在这里插入图片描述

核心点释义

VA-X11 方式渲染

如上图所示,黑色线条为ffmpeg常规解码流程。红色线框加入了为实现0-copy渲染所需要的必要步骤。蓝色线框为利用libva直接渲染到x11窗口。橙色线框为借助egl和opengl渲染。主要增加了两个核心点。第一是在打开解码器前的配置,第二是解码后如何从avframe得到vasurfaceID并零复制渲染。

在①②③处,首先使用Xopendisplay得到默认显示设备指针,其次并创建出要显示的目标窗口。此处其实也并不一定要直接用Xlib库创建窗口,作为不依赖其他图形库的Demo程序,用X11窗口是比较合适的,当然也可以用qt创建QWidget,用其句柄作为之后的渲染目标。然后用vaGetDisPlay得到VADisPlay对象。VADisPlay对象比较重要,使用vaapi硬解码,ffmpeg要求我们必须将申请的vadisplay指针赋值给AVCodecContext中的hw_device_ctx的hwctx的display,否则在后面是无法得到有效的vasurfaceID。这也能理解,作为承载vaapi解码上下文的重要对象,参考“知识背景”中的第二节[vaapi 解码流程],vaBeginPicture、vaEndPicture等函数都需要依赖这个vadisplay,而这些函数其实被实现在ffmpeg解码函数中的。对于老版本的fffmpeg,vaEndPicture等函数是实现在 avcodec_decode_video2() 中。得到VADisplay后就是用vaInitialize对vaapi进行初始化了。

上面的流程图是新版本(4.x)ffmpeg的vaapi硬解初始化配置方法。对于老版的ffmpeg就比较复杂了。需要使用vaapi_context,用户自己实现createconfig、createcontext,得到configID和contextid,并将vaapi_context赋值给AVCodecContext的hwaccel_context(下面代码132行)。可以看ffmpeg源码中关于新旧版本context(old_context)的实现区别:

以下代码位置:<https://github.com/FFmpeg/FFmpeg/blob/release/4.4/libavcodec/vaapi_decode.c>int ff_vaapi_decode_init(AVCodecContext *avctx){VAAPIDecodeContext *ctx = avctx->internal->hwaccel_priv_data;VAStatus vas;int err;ctx->va_config  = VA_INVALID_ID;ctx->va_context = VA_INVALID_ID;#if FF_API_STRUCT_VAAPI_CONTEXTif (avctx->hwaccel_context) {av_log(avctx, AV_LOG_WARNING, "Using deprecated struct ""vaapi_context in decode.\n");ctx->have_old_context = 1;ctx->old_context = avctx->hwaccel_context;// Really we only want the VAAPI device context, but this// allocates a whole generic device context because we don't// have any other way to determine how big it should be.ctx->device_ref =av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);if (!ctx->device_ref) {err = AVERROR(ENOMEM);goto fail;}ctx->device = (AVHWDeviceContext*)ctx->device_ref->data;ctx->hwctx  = ctx->device->hwctx;ctx->hwctx->display = ctx->old_context->display;// The old VAAPI decode setup assumed this quirk was always// present, so set it here to avoid the behaviour changing.ctx->hwctx->driver_quirks =AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS;}#endif#if FF_API_STRUCT_VAAPI_CONTEXTif (ctx->have_old_context) {ctx->va_config  = ctx->old_context->config_id;ctx->va_context = ctx->old_context->context_id;av_log(avctx, AV_LOG_DEBUG, "Using user-supplied decoder ""context: %#x/%#x.\n", ctx->va_config, ctx->va_context);} #endif

进入正式的解码循环并解完一帧后,关键点就是我们就可以从AVFrame的data[3]中得到vasurfaceID。这点我们可以从ffmpeg源码中得到验证,ffmpeg内部map这个VAImage时也是同样的操作。

以下代码位置:https://github.com/FFmpeg/FFmpeg/blob/release/4.4/libavutil/hwcontext_vaapi.c
在这里插入图片描述

得到VASurfaceID后,就可以使用VAPutSurface函数将指定的视频帧请求渲染到之前申请的x11窗口句柄上了。

EGL 方式渲染

如果我们想使用opengl进行渲染,稍微麻烦一点。需要借助EGL帮我们得到共享的纹理数据。

如上面流程图中的橙色线框 ⑥ ⑦处,首先我们需要在解码循环前初始化EGL环境和opengl环境。 这块是标准的初始化过程,也不会和VADisPlay、VASurfaceID建立任何的关系。唯一有点关系的是在初始化egl时因为我们要显示到一个可见的窗口中,所以使用eglCreateWindowSurface函数时需要一个X11 window(在流程图的①处我们已经通过XCreateWindow创建了一个),并将其指针作为egl的渲染目标窗口。

重点来到⑧ ⑨ ⑩ ⑪ 。不变的是我们依旧从AVFrame的data[3]中得到VASurfaceID,变化的是我们需要利用libva提供的vaExportSurfaceHandle函数,结合VASurfaceID 导出当前VASurfaceID所对应的VADRMPRIMESurfaceDescriptor结构体。有了这个结构体后我们可以从里面获取到解码数据的比如平面信息(NV12?YUV420P?)和DRM Prime的文件描述符、宽高、像素格式等。有了这些数据我们就可以用其填充eglCreateImageKHR所需要的attributes,要注意attributes 必须与 VADRMPRIMESurfaceDescriptor 提供的信息匹配。创建出EGLImage之后,我们就可以用glEGLImageTargetTexture2DOES将其绑定到我们的opengl(es)纹理之上。之后就可以进行正常的或其他的opengl渲染动作。

至此,vaapi解码出的视频数据已经被0-copy的方式所渲染出来,核心动作全部在GPU上进行,CPU的占用率只占5%以下。

在这里插入图片描述
关注公众号 QTShared 获取源码。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/722318.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

智能物联时代下RFID技术在汽车零部件智能制造中的引领作用

RFID&#xff08;Radio Frequency Identification&#xff0c;射频识别&#xff09;技术在汽车零部件加工中有广泛的应用&#xff0c;其工作原理是通过无线电频率进行自动识别。在汽车零部件加工中&#xff0c;RFID技术可以发挥重要作用&#xff0c;提高生产效率、降低成本和减…

【C++ 学习】C++ 传值 传指针 传引用

C 传值 传指针 传引用 文章目录 C 传值 传指针 传引用引用const 引用指针的引用和引用的指针引用作为函数参数例子 C 函数传参主要是三种方法&#xff1a;传值、传指针、传引用。 说在前面&#xff1a; 传引用相对来说是简单一点的&#xff0c;也更加优雅。 明确几点&#x…

js形参传递特殊字符

在前端我们给其他页面传值或者传数据到后台的时候&#xff0c;字符串经常将一些特殊符号识别成字符集。这种情况下会将数据打断或者打乱&#xff0c;比如字符串里面包含*/&这些符号的时候就会错误。 我们可以通过将字符中的特殊字符替换成十六进制的字符&#xff0c;一些特…

【Linux从青铜到王者】进程信号

——————————————————————————————————————————— 信号入门 在了解信号之前有许多要理解的相关概念 我们可以先通过一个生活例子来初步认识一下信号 1.生活角度的信号 你在网上买了很多件商品&#xff0c;再等待不同商品快递的到来…

LeetCode hot100-4

给定一个数组 nums&#xff0c;编写一个函数将所有 0 移动到数组的末尾&#xff0c;同时保持非零元素的相对顺序。请注意 &#xff0c;必须在不复制数组的情况下原地对数组进行操作。我的解法可以通过&#xff0c;而且时间能打败99.98%的Java 思路就是找出有几个0。然后遍历数…

从事测绘地信,你需要这些插件、软件、小工具、图源...

特别声明&#xff0c;本篇是来自公众号GIS前沿的资源&#xff0c;看着比较好&#xff0c;特别给大家推荐。加粗样式 今天&#xff0c;我们又来汇总了一些工作中实用的插件、小工具、数据等等&#xff0c;小助手又来帮你提高工作效率了****。 因为小助手每年都会总结一次&…

15.Django总结

文章目录 1.Django创建项目的命令2.MVC,MVT的理解3.Django中间件的使用4.WSGI,uWSGI服务器 和 uwsgi协议5.nginx和uWISG 服务器之间如何配合工作的6.django开发中数据库做过什么优化7.Python中三大框架各自的应用场景8.django如何提升性能(高并发)9. 什么是restful api谈谈你的…

Verilog宏、`include<要包含的路径/文件>

Verilog宏 Verilog对宏的定义如下&#xff1a; define TESTEQ1 4b1101 定义的宏稍后将被引用&#xff0c;如下所示&#xff1a; 如果&#xff08;请求TESTEQ1&#xff09; “ifdef”和“endif”构造执行以下操作&#xff1a; •确定是否定义了宏。 •定义条件编译。 如果定义了…

MySQL性能优化-数据库调优有哪些维度可以选择

数据库调优的目标 简单来说&#xff0c;数据库调优的目的就是要让数据库运行得更快&#xff0c;也就是说响应的时间更快&#xff0c;吞吐量更大。 不过随着用户量的不断增加&#xff0c;以及应用程序复杂度的提升&#xff0c;我们很难用“更快”去定义数据库调优的目标&#…

windows常用命令总结

Windows常用命令总结如下&#xff1a; 系统管理与维护&#xff1a; shutdown&#xff1a;用于关闭或重新启动计算机。例如&#xff0c;shutdown -s -t 60 表示60秒后关机&#xff0c;shutdown -r -t 60 表示60秒后重启。systeminfo&#xff1a;显示关于计算机及其操作系统的详细…

掘根宝典之C语言if,switch,break,continue,逻辑运算符(||,,!),?:运算符

if语句 C语言中的if语句用于执行给定条件下的一部分代码。 if语句的语法如下&#xff1a; if (condition) {// code to be executed if condition is true }在这里&#xff0c;condition是一个表达式&#xff0c;如果为真&#xff0c;则执行if语句块中的代码。如果条件为假&…

蓝桥集训之重新排序

蓝桥集训之重新排序 核心思想&#xff1a;差分 利用差分 在一段区间内 最终求和得到每个位置数计算的次数然后分别从大到小排序w[i]和s[i]数组 #include<iostream>#include<algorithm>using namespace std;typedef long long LL;const int N 100010;int w[N],s…

Python并发编程:协程-gevent模块

一 gevent模块 Gevent是一个第三方库&#xff0c;可以轻松通过gevent实现并发同步或异步编程。在gevent中用到的主要模式是Greenlet&#xff0c;它是以C扩展模块形式接入Python的轻量级协程。Greenlet 全部运行在主程序操作系统进程的内部&#xff0c;但它们被协作式地调度。 …

Stable Diffusion ———LDM、SD 1.0, 1.5, 2.0、SDXL、SDXL-Turbo等版本之间关系现原理详解

前言 2021年5月&#xff0c;OpenAI发表了《扩散模型超越GANs》的文章&#xff0c;标志着扩散模型&#xff08;Diffusion Models&#xff0c;DM&#xff09;在图像生成领域开始超越传统的GAN模型&#xff0c;进一步推动了DM的应用。 然而&#xff0c;早期的DM直接作用于像素空…

cmd模式下启动mysql

1.打开cmd输入services.msc&#xff0c;找到MYSQL&#xff0c;右击属性&#xff0c;找到可执行文件路径&#xff0c;加载到环境变量。 2.打开cmd&#xff0c;启动MYSQL&#xff1a;输入net start mysql; 3.登陆MYSQL&#xff0c;需要管理权限&#xff1b; 输入&#xff1a;my…

HALCON 快速入门手册

HALCON 快速入门手册 1 什么是 HALCON HALCON 是德国 MVtec 公司开发的一套完善的标准的机器视觉算法包&#xff0c;拥有应用广泛 的机器视觉集成开发环境。它节约了产品成本&#xff0c;缩短了软件开发周期——HALCON 灵活的 架构便于机器视觉&#xff0c;医学图像和图像分析应…

Docker容器化解决方案

什么是Docker&#xff1f; Docker是一个构建在LXC之上&#xff0c;基于进程容器的轻量级VM解决方案&#xff0c;实现了一种应用程序级别的资源隔离及配额。Docker起源于PaaS提供商dotCloud 基于go语言开发&#xff0c;遵从Apache2.0开源协议。 Docker 自开源后受到广泛的关注和…

数据链路层----滑动窗口协议的相关计算

目录 1.窗口大小的相关计算 •停等协议&#xff1a; •后退N帧协议&#xff1a; •选择重传协议&#xff1a; 2.信道利用率相关计算 •停等协议的信道利用率&#xff1a; •连续ARQ&#xff08;后退N帧协议&#xff0c;选择重传协议&#xff09;的信道利用率&#xff1a;…

工具函数模板题(蓝桥杯 C++ 代码 注解)

目录 一、Vector容器&#xff1a; 二、Queue队列 三、Map映射 四、题目&#xff08;快递分拣 vector&#xff09;&#xff1a; 代码&#xff1a; 五、题目&#xff08;CLZ银行问题 queue&#xff09;&#xff1a; 代码&#xff1a; 六、题目&#xff08;费里的语言 map&…

每日学习总结20240301

20240301 1. strchr VS strrchr strchr和strrchr是C语言标准库中的字符串处理函数&#xff0c;用于在字符串中查找特定字符的位置。 1.1 strchr函数 strchr函数用于在字符串中查找第一次出现指定字符的位置&#xff0c;并返回该位置的指针。函数原型如下&#xff1a; char…