Qt / Qml 视频硬解码(CUDA)中如何实现无上传硬渲染(一)

news2024/11/16 4:49:01

【写在前面】

        很多时候,我们在对视频的解码和渲染的处理都要经过以下步骤:

  • 软解码,视频帧位于内存
    • 软渲染,需要拷贝到图像然后渲染;硬渲染则需要上传纹理,然后渲染
  • 硬解码,视频帧位于显存
    • 软渲染,需要下载到内存,然后拷贝到图像再渲染;硬渲染则直接拷贝到纹理,然后渲染

        然而,对于超高分辨率( 4K 8K )而言,上传下载带来的的性能损失太大了( CPU瓶颈 ),为了实现更流畅的体验和更低的资源占用,应当考虑更好的方案。

        当然,这里没必要提软解码,因为无论如何都需要上传( 硬渲染 )。

        另一方面,现在流行的硬解码大多使用 Nvidia CUDA( cuvid ),因此本篇只以 CUDA 硬解为例来实现硬渲染,其他硬解思路基本一致。


【需要的准备】

        首先,我假设你已经有一个拉流器( live555 或 ffmpeg,本地文件则不需要 ),然后有一个 NV 的硬解码器 NVDecoder,另外需要一定的 OpenGL 基础,因为我这里的硬渲染需要使用 OpenGL

        需要准备好的工具:

  • 拉流器( 本地文件直接取码流即可 )
  • Nvidia硬解码器
  • OpenGL环境( 因为这里是Qt,所以使用QOpenGL )
  • CUDA环境( 我这里的版本是CUDA 11.0 )

【正文开始】

        注意,我这里只是简单的使用了 RGBA( 一般都是NV12 )。

        实际上,Nvidia 官方的示例相当明了:

        其核心思路是将 CUDA 图形资源与 OpenGL 上下文关联起来:

CUgraphicsResource cuda_tex_resource;
ck(cuGraphicsGLRegisterBuffer(&cuda_tex_resource, m_pbo.bufferId(), CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD));
ck(cuGraphicsMapResources(1, &cuda_tex_resource, 0));
CUdeviceptr d_tex_buffer;
size_t d_tex_size;
ck(cuGraphicsResourceGetMappedPointer(&d_tex_buffer, &d_tex_size, cuda_tex_resource));
GetImageHW((CUdeviceptr)g_ppFrame, (uchar *)d_tex_buffer, m_videoWidth * 4, m_videoHeight);
ck(cuGraphicsUnmapResources(1, &cuda_tex_resource, 0));
  • 这里的 g_ppFrame,它是 NVDecoder 解码出来的视频帧的显存地址( CUdeviceptr )。
  • m_pbo 则是 OpenGL 中的像素缓冲对象 ( PBO )。
  • 至于 GetImageHW,只是简单封装了显存拷贝函数。

        因此,这里的工作相当简单,只有注册 & 关联 & 拷贝

        经过这些操作,现在视频帧到达 PBO,渲染就轻而易举了:

m_pbo.bind();
if (m_texture.isCreated()) {
    m_texture.bind();
    m_texture.setData(0, 0, 0, m_videoWidth, m_videoHeight, 4, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, nullptr);
}
m_pbo.release();

m_program.bind();
m_vbo.bind();
m_program.enableAttributeArray(0);
m_program.setAttributeBuffer(0, GL_FLOAT, 0, 2, 2 * sizeof(GLfloat));

m_program.enableAttributeArray(1);
m_program.setAttributeBuffer(1, GL_FLOAT, 2 * 4 * sizeof(GLfloat), 2, 2 * sizeof(GLfloat));

m_program.setUniformValue("texture", 0);

glDrawArrays(GL_QUADS, 0, 4);

m_vbo.release();
m_program.release();

        将 PBO 的数据拷贝至 OpenGL Texture,然后绘制即可。

        当然,大致的思路就是这样,然而各种坑也相当多,比如 CUDA 上下文 必须和 OpenGL 上下文 在同一个线程

        接着将整个流程整理一下,放入 Qt 中,先实现一个 Renderer

class Renderer : public QObject, protected QOpenGLFunctions
{
    Q_OBJECT

public:
    Renderer(QObject *window) :
        m_texture(QOpenGLTexture::Target2D)
      , m_vbo(QOpenGLBuffer::VertexBuffer)
      , m_pbo(QOpenGLBuffer::PixelUnpackBuffer)
      , m_window(window)
    {

    }

    ~Renderer()
    {
        if (m_vbo.isCreated()) m_vbo.destroy();
        if (m_pbo.isCreated()) m_pbo.destroy();
        if (m_texture.isCreated()) m_texture.destroy();
    }

    static int stream_callback(int channelId, void *userPtr, int mediaType, char *pbuf, FFS_FRAME_INFO *frameInfo)
    {
        Q_UNUSED(channelId);

        auto _this = reinterpret_cast<Renderer *>(userPtr);

        if (mediaType == MEDIA_TYPE_VIDEO && frameInfo) {
            auto frameWidth = frameInfo->width;
            auto frameHeight = frameInfo->height;

            if (!_this->m_initCodec) {
                _this->initializeVideoSize(frameWidth, frameHeight);

                int errCode;
                std::string erroStr;
                _this->m_deocder_handle = NvDecoder_Create(FFmpeg2NvCodecId(frameInfo->codec), _this->m_pbo.bufferId()
                                                           , frameWidth, frameHeight, false, true, rgba, errCode, erroStr);
                qDebug() << __func__ << "NvDecoder_Create:" << _this->m_deocder_handle << errCode << QString::fromStdString(erroStr);

                cuMemAlloc((CUdeviceptr *)&g_ppFrame, frameWidth * frameHeight * 4);

                _this->m_initCodec = true;
            }

            if (_this->m_deocder_handle && pbuf) {
                uint8_t **ppFrame;
                int nFrameReturned = 0;
                int nFrameLen = 0;
                int nRet = NvDecoder_DecodeHW(_this->m_deocder_handle, (const uint8_t *)pbuf, frameInfo->length, &ppFrame, &nFrameLen, &nFrameReturned);
                //qDebug() << __func__ << "NvDecoder_DecodeHW:" << nRet << nFrameReturned << nFrameLen;
                for (int i = 0; i < nFrameReturned; i++) {
                    GetImageHW((CUdeviceptr)ppFrame[i], g_ppFrame, frameWidth * 4, frameHeight);
                    QMetaObject::invokeMethod(_this->m_window, "update");
                    std::unique_lock<std::mutex> locker(_this->m_mutex);
                    _this->m_condition.wait_for(locker, std::chrono::milliseconds(100));
                }
            }
        }

        return 0;
    }

    void initializeGL(int w, int h)
    {
        m_width = w;
        m_height = h;

        initializeOpenGLFunctions();

        initializeShader();

        GLfloat points[] {
            -1.0f, 1.0f,
            1.0f, 1.0f,
            1.0f, -1.0f,
            -1.0f, -1.0f,

            0.0f, 0.0f,
            1.0f, 0.0f,
            1.0f, 1.0f,
            0.0f, 1.0f
        };

        m_vbo.create();
        m_vbo.bind();
        m_vbo.allocate(points, sizeof(points));
        m_vbo.release();

        if (!context) {
            ck(cuInit(0));
            CUdevice cuDevice;
            ck(cuDeviceGet(&cuDevice, 0));
            char szDeviceName[80];
            ck(cuDeviceGetName(szDeviceName, sizeof(szDeviceName), cuDevice));
            qDebug() << "GPU in use: " << szDeviceName;
            ck(cuCtxCreate(&context, CU_CTX_SCHED_BLOCKING_SYNC, cuDevice));
        }

        FFS_Init(&m_ffs_handle);

        FFS_OpenStream(m_ffs_handle, 1000, (char *)m_videoUrl.toStdString().c_str(), RTP_OVER_TCP, MEDIA_TYPE_VIDEO | MEDIA_TYPE_AUDIO | MEDIA_TYPE_EVENT
                       , this, (void *)&stream_callback, 1000, 1);
    }

public slots:
    void render()
    {
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glDisable(GL_DEPTH_TEST);
        glDisable(GL_CULL_FACE);
        glDepthMask(false);

        m_pbo.bind();
        if (m_texture.isCreated()) {
            m_texture.bind();
            m_texture.setData(0, 0, 0, m_videoWidth, m_videoHeight, 4, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, nullptr);
        }
        m_pbo.release();

        m_program.bind();
        m_vbo.bind();
        m_program.enableAttributeArray(0);
        m_program.setAttributeBuffer(0, GL_FLOAT, 0, 2, 2 * sizeof(GLfloat));

        m_program.enableAttributeArray(1);
        m_program.setAttributeBuffer(1, GL_FLOAT, 2 * 4 * sizeof(GLfloat), 2, 2 * sizeof(GLfloat));

        m_program.setUniformValue("texture", 0);

        glDrawArrays(GL_QUADS, 0, 4);

        m_vbo.release();
        m_program.release();
    }

    void initializeVideoSize(int w, int h)
    {
        m_videoWidth = w;
        m_videoHeight = h;
        m_updateResource = true;
    }

    void resizeGL(int w, int h)
    {
        if (m_width != w || m_height != h) {
            m_width = w;
            m_height = h;
            glViewport(0, 0, w, h);
        }
    }

    void display()
    {
        if (m_initCodec) {
            ck(cuCtxSetCurrent(context));

            if (m_updateResource) {
                if (m_texture.isCreated()) m_texture.destroy();
                m_texture.create();
                m_texture.bind();
                m_texture.setMinificationFilter(QOpenGLTexture::Nearest);
                m_texture.setMagnificationFilter(QOpenGLTexture::Nearest);
                m_texture.setWrapMode(QOpenGLTexture::ClampToEdge);
                m_texture.setSize(m_videoWidth, m_videoHeight, 4);
                m_texture.setFormat(QOpenGLTexture::RGBAFormat);
                m_texture.allocateStorage(QOpenGLTexture::BGRA, QOpenGLTexture::UInt8);
                m_texture.release();

                if (m_pbo.isCreated()) m_pbo.destroy();
                m_pbo.create();
                m_pbo.bind();
                m_pbo.allocate(nullptr, m_videoWidth * m_videoHeight * 4);
                m_pbo.setUsagePattern(QOpenGLBuffer::StreamDraw);
                m_pbo.release();

                ck(cuGraphicsGLRegisterBuffer(&cuda_tex_resource, m_pbo.bufferId(), CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD));

                m_updateResource = false;
            }

            ck(cuGraphicsMapResources(1, &cuda_tex_resource, 0));
            CUdeviceptr d_tex_buffer;
            size_t d_tex_size;
            ck(cuGraphicsResourceGetMappedPointer(&d_tex_buffer, &d_tex_size, cuda_tex_resource));
            GetImageHW((CUdeviceptr)g_ppFrame, (uchar *)d_tex_buffer, m_videoWidth * 4, m_videoHeight);
            ck(cuGraphicsUnmapResources(1, &cuda_tex_resource, 0));

            render();
        }

        m_condition.notify_one();
    }

private:
    void initializeShader()
    {
        if (!m_program.addShaderFromSourceCode(QOpenGLShader::Vertex,
                                               "#version 330 core\n"
                                               "layout(location = 0) in vec4 position;"
                                               "layout(location = 1) in vec2 texCoord0;"
                                               "out vec2 texCoord;"
                                               "void main(void)"
                                               "{"
                                               "    gl_Position = position;"
                                               "    texCoord = texCoord0;"
                                               "}"))
            qDebug() << m_program.log();

        if (!m_program.addShaderFromSourceCode(QOpenGLShader::Fragment,
                                               "#version 330 core\n"
                                               "in vec2 texCoord;"
                                               "out vec4 FragColor;"
                                               "uniform sampler2D texture;"
                                               "void main(void)"
                                               "{"
                                               "    FragColor = texture2D(texture, texCoord);"
                                               "}"))
            qDebug() << m_program.log();

        if (!m_program.link())
            qDebug() << m_program.log();

        if (!m_program.bind())
            qDebug() << m_program.log();
    }


    bool m_initCodec = false;
    bool m_updateResource = false;
    int m_width, m_height;
    int m_videoWidth, m_videoHeight;
    std::mutex m_mutex;
    std::condition_variable m_condition;
    QOpenGLTexture m_texture;
    QOpenGLBuffer m_vbo, m_pbo;
    QOpenGLShaderProgram m_program;
    CUgraphicsResource cuda_tex_resource;
    CUcontext context = nullptr;
    void *m_ffs_handle = nullptr, *m_deocder_handle = nullptr;
    QString m_videoUrl = "rtsp://admin:pass123456@192.168.0.101:554/h264/ch1/main/av_stream";
    QObject *m_window = nullptr;
};

        看起来有点复杂,然而真正要做成产品远不止如此,但这里不需要管那么多,先屏蔽 stream_callback,整个流程就是标准的 OpenGL 使用流程:

  • 初始化 OpenGL 的各种缓冲&着色器。
  • render() 中发出各种绘制命令。

        而 stream_callback 则是拉流后的回调,此时拿到的即是码流数据,需要进行解码:

  • 使用帧信息初始化解码器。
  • 接着使用 NVDecoder 解码视频帧,并拷贝至 g_ppFrame,此时便接上了正文开头。

        渲染器有了,最后我们只需要在 QWidget / Qml 中创建调用即可。

        QWdiget 需要借助 QOpenGLWidget

class VideoWidget: public QOpenGLWidget
{
public:
    VideoWidget(QWidget* parent = nullptr)
    {
        m_renderer = new Renderer(this);
    }

    virtual void initializeGL() override
    {
        m_renderer->initializeGL(width(), height());
    }

    virtual void paintGL() override
    {
        m_renderer->display();
    }

    virtual void resizeGL(int w, int h) override
    {
        m_renderer->resizeGL(w, h);
    }

private:
    Renderer *m_renderer = nullptr;
};

        非常简单,因为渲染器的设计正是如此。

        Qml 中如何使用呢?我之前写过一篇文章:

现代OpenGL系列教程(零)---在Qt/Quick中使用OpenGLhttps://blog.csdn.net/u011283226/article/details/83217741

        因此,这里的实现为:

class VideoItem : public QQuickItem
{
    Q_OBJECT

public:
    VideoItem()
    {
        connect(this, &QQuickItem::windowChanged, this, [this](QQuickWindow *window){
            if (window) {
                connect(window, &QQuickWindow::beforeRendering, this, &VideoItem::sync,
                        Qt::DirectConnection);
                connect(window, &QQuickWindow::sceneGraphInvalidated, this, &VideoItem::cleanup,
                        Qt::DirectConnection);
                window->setClearBeforeRendering(false);
            }
        });
    }

public slots:
    void sync()
    {
        if (!m_renderer) {
            m_renderer = new Renderer(window());
            m_renderer->initializeGL(window()->width(), window()->height());
            m_renderer->resizeGL(window()->width(), window()->height());
            connect(window(), &QQuickWindow::beforeRendering, this, [this]() {
                window()->resetOpenGLState();
                m_renderer->display();
            }, Qt::DirectConnection);
            connect(window(), &QQuickWindow::widthChanged, this, [this]() {
                m_renderer->resizeGL(window()->width(), window()->height());
            });
            connect(window(), &QQuickWindow::heightChanged, this, [this]() {
                m_renderer->resizeGL(window()->width(), window()->height());
            });
        }
    }

    void cleanup()
    {
        if (m_renderer) {
            delete m_renderer;
            m_renderer = nullptr;
        }
    }

private:
    Renderer *m_renderer = nullptr;
};

        运行效果:


 【结语】

        最后,本篇代码实际都是可以使用的,不过需要根据你们自己的项目进行改进。

        当然了,因为只是 demo,没有帧率控制,没有各种网络情况的处理,没有解码和渲染的控制等等,这些都需要自己慢慢优化了。

        限于篇幅,下一篇将带来 Qml 中更好的实现和集成,敬请期待ヾ( ̄▽ ̄)~~

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

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

相关文章

OPengl学习(四)——顶点数组

文章目录1、 问题2、步骤2.1 激活数组2.2 指定数组的数据2.3 解引用和渲染3、例子1、 问题 1、在前面我们实现一个多彩三角形&#xff0c;调用三次glvertext&#xff08;&#xff09;函数&#xff0c;如果在多边形&#xff0c;如20条边的&#xff0c;那么就要使用22次函数&…

【大数据之路】数据管理篇 《三》存储和成本管理 【搬运小结】

文章目录【大数据之路】数据管理篇 《三》存储和成本管理1.1数据压缩1.2存储治理项优化1.3生命周期管理1.3.1 生命周期管理策略1.3.2 生命周期管理策略1.4数据成本计量【大数据之路】数据管理篇 《三》存储和成本管理 1.1数据压缩 在分布式文件系统中&#xff0c;为了提高数据…

python制作课堂点名系统,从此老师对我关爱有加

前言 大家早好、午好、晚好吖 ❤ ~ 准备工作 首先我们需要准备好点名的姓名文件&#xff0c;使用的时候导入进去就可以开始点名了。 新建一个文本文档&#xff0c;将姓名设置设置好&#xff0c;如下&#xff1a; 使用系统库和第三方库都比较常规 from PyQt5.QtWidgets impo…

Attention机制的具体计算过程

一、介绍Query、Key、Value的来源一个输入&#xff0c;经过embedding位置编码后得到最终的输入X&#xff08;512维&#xff09;&#xff0c;最终的输入X与矩阵参数WQ&#xff08;512*64维&#xff09;相乘&#xff0c;得到Query&#xff1b;与矩阵参数WK&#xff08;512*64维&a…

Java反射机制

目录 反射问题的引出 Java程序在计算机中部署的三个阶段 反射的主要相关类 反射机制的优缺点 调优 反射常用类—Class 特点 常用方法 获取映射Class类对象的四种方式 类加载的三个阶段 加载阶段 Loading 链接阶段 Linking 验证 Verification 准备 Preparation 解…

OpenGL示例源码opengl_examples编译

下载好源码并创建编译目录opengl_build 打开CMake-GUI选择源码目录及二进制编译目录:

C#学习记录——【实例】读写ini文件

『知识有两种&#xff0c;一种是你知道的&#xff0c;一种是你知道在哪里能找到的&#xff01;』—— 塞缪尔约翰逊 1、概念 C#读写ini文件之前要了解的概念&#xff1a;INI就是扩展名为"INI"的文件,其实他本身是个文本文件,可以用记事本打开,主要存放的是用户所做…

axios拦截器使用和知识点补充

axios拦截器使用和知识点补充axios拦截器使用axios基地址ajax知识点补充onreadstatechange事件Ajax组成部分了解get请求与post请求区别其他请求方法了解axios拦截器使用 <link rel"stylesheet" href"./lib/bootstrap-v4.6.0.css" /><style>bod…

农业机器人研究进展

文章目录一、农业机器人二、国际农业机器人现状三、我国农业机器人发展情况四、农业机器人展望五、结束语2022年9月17-18日&#xff0c;第十一届中国智能产业高峰论坛成功在厦门举办。大会主论坛上&#xff0c;CAAI副理事长、中国工程院院士、国家农业信息化工程技术研究中心研…

Java实现二叉树

一、树 1、树简介 树是一种非线性的数据结构&#xff0c;具有n个结点其数据存储形式像一棵倒挂的树&#xff0c;树有一个根结点没有前驱结点&#xff0c;树有多个叶子结点没有后继结点&#xff0c;树有多个中间结点既有前驱结点又有后继结点。 树结构中子树之间不能有交集。 n个…

【Node.js实战】一文带你开发博客项目之联调(导入HTML、Nginx反向代理、CORS解决跨域、与前端联调)

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;也会涉及到服务端 &#x1f4c3;个人状态&#xff1a; 在校大学生一枚&#xff0c;已拿多个前端 offer&#xff08;秋招&#xff09; &#x1f680;未…

Tapdata Cloud 场景通关系列: Oracle → MySQL 异构实时同步

【前言】作为中国的 “Fivetran/Airbyte”, Tapdata Cloud 自去年发布云版公测以来&#xff0c;吸引了近万名用户的注册使用。应社区用户上生产系统的要求&#xff0c;Tapdata Cloud 3.0 将正式推出商业版服务&#xff0c;提供对生产系统的 SLA 支撑。Tapdata 目前专注在实时数…

二叉树的遍历(非递归)

二叉树的遍历 遍历二叉树, 是指按一定的规则和顺序访问二叉树的所有结点, 使每一个结点都被访问一次, 而且只被访问一次. 由于二叉树是非线性结构, 因此, 二叉树的遍历实质上是将二叉树的各个结点排列成一个线性序列. DFS: 前序, 中序及后序. BFS: 是指沿着二叉树的宽度优先遍…

Leetcode.1806 还原排列的最少操作步数

题目链接 Leetcode.1806 还原排列的最少操作步数 题目描述 给你一个偶数 ​n​n​n​​​​​ &#xff0c;已知存在一个长度为 nnn 的排列 permpermperm &#xff0c;其中 perm[i]iperm[i] iperm[i]i​&#xff08;下标 从 0 开始 计数&#xff09;。 一步操作中&#xff0…

OLAP和OLTP的区别

OLAP和OLTP的区别 OLAP&#xff1a; (Online transaction processing):在线/联机事务处理。典型的OLTP类操作都比较简单&#xff0c;主要是对数据库中的数据进行增删改查&#xff0c;操作主体一般是产品的用户。 OLTP&#xff1a; (Online analytical processing):指联机分…

Vue新一代状态管理工具—Pinia—都2023年了,快学起来吧!

Pinia 基本介绍 Pinia 是 Vue.js 的轻量级状态管理库 官方网站&#xff1a;https://pinia.vuejs.org/ 中文文档: https://pinia.web3doc.top/introduction.html 为什么学习pinia? pinia和vuex4一样&#xff0c;也是vue 官方 状态管理工具(作者是 Vue 核心团队成员&#xff…

基于JAVA SSM框架的影院管理系统源码,实现包括影院管理,电影管理,影厅管理,排片管理,选座售票,演员管理,影片评论等功能

介绍 下载地址&#xff1a;基于JAVA SSM框架的影院管理系统源码 该项目是一个电影信息管理、发布、展示平台&#xff0c;终端用户可以浏览、购票、评论。项目主要实现包括影院管理&#xff0c;电影管理&#xff0c;影厅管理&#xff0c;排片管理&#xff0c;选座售票&#xff…

连号区间数(第四届蓝桥杯省赛C++B组,第四届蓝桥杯省赛JAVAB组)

题目详细&#xff1a;解题思路&#xff1a;对于这个题目如果一开始没有思路的话我们可以先想一下暴力写法暴力的话就是不断的枚举每个区间然后判断这个区间是否合法这样写下来用了三重循环而对于题目我们只能通过部分样例所以我们就要想办法取缩减它的时间对于遍历每个区间我们…

【SpringBoot】使用AOP+注解实现请求参数的指定自动填充

首先定义一个加在方法上的注解 import java.lang.annotation.*;/*** 开启自动参数填充*/ Retention(RetentionPolicy.RUNTIME) Target({ElementType.METHOD}) Documented Inherited public interface AutoParameterFill {/*** 要填充的字段名,不写的话默认下面类的子类中的字段…

Redis未授权访问漏洞(一)先导篇

前言 Redis默认情况下&#xff0c;会绑定在0.0.0.0:6379&#xff0c;如果没有进行采用相关的策略&#xff0c;比如添加防火墙规则避免其他非信任来源ip访问等&#xff0c;这样将会将Redis服务暴露到公网上。 如果在没有设置密码认证&#xff08;一般为空&#xff09;的情况下…