得到上节所采集到的桌面纹理数据之后,我们就可以对其进行编码了。
NVENC文档
在开始之前,先放出NVENC的文档下载地址给大家,免得大家找不到:
nvenc下载链接: https://developer.nvidia.com/nvidia-video-codec-sdk/download
在下载到的压缩包里,有nvenc相关的头文件和库、与nvenc相关的所有使用文档和开发文档(包括ffmpeg用户该如何去编译和使用的),和开发示例。
Nvidia官方的官方例子可以说非常全,对于cuda、d3d9、d3d11以及opengl等用户,它都各自编写了一个例子,来教用户们使用。
详细开发步骤
官方的例子固然完美,但是学习起来还是要些功夫的。下面,我将按照文档的步骤(有部分省略),来给大家介绍一下如何使用。
同步与异步
NVENC有两种开发方式,分别是同步和异步。对于异步方式,NVENC仅支持在Windows 7以上的系统去使用,对于Linux系统的用户来说,只能使用同步的方式(Mac系统在官方文档中并无提及,不过似乎苹果主要是和AMD进行合作的)。异步是NVENC官方推荐的一个方式,它们提倡尽量使用异步,主要也是让用户可以在编码未完成这段时间利用起来,干点别的事情。对于到底采用同步的方式还是异步的方式,得根据自己的实际情况来考虑。对于异步,显然准备工作会更为复杂,还得对时间控制得比较精确,来保证我们得直播帧率的稳定。对于同步和异步,本节都会进行涉及,特别是使用异步时需要注意的一些点,也会在讲解完同步之后进行提及。
1.创建nvenc实例
使用nvenc得第一个步骤,就是创建nvenc实例,它会返回一个实例对象给我们,在我们后续的nvenc api使用里,都需要通过这个实例对象来进行调用。代码如下:
NV_ENCODE_API_FUNCTION_LIST NVENC_API_List;
NVENC_API_List.version = NV_ENCODE_API_FUNCTION_LIST_VER;//这个是固定的,照写即可,不写将会报错
NvEncodeAPICreateInstance(&NVENC_API_List);
2.打开encode session:
接下来便是打开encode session。它需要我们传入一个结构体,来描述与这个session有关的信息,主要有以下这些:
- 版本号:固定的值:NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER
- 设备:与编码相关的设备环境。这里我们要将d3d11的纹理传给nvenc进行编码,因此我们要填写d3d11的设备。
- 设备类型:这里指定NV_ENC_DEVICE_TYPE_DIRECTX,除此之外还有cuda、opengl相关的类型,可以在文档查找
- api版本号:固定的值:NVENCAPI_VERSION
代码如下:
NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS session_params = {0};//不要的值记得清零
void* nvencoder;
session_params.version = NV_ENC_OPEN_ENCODE_SESSION_EX_PARAMS_VER;//固定版本号
session_params.device = opt->p_DirectXDevice;
session_params.deviceType = NV_ENC_DEVICE_TYPE_DIRECTX;
session_params.apiVersion = NVENCAPI_VERSION;
NVENC_API_List.nvEncOpenEncodeSessionEx(&session_params, &nvencoder)
3.获取presetconfig并复制到config中
presetconfig主要是指nvenc给你预先设好的一些编码器配置参数。这样做的好处就是你可以仅修改自己感兴趣的部分,不感兴趣的部分让它来帮你填写就好了。你可以使用枚举的方式,来枚举出所有支持的presetconfig,具体可以参阅文档,这里我直接使用NV_ENC_PRESET_DEFAULT_GUID即可。 代码如下:
NV_ENC_PRESET_CONFIG preset_config = { NV_ENC_PRESET_CONFIG_VER,{NV_ENC_CONFIG_VER} };//里面的两个值也是固定的版本号,记得填上,不填的话就报错
NVENC_API_List.nvEncGetEncodePresetConfig(this->nvencoder, NV_ENC_CODEC_H264_GUID, NV_ENC_PRESET_LOW_LATENCY_HQ_GUID, &preset_config);
获得presetconfig后,我们便可以将其拷贝到编码器参数上,并根据自己感兴趣的修改。代码如下:
NV_ENC_CONFIG config = {0};
memcpy(&config, &preset_config.presetCfg, sizeof(config));
config.rcParams.rateControlMode = NV_ENC_PARAMS_RC_CBR;
config.rcParams.averageBitRate = opt->bitrate;
config.gopLength = opt->gop_size;
这里我修改了速率控制模式,平均比特率与gop长度这三个参数,我们分别来看一下都代表着什么:
- rateControlMode:速率控制模式,基本上可以分为三种:固定量化参数,固定比特率,可变比特率。对于网络传输来说,选用固定比特率的模式,能让网络更为平滑。
- averageBitRate:平均比特率,比特率完全固定的话,在含有运动场景的图像编码中,会使得图像的质量变差,导致最终编码效果的不稳定。平均比特率可以说是一种折中方案,使得编码器能在静止的画面中降低比特率,在含有运动场景的画面中来提升比特率,从而做到一种平均。
- gopLength:gop长度,它是指两个I帧之间的距离。I帧是帧内编码,当发现丢帧现象时,P帧和B帧由于参考对象的错误,会导致解码出来的画面不正常,只有当重新碰到I帧时,才能解码出正确的画面。因此这个值设得越大,当发现丢帧情况时,可能会使得恢复正常画面的时间更长。
4.设置编码器初始化参数并进行初始化
前面的config并不是编码器的全部参数,编码器的全部参数被存放到一个更大的结构体中,除了config里包含的参数之外,里面还存放着编码图像的宽高,FPS等参数,具体如下:
NV_ENC_INITIALIZE_PARAMS encoder_init_params = {0};
encoder_init_params.encodeConfig = &config;
encoder_init_params.encodeConfig->version = NV_ENC_CONFIG_VER;
encoder_init_params.encodeGUID = NV_ENC_CODEC_H264_GUID;
encoder_init_params.presetGUID = NV_ENC_PRESET_DEFAULT_GUID;
encoder_init_params.version = NV_ENC_INITIALIZE_PARAMS_VER;
encoder_init_params.encodeWidth = opt->width;
encoder_init_params.encodeHeight = opt->height;
encoder_init_params.darWidth = opt->width;
encoder_init_params.darHeight = opt->height;
encoder_init_params.frameRateNum = opt->frame_rate.num;
encoder_init_params.frameRateDen = opt->frame_rate.den; this->frame_rate = opt->frame_rate;
encoder_init_params.enablePTD = 1;
encoder_init_params.maxEncodeWidth = opt->width;
encoder_init_params.maxEncodeHeight = opt->height;
NVENC_API_List.nvEncInitializeEncoder(nvencoder, &encoder_init_params);
其中,enablePTD这个参数表示是否开启picture type decision。如果开启,对于编码图片的类型(即我们之前所讲到的I帧B帧P帧这些类型),将有编码器为我们设定。
5.同步编码方式的具体步骤
首先,我们要准备输入。nvenc有两种准备输入的方式,一种是通过nvenc自带的接口来创建,即NvEncCreateInputBuffer。这种方式所准备的输入数据是存在于系统内存中的。由于我们采集到的纹理是在显存中,因此我们需要使用第二种方式。具体步骤如下:
注册输入资源
NV_ENC_REGISTER_RESOURCE resource = { 0 };
resource.version = NV_ENC_REGISTER_RESOURCE_VER;
resource.resourceType = NV_ENC_INPUT_RESOURCE_TYPE_DIRECTX;
resource.bufferFormat = NV_ENC_BUFFER_FORMAT_ARGB;
resource.bufferUsage = NV_ENC_INPUT_IMAGE;
resource.resourceToRegister = texture;
resource.width = width;
resource.height = height;
resource.pitch = 0;
其中,texture便是我们采集到的纹理,bufferusage我们的资源类型,这里应该是NV_ENC_INPUT_IMAGE。
输入资源映射
NV_ENC_MAP_INPUT_RESOURCE mapResource = { NV_ENC_MAP_INPUT_RESOURCE_VER };
mapResource.registeredResource = resource.registeredResource;
NVENC_API_List.nvEncMapInputResource(nvencoder, &mapResource);
创建比特流缓冲区
这一步的作用主要是给编码输出提供容器。代码如下:
NV_ENC_CREATE_BITSTREAM_BUFFER BitstreamBuffer = { NV_ENC_CREATE_BITSTREAM_BUFFER_VER };
NVENC_API_List.nvEncCreateBitstreamBuffer(nvencoder, &BitstreamBuffer);
准备图像参数,开始编码
NV_ENC_PIC_PARAMS pic_params = { 0 };
pic_params.version = NV_ENC_PIC_PARAMS_VER;
pic_params.pictureStruct = NV_ENC_PIC_STRUCT_FRAME;
pic_params.inputBuffer = mapResource.mappedResource;
pic_params.bufferFmt = NV_ENC_BUFFER_FORMAT_ARGB;
pic_params.inputWidth = 1920;
pic_params.inputHeight = 1080;
pic_params.outputBitstream = BitstreamBuffer.bitstreamBuffer;
pic_params.inputTimeStamp = timestamp;
NVENC_API_List.nvEncEncodePicture(nvencoder, &pic_params);
其中,NV_ENC_BUFFER_FORMAT_ARGB和我们采集所采集的纹理的像素格式是对应的,在dxgi中为DXGI_FORMAT_B8G8R8A8_UNORM,他们两都是一个格式的,虽然一个说是ARGB,一个说是什么B8G8R8A8,都代表着标准的RGB格式,即B为最低8位,依此下来是G、R、A。timestampe是我们的时间戳,由于我们是六十帧的,时间戳应该按照16毫秒这样递增,当然这个数字不是准确的,可以采用更为精确的方式进行表示。最终的编码输出会帮我们带上时间戳参数。
获取CPU访问锁,获取输出
编码的最后一步就是获取输出了。最终的输出会从显存拷贝到系统内存,在此期间我们要等待这个操作的完毕,即GPU与CPU之间的同步。在获得访问锁之后,我们应该对数据进行拷贝,然后再释放它。具体代码如下:
NV_ENC_LOCK_BITSTREAM lockBitstreamData = { NV_ENC_LOCK_BITSTREAM_VER };
lockBitstreamData.outputBitstream = BitstreamBuffer.bitstreamBuffer;
lockBitstreamData.doNotWait = 0;
NVENC_API_List.nvEncLockBitstream(nvencoder, &lockBitstreamData);
unsigned char* out_data = NULL;
int datasize = 0;
datasize = lockBitstreamData.bitstreamSizeInBytes;
out_data = (unsigned char*)malloc(datasize);
memcpy(out_data, lockBitstreamData.bitstreamBufferPtr, datasize);
lock的过程是阻塞的,这个需要注意。至此,整个编码过程就完成了。这一过程是同步的方式,下面我们讲一下异步的。
6.异步编码方式以及需要注意的点
异步编码方式是nvenc官方所推荐使用的方式,若想实现更为高效的时间利用,异步编码方式是一个很好的选择。下面我大致讲一下异步编码的步骤以及需要注意的点。
确保多线程环境的访问安全
异步编码方式需要在多线程环境下使用,因此,nvenc文档中第一点就提到,需要保证你的输入输出资源是能允许在多线程情况下进行访问且保证安全的。对于如何去保证,文档中没有具体说明(只提到到了D3D9的部分,D3D9设备在创建时应带上MULTITHREADED的标志位)。对于D3D11来说,默认情况下是允许多线程的,只需要开启多线程访问即可。在创建D3D11设备时,不要带上D3D11_CREATE_DEVICE_SINGLETHREADED的标志位。创建完设备之后,我们应该获取D3D11的多线程接口,并开启多线程访问,具体操作如下:
ID3D11Multithread *multithread;
p_d3dDevice->QueryInterface(IID_PPV_ARGS(&multithread))
multithread->SetMultithreadProtected(true);
初始化中开启异步模式
在编码器初始化参数中,应将enableEncodeAsync这个参数设为1,从而开启异步模式。
创建编码完成事件并进行注册
异步编码方式采用事件来通知用户编码的完成。对于事件的创建,我们可以调用CreateEvent来进行创建。创建完毕后,调用NvEncRegisterAsyncEvent来进行注册。事件处理完毕后,我们要对其进行销毁,调用NvEncUnregisterAsyncEvent即可。
编码前指定completion event
在开始编码之前,我们需要在输入图像参数中设置completion event,来指定当前的编码完成的事件。
创建等待完成线程,等待编码完成
我们应该创建另一条线程,来等待编码事件的完成。等待事件的完成可以调用WaitForSingleObject。需要注意的是,我们应该以调用编码API(NvEncEncodePicture)的顺序来等待事件,这样才能保证我们得出来的图片是按照显示顺序的。做这一步,主要也是为了考虑双向参考的B帧存在的情况,nvenc在编码的底层会帮我们处理好重排序的问题,让输出的图片以显示顺序来进行排序,而不是编码顺序。
异步编码方式具体要注意的点就这些,其他的步骤和同步的方式基本相同,照做即可。编码完成后,我们就可以将编码后的图像放入编码完成队列,然后用rtmp协议对其进行发送了。