【写在前面】
很多时候,我们在对视频的解码和渲染的处理都要经过以下步骤:
-
软解码,
视频帧位于内存
。-
软渲染,
需要拷贝到图像然后渲染;硬渲染则需要上传纹理,然后渲染
。
-
软渲染,
-
硬解码,
视频帧位于显存
。-
软渲染,
需要下载到内存,然后拷贝到图像再渲染;硬渲染则直接拷贝到纹理,然后渲染
。
-
软渲染,
一般我们处理硬解时都会将解码帧下载到内存,然后渲染( 方便处理 )。
然而,对于超高分辨率( 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中使用OpenGL
https://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 中更好的实现和集成,敬请期待ヾ( ̄▽ ̄)~~