以前做的都是追求最高吞吐率的编码,简单的说就是尽可能在最短的时间里把所有的帧都编码。方法就是在配置编码器的时候开多个frame buffer, 开始编码时先送多个帧数据进去,这样编码器编完一帧就可以立刻开始编下一帧,对应的mediasdk的例子sample_encode的参数是-async n (n表示要同时创建的frame buffer的数量)。
最近LD给了块高级货 Arc A380独显,要求实现一个GPU硬件加速的串流功能,串流就是尽可能快的把传过来的视频帧编码,再把编码码流传给业务层,通过网络发送到接收端去解码。对串流编码的需求就是追求编码的最低延迟,也就是编的越快越好。这种场景主要用作视频会议或者游戏的网络串流上,这样才不会造成开会的时候这边说一句,那边要过好几秒才有反应;或者游戏时候手柄的命令和看到的画面之间有延迟导致这边看着画面自己还没死,但是实际服务器那边你已经死了的情况。
在网上搜了一下相关的文章,挖了个10年前的老坟Video Conferencing features of Intel Media Software Development Kit 大概介绍了一下这方面的一些功能的需求
低延迟编码可能会有这么几个需求
下面参照文档来实现一个低延迟编码功能,原始程序用mediasdk github上的tutorial simple_6_encode_vmem_lowlatency 这个示例程序是个单线程的程序,基本就是一个main函数走到底,可读性比sample_encode强很多,可以很容易的加一些代码进去看看效果。而且例程里frame buffer开在了vmem显存里,编码效率很高,省去了很多改写优化工作。
整个程序的流程是这样的
先给原始代码增加指定独显集显编码和基于NV12文件编码功能,运行一下
simple_encode.exe -hw -dGfx -g 3840x2160 -b 40000 -f 30/1 --measure-latency jellyfish-4k-nv12.yuv output.h264
得到输出
可以看到是用A380做的编码,原始程序是包含B帧的,每帧编码平均耗时92ms , 一帧编码最长耗时264ms(最大耗时发生在第一帧编码,后面就快很多), 最短耗时20ms。
总编码速度是88fps, 但是每帧92ms的延时确实有点长,即使是30fps的显示速度,每帧送进去编码,等拿到数据,屏幕上已经显示3帧了。这个延迟有点大。
对于编码器的设置,所有的设置参数都存放在mfxEncParams里
- //7. Initialize the Media SDK encoder
- sts = mfxENC.Init(&mfxEncParams);
所以要做的的就是给mfxEncParams设置以下参数
- mfxEncParams.AsyncDepth = 1;
- mfxEncParams.mfx.GopRefDist = 1;
- mfxEncParams.mfx.NumRefFrame = 1;
运行一下,
不要B帧的话,虽然编码速度降到了45fps, 但是每帧编码的延迟降到了11.9ms,有点爽了
考虑到现代硬件的并发性特点,从H265标准开始,标准增加了tiled encoding的部分。既允许把一帧画面分成几个部分,发给多个硬件并行编码。
对于MediaSDK的初始化函数来说,传进去的mfxEncParams.mfx这个mfxInfoMFX结构体本身大小也有限,所以只能设置一些很常用的参数。对于其他的设置,则需要用到mfxEncParams.ExtParam这个扩展设置。设置tiled encoding需要设置mfxExtHEVCTiles
- //HEVC编码使用tiled encoding选项,使用双路硬件编码器
- mfxExtHEVCTiles extendedHEVCTiles;
- memset(&extendedHEVCTiles, 0, sizeof(extendedHEVCTiles));
- extendedHEVCTiles.Header.BufferId = MFX_EXTBUFF_HEVC_TILES;
- extendedHEVCTiles.Header.BufferSz = sizeof(extendedHEVCTiles);
- extendedHEVCTiles.NumTileRows = 1;
- extendedHEVCTiles.NumTileColumns = 2; //设立设置1 row, 2 col
-
-
- std::vector
m_EncExtParams; - m_EncExtParams.push_back((mfxExtBuffer *)&extendedHEVCTiles);
- mfxEncParams.ExtParam = &m_EncExtParams[0];
- mfxEncParams.NumExtParam = m_EncExtParams.size();
-
-
- ...
-
-
- //7. Initialize the Media SDK encoder
- sts = mfxENC.Init(&mfxEncParams);
运行一下
每帧编码的延迟 11.9ms -> 7.1ms,这个就很爽了
CodecId这里可以设置AVC,HEVC等编码格式
TargetUsage设置个人感觉是设置编码算法的复杂度,对性能影响很大,设为MFX_TARGETUSAGE_BEST_SPEED
或者MFX_TARGETUSAGE_BALANCED
最好, 设置成MFX_TARGETUSAGE_BEST_QUALITY
会慢很多
- //3. Initialize encoder parameters
- // - In this example we are encoding an AVC (H.264) stream
- mfxVideoParam mfxEncParams;
- memset(&mfxEncParams, 0, sizeof(mfxEncParams));
- //设置H264还是H265编码
- mfxEncParams.mfx.CodecId = MFX_CODEC_HEVC; // MFX_CODEC_AVC;
-
- //编码画质的设置, best_speed编码速度最快,如果画质可以接受可以设这个,否则可以设置MFX_TARGETUSAGE_BALANCED
- mfxEncParams.mfx.TargetUsage = MFX_TARGETUSAGE_BEST_SPEED; //MFX_TARGETUSAGE_BALANCED;
测试一下,
MFX_TARGETUSAGE_QUALITY
MFX_TARGETUSAGE_BALANCED
MFX_TARGETUSAGE_SPEED
如果画质能够接受的话(编码码率设置的足够大,比如测试用的40Mbps, 很多人肉眼是看不出balanced和speed模式的区别的),使用MFX_TARGETUSAGE_SPEED, 可以进一步的把7.1ms缩短到 6ms :)
这个是基于每帧设置的,所以需要设置mfxEncodeCtrl,这个参数要在每帧编码的时候,调用EncodeFrameAsync()传进去。
修改EncodeFrameAsync()的第一个参数,原始代码传进去的是NULL
- // Encode a frame asychronously (returns immediately)
- sts = mfxENC.EncodeFrameAsync(NULL, &pmfxSurfaces[nEncSurfIdx], &mfxBS, &syncp);
需要改为
- mfxEncodeCtrl EncodeCtrl;
- memset(&EncodeCtrl, 0, sizeof(mfxEncodeCtrl));
-
- ...
-
- //把当前帧编码成I frame
- EncodeCtrl.FrameType =
- MFX_FRAMETYPE_I | MFX_FRAMETYPE_REF | MFX_FRAMETYPE_IDR;
-
- // Encode a frame asychronously (returns immediately)
- sts = mfxENC.EncodeFrameAsync(&EncodeCtrl, pmfxSurfaces[nEncSurfIdx], &mfxBS, &syncp);
验证一下, 强制第3帧和第6帧为I帧
- frame_count++;
- if ((frame_count % 3) == 0)
- { //这里强制IDR的参数有点不一样,264要设MFX_FRAMETYPE_I, HEVC要设MFX_FRAMETYPE_IDR
- EncodeCtrl.FrameType = MFX_FRAMETYPE_I | MFX_FRAMETYPE_REF | MFX_FRAMETYPE_IDR;
- }
-
- // Encode a frame asychronously (returns immediately)
- sts = mfxENC.EncodeFrameAsync(&EncodeCtrl, pmfxSurfaces[nEncSurfIdx], &mfxBS, &syncp);
运行
第3和第6帧和第一帧一样,都变成IDR帧了
这个也是基于每帧设置的,需要设置mfxEncodeCtrl,
- //测试SEI
- char m_seiData[100];
- mfxPayload m_mySEIPayload;
- memset(&m_mySEIPayload, 0, sizeof(mfxPayload));
-
- int i;
- for (i = 0; i < 16; i++)
- {
- //set 16byte UUID 0x00 01 02 03 04 05 ... 0f
- m_seiData[i] = i;
- }
- sprintf(m_seiData+16,"frame counter = %08d\n", frame_count);
-
- m_mySEIPayload.Type = 5; //user data unregister
- m_mySEIPayload.BufSize = 0x29;
- m_mySEIPayload.NumBit = m_mySEIPayload.BufSize * 8;
- m_mySEIPayload.Data = (mfxU8 *)m_seiData;
- mfxPayload * m_payloads[1];
- m_payloads[0] = &m_mySEIPayload;
-
- EncodeCtrl.Payload = (mfxPayload **)&m_payloads[0];
- EncodeCtrl.NumPayload = 1;
-
- // Encode a frame asychronously (returns immediately)
- sts = mfxENC.EncodeFrameAsync(&EncodeCtrl, pmfxSurfaces[nEncSurfIdx], &mfxBS, &syncp);
看一下码流里的SEI信息
SEI信息 UUID 0x00 01 02 03 ... 0E 0F 和字符串frame count = %08d (0x 66 72 61 ... 30 30 32)已经在里面了
上篇文章通过设置编码器的参数,实现了基于NV12输入的低延迟编码,将单帧4K分辨率的图像编码时间压缩到了6~7ms左右。通常串流传过来的图像是从3D渲染引擎那边传过来的RGB图像,所以接下来实现RGB输入。
参考MediaSDK sample_encode的方法,需要把RGB的输入通过调用ID3D11VideoContext::VideoProcessorBlt()转换成NV12的格式,然后再传给EncodeFrameAsync()做编码。这个函数是借助显卡里的VPP模块做颜色转换和图像的缩放。
参考我以前的代码,加入这个颜色转换程序以后,从RGB图像输入到获取到编码码流的时间增加到了10ms左右
用GPUView分析了一下GPU的工作时间
颜色空间的转换走的是video processing模块,硬件用了3.8ms左右,接着video decode做编码,用了5.7ms左右。 video processing用的时间每次都比较固定。
这部分的优化咨询了一下大佬,从Intel的Gen12架构开始的GPU已经开始支持RGB格式的帧直接输入了,这部分颜色转换会在编码器video decode模块里做,效率比video processing更高。测试了一下, 把帧buffer的格式改为DXGI_FORMAT_B8G8R8A8_UNORM的格式( 只支持BGR格式, 不支持RGB格式) 直接送去编码,初始化输入格式的参数需要做如下修改:
- //修改这里,直接让encoder接收RGBA的数据,从Gen12架构开始支持RGBA帧的直接输入
- mfxEncParams.mfx.FrameInfo.FourCC = MFX_FOURCC_RGB4; // MFX_FOURCC_NV12;
-
- mfxEncParams.mfx.FrameInfo.ChromaFormat = MFX_CHROMAFORMAT_YUV444; // MFX_CHROMAFORMAT_YUV420;
- mfxEncParams.mfx.FrameInfo.PicStruct = MFX_PICSTRUCT_PROGRESSIVE;
GPUView的数据如下
用video decoding做色彩空间转换的编码的速度比走video processing+video decoding的方式可以省2ms
前面做的都是YUV或者RGB 8bit的输入,考虑到有可能有RGB10bit的输入,顺便把10bit的编码也实现一下
其实也非常简单
- mfxEncParams.mfx.CodecProfile = MFX_PROFILE_HEVC_MAIN10; //Parameter for HEVC 10bit
- mfxEncParams.mfx.CodecLevel = MFX_LEVEL_HEVC_51; //Parameter for HEVC 10bit
- mfxEncParams.mfx.FrameInfo.FourCC = MFX_FOURCC_A2RGB10; // MFX_FOURCC_NV12;
- mfxEncParams.mfx.FrameInfo.BitDepthChroma = 10;
- mfxEncParams.mfx.FrameInfo.BitDepthLuma = 10;
-
- ...
-
- //7. Initialize the Media SDK encoder
- sts = mfxENC.Init(&mfxEncParams);
- //
- // Intel Media SDK memory allocator entrypoints....
- //
- mfxStatus _simple_alloc(mfxFrameAllocRequest* request, mfxFrameAllocResponse* response)
- {
- HRESULT hRes;
-
- // Determine surface format
- DXGI_FORMAT format;
- if (MFX_FOURCC_NV12 == request->Info.FourCC)
- format = DXGI_FORMAT_NV12;
- else if (MFX_FOURCC_RGB4 == request->Info.FourCC)
- format = DXGI_FORMAT_B8G8R8A8_UNORM;
- else if (MFX_FOURCC_YUY2== request->Info.FourCC)
- format = DXGI_FORMAT_YUY2;
- else if (MFX_FOURCC_P8 == request->Info.FourCC ) //|| MFX_FOURCC_P8_TEXTURE == request->Info.FourCC
- format = DXGI_FORMAT_P8;
- else if (MFX_FOURCC_A2RGB10 == request->Info.FourCC)
- format = DXGI_FORMAT_R10G10B10A2_UNORM;
- else
- format = DXGI_FORMAT_UNKNOWN;
-
- ...
运行一下
simple_encode.exe -hw -dGfx -g 3840x2160x10 -b 40000 -f 30/1 --measure-latency jellyfish-4k-uhd-hevc-10bit-gbrp10le.rgb output.h265
10bit编码竟然没有增加太多的编码时间, Good :)
到这里,这个低延迟编码的功能就实现的差不多了,总体来说,可以把编码的延迟做到9~10ms, 基本满足了老板的要求。收工 :)
最后还是老规矩,代码奉上,仅供参考