飞道的博客

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

471人阅读  评论(0)

【写在前面】

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

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

        一般我们处理硬解时都会将解码帧下载到内存,然后渲染( 方便处理 )。

        然而,对于超高分辨率( 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 资源关联起来:


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

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

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


  
  1. m_pbo. bind();
  2. if (m_texture. isCreated()) {
  3. m_texture. bind();
  4. m_texture. setData( 0, 0, 0, m_videoWidth, m_videoHeight, 4, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, nullptr);
  5. }
  6. m_pbo. release();
  7. m_program. bind();
  8. m_vbo. bind();
  9. m_program. enableAttributeArray( 0);
  10. m_program. setAttributeBuffer( 0, GL_FLOAT, 0, 2, 2 * sizeof(GLfloat));
  11. m_program. enableAttributeArray( 1);
  12. m_program. setAttributeBuffer( 1, GL_FLOAT, 2 * 4 * sizeof(GLfloat), 2, 2 * sizeof(GLfloat));
  13. m_program. setUniformValue( "texture", 0);
  14. glDrawArrays(GL_QUADS, 0, 4);
  15. m_vbo. release();
  16. m_program. release();

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

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

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


  
  1. class Renderer : public QObject, protected QOpenGLFunctions
  2. {
  3. Q_OBJECT
  4. public:
  5. Renderer(QObject *window) :
  6. m_texture(QOpenGLTexture::Target2D)
  7. , m_vbo(QOpenGLBuffer::VertexBuffer)
  8. , m_pbo(QOpenGLBuffer::PixelUnpackBuffer)
  9. , m_window(window)
  10. {
  11. }
  12. ~ Renderer()
  13. {
  14. if (m_vbo. isCreated()) m_vbo. destroy();
  15. if (m_pbo. isCreated()) m_pbo. destroy();
  16. if (m_texture. isCreated()) m_texture. destroy();
  17. }
  18. static int stream_callback(int channelId, void *userPtr, int mediaType, char *pbuf, FFS_FRAME_INFO *frameInfo)
  19. {
  20. Q_UNUSED(channelId);
  21. auto _this = reinterpret_cast<Renderer *>(userPtr);
  22. if (mediaType == MEDIA_TYPE_VIDEO && frameInfo) {
  23. auto frameWidth = frameInfo->width;
  24. auto frameHeight = frameInfo->height;
  25. if (!_this->m_initCodec) {
  26. _this-> initializeVideoSize(frameWidth, frameHeight);
  27. int errCode;
  28. std::string erroStr;
  29. _this->m_deocder_handle = NvDecoder_Create( FFmpeg2NvCodecId(frameInfo->codec), _this->m_pbo. bufferId()
  30. , frameWidth, frameHeight, false, true, rgba, errCode, erroStr);
  31. qDebug() << __func__ << "NvDecoder_Create:" << _this->m_deocder_handle << errCode << QString:: fromStdString(erroStr);
  32. cuMemAlloc((CUdeviceptr *)&g_ppFrame, frameWidth * frameHeight * 4);
  33. _this->m_initCodec = true;
  34. }
  35. if (_this->m_deocder_handle && pbuf) {
  36. uint8_t **ppFrame;
  37. int nFrameReturned = 0;
  38. int nFrameLen = 0;
  39. int nRet = NvDecoder_DecodeHW(_this->m_deocder_handle, ( const uint8_t *)pbuf, frameInfo->length, &ppFrame, &nFrameLen, &nFrameReturned);
  40. //qDebug() << __func__ << "NvDecoder_DecodeHW:" << nRet << nFrameReturned << nFrameLen;
  41. for ( int i = 0; i < nFrameReturned; i++) {
  42. GetImageHW((CUdeviceptr)ppFrame[i], g_ppFrame, frameWidth * 4, frameHeight);
  43. QMetaObject:: invokeMethod(_this->m_window, "update");
  44. std::unique_lock<std::mutex> locker(_this->m_mutex);
  45. _this->m_condition. wait_for(locker, std::chrono:: milliseconds( 100));
  46. }
  47. }
  48. }
  49. return 0;
  50. }
  51. void initializeGL(int w, int h)
  52. {
  53. m_width = w;
  54. m_height = h;
  55. initializeOpenGLFunctions();
  56. initializeShader();
  57. GLfloat points[] {
  58. -1.0f, 1.0f,
  59. 1.0f, 1.0f,
  60. 1.0f, -1.0f,
  61. -1.0f, -1.0f,
  62. 0.0f, 0.0f,
  63. 1.0f, 0.0f,
  64. 1.0f, 1.0f,
  65. 0.0f, 1.0f
  66. };
  67. m_vbo. create();
  68. m_vbo. bind();
  69. m_vbo. allocate(points, sizeof(points));
  70. m_vbo. release();
  71. if (!context) {
  72. ck( cuInit( 0));
  73. CUdevice cuDevice;
  74. ck( cuDeviceGet(&cuDevice, 0));
  75. char szDeviceName[ 80];
  76. ck( cuDeviceGetName(szDeviceName, sizeof(szDeviceName), cuDevice));
  77. qDebug() << "GPU in use: " << szDeviceName;
  78. ck( cuCtxCreate(&context, CU_CTX_SCHED_BLOCKING_SYNC, cuDevice));
  79. }
  80. FFS_Init(&m_ffs_handle);
  81. FFS_OpenStream(m_ffs_handle, 1000, ( char *)m_videoUrl. toStdString(). c_str(), RTP_OVER_TCP, MEDIA_TYPE_VIDEO | MEDIA_TYPE_AUDIO | MEDIA_TYPE_EVENT
  82. , this, ( void *)&stream_callback, 1000, 1);
  83. }
  84. public slots:
  85. void render()
  86. {
  87. glClearColor( 0.2f, 0.3f, 0.3f, 1.0f);
  88. glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
  89. glDisable(GL_DEPTH_TEST);
  90. glDisable(GL_CULL_FACE);
  91. glDepthMask( false);
  92. m_pbo. bind();
  93. if (m_texture. isCreated()) {
  94. m_texture. bind();
  95. m_texture. setData( 0, 0, 0, m_videoWidth, m_videoHeight, 4, QOpenGLTexture::RGBA, QOpenGLTexture::UInt8, nullptr);
  96. }
  97. m_pbo. release();
  98. m_program. bind();
  99. m_vbo. bind();
  100. m_program. enableAttributeArray( 0);
  101. m_program. setAttributeBuffer( 0, GL_FLOAT, 0, 2, 2 * sizeof(GLfloat));
  102. m_program. enableAttributeArray( 1);
  103. m_program. setAttributeBuffer( 1, GL_FLOAT, 2 * 4 * sizeof(GLfloat), 2, 2 * sizeof(GLfloat));
  104. m_program. setUniformValue( "texture", 0);
  105. glDrawArrays(GL_QUADS, 0, 4);
  106. m_vbo. release();
  107. m_program. release();
  108. }
  109. void initializeVideoSize(int w, int h)
  110. {
  111. m_videoWidth = w;
  112. m_videoHeight = h;
  113. m_updateResource = true;
  114. }
  115. void resizeGL(int w, int h)
  116. {
  117. if (m_width != w || m_height != h) {
  118. m_width = w;
  119. m_height = h;
  120. glViewport( 0, 0, w, h);
  121. }
  122. }
  123. void display()
  124. {
  125. if (m_initCodec) {
  126. ck( cuCtxSetCurrent(context));
  127. if (m_updateResource) {
  128. if (m_texture. isCreated()) m_texture. destroy();
  129. m_texture. create();
  130. m_texture. bind();
  131. m_texture. setMinificationFilter(QOpenGLTexture::Nearest);
  132. m_texture. setMagnificationFilter(QOpenGLTexture::Nearest);
  133. m_texture. setWrapMode(QOpenGLTexture::ClampToEdge);
  134. m_texture. setSize(m_videoWidth, m_videoHeight, 4);
  135. m_texture. setFormat(QOpenGLTexture::RGBAFormat);
  136. m_texture. allocateStorage(QOpenGLTexture::BGRA, QOpenGLTexture::UInt8);
  137. m_texture. release();
  138. if (m_pbo. isCreated()) m_pbo. destroy();
  139. m_pbo. create();
  140. m_pbo. bind();
  141. m_pbo. allocate( nullptr, m_videoWidth * m_videoHeight * 4);
  142. m_pbo. setUsagePattern(QOpenGLBuffer::StreamDraw);
  143. m_pbo. release();
  144. ck( cuGraphicsGLRegisterBuffer(&cuda_tex_resource, m_pbo. bufferId(), CU_GRAPHICS_REGISTER_FLAGS_WRITE_DISCARD));
  145. m_updateResource = false;
  146. }
  147. ck( cuGraphicsMapResources( 1, &cuda_tex_resource, 0));
  148. CUdeviceptr d_tex_buffer;
  149. size_t d_tex_size;
  150. ck( cuGraphicsResourceGetMappedPointer(&d_tex_buffer, &d_tex_size, cuda_tex_resource));
  151. GetImageHW((CUdeviceptr)g_ppFrame, (uchar *)d_tex_buffer, m_videoWidth * 4, m_videoHeight);
  152. ck( cuGraphicsUnmapResources( 1, &cuda_tex_resource, 0));
  153. render();
  154. }
  155. m_condition. notify_one();
  156. }
  157. private:
  158. void initializeShader()
  159. {
  160. if (!m_program. addShaderFromSourceCode(QOpenGLShader::Vertex,
  161. "#version 330 core\n"
  162. "layout(location = 0) in vec4 position;"
  163. "layout(location = 1) in vec2 texCoord0;"
  164. "out vec2 texCoord;"
  165. "void main(void)"
  166. "{"
  167. " gl_Position = position;"
  168. " texCoord = texCoord0;"
  169. "}"))
  170. qDebug() << m_program. log();
  171. if (!m_program. addShaderFromSourceCode(QOpenGLShader::Fragment,
  172. "#version 330 core\n"
  173. "in vec2 texCoord;"
  174. "out vec4 FragColor;"
  175. "uniform sampler2D texture;"
  176. "void main(void)"
  177. "{"
  178. " FragColor = texture2D(texture, texCoord);"
  179. "}"))
  180. qDebug() << m_program. log();
  181. if (!m_program. link())
  182. qDebug() << m_program. log();
  183. if (!m_program. bind())
  184. qDebug() << m_program. log();
  185. }
  186. bool m_initCodec = false;
  187. bool m_updateResource = false;
  188. int m_width, m_height;
  189. int m_videoWidth, m_videoHeight;
  190. std::mutex m_mutex;
  191. std::condition_variable m_condition;
  192. QOpenGLTexture m_texture;
  193. QOpenGLBuffer m_vbo, m_pbo;
  194. QOpenGLShaderProgram m_program;
  195. CUgraphicsResource cuda_tex_resource;
  196. CUcontext context = nullptr;
  197. void *m_ffs_handle = nullptr, *m_deocder_handle = nullptr;
  198. QString m_videoUrl = "rtsp://admin:pass123456@192.168.0.101:554/h264/ch1/main/av_stream";
  199. QObject *m_window = nullptr;
  200. };

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

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

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

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

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

        QWdiget 需要借助 QOpenGLWidget


  
  1. class VideoWidget: public QOpenGLWidget
  2. {
  3. public:
  4. VideoWidget(QWidget* parent = nullptr)
  5. {
  6. m_renderer = new Renderer( this);
  7. }
  8. virtual void initializeGL() override
  9. {
  10. m_renderer-> initializeGL( width(), height());
  11. }
  12. virtual void paintGL() override
  13. {
  14. m_renderer-> display();
  15. }
  16. virtual void resizeGL(int w, int h) override
  17. {
  18. m_renderer-> resizeGL(w, h);
  19. }
  20. private:
  21. Renderer *m_renderer = nullptr;
  22. };

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

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

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

        因此,这里的实现为:


  
  1. class VideoItem : public QQuickItem
  2. {
  3. Q_OBJECT
  4. public:
  5. VideoItem()
  6. {
  7. connect( this, &QQuickItem::windowChanged, this, [ this](QQuickWindow *window){
  8. if (window) {
  9. connect(window, &QQuickWindow::beforeRendering, this, &VideoItem::sync,
  10. Qt::DirectConnection);
  11. connect(window, &QQuickWindow::sceneGraphInvalidated, this, &VideoItem::cleanup,
  12. Qt::DirectConnection);
  13. window-> setClearBeforeRendering( false);
  14. }
  15. });
  16. }
  17. public slots:
  18. void sync()
  19. {
  20. if (!m_renderer) {
  21. m_renderer = new Renderer( window());
  22. m_renderer-> initializeGL( window()-> width(), window()-> height());
  23. m_renderer-> resizeGL( window()-> width(), window()-> height());
  24. connect( window(), &QQuickWindow::beforeRendering, this, [ this]() {
  25. window()-> resetOpenGLState();
  26. m_renderer-> display();
  27. }, Qt::DirectConnection);
  28. connect( window(), &QQuickWindow::widthChanged, this, [ this]() {
  29. m_renderer-> resizeGL( window()-> width(), window()-> height());
  30. });
  31. connect( window(), &QQuickWindow::heightChanged, this, [ this]() {
  32. m_renderer-> resizeGL( window()-> width(), window()-> height());
  33. });
  34. }
  35. }
  36. void cleanup()
  37. {
  38. if (m_renderer) {
  39. delete m_renderer;
  40. m_renderer = nullptr;
  41. }
  42. }
  43. private:
  44. Renderer *m_renderer = nullptr;
  45. };

        运行效果:


 【结语】

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

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

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


转载:https://blog.csdn.net/u011283226/article/details/128613596
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场