OpenGL 到 FFMpeg 编码
OpenGL to FFMpeg encode
我有一个 opengl
缓冲区,我需要将其直接转发到 ffmpeg
以进行基于 nvenc 的 h264
编码。
我目前的做法是glReadPixels
从帧缓冲区中取出像素,然后将该指针传递给ffmpeg
,这样它就可以将帧编码为H264
RTSP
的数据包。然而,这很糟糕,因为我必须将 GPU 内存中的字节复制到 CPU 内存中,只将它们复制回 GPU 中进行编码。
首先要检查的是它可能 "bad" 但它是否 运行 足够快?提高效率总是好的,但如果有效,请不要破坏它。
如果真的有性能问题...
1 仅使用 FFMPEG 软件编码,无需硬件辅助。然后你只会从 GPU 复制到 CPU 一次。 (如果视频编码器在 GPU 上并且您通过 RTSP 发送数据包,则编码后还有第二个 GPU CPU。)
2 寻找一个 NVIDIA(我假设这是你谈论 nvenc 的 GPU)纹理格式的 GL 扩展 and/or 将在 GPU H264 编码上直接执行到 OpenGL 缓冲区的命令。
如果您查看发布日期与此答复的日期,您会发现我花了很多时间在这上面。 (这是我过去 4 周的全职工作)。
因为我很难让它工作,所以我会写一个简短的指南,希望能帮助找到它的人。
大纲
我的基本流程是 OGL 帧缓冲对象颜色附件(纹理)→ nvenc(nvidia 编码器)
注意事项
一些注意事项:
1) nvidia 编码器可以接受 YUV 或 RGB 类型的图像。
2) FFMPEG 4.0 及以下无法将RGB 图像传递给nvenc。
3) 根据我的问题,FFMPEG updated 接受 RGB 作为输入。
有几件不同的事情需要了解:
1) AVHWDeviceContext- 将此视为 ffmpegs 设备抽象层。
2) AVHWFramesContext- 将此视为 ffmpegs 硬件框架抽象层。
3) cuMemcpy2D- 将 cuda 映射的 OGL 纹理复制到 ffmpeg 创建的 cuda 缓冲区所需的方法。
综合性
本指南是对标准软件编码指南的补充。这不是完整的代码,只能在标准流程之外使用。
代码详情
设置
您需要先获取您的 GPU 名称,为此我找到了一些代码(我不记得我从哪里得到的)进行了一些 cuda 调用并获得了 GPU 名称:
int getDeviceName(std::string& gpuName)
{
//Setup the cuda context for hardware encoding with ffmpeg
NV_ENC_BUFFER_FORMAT eFormat = NV_ENC_BUFFER_FORMAT_IYUV;
int iGpu = 0;
CUresult res;
ck(cuInit(0));
int nGpu = 0;
ck(cuDeviceGetCount(&nGpu));
if (iGpu < 0 || iGpu >= nGpu)
{
std::cout << "GPU ordinal out of range. Should be within [" << 0 << ", "
<< nGpu - 1 << "]" << std::endl;
return 1;
}
CUdevice cuDevice = 0;
ck(cuDeviceGet(&cuDevice, iGpu));
char szDeviceName[80];
ck(cuDeviceGetName(szDeviceName, sizeof(szDeviceName), cuDevice));
gpuName = szDeviceName;
epLog::msg(epMSG_STATUS, "epVideoEncode:H264Encoder", "...using device \"%s\"", szDeviceName);
return 0;
}
接下来您需要设置硬件设备和硬件框架上下文:
getDeviceName(gpuName);
ret = av_hwdevice_ctx_create(&m_avBufferRefDevice, AV_HWDEVICE_TYPE_CUDA, gpuName.c_str(), NULL, NULL);
if (ret < 0)
{
return -1;
}
//Example of casts needed to get down to the cuda context
AVHWDeviceContext* hwDevContext = (AVHWDeviceContext*)(m_avBufferRefDevice->data);
AVCUDADeviceContext* cudaDevCtx = (AVCUDADeviceContext*)(hwDevContext->hwctx);
m_cuContext = &(cudaDevCtx->cuda_ctx);
//Create the hwframe_context
// This is an abstraction of a cuda buffer for us. This enables us to, with one call, setup the cuda buffer and ready it for input
m_avBufferRefFrame = av_hwframe_ctx_alloc(m_avBufferRefDevice);
//Setup some values before initialization
AVHWFramesContext* frameCtxPtr = (AVHWFramesContext*)(m_avBufferRefFrame->data);
frameCtxPtr->width = width;
frameCtxPtr->height = height;
frameCtxPtr->sw_format = AV_PIX_FMT_0BGR32; // There are only certain supported types here, we need to conform to these types
frameCtxPtr->format = AV_PIX_FMT_CUDA;
frameCtxPtr->device_ref = m_avBufferRefDevice;
frameCtxPtr->device_ctx = (AVHWDeviceContext*)m_avBufferRefDevice->data;
//Initialization - This must be done to actually allocate the cuda buffer.
// NOTE: This call will only work for our input format if the FFMPEG library is >4.0 version..
ret = av_hwframe_ctx_init(m_avBufferRefFrame);
if (ret < 0) {
return -1;
}
//Cast the OGL texture/buffer to cuda ptr
CUresult res;
CUcontext oldCtx;
m_inputTexture = texture;
res = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
res = cuCtxPushCurrent(*m_cuContext);
res = cuGraphicsGLRegisterImage(&cuInpTexRes, m_inputTexture, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
res = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
//Assign some hardware accel specific data to AvCodecContext
c->hw_device_ctx = m_avBufferRefDevice;//This must be done BEFORE avcodec_open2()
c->pix_fmt = AV_PIX_FMT_CUDA; //Since this is a cuda buffer, although its really opengl with a cuda ptr
c->hw_frames_ctx = m_avBufferRefFrame;
c->codec_type = AVMEDIA_TYPE_VIDEO;
c->sw_pix_fmt = AV_PIX_FMT_0BGR32;
// Setup some cuda stuff for memcpy-ing later
m_memCpyStruct.srcXInBytes = 0;
m_memCpyStruct.srcY = 0;
m_memCpyStruct.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
m_memCpyStruct.dstXInBytes = 0;
m_memCpyStruct.dstY = 0;
m_memCpyStruct.dstMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
请记住,尽管上面做了很多工作,但显示的代码是标准软件编码代码之外的代码。确保也包括所有这些 calls/object 初始化。
与软件版本不同,输入 AVFrame 对象所需要做的就是在调用 alloc 之后获取缓冲区:
// allocate RGB video frame buffer
ret = av_hwframe_get_buffer(m_avBufferRefFrame, rgb_frame, 0); // 0 is for flags, not used at the moment
注意它接受 hwframe_context 作为参数,这是它知道在 gpu 上分配什么设备、大小、格式等的方式。
调用对每一帧进行编码
现在我们已经设置好了,可以开始编码了。在每次编码之前,我们需要将帧从纹理复制到 cuda 缓冲区。我们通过将 cuda 数组映射到纹理然后将该数组复制到 cuDeviceptr(由上面的 av_hwframe_get_buffer 调用分配)来实现:
//Perform cuda mem copy for input buffer
CUresult cuRes;
CUarray mappedArray;
CUcontext oldCtx;
//Get context
cuRes = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
cuRes = cuCtxPushCurrent(*m_cuContext);
//Get Texture
cuRes = cuGraphicsResourceSetMapFlags(cuInpTexRes, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
cuRes = cuGraphicsMapResources(1, &cuInpTexRes, 0);
//Map texture to cuda array
cuRes = cuGraphicsSubResourceGetMappedArray(&mappedArray, cuInpTexRes, 0, 0); // Nvidia says its good practice to remap each iteration as OGL can move things around
//Release texture
cuRes = cuGraphicsUnmapResources(1, &cuInpTexRes, 0);
//Setup for memcopy
m_memCpyStruct.srcArray = mappedArray;
m_memCpyStruct.dstDevice = (CUdeviceptr)rgb_frame->data[0]; // Make sure to copy devptr as it could change, upon resize
m_memCpyStruct.dstPitch = rgb_frame->linesize[0]; // Linesize is generated by hwframe_context
m_memCpyStruct.WidthInBytes = rgb_frame->width * 4; //* 4 needed for each pixel
m_memCpyStruct.Height = rgb_frame->height; //Vanilla height for frame
//Do memcpy
cuRes = cuMemcpy2D(&m_memCpyStruct);
//release context
cuRes = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
现在我们只需调用 send_frame 就可以了!
ret = avcodec_send_frame(c, rgb_frame);
注意:我省略了大部分代码,因为它不适用于 public。我可能有一些细节不正确,这就是我理解过去一个月收集的所有数据的方式……请随时纠正任何不正确的地方。此外,有趣的是,在此过程中,我的计算机崩溃了,我失去了所有初步调查(我没有检查源代码控制的所有内容),其中包括我在互联网上找到的所有各种示例代码。因此,如果您看到属于您的东西,请大声说出来。这可以帮助其他人得出我得出的结论。
大喊大叫
在 https://webchat.freenode.net/ 向 BtbN 大声喊叫#ffmpeg,如果没有他们的帮助,我不会得到这些。
我有一个 opengl
缓冲区,我需要将其直接转发到 ffmpeg
以进行基于 nvenc 的 h264
编码。
我目前的做法是glReadPixels
从帧缓冲区中取出像素,然后将该指针传递给ffmpeg
,这样它就可以将帧编码为H264
RTSP
的数据包。然而,这很糟糕,因为我必须将 GPU 内存中的字节复制到 CPU 内存中,只将它们复制回 GPU 中进行编码。
首先要检查的是它可能 "bad" 但它是否 运行 足够快?提高效率总是好的,但如果有效,请不要破坏它。
如果真的有性能问题...
1 仅使用 FFMPEG 软件编码,无需硬件辅助。然后你只会从 GPU 复制到 CPU 一次。 (如果视频编码器在 GPU 上并且您通过 RTSP 发送数据包,则编码后还有第二个 GPU CPU。)
2 寻找一个 NVIDIA(我假设这是你谈论 nvenc 的 GPU)纹理格式的 GL 扩展 and/or 将在 GPU H264 编码上直接执行到 OpenGL 缓冲区的命令。
如果您查看发布日期与此答复的日期,您会发现我花了很多时间在这上面。 (这是我过去 4 周的全职工作)。
因为我很难让它工作,所以我会写一个简短的指南,希望能帮助找到它的人。
大纲
我的基本流程是 OGL 帧缓冲对象颜色附件(纹理)→ nvenc(nvidia 编码器)
注意事项
一些注意事项:
1) nvidia 编码器可以接受 YUV 或 RGB 类型的图像。
2) FFMPEG 4.0 及以下无法将RGB 图像传递给nvenc。
3) 根据我的问题,FFMPEG updated 接受 RGB 作为输入。
有几件不同的事情需要了解:
1) AVHWDeviceContext- 将此视为 ffmpegs 设备抽象层。
2) AVHWFramesContext- 将此视为 ffmpegs 硬件框架抽象层。
3) cuMemcpy2D- 将 cuda 映射的 OGL 纹理复制到 ffmpeg 创建的 cuda 缓冲区所需的方法。
综合性
本指南是对标准软件编码指南的补充。这不是完整的代码,只能在标准流程之外使用。
代码详情
设置
您需要先获取您的 GPU 名称,为此我找到了一些代码(我不记得我从哪里得到的)进行了一些 cuda 调用并获得了 GPU 名称:
int getDeviceName(std::string& gpuName)
{
//Setup the cuda context for hardware encoding with ffmpeg
NV_ENC_BUFFER_FORMAT eFormat = NV_ENC_BUFFER_FORMAT_IYUV;
int iGpu = 0;
CUresult res;
ck(cuInit(0));
int nGpu = 0;
ck(cuDeviceGetCount(&nGpu));
if (iGpu < 0 || iGpu >= nGpu)
{
std::cout << "GPU ordinal out of range. Should be within [" << 0 << ", "
<< nGpu - 1 << "]" << std::endl;
return 1;
}
CUdevice cuDevice = 0;
ck(cuDeviceGet(&cuDevice, iGpu));
char szDeviceName[80];
ck(cuDeviceGetName(szDeviceName, sizeof(szDeviceName), cuDevice));
gpuName = szDeviceName;
epLog::msg(epMSG_STATUS, "epVideoEncode:H264Encoder", "...using device \"%s\"", szDeviceName);
return 0;
}
接下来您需要设置硬件设备和硬件框架上下文:
getDeviceName(gpuName);
ret = av_hwdevice_ctx_create(&m_avBufferRefDevice, AV_HWDEVICE_TYPE_CUDA, gpuName.c_str(), NULL, NULL);
if (ret < 0)
{
return -1;
}
//Example of casts needed to get down to the cuda context
AVHWDeviceContext* hwDevContext = (AVHWDeviceContext*)(m_avBufferRefDevice->data);
AVCUDADeviceContext* cudaDevCtx = (AVCUDADeviceContext*)(hwDevContext->hwctx);
m_cuContext = &(cudaDevCtx->cuda_ctx);
//Create the hwframe_context
// This is an abstraction of a cuda buffer for us. This enables us to, with one call, setup the cuda buffer and ready it for input
m_avBufferRefFrame = av_hwframe_ctx_alloc(m_avBufferRefDevice);
//Setup some values before initialization
AVHWFramesContext* frameCtxPtr = (AVHWFramesContext*)(m_avBufferRefFrame->data);
frameCtxPtr->width = width;
frameCtxPtr->height = height;
frameCtxPtr->sw_format = AV_PIX_FMT_0BGR32; // There are only certain supported types here, we need to conform to these types
frameCtxPtr->format = AV_PIX_FMT_CUDA;
frameCtxPtr->device_ref = m_avBufferRefDevice;
frameCtxPtr->device_ctx = (AVHWDeviceContext*)m_avBufferRefDevice->data;
//Initialization - This must be done to actually allocate the cuda buffer.
// NOTE: This call will only work for our input format if the FFMPEG library is >4.0 version..
ret = av_hwframe_ctx_init(m_avBufferRefFrame);
if (ret < 0) {
return -1;
}
//Cast the OGL texture/buffer to cuda ptr
CUresult res;
CUcontext oldCtx;
m_inputTexture = texture;
res = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
res = cuCtxPushCurrent(*m_cuContext);
res = cuGraphicsGLRegisterImage(&cuInpTexRes, m_inputTexture, GL_TEXTURE_2D, CU_GRAPHICS_REGISTER_FLAGS_READ_ONLY);
res = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
//Assign some hardware accel specific data to AvCodecContext
c->hw_device_ctx = m_avBufferRefDevice;//This must be done BEFORE avcodec_open2()
c->pix_fmt = AV_PIX_FMT_CUDA; //Since this is a cuda buffer, although its really opengl with a cuda ptr
c->hw_frames_ctx = m_avBufferRefFrame;
c->codec_type = AVMEDIA_TYPE_VIDEO;
c->sw_pix_fmt = AV_PIX_FMT_0BGR32;
// Setup some cuda stuff for memcpy-ing later
m_memCpyStruct.srcXInBytes = 0;
m_memCpyStruct.srcY = 0;
m_memCpyStruct.srcMemoryType = CUmemorytype::CU_MEMORYTYPE_ARRAY;
m_memCpyStruct.dstXInBytes = 0;
m_memCpyStruct.dstY = 0;
m_memCpyStruct.dstMemoryType = CUmemorytype::CU_MEMORYTYPE_DEVICE;
请记住,尽管上面做了很多工作,但显示的代码是标准软件编码代码之外的代码。确保也包括所有这些 calls/object 初始化。
与软件版本不同,输入 AVFrame 对象所需要做的就是在调用 alloc 之后获取缓冲区:
// allocate RGB video frame buffer
ret = av_hwframe_get_buffer(m_avBufferRefFrame, rgb_frame, 0); // 0 is for flags, not used at the moment
注意它接受 hwframe_context 作为参数,这是它知道在 gpu 上分配什么设备、大小、格式等的方式。
调用对每一帧进行编码
现在我们已经设置好了,可以开始编码了。在每次编码之前,我们需要将帧从纹理复制到 cuda 缓冲区。我们通过将 cuda 数组映射到纹理然后将该数组复制到 cuDeviceptr(由上面的 av_hwframe_get_buffer 调用分配)来实现:
//Perform cuda mem copy for input buffer
CUresult cuRes;
CUarray mappedArray;
CUcontext oldCtx;
//Get context
cuRes = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
cuRes = cuCtxPushCurrent(*m_cuContext);
//Get Texture
cuRes = cuGraphicsResourceSetMapFlags(cuInpTexRes, CU_GRAPHICS_MAP_RESOURCE_FLAGS_READ_ONLY);
cuRes = cuGraphicsMapResources(1, &cuInpTexRes, 0);
//Map texture to cuda array
cuRes = cuGraphicsSubResourceGetMappedArray(&mappedArray, cuInpTexRes, 0, 0); // Nvidia says its good practice to remap each iteration as OGL can move things around
//Release texture
cuRes = cuGraphicsUnmapResources(1, &cuInpTexRes, 0);
//Setup for memcopy
m_memCpyStruct.srcArray = mappedArray;
m_memCpyStruct.dstDevice = (CUdeviceptr)rgb_frame->data[0]; // Make sure to copy devptr as it could change, upon resize
m_memCpyStruct.dstPitch = rgb_frame->linesize[0]; // Linesize is generated by hwframe_context
m_memCpyStruct.WidthInBytes = rgb_frame->width * 4; //* 4 needed for each pixel
m_memCpyStruct.Height = rgb_frame->height; //Vanilla height for frame
//Do memcpy
cuRes = cuMemcpy2D(&m_memCpyStruct);
//release context
cuRes = cuCtxPopCurrent(&oldCtx); // THIS IS ALLOWED TO FAIL
现在我们只需调用 send_frame 就可以了!
ret = avcodec_send_frame(c, rgb_frame);
注意:我省略了大部分代码,因为它不适用于 public。我可能有一些细节不正确,这就是我理解过去一个月收集的所有数据的方式……请随时纠正任何不正确的地方。此外,有趣的是,在此过程中,我的计算机崩溃了,我失去了所有初步调查(我没有检查源代码控制的所有内容),其中包括我在互联网上找到的所有各种示例代码。因此,如果您看到属于您的东西,请大声说出来。这可以帮助其他人得出我得出的结论。
大喊大叫
在 https://webchat.freenode.net/ 向 BtbN 大声喊叫#ffmpeg,如果没有他们的帮助,我不会得到这些。