1. 介绍
webrtc全称为web Real-time communication,使用此技术可以为应用添加基于开发标准的实时通信能力。它支持在设备之间发送音视频和通用数据,为用户提供强大的音视频通信解决方案。
2. 背景
webrtc windows端在进行桌面或窗口共享时,由于采集出的分辨率较大,如果仍然使用软件编码,会对机器带来较大的压力,同时引入了更多的编码延时。为此我们对webrtc编码部分进行了补充,添加了硬件加速编码,同时适配了部分机器不支持硬件编码方案或硬编失败时,自动回退到软件编码,提高产品的稳定性。
本章主要简单分享编码部分的一些优化及问题。
3. 主要工作
3.1 选择硬件编码方案
支持D3D11硬件加速的编码器没有跨平台的,不同GPU下的编码方式不同。所以如果使用调用各个GPU编码器API进行编码的话,工作量很大,由于资源有限,所以采用了ffmpeg的方案进行硬件加速。
在选择使用ffmpeg作为硬件编码方案后,但同时引入了新的问题:
- 由于webrtc自带ffmpeg源码,如果再使用新的ffmpeg库会带来包的冗余,也不利于维护。因此需要修改webrtc支持ffmpeg的硬件编码。
- NVIDIA的编码器硬件加速默认使用的D3D11纹理,但Intel的QSV使用的是qsv纹理,需要进行纹理的映射转换。
3.2 添加硬件编码器
创建硬件编码器工厂,添加硬件编码器,将编码器工厂传递给webrtc的CreatePeerConnectionFactory,此过程不再细说。重点说一下ffmpeg初始化硬件设备上下文部分。
参考ffmpeg的vaapi_encode.c中的set_hwframe_ctx方法,在创建设备上下文时,直接调用av_hwdevice_ctx_create
方法。但我们的帧数据是D3D11纹理,和ffmpeg中的创建编码器纹理的上下文不同,如果使用av_hwdevice_ctx_create
直接调用,那么就需要使用共享纹理去处理,为了不引入纹理间更多了操作,采用了自定义创建设备上下文的方式。
可以详细看一下av_hwdevice_ctx_create
的实现,三个重要的方法av_hwdevice_ctx_alloc、device_ctx->internal->hw_type->device_create和av_hwdevice_ctx_init,下面我们自定义实现设备上下文的创建:
// Create D3D11 Hardware Device Context.
hwDeviceCtx.reset(av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_D3D11VA));
if (hwDeviceCtx == nullptr) {
RTC_LOG(LS_ERROR) << "Failed to alloc hardware device context.";
ret = WEBRTC_VIDEO_CODEC_MEMORY;
break;
}
auto deviceCtxData = (AVHWDeviceContext*)hwDeviceCtx->data;
if (deviceCtxData == nullptr) {
RTC_LOG(LS_ERROR) << "Failed to get hardware device context.";
ret = WEBRTC_VIDEO_CODEC_ERROR;
break;
}
auto d3d11DevCtx = (AVD3D11VADeviceContext*)deviceCtxData->hwctx;
if (d3d11DevCtx == nullptr) {
RTC_LOG(LS_ERROR) << "Failed to get d3d device context.";
ret = WEBRTC_VIDEO_CODEC_ERROR;
break;
}
memset(d3d11DevCtx, 0, sizeof(AVD3D11VADeviceContext));
// bug: avcodec_free_context will release the d3device.
d3device->AddRef();
d3d11DevCtx->device = d3device.Get();
auto doNothing = [](void*) {};
d3d11DevCtx->lock_ctx = (void*)1;
d3d11DevCtx->lock = doNothing;
d3d11DevCtx->unlock = doNothing;
auto err = av_hwdevice_ctx_init(hwDeviceCtx.get());
if (err < 0) {
RTC_LOG(LS_ERROR) << "Failed to init hardware device." << err;
ret = WEBRTC_VIDEO_CODEC_ERROR;
break;
}
需要注意两个地方:
- 传入的D3D设备,需要增加引用计数,ffmpeg在退出时会自动将引用计数减一,如果没有增加引用计数,会导致D3D设备被释放。
- QSV的设备类型是AV_HWDEVICE_TYPE_QSV,NVIDIA的设备类型为AV_HWDEVICE_TYPE_D3D11VA,AMD的设备类型也是AV_HWDEVICE_TYPE_D3D11VA,所以针对QSV需要进行设备派生映射
// 在创建D3D11VA设备上下文后(即上面代码后),添加如下代码
if (avHwDeviceType == AV_HWDEVICE_TYPE_QSV) {
// create derived hardware context.
AVBufferRef* avHwDevCtxDerived = nullptr;
auto err = av_hwdevice_ctx_create_derived(
&avHwDevCtxDerived, avHwDeviceType, hwDeviceCtx.get(), 0);
if (err < 0) {
RTC_LOG(LS_ERROR) << "Failed to create derived hardware context.";
ret = WEBRTC_VIDEO_CODEC_MEMORY;
break;
}
hwDeviceCtx.reset(avHwDevCtxDerived);
}
3.3 硬件编码
通过3.2创建硬件设备上下文后,就可以出创建该硬件上下文的视频帧,然后需要将采集到的源纹理数据传递给视频帧。
设备类型是AV_HWDEVICE_TYPE_D3D11VA的,帧类型为AV_PIX_FMT_D3D11;而AV_HWDEVICE_TYPE_QSV设备的帧类型为AV_PIX_FMT_QSV。
因此NVIDIA和AMD设备上的帧数据传递,只需要调用D3D11纹理拷贝即可。
而Intel设备的帧数据需要进行纹理映射,将视频帧的QSV纹理数据映射为D3D11纹理,然后将源数据数据拷贝即可。
// If this is a frame from a derived context,
// we'll need to map it to D3D11
ComPtr<ID3D11Texture2D> dstTexture = nullptr;
if (avHWFrame->format != AV_PIX_FMT_D3D11) {
ScopedAVFrame d3d11Frame(av_frame_alloc());
d3d11Frame->format = AV_PIX_FMT_D3D11;
int err = av_hwframe_map(d3d11Frame.get(), avHWFrame.get(),
AV_HWFRAME_MAP_WRITE | AV_HWFRAME_MAP_OVERWRITE);
if (err < 0) {
RTC_LOG(LS_ERROR) << "Failed to map D3D11 frame.";
return WEBRTC_VIDEO_CODEC_ERROR;
}
// Get the texture from the mapped frame
dstTexture = (ID3D11Texture2D*)d3d11Frame->data[0];
} else {
// Otherwise, we can just use the texture inside the original frame
dstTexture = (ID3D11Texture2D*)avHWFrame->data[0];
}
数据准备完成,ffmpeg编码部分就大同小异了。
4 总结
本章主要简单分享了windows下不同GPU下硬解加速的实现,仍然还有许多问题需要解决,比如采集到的纹理数据是BGRA的,但是编码器只能使用nv12格式,因此需要将bgra纹理转化为nv12。还有为了编码器支持vbr模式,不同的GPU下编码器的配置方案是不同的,这个需要关注。再有就是webrtc需要在编码中根据网络质量动态调节编码器目标码率,而有的ffmpeg版本过低的话,编码器是不支持动态调节的;等等这些一些问题都需要我们就去修改测试适配。