1.简介
VENC 模块,即视频编码模块。本模块支持多路实时编码,且每路编码独立,编码协议和编码 profile 可以不同。本模块支持视频编码同时,调度 Region 模块对编码图像内容进行叠加和遮挡。
VENC 模块的输入源包括三类:
- 用户态读取图像文件向编码模块发送数据;
- 视频输入(VIU)模块采集的图像经视频处理子系统(VPSS)发送到编码模块;
- 视频输入(VIU)模块采集的图像直接发送到编码模块
不同型号的芯片支持不同的编码规格,芯片支持的编码规格如表所示:
2 功能描述
2.1 编码数据流程图
典型的编码流程包括了输入图像的接收、图像内容的遮挡和覆盖、图像的编码、以及码流的输出等过程。
VENC 模块由编码通道子模块(VENC)和编码协议子模块(H.264/H.265/JPEG/MJPEG)组成。
通道支持接收 YUV 格式图像输入,支持格式为 Semi-planar YUV 4:2:0 或 Semi-planarYUV 4:2:2。
H.264/H.265 只支持 Semi-planar YUV 4:2:0
JPEG/MJPEG 支持 Semiplanar YUV 4:2:0 或 Semi-planar YUV 4:2:2
另外, Hi3518EV200 能够支持单分量输入(只存在 Y 分量)。通道模块接收外部原始图像数据,而不关心图像数据是来自哪个外
部模块。
通道接收到图像之后,比较图像尺寸和编码通道尺寸:
如果输入图像比编码通道尺寸大, VENC 将按照编码通道尺寸大小,调用 VGS 对源图像进行缩小,然后对缩小之后的图像进行编码。
如果输入图像比编码通道尺寸小, VENC 丢弃源图像。 VENC 不支持放大输入图像编码。
如果输入图像与编码通道尺寸相当, VENC 直接接受源图像,进行编码。
2.2 编码通道
编码通道作为基本容器,保存编码通道的多种用户设置和管理编码通道的多种内部资源。编码通道完成图像转化为码流的功能,具体由码率控制器和编码器协同完成。这里的编码器指的是狭义上的编码器,只完成编码功能。码率控制器提供了对编码参数的控制和调整,从而对输出码率进行控制。
2.3 码率控制
码率控制器实现对编码码率进行控制。
从信息学的角度分析,图像的压缩比越低,压缩图像的质量越高;图像压缩比例越高,压缩图像的质量越低。对于场景变化的真实场景,图像质量稳定,编码码率会波动;编码码率稳定,图像质量会波动。以 H.264 编码为例,通常图像 Qp 越低,图像的质量越好,码率越高;图像 Qp 越高,图像质量越差,码率越低。
码率控制是针对连续的编码码流而言,所以, JPEG 协议编码通道不包括码率控制功能。
码率控制器分别提供了对 H.264\H.265\MJPEG 协议编码通道 CBR、 VBR、 FIXQP 等三种码率控制模式,对图像质量和码率进行调节。
CBR
CBR(Constant Bit Rate)固定比特率。即在码率统计时间内保证编码码率平稳。码率稳定主要由两个量来评估,这两个量都可以由用户在创建编码通道时指定。
码率统计时间 u32StatTime
单位为秒(s),码率统计时间越长,每帧图像的码率波动对于码率调节的影响越弱,码率的调节会更缓慢,图像质量的波动会更轻微;码率统计时间越短,每帧图像的码率波动对于码率调节的影响越强,图像码率的调节会更灵敏,图像质量的波动会更剧烈。
行级码率控制调节幅度 u32RowQpDelta
行级码率控制调节幅度是一帧内行级调节的最大范围,其中行级以宏块行为单位。调节幅度越大,允许行级调整的 QP 范围越大,码率越平稳。对于图像复杂度分布不均匀的场景,行级码率控制调节幅度设置过大会带来图像质量不均匀。
VBR
VBR(Variable Bit Rate)可变比特率,即允许在码率统计时间内编码码率波动,从而保证编码图像质量平稳。以 H.264 编码为例, VENC 模块提供用户可设置 MaxQp,MinQp, MaxBitrate 和 ChangePos。 MaxQp, MinQp 用于控制图像的质量范围,MaxBitrate 用于钳位码率统计时间内的最大编码码率, ChangePos 用于控制开始调整Qp 的码率基准线。当编码码率大于 MaxBitrate*ChangePos 时,图像 qp 会逐步向MaxQp 调整,如果图像 QP 达到 MaxQp, QP 会被钳位到最大值, MaxBitrate 的钳位效果失效,编码码率有可能会超出 MaxBitrate。当编码码率小于 MaxBitrate*ChangePos时,图像 QP 会逐步向 MinQp 调整,如果图像 QP 达到 MinQp,此时编码的码率已经达到最大值,而且图像质量最好。
FIXQP
Fix Qp 固定 Qp 值。在码率统计时间内,编码图像所有宏块 Qp 值相同,采用用户设定的图像 Qp 值, I 帧和 P 帧的 QP 值可以分别设置。
三.函数图谱
SAMPLE_COMM_VENC_Start(step5)//start stream venc
SAMPLE_COMM_SYS_GetPicSize
HI_MPI_VENC_CreateChn
HI_MPI_VENC_StartRecvPic
SAMPLE_COMM_VENC_BindVpss
HI_MPI_SYS_Bind
SAMPLE_COMM_VENC_StartGetStream(step6)//stream venc process -- get stream, then save it to file.
pthread_create(&gs_VencPid, 0, SAMPLE_COMM_VENC_GetVencStreamProc, (HI_VOID*)&gs_stPara);
SAMPLE_COMM_VENC_StopGetStream(step7)//exit process
四.编码器设置
1.venc编码通道设置HI_MPI_VENC_CreatChn()
2开启HI_MPI_VENC_StartRecPic()
3.绑定来源 VPSS通道-->编码通道。
VENC_CHN_ATTR_S stVencChnAttr;
VENC_ATTR_H264_S stH264Attr;
VIDEO_NORM_E enNorm = VIDEO_ENCODING_MODE_NTSC;
VENC_ATTR_H264_CBR_S stH264Cbr; //定义 H.264 编码通道 CBR 属性结构
SAMPLE_RC_E enRcMode = SAMPLE_RC_CBR;
VpssGrp = 0;
VpssChn = 0;
VencChn = 0;
SIZE_S stPicSize;
stPicSize->u32Width = 1280;
stPicSize->u32Height = 720;
/******************************************
*step 1: Create Venc Channel
******************************************/
stVencChnAttr.stVeAttr.enType = enType;
stH264Attr.u32MaxPicWidth = stPicSize.u32Width; //编码图像最大宽度
stH264Attr.u32MaxPicHeight = stPicSize.u32Height;
stH264Attr.u32PicWidth = stPicSize.u32Width; //编码图像宽度
stH264Attr.u32PicHeight = stPicSize.u32Height;
stH264Attr.u32BufSize = stPicSize.u32Width * stPicSize.u32Height;//码流 buffer 大小
stH264Attr.u32Profile = u32Profile;/*视频等级0: baseline; 1:MP; 2:HP; 3:svc_t */
stH264Attr.bByFrame = HI_TRUE;/*帧/包模式获取码流 (1:按帧获取 0:按包获取)*/
stH264Attr.u32BFrameNum = 0;/*编码支持 B 帧的个数 0: not support B frame; >=1: number of B frames */
stH264Attr.u32RefNum = 1;/*编码支持参考帧的个数 0: default; number of refrence frame*/
memcpy(&stVencChnAttr.stVeAttr.stAttrH264e, &stH264Attr, sizeof(VENC_ATTR_H264_S));
stVencChnAttr.stRcAttr.enRcMode = VENC_RC_MODE_H264CBR;
stH264Cbr.u32Gop = (VIDEO_ENCODING_MODE_PAL== enNorm)?25:30;//H.264 gop 值
stH264Cbr.u32StatTime = 1;//CBR 码率统计时间,以秒为单位
stH264Cbr.u32SrcFrmRate = (VIDEO_ENCODING_MODE_PAL== enNorm)?25:30;//VI 输入帧率,以 fps 为单位
stH264Cbr.fr32DstFrmRate = (VIDEO_ENCODING_MODE_PAL== enNorm)?25:30;//编码器输出帧率,以 fps 为单位
stH264Cbr.u32BitRate = 1024*2;//平均 bitrate,以 kbps 为单位
stH264Cbr.u32FluctuateLevel = 0; //最大码率相对平均码率的波动等级
memcpy(&stVencChnAttr.stRcAttr.stAttrH264Cbr, &stH264Cbr, sizeof(VENC_ATTR_H264_CBR_S));
s32Ret = HI_MPI_VENC_CreateChn(VencChn, &stVencChnAttr);//创建编码通道
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_CreateChn [%d] faild with %#x!\n",VencChn, s32Ret);
return s32Ret;
}
/******************************************
*step 2: 开启编码通道接收输入图像
******************************************/
s32Ret = HI_MPI_VENC_StartRecvPic(VencChn);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_StartRecvPic faild with%#x!\n", s32Ret);
return HI_FAILURE;
}
/******************************************
*step3 : 绑定VPSS通道和编码通道
******************************************/
s32Ret = SAMPLE_COMM_VENC_BindVpss(VencChn, VpssGrp, VpssChn);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("Start Venc failed!\n");
goto END_VENC_1080P_CLASSIC_5;
}
第五步:开始获取视频流并并存储到文件
整个系统我们就设置好了,下一步要操作地是开始接收H264码流并将码流保存起来。这里用到多线程,用pthread_create创建个线程函数,在线程函数里用select多路IO复用来获取码流并保存
gs_stPara.bThreadStart = HI_TRUE;
gs_stPara.s32Cnt = s32Cnt;
return pthread_create(&gs_VencPid, 0, SAMPLE_COMM_VENC_GetVencStreamProc, (HI_VOID*)&gs_stPara);
线程处理程序:
HI_VOID* VENC_GetVencStreamProc(HI_VOID *p)
{
SAMPLE_VENC_GETSTREAM_PARA_S *pstPara;
HI_S32 s32ChnTotal;
VENC_CHN VencChn;
HI_S32 s32Ret;
PAYLOAD_TYPE_E enPayLoadType[VENC_MAX_CHN_NUM];
HI_CHAR aszFileName[VENC_MAX_CHN_NUM][64];
VENC_CHN_ATTR_S stVencChnAttr;
struct timeval TimeoutVal;
fd_set read_fds;
HI_S32 VencFd[VENC_MAX_CHN_NUM],maxfd;
VENC_STREAM_S stStream;
VENC_CHN_STAT_S stStat;
FILE *pFile[VENC_MAX_CHN_NUM];
HI_S32 i;
pstPara = (SAMPLE_VENC_GETSTREAM_PARA_S*)p;
s32ChnTotal = pstPara->s32Cnt;//pstPara->s32Cnt是由参数传进来的,为1
for (i = 0; i < s32ChnTotal; i++)
{
VencChn = i;
s32Ret = HI_MPI_VENC_GetChnAttr(VencChn, &stVencChnAttr);
//这里是为了取得是什么编码类型,以便确定保存文件的后缀名
//比如这里是H264编码,所以保存文件的后缀后就是.h264
if(s32Ret != HI_SUCCESS)
{
SAMPLE_PRT("HI_MPI_VENC_GetChnAttr chn[%d] failed with %#x!\n", \
VencChn, s32Ret);
return NULL;
}
enPayLoadType[i] = stVencChnAttr.stVeAttr.enType;
sprintf(aszFileName[i], "stream_chn0%d%s",filename, i, ".h264");//20171101085639.h264
pFile[i] = fopen(aszFileName[i], "wb");
if (!pFile[i])
{
SAMPLE_PRT("open file[%s] failed!\n", aszFileName[i]);
return NULL;
}
//编码好的视频流文件描述符
VencFd[i] = HI_MPI_VENC_GetFd(i);//获取编码器的文件描述符,以便后面能用select来IO复用
if (VencFd[i] < 0)
{
SAMPLE_PRT("HI_MPI_VENC_GetFd failed with %#x!\n", VencFd[i]);
return NULL;
}
if (maxfd <= VencFd[i])
{
maxfd = VencFd[i];
}
}
//当main函数所在的线程接收到两个键盘字符或ctrl+c时,pstPara->bThreadStart会为假,跳出while循环
//然后往下执行关闭前面打开的文件,执行完这个VENC_GetVencStreamProc线程函数,线程结束
while (HI_TRUE == pstPara->bThreadStart)
{
/*IO复用4步骤
1.清空文件集合FD_ZERO(&read_fds);
2.将文件加入文件集合FD_SET(VencFd[i], &read_fds);
3.设置超时时间并用select(maxfd + 1, &read_fds, NULL, NULL, &TimeoutVal)来等待文件状态有变化唤醒线程
或超时唤醒
4.FD_ISSET查询文件状态是否有变化,有变化则处理
*/
FD_ZERO(&read_fds);
for (i = 0; i < s32ChnTotal; i++)
{
FD_SET(VencFd[i], &read_fds);
}
TimeoutVal.tv_sec = 2;
TimeoutVal.tv_usec = 0;
s32Ret = select(maxfd + 1, &read_fds, NULL, NULL, &TimeoutVal);
if (s32Ret < 0)
{
SAMPLE_PRT("select failed!\n");
break;
}
else if (s32Ret == 0)
{
SAMPLE_PRT("get venc stream time out, exit thread\n");
continue;
}
else
{
for (i = 0; i < s32ChnTotal; i++)
{
if (FD_ISSET(VencFd[i], &read_fds))
{
memset(&stStream, 0, sizeof(stStream));
//查询是否有码流,并将码流信息填充到stStat结构体中
s32Ret = HI_MPI_VENC_Query(i, &stStat);
if (HI_SUCCESS != s32Ret)
{
SAMPLE_PRT("HI_MPI_VENC_Query chn[%d] failed with %#x!\n", i, s32Ret);
break;
}
if(0 == stStat.u32CurPacks)
{
SAMPLE_PRT("NOTE: Current frame is NULL!\n");
continue;
}
//分配内存以便保存码流包数据
stStream.pstPack = (VENC_PACK_S*)malloc(sizeof(VENC_PACK_S) * stStat.u32CurPacks);
if (NULL == stStream.pstPack)
{
SAMPLE_PRT("malloc stream pack failed!\n");
break;
}
stStream.u32PackCount = stStat.u32CurPacks;
//printf("stStream.u32PackCount=%d\n",stStream.u32PackCount);
//获取码流数据并保存到stStream结构体中
s32Ret = HI_MPI_VENC_GetStream(i, &stStream, HI_TRUE);
if (HI_SUCCESS != s32Ret)
{
free(stStream.pstPack);//获取失败则要释放前面分配的内存,否则会造成内存溢出
stStream.pstPack = NULL;
SAMPLE_PRT("HI_MPI_VENC_GetStream failed with %#x!\n", s32Ret);
break;
}
HI_S32 u32PackIndex;
for (u32PackIndex= 0;u32PackIndex < stStream.u32PackCount; u32PackIndex++)
{
fwrite( stStream.pstPack[u32PackIndex].pu8Addr+stStream.pstPack[u32PackIndex].u32Offset,\
stStream.pstPack[u32PackIndex].u32Len- stStream.pstPack[u32PackIndex].u32Offset, \
1, pFile[i]);
fflush(pFile[i]);
#if 0
printf("stStream.u32PackCount=%d,stStream.pstPack[%d].pu8Addr=0x%08x,\
stStream.pstPack[%d].u32Offset=%d,stStream.pstPack[%d].u32Len=%d\n",\
stStream.u32PackCount,u32PackIndex,stStream.pstPack[u32PackIndex].pu8Addr,\
u32PackIndex,stStream.pstPack[u32PackIndex].u32Offset,u32PackIndex,stStream.pstPack[u32PackIndex].u32Len);
//添加打印信息,查看保存码流内容的内存是怎么样的
#endif
}
s32Ret = HI_MPI_VENC_ReleaseStream(i, &stStream);//保存后要释放码流
if (HI_SUCCESS != s32Ret)
{
free(stStream.pstPack);//获取失败则要释放前面分配的内存,否则会造成内存溢出
stStream.pstPack = NULL;
break;
}
free(stStream.pstPack);//释放码流后,也要释放分配的内存,避免内存溢出
stStream.pstPack = NULL;
}
}
}
}
for (i = 0; i < s32ChnTotal; i++)
{
fclose(pFile[i]);//关闭各个文件
}
return NULL;
}