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;提高生产效率、降低成本和减…

js形参传递特殊字符

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

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

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

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

特别声明&#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谈谈你的…

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

数据库调优的目标 简单来说&#xff0c;数据库调优的目的就是要让数据库运行得更快&#xff0c;也就是说响应的时间更快&#xff0c;吞吐量更大。 不过随着用户量的不断增加&#xff0c;以及应用程序复杂度的提升&#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…

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…

你都了解2024程序员拿捏高薪的新方式吗?

2024年&#xff0c;程序员该如何拿高薪呢&#xff1f; 道理是这么讲&#xff0c;那我们到底应该如何去践行呢&#xff1f;以我自身的经验来看&#xff0c;网络接单无疑是我们程序员来钱最快的方式&#xff0c;既可以做到兼顾本职工作和快点搞钱&#xff0c;又可以充分利用好每一…

信息安全系列04-安全启动介绍

本文框架 1. 基本概念1.1 基本概念回顾1.2 数字签名及验签流程 2. 安全启动实施2.1 信任根选择2.1.1 使用HSM作为信任根2.1.2 使用最底层Bootloader作为信任根 2.2 校验方法确认2.2.1 基于非对称加密算法&#xff08;数字签名&#xff09;2.2.2 基于对称加密算法 2.3 安全启动方…

llvm AST consumer 示例

示例源码 Makefile LLVM_CONFIG ? llvm-config #CXX : clang ifndef VERBOSE QUIET : endifSRC_DIR ? $(PWD) LDFLAGS $(shell $(LLVM_CONFIG) --ldflags) COMMON_FLAGS -Wall -Wextra CXXFLAGS $(COMMON_FLAGS) $(shell $(LLVM_CONFIG) --cxxflags) LCXX :$(shell $(L…

前端面试题 ===> 【JavaScript - 高级】

公众号&#xff1a;需要以下pdf&#xff0c;关注下方 2023已经过完了&#xff0c;让我们来把今年的面试题统计号&#xff0c;来备战今年的金三银四&#xff01;所以&#xff0c;不管你是社招还是校招&#xff0c;下面这份前端面试工程师高频面试题&#xff0c;请收好。 JavaScr…

Kubernetes基础(二十七)-nodePort/targetPort/port/containerPort/hostPort

1 nodePort/targetPort/port/containerPort 1.1 实现层级 1.2 配置方式 ########service########### apiVersion: v1 kind: Service metadata: labels: name: app1 name: app1 namespace: default spec: type: NodePort ports: - <strong>port: 8080 t…

基于C#开发OPC DA客户端——搭建KEPServerEX服务

简介 OPC DA (OLE for Process Control Data Access) 是一种工业自动化领域中的通信协议标准&#xff0c;它定义了应用程序如何访问由OPC服务器提供的过程控制数据。OPC DA标准允许软件应用程序&#xff08;客户端&#xff09;从OPC服务器读取实时数据或向服务器写入数据&…

python:pyecharts 画基金净值 月K线图

pip install pyecharts1.9.1 pyecharts-1.9.1-py3-none-any.whl 我想在本地&#xff08;PC) 画出 基金净值 月K线图&#xff0c;不想每次看图都需联网。 cd my_dir mkdir echarts cd echarts curl -O https://assets.pyecharts.org/assets/echarts.min.js 修改一下开源代码 …

记录前端面试的一些笔试题(持续更新......)

文章目录 js相关数组去重数组对象去重 实现数组unshift数组扁平化tree型数据扁平化list数据转tree型数据 对象深拷贝防抖/节流函数柯里化函数管道 随便记录一些&#xff0c;面试或者工作中都会用到&#xff0c;实现的方法很多&#xff0c;这里只是一小部分&#xff0c;有更好的…