以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
本文将详细介绍博文第二季3:sample_venc.c的整体分析提及的“创建配置编码通道”。
我们首先介绍VENC模块相关的概念,然后绘制该模块的函数调用关系图谱,接着分析具体代码。
VENC模块,即视频编码模块。
本模块支持多路实时编码,且每路编码独立,即每路的编码协议和编码profile可以不同。
本模块也支持在视频编码时,调度Region模块对编码图像的内容进行叠加和遮挡。
VENC 模块的输入源包括三类:
(1)用户态读取图像文件向编码模块发送数据。
(2)VI模块采集的图像直接发送到编码模块。
(3)VI模块采集的图像经视频处理子系统(VPSS)发送到编码模块。(我们分析这类!)
不同型号的芯片支持不同的编码格式,由下表可知 HI3518E 支持H.264编码。
VENC模块的上下文如下图所示。
编码流程包括:输入图像的接收、图像内容的遮挡和覆盖、图像的编码、以及码流的输出等过程。
其中VENC模块包含图像接收通道、具体协议(H.264/H.265/JPEG/MJPEG)编码通道。
图像接收通道支持接收 YUV 格式的图像输入(H.264/H.265 只支持YUV420SP,JPEG/MJPEG 支持YUV420SP或YUV422SP),另外HI3518E也支持单Y分量的输入。图像接收通道接收外部的原始图像数据,而不关心图像数据是来自哪个外部模块。
图像接收通道接收到图像之后,会比较图像尺寸和编码通道尺寸:
如果输入图像比编码通道尺寸大,VENC模块将按照编码通道尺寸大小,调用 VGS 对源图像进行缩小,然后对缩小之后的图像进行编码。
如果输入图像比编码通道尺寸小,VENC模块将丢弃源图像,因为不支持放大输入图像编码。
如果输入图像与编码通道尺寸相当,VENC模块直接接收源图像,进行编码。
该模块支持对图像内容的遮挡和叠加。
具体协议编码通道的功能框图如下,从中可知具体编码通道由码率控制器、编码器协同完成,主要实现图像转化为编码码流的功能。这里的编码器,指的是狭义上的编码器,只完成编码功能。码率控制器,提供对编码参数的控制和调整,从而对输出码率进行控制。
对于 H.264、H.265、MJPEG 协议的编码通道,码率控制器提供 CBR(固定比特率)、VBR(可变比特率)、FIXQP(固定QP) 这三种码率控制模式,对图像质量和码率进行调节。
阅读本节内容前,先理解博文第6季1:H264编码原理与基本概念_天糊土的博客的内容。
编码码流帧配置支持两种模式:单包模式和多包模式。也就是说,我们可以将一个帧的数据作为一个码流包发送(即单包模式),也可以将一个帧搞成几个(H.264为4个)码流包进行发送(即多包模式)。注意这里说的多包,特指在不调用分包接口的情况下,也就是说不对一帧数据进行“子帧”划分,或者说,以一帧完整的图像数据作为单位的。
它们的示意图如下(以H.264为例)。
(1)多包模式。对于H.264,当为I帧时,调用HI_MPI_VENC_GetStream接口,一个I帧包含4个NAL包(4个NAL包分别为sps包、pps包、sei包、Islice包,这里假设pps包只有一个,且4个NAL包是独立的,包类型不同);对于JPEG,一帧图像包含2个包(1个图像参数包,1个图像数据包,2个包是独立的,包类型不同)。
(2)单包模式:对于H.264,当为I帧时,调用HI_MPI_VENC_GetStream接口,一个I帧包含1个NAL 包(该NAL包的包类型为Islice包,且包含sps、pps、sei、Islice的数据);对于JPEG,一帧图像只有1个包(该包的包类型为图像数据包,且包含图像参数包的数据)。
两种模式可通过 ko 加载时设置模块参数 OneStreamBuffer 来选择。OneStreamBuffer=1表示单包模式;OneStreamBuffer=0 表示多包模式,系统默认 OneStreamBuffer=0。
(接下来这段话描述的是调用分包接口的情形?)
当用户调用分包接口(例如HI_MPI_VENC_SetH264SliceSplit) 时,一帧会被分成多个slice,如果用户选择单包模式,对于 I 帧来说,该帧第一个 ISlice 包会包含 sps、pps、sei 的数据,该帧的其他 ISlice 则没有。即对于 H.264,sps、pps、sei 的数据只会出现在 I 帧的第一个 Islice 中(Islice中的字母I表示I帧),并合为 1 个包,且包类型为 ISlice;对于 JPEG/MJPEG 来说,图像参数包只会出现在一帧的第一个数据包中并合为 1 个包,且包类型为数据包。
根据上面的描述,案例里使用的是多包模式(没有设置这个参数,所以使用默认的)。
编码码流 buffer 配置支持两种模式:一般模式和省内存模式。
一般模式:考虑到超大帧的情况,码流 Buffer 大小配置的下限为:H264 和 H265是通道宽 x 通道高 x3/4,JPEG 和 MJPEG 是通道宽 x 通道高。
省内存模式:码流 Buffer 大小配置的下限是 32*1024 bytes,此模式需要用户保证码流 buffer 大小设置合理,否则会出现因码流 buffer 不足而不断重编或者丢帧的情况。
两种模式可通过 ko 加载时设置相应的模块参数来选择。模块参数值为 1 表示省内存模式,模块参数值 0 表示一般模式。
hi35xx_h264e.ko 模块参数:H264eMiniBufMode。
hi35xx_h265e.ko 模块参数:H265eMiniBufMode。
hi35xx_jpege.ko 模块参数:JpegeMiniBufMode。
注意VENC模块也有“设备”“通道”的概念。这里的“设备”即VENC模块这个硬件单元,只有一个,编号为0。这个VENC设备的内部也有几个通道(对应着不同协议),将来分别与VPSS模块的通道进行绑定。我们的案例里VENC设备具有3个通道,分别与VPSS模块的3个通道进行绑定。
VENC模块的函数调用关系如下:
SAMPLE_COMM_VENC_Start
SAMPLE_COMM_SYS_GetPicSize
HI_MPI_VENC_CreateChn//创建编码通道
HI_MPI_VENC_StartRecvPic//接收图片并开始编码
SAMPLE_COMM_VENC_BindVpss
HI_MPI_SYS_Bind//绑定VPSS和VENC
由此可知,该模块涉及以下几个步骤:
开启VENC模块(先创建编码通道,然后接收图片并开始编码)
绑定VPSS的通道到编码通道
下面我们将详细介绍这几个步骤涉及到的概念与代码细节。
VENC模块的代码与分析如下。由于三路编码通道的操作基本相同,我们分析通道0即可。
/******************************************
step 5: start stream venc
******************************************/
/*** HD1080P **/
printf("\t c) cbr.\n");
printf("\t v) vbr.\n");
printf("\t f) fixQp\n");
printf("please input choose rc mode!\n");
c = (char)getchar();
switch(c)//首先选择码率控制模式,c表示固定码率,v表示可变码率,f表示固定QP
{
case 'c':
enRcMode = SAMPLE_RC_CBR;
break;
case 'v':
enRcMode = SAMPLE_RC_VBR;
break;
case 'f':
enRcMode = SAMPLE_RC_FIXQP;
break;
default:
printf("rc mode! is invaild!\n");
goto END_VENC_1080P_CLASSIC_4;
}
/*** enSize[0] **/
if(s32ChnNum >= 1)//开始对编码通道0进行编码
{
VpssGrp = 0;//
VpssChn = 0;//将来绑定VPSS模块的GROUP0中的通道0
VencChn = 0;
//这个函数我们重点分析
s32Ret = SAMPLE_COMM_VENC_Start(VencChn, enPayLoad[0],\
gs_enNorm, enSize[0], enRcMode,u32Profile);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
//将VENC模块的通道0,与VPSS模块的GROUP0中的通道0,进行绑定
s32Ret = SAMPLE_COMM_VENC_BindVpss(VencChn, VpssGrp, VpssChn);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
}
/*** enSize[1] **/
if(s32ChnNum >= 2)
{
VpssChn = 1;
VencChn = 1;
s32Ret = SAMPLE_COMM_VENC_Start(VencChn, enPayLoad[1], \
gs_enNorm, enSize[1], enRcMode,u32Profile);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
s32Ret = SAMPLE_COMM_VENC_BindVpss(VencChn, VpssGrp, VpssChn);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
}
/*** enSize[2] **/
if(s32ChnNum >= 3)
{
VpssChn = 2;
VencChn = 2;
s32Ret = SAMPLE_COMM_VENC_Start(VencChn, enPayLoad[2], \
gs_enNorm, enSize[2], enRcMode,u32Profile);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
s32Ret = SAMPLE_COMM_VENC_BindVpss(VencChn, VpssGrp, VpssChn);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
}
参数1,表示VENC模块的某个通道。参数2,表示编码格式(是指VENC对视频流的编码压缩方式,并非指图像像素格式YUV这种概念。案例里三路编码通道的编码格式居然都是H264)。参数3,表示图像制式(这里是NTSC)。参数4,表示图像分辨率(枚举变量而已,但对应着具体的分辨率)。参数5,表示码率控制模式(即CBR\VBR\FIXQP)。参数6,表示profle(啥意思)。
该函数内部的流程如下:
(1)首先调用SAMPLE_COMM_SYS_GetPicSize函数获取图像的分辨率(因为需要将这些分辨率信息赋值给某些结构体的成员);
(2)然后switch语句判断参数2(即判断是哪种编码格式),由于参数2是PT_H264,因此接下来填充结构体变量stH264Attr的成员(这个结构体变量包含着H264协议编码通道的属性或者说设置信息)。
(3)接下来根据码率控制模式(我们输入的是“c”或者“v”或者“f”对应着CBR\VBR\FIXQP),来填充结构体变量stH264Cbr或stH264FixQp或stH264Vbr,以及结构体变量stVencChnAttr的成员(具体成员的含义有时间可以看看)。
(4)然后调用HI_MPI_VENC_CreateChn函数来创建通道,传入的参数1是VENC模块的某个通道,参数2是(3)中的结构体变量stVencChnAttr。
(5)最后调用HI_MPI_VENC_StartRecvPic函数,开始进行图像的接收与编码。
这个函数内容与分析如下:
/*****************************************
* function : venc bind vpss
*******************************************/
HI_S32 SAMPLE_COMM_VENC_BindVpss(VENC_CHN VeChn,VPSS_GRP VpssGrp,VPSS_CHN VpssChn)
{
HI_S32 s32Ret = HI_SUCCESS;
MPP_CHN_S stSrcChn;
MPP_CHN_S stDestChn;
//绑定关系中,源头的相关信息
stSrcChn.enModId = HI_ID_VPSS;//HI3518E中的哪个硬件单元,这里是VPSS模块硬件单元
stSrcChn.s32DevId = VpssGrp;//VPSS模块中的哪个GROUP,这里VpssGrp=0
stSrcChn.s32ChnId = VpssChn;//GROUP0中的哪个通道,这里是通道VpssChn
//绑定关系中,接收方的相关信息
stDestChn.enModId = HI_ID_VENC;/HI3518E中的哪个硬件单元,这里是VENC模块硬件单元
stDestChn.s32DevId = 0;//VENC模块中的哪个设备,这里是设备0
stDestChn.s32ChnId = VeChn;//设备0中的哪个通道,这里是通道VeChn
s32Ret = HI_MPI_SYS_Bind(&stSrcChn, &stDestChn);
if (s32Ret != HI_SUCCESS)
{
SAMPLE_PRT("failed with %#x!\n", s32Ret);
return HI_FAILURE;
}
return s32Ret;
}