摘要
在云端渲染场景中,服务端高性能硬件除了可以快速处理特效,也可以利用其实现快速编解码的操作,为云端特效处理提速。本文将介绍如何利用服务端GPU实现硬编硬解及一些特殊场景下的优化。
正文
提到音视频编解码,大家可能会第一时间想到ffmpeg,大家平时可能会常用ffmpeg命令行做一些音视频处理。ffmpeg是一个非常强大的跨平台的编解码库,在其编解码能力中,除了对一些软编软解能力的封装外,对不同平台不同硬件特性做了封装处理,比如MacOS及iOS上的VideoToolBox,Android平台的MediaCodec,Intel核显加速QSV,NVIDIA的NVCodec等,这里主要介绍在服务器端利用ffmpeg中对NVIDIA显卡加速的封装实现硬编硬解,因篇幅有限,这里会重点介绍如何针对渲染场景做些特殊处理,对于官方示例中已有的案例不会做详细介绍。
以下会基于h264_nvenc,h264_cuvid来介绍相关的编解码操作。关于这个编解码器,这篇文章对其表现做了详细的介绍。https://developer.nvidia.com/zh-cn/blog/turing-h264-video-encoding-speed-and-quality/。
一、安装
首先从ffmpeg的编译安装开始讲起,因为需要使用nvcodec相关的能力,所以在ffmpeg时需要开启相关的功能进行编译。根据官方文档https://trac.ffmpeg.org/wiki/HWAccelIntro,只需要两步操作(在这之前需要保证机器上有NVIDIA显卡以及驱动及cuda已经正常安装)。
第一步,将nv-codec相关库下载编译。
git clone https://git.videolan.org/git/ffmpeg/nv-codec-headers.git
cd nv-codec-headers
make
sudo make install
第二步,在编译ffmpeg时开启相关选项。./configure ... --enable-cuda --enable-cuvid --enable-nvenc ...
二、编解码
在官方示例中有三个示例可以了解编解码流程:
1. https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/hw_decode.c,实现了输入一个视频,同时可以指定编码器,输出yuv信息的功能。
2. https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/decode_video.c,实现了输入一个视频,输出yuv信息的功能。
3. https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/encode_video.c,实现了指定编码格式,输出视频的功能。
图1 ffmpeg基本渲染流程
在示例中,体现了如图1所示通用的解码流程,编码相反。下面对示例中的几个API做下解释。
1. av_hwdevice_ctx_create()
创建硬件设备相关的上下文信息AVHWDeviceContext和对硬件设备进行初始化。这一步在后续针对使用nvidia设备优化中会使用到。
2. av_hwdevice_find_type_by_name()
根据名称查找对应的AVHWDeviceType,支持的类型如源码所示https://github.com/FFmpeg/FFmpeg/blob/master/libavutil/hwcontext.h#L27,这里其实就是指定使用和硬件特性相关的编解码方式。
保证使用的ffmpeg版本支持这种解码方式,可以通过第一步编译出的的ffmpeg的bin来确认,如图2所示。
图2 查看当前ffmpeg版本支持的硬件加速类型
3. avcodec_find_encoder_by_name()
查找ffmpeg支持的编码器,可通过 ffmpeg -encoders 查看,如图3所示。
图3 查看当前ffmpeg版本支持的编码器
4. avcodec_find_decoder_by_name()
查找ffmpeg支持的解码器,可通过 ffmpeg -decoders 查看。
5. 在官方编码示例中:https://github.com/FFmpeg/FFmpeg/blob/master/doc/examples/encode_video.c#L124。
if (codec->id == AV_CODEC_ID_H264)
av_opt_set(c->priv_data, "preset", "slow", 0);
以上这段代码,是设置编码器相关的参数,比如码率等,但它是通过字符串配置的,那么如何知道不同的编码器对应着什么参数,我们可以通过 `ffmpeg -h encoder=编码器名称` 查看具体的参数,上面 `fmpeg -encoders`中获取的列表就是不同的编码器参数,如图4。比如在本文中着重讲到的编码器和解码器为`h264_nvenc`,`h264_cuvid`。
图4 查看编码器支持的视频参数
三、格式转换
图5 ffmpeg示例中的解码流程
在云端渲染场景中,最终还是需要我们将解码帧转为OpenGL相关的纹理进行相关的操作。按照图5流程,需要把解码后的帧数据拷贝到CPU中,然后再进行格式的转换,通过示例可以看到数据在GPU和CPU中左右横跳,所以针对这种情况,我们需要做进一步优化。
通过对相关API的查询,我们了解到,通过硬解直接拿到的AVFrame中存放的数据是从解码器中直接拿到的数据指针,比如使用NVIDIA相关平台h264_cuvid拿到的是cuda memory,MacOS平台使用videotoolbox拿到的是数据指针CVPixelBufferRef,这些数据结构都提供了平台相关API来构造OpengGL纹理,或者Mac下可以转为Metel texture,如此便可改为图6流程。
图6 在云端渲染中优化后的解码流程
以下代码为AVFrame转为OpenGL纹理的过程,因为ffmpeg的API中存在大量void类型指针,而且文档会标明所有可能的类型,比如:https://ffmpeg.org/doxygen/3.3/structAVBufferRef.html#acb8452e99cd75074b93800b532c6ea4b。
所以一些情况下通过源码可以了解到ffmpeg与平台类型API是如何交互的,以及不同数据类型是如何交互,https://github.com/FFmpeg/FFmpeg/blob/master/libavcodec/cuviddec.c#L461。编码则同上面流程相反。
// cudaContext可以从设置的VideoCodecContext->hw_device_ctx中拿到
cuCtxPushCurrent(cudaContext);
// 映射OpenGL memory和Cuda Memory
cuGraphicsGLRegisterImage(&(cuTexRes[channel]), texYuv[channel], GL_TEXTURE_2D, cudaGraphicsRegisterFlagsWriteDiscard);
cuGraphicsMapResources(1, &(cuTexRes[channel]), 0);
CUarray mapped_array;
cuGraphicsSubResourceGetMappedArray(&mapped_array, cuTexRes[channel], 0, 0);
// 内存拷贝
CUDA_MEMCPY2D cpy = {
.srcMemoryType = CU_MEMORYTYPE_DEVICE,
.srcDevice = (CUdeviceptr)((AVFrame *)frame->data[channel]),
.srcPitch = width,
.dstMemoryType = CU_MEMORYTYPE_ARRAY,
.dstArray = mapped_array,
.WidthInBytes = width,
.Height = height,
};
cuMemcpy2D(&cpy);
// 释放相关资源,gl纹理可正常使用
cuGraphicsUnmapResources(1, &(cuTexRes[channel]), 0);
cuCtxPopCurrent(&cudaContext);
四、多任务场景
通过上面的示例,我们利用了NVIDIA提供的硬件加速能力来提升编解码速度,但是对于多任务场景下还有一些需要注意的地方。
1. 对于编码,NVIDIA做了明确的限制,桌面级显卡大多限制了同时3路编码并发,而对于服务器级别显卡则没有限制。https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new。
2. mps( Multi-Process Service) server 是 NVIDIA 提供的一个守护进程程序,该程序会拥有整个GPU的context,其他使用GPU的进程与mps server进行通信,由mps server完成context的分配和调度,避免多进程在GPU上频繁的进行context switch,提升多进程对GPU的使用率。正如官方文档中介绍:https://docs.nvidia.com/deploy/mps/index.html。
图7 不使用mps时多进程同时使用GPU调度场景
图8 使用mps时多进程同时使用GPU调度场景
启用方式如下:
#获取当前的GPU编号,一般服务器物理机上可能有多张显卡,所以需要确认当前实例使用的GPU的编号,默认是0,保险起见通过命令获取
DEVICE_NUM=`nvidia-smi --query-gpu=index --format=csv | tail -n 1`
export CUDA_VISIBLE_DEVICES=$DEVICE_NUM
#运行mps server进程
nvidia-cuda-mps-control -d
总结
通过对ffmpeg与硬件相关交互的了解,我们可以针对不同使用场景做更加精细的优化,除了特效处理场景下对OpenGL相关的交互,还可以继续探索譬如机器学习场景下cuda相关数据的交互,使得编解码耗时不再成为云端特效运行效率的瓶颈。
参考文献
[1]ffmpeg: https://github.com/FFmpeg/FFmpeg/
[2]HWAccelIntro: https://trac.ffmpeg.org/wiki/HWAccelIntro
[3]Video Encode and Decode GPU Support Matrix: https://developer.nvidia.com/video-encode-and-decode-gpu-support-matrix-new
[4]mps:https://docs.nvidia.com/deploy/mps/index.html
[5]libx264 vs nvenc: https://developer.nvidia.com/zh-cn/blog/turing-h264-video-encoding-speed-and-quality/
作者:快手Y-tech团队
来源:微信公众号:快手Y-tech
出处:https://mp.weixin.qq.com/s/xIXcCotfzB3yQmyqhtT0vw
本文暂时没有评论,来添加一个吧(●'◡'●)