基于Intel MediaSDK的低延迟编码实现

本文讲述了如何在拥有ArcA380独显的环境中,通过调整编码参数如使用I/P帧、HEVCtiledencoding、10bit编码和GPU直通等技术,降低视频编码的延迟,实现低延迟视频串流,适用于视频会议和游戏应用。
摘要由CSDN通过智能技术生成

以前做的都是追求最高吞吐率的编码,简单的说就是尽可能在最短的时间里把所有的帧都编码。方法就是在配置编码器的时候开多个frame buffer, 开始编码时先送多个帧数据进去,这样编码器编完一帧就可以立刻开始编下一帧,对应的mediasdk的例子sample_encode的参数是-async n (n表示要同时创建的frame buffer的数量)。

最近LD给了块高级货 Arc A380独显,要求实现一个GPU硬件加速的串流功能,串流就是尽可能快的把传过来的视频帧编码,再把编码码流传给业务层,通过网络发送到接收端去解码。对串流编码的需求就是追求编码的最低延迟,也就是编的越快越好。这种场景主要用作视频会议或者游戏的网络串流上,这样才不会造成开会的时候这边说一句,那边要过好几秒才有反应;或者游戏时候手柄的命令和看到的画面之间有延迟导致这边看着画面自己还没死,但是实际服务器那边你已经死了的情况。

关于低延迟编码

在网上搜了一下相关的文章,挖了个10年前的老坟Video Conferencing features of Intel Media Software Development Kit  大概介绍了一下这方面的一些功能的需求

低延迟编码可能会有这么几个需求

  1. 只编I帧或者I,P帧,不编B帧,因为B帧需要2个参考帧,一个前向一个后向,这样在获取当前帧以后最少要多等1帧的画面才能开始编码,会多造成15~30ms的延时。
  2. 对于网络问题造成的传输数据错误或者丢失,要能随时指定当前帧编码成I帧
  3. 编码过程中可以动态改变分辨率或者编码码率,以应对网络传输条件的变化
  4. 支持10bit编码,满足高画质的需求
  5. 支持SEI消息,里面可以放一些编码器这边要传给解码端的数据

下面参照文档来实现一个低延迟编码功能,原始程序用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帧了。这个延迟有点大。

只编码I,P帧(不要B帧)

对于编码器的设置,所有的设置参数都存放在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,有点爽了

HEVC编码时使用tiled encoding

考虑到现代硬件的并发性特点,从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<mfxExtBuffer*> 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  :)

强制当前帧编码为I帧

这个是基于每帧设置的,所以需要设置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帧了

设置SEI消息

这个也是基于每帧设置的,需要设置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)已经在里面了

支持RGB格式的输入

上篇文章通过设置编码器的参数,实现了基于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

更高的画质 10bit格式编码

前面做的都是YUV或者RGB 8bit的输入,考虑到有可能有RGB10bit的输入,顺便把10bit的编码也实现一下

其实也非常简单

  • 首先就是初始化的参数要调整一下,告诉编码器输入的是10bit的RGB像素,输出格式是HEVC 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);
  • 其次就是创建GPU frame buffer要创建成DXGI_FORMAT_R10G10B10A2_UNORM格式的。
//
// 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, 基本满足了老板的要求。收工 :)

最后还是老规矩,代码奉上,仅供参考

https://gitee.com/tisandman/low_latency_encoding

评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值