以下内容源于朱有鹏嵌入式课程的学习与整理,如有侵权请告知删除。
前言
本文将详细介绍博文第二季3:sample_venc.c的整体分析提及的“ 保存编码得到的码流 ”。
即把编码得到的三路码流(三路码流都是H264格式的,只是分辨率不同)保存为裸流文件(这里保持为stream_ch0.h264、stream_ch1.h264、stream_ch2.h264文件)。
我们首先列出该部分(称之为StoF)的函数调用关系图谱,然后分析其具体代码细节。
一、StoF的函数调用关系
StoF部分的函数调用关系如下所示:
SAMPLE_COMM_VENC_StartGetStreamSAMPLE_COMM_VENC_GetVencStreamProc//是一个线程函数//step1HI_MPI_VENC_GetChnAttrSAMPLE_COMM_VENC_GetFilePostfix//添加文件后缀(比如.h264)pFile[i] = fopen(aszFileName[i], "wb")HI_MPI_VENC_GetFd//获取编码通道对应的设备文件句柄//step2step2_1 :HI_MPI_VENC_Query //查询编码通道状态step2_2 step2_3 :malloc//申请码流空间step2_4 :HI_MPI_VENC_GetStream//获取一帧图像的编码码流step2_5 :SAMPLE_COMM_VENC_SaveStream//保存码流到文件中(若RTSP发送则修改这里)SAMPLE_COMM_VENC_SaveH264fwritestep2_6 :HI_MPI_VENC_ReleaseStream//释放编码码流step2_7 :free//释放buff?//step3close //关闭文件
由此可知,该部分包括以下几个步骤:
- 获取编码通道对应的设备文件句柄
- 查询编码通道的状态
- 申请码流空间
- 获取一帧图像的编码码流
- 保存码流到文件中
- 释放编码码流
下面我们将详细介绍这几个步骤的代码细节。
二、StoF的代码详解
1、StoF代码整体分析
StoF的代码如下,主要是SAMPLE_COMM_VENC_StartGetStream,其参数s32ChnNum在step1时被初始化为3。
/******************************************step 6: stream venc process -- get stream, then save it to file. ******************************************/s32Ret = SAMPLE_COMM_VENC_StartGetStream(s32ChnNum);if (HI_SUCCESS != s32Ret){SAMPLE_PRT("Start Venc failed!\n");goto END_VENC_1080P_CLASSIC_5;}printf("please press twice ENTER to exit this sample\n");getchar();getchar();
SAMPLE_COMM_VENC_GetVencStreamProc函数内容与分析如下,该函数获取每个编码通道的码流并保存为裸流文件。
/******************************************************************************
* funciton : get stream from each channels and save them
******************************************************************************/
HI_VOID* SAMPLE_COMM_VENC_GetVencStreamProc(HI_VOID *p)
{HI_S32 i;HI_S32 s32ChnTotal;VENC_CHN_ATTR_S stVencChnAttr;SAMPLE_VENC_GETSTREAM_PARA_S *pstPara;HI_S32 maxfd = 0;struct timeval TimeoutVal;fd_set read_fds;HI_S32 VencFd[VENC_MAX_CHN_NUM];HI_CHAR aszFileName[VENC_MAX_CHN_NUM][64];FILE *pFile[VENC_MAX_CHN_NUM];char szFilePostfix[10];VENC_CHN_STAT_S stStat;VENC_STREAM_S stStream;HI_S32 s32Ret;VENC_CHN VencChn;PAYLOAD_TYPE_E enPayLoadType[VENC_MAX_CHN_NUM];pstPara = (SAMPLE_VENC_GETSTREAM_PARA_S*)p;s32ChnTotal = pstPara->s32Cnt;//这个表示有多少个编码通道,这里等于3/******************************************step 1: check & prepare save-file & venc-fd******************************************/if (s32ChnTotal >= VENC_MAX_CHN_NUM)//做一些错误检测{SAMPLE_PRT("input count invaild\n");return NULL;}for (i = 0; i < s32ChnTotal; i++)//即for(i=0;i<3;i++){/* decide the stream file name, and open file to save stream */VencChn = i;//将分别为 0,1,2//获取venc模块通道i的属性,这个属性在SAMPLE_COMM_VENC_Start函数中设置s32Ret = HI_MPI_VENC_GetChnAttr(VencChn, &stVencChnAttr);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;//获取图像的传输格式//根据图像的传输格式,来决定文件名的后缀,保存在szFilePostfix这个字符数组里s32Ret = SAMPLE_COMM_VENC_GetFilePostfix(enPayLoadType[i], szFilePostfix);if(s32Ret != HI_SUCCESS){SAMPLE_PRT("SAMPLE_COMM_VENC_GetFilePostfix [%d] failed with %#x!\n", \stVencChnAttr.stVeAttr.enType, s32Ret);return NULL;}//合成裸流文件的文件名“stream_chn0.h264”“stream_chn1.h264”“stream_chn2.h264”sprintf(aszFileName[i], "stream_chn%d%s", i, szFilePostfix);//“读改写”三部曲之“读”(打开)pFile[i] = fopen(aszFileName[i], "wb");if (!pFile[i]){SAMPLE_PRT("open file[%s] failed!\n", aszFileName[i]);return NULL;}/* Set Venc Fd. */VencFd[i] = HI_MPI_VENC_GetFd(i);//获取编码通道对应的设备文件句柄,i表示编码通道号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];}}/******************************************step 2: Start to get streams of each channel.******************************************/while (HI_TRUE == pstPara->bThreadStart)//上层传过来的,这个成立{//select函数相关的内容见相关博客,这里先略过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))//这里表示啥意思?{/*******************************************************step 2.1 : query how many packs in one-frame stream.*******************************************************/memset(&stStream, 0, sizeof(stStream));//查看编码通道的状态,i是编码通道号,stState是输出型参数,通道状态信息在其中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;}/*******************************************************step 2.2 :suggest to check both u32CurPacks and u32LeftStreamFrames at the same time,for example:if(0 == stStat.u32CurPacks || 0 == stStat.u32LeftStreamFrames){SAMPLE_PRT("NOTE: Current frame is NULL!\n");continue;}*******************************************************/if(0 == stStat.u32CurPacks){SAMPLE_PRT("NOTE: Current frame is NULL!\n");continue;}/*******************************************************step 2.3 : malloc corresponding number of pack nodes.*******************************************************///这个结构体指针指向所申请的(由一帧的所有数据包组成的)空间//其中VENC_PACK_S是数据包结构体//stStat.u32CurPacks是一帧图像包含的数据包数量stStream.pstPack = (VENC_PACK_S*)malloc(sizeof(VENC_PACK_S) * stStat.u32CurPacks);if (NULL == stStream.pstPack){SAMPLE_PRT("malloc stream pack failed!\n");break;}/*******************************************************step 2.4 : call mpi to get one-frame stream*******************************************************/stStream.u32PackCount = stStat.u32CurPacks;//该函数获取编码的码流,参数1表示编码通道号,//参数2表示码流结构体指针,参数3表示码流超时时间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;}/*******************************************************step 2.5 : save frame to file*******************************************************///该函数将码流写进文件里面s32Ret = SAMPLE_COMM_VENC_SaveStream(enPayLoadType[i], pFile[i], &stStream);if (HI_SUCCESS != s32Ret){free(stStream.pstPack);stStream.pstPack = NULL;SAMPLE_PRT("save stream failed!\n");break;}/*******************************************************step 2.6 : release stream*******************************************************/s32Ret = HI_MPI_VENC_ReleaseStream(i, &stStream);if (HI_SUCCESS != s32Ret){free(stStream.pstPack);stStream.pstPack = NULL;break;}/*******************************************************step 2.7 : free pack nodes*******************************************************/free(stStream.pstPack);stStream.pstPack = NULL;}}}}/******************************************************** step 3 : close save-file*******************************************************/for (i = 0; i < s32ChnTotal; i++){fclose(pFile[i]);}return NULL;
}
下面我们将重点分析这两个内容:
(1) HI_MPI_VENC_GetStream(i, &stStream, HI_TRUE)
(2)SAMPLE_COMM_VENC_SaveStream(enPayLoadType[i], pFile[i], &stStream)
2、SAMPLE_COMM_VENC_SaveStream函数分析
该函数有3个参数,参数1表示图像传输格式,参数2表示码流要保存到哪个文件,参数3表示码流的结构体指针。该函数内部通过判断参数1来选择不同的保存方式,案例里参数1是PT_H264,于是下一步是SAMPLE_COMM_VENC_SaveH264这个函数。
SAMPLE_COMM_VENC_SaveH264函数包括2个参数,参数1表示码流要保存到哪个文件,参数2表示码流的结构体指针。该函数的内容与分析如下所示。
/******************************************************************************
* funciton : save H264 stream
******************************************************************************/
HI_S32 SAMPLE_COMM_VENC_SaveH264(FILE* fpH264File, VENC_STREAM_S *pstStream)
{HI_S32 i;for (i = 0; i < pstStream->u32PackCount; i++){ /*每个数据包的首地址 + 每个数据包的有效数据相对于首地址的偏移量*/fwrite(pstStream->pstPack[i].pu8Addr + pstStream->pstPack[i].u32Offset,/*每个数据包的长度 - 每个数据包的有效数据相对于首地址的偏移量 = 有效数据长度*/pstStream->pstPack[i].u32Len-pstStream->pstPack[i].u32Offset, 1,//写入一次fpH264File);//写入哪个文件fflush(fpH264File);//表示立即将输出缓冲区的数据写入该文件中?}return HI_SUCCESS;
}
关于fwrite、fflush函数的用法,见博客fwrite函数与fflush函数_天糊土的博客-CSDN博客。
3、HI_MPI_VENC_GetStream函数分析
该函数的具体介绍,见文档《HiMPP IPC V2.0 媒体处理软件开发参考.pdf_1111》的第539页。这里仅摘抄与整理部分内容。
函数原型:
HI_S32 HI_MPI_VENC_GetStream(VENC_CHN VeChn, VENC_STREAM_S *pstStream, HI_S32 s32MilliSec);
调用实例:
s32Ret = HI_MPI_VENC_GetStream(i, &stStream, HI_TRUE);
其中参数1表示编码通道的编号,参数2是输出型参数,表示码流的结构体指针,参数3表示获取码流的超时时间。
(1)支持设置超时方式
取-1表示阻塞(即如果缓冲无数据,则会等待有数据时才返回获取成功),取0表示非阻塞(即如果缓冲无数据,则立即返回失败),取大于0的数字表示超时时间(即如果缓冲无数据,则会等待用户设定的超时时间,若在设定的时间内有数据则返回获取成功,否则返回超时失败)。
(2)支持按帧或者按包的方式获取码流
如果按包的方式获取码流,对于 H.264/265 编码协议,每次获取的是一个 NAL 单元。
(3)帧码流结构体 VENC_STREAM_S的详解
//“ 帧码流 ” 表示一帧图像对应的码流
typedef struct hiVENC_STREAM_S
{VENC_PACK_S *pstPack; //指向帧码流包的指针HI_U32 u32PackCount; //帧码流的所有包的个数HI_U32 u32Seq; //码流序列号,按帧获取则表示帧序号,按包获取则表示包序号 union{ //码流特征信息VENC_STREAM_INFO_H264_S stH264Info; /*the stream info of h264*/VENC_STREAM_INFO_JPEG_S stJpegInfo; /*the stream info of jpeg*/VENC_STREAM_INFO_MPEG4_S stMpeg4Info; /*the stream info of mpeg4*/VENC_STREAM_INFO_H265_S stH265Info; /*the stream info of h265*/};
}VENC_STREAM_S;
A、帧码流包结构体 VENC_PACK_S
typedef struct hiVENC_PACK_S
{HI_U32 u32PhyAddr; //每个码流包的物理地址 /*the physics address of stream*/HI_U8 *pu8Addr; //每个码流包首地址 /*the virtual address of stream*/HI_U32 u32Len; //每个码流包长度 /*the length of stream*/HI_U64 u64PTS; //时间戳,单位us /*PTS*/HI_BOOL bFrameEnd; //帧结束标识 /*frame end*///码流类型,支持 H.264/JPEG/MPEG-4 协议类型的数据包。VENC_DATA_TYPE_U DataType; /*the type of stream*/HI_U32 u32Offset;//每个码流包中有效数据与码流包首地址 pu8Addr 的偏移。HI_U32 u32DataNum;//当前码流包(类型由 DataType 指定)数据中包含其他类型码流包的个数。VENC_PACK_INFO_S stPackInfo[8];//当前码流包数据中包含其他类型码流包数据信息
}VENC_PACK_S;/*
(1)关于帧结束标志
HI_TRUE表示该码流包是该帧的最后一个包,HI_FALSE表示该码流包不是该帧的最后一个包。*/
VENC_PACK_S类型的指针变量pstPack,它指向一组 VENC_PACK_S 的内存空间,该空间由调用者分配。如果是按包获取,则此空间不小于 sizeof(VENC_PACK_S)的大小;如果按帧获取,则此空间不小于 N × sizeof(VENC_PACK_S)的大小,其中 N 代表当前帧之中的包的个数,可以在 select 之后通过查询接口获得。
B、码流包个数 u32PackCount
此值指定 pstPack 中 VENC_PACK_S 的个数。按包获取时,u32PackCount 必须不小于 1;按帧获取时,u32PackCount 必须不小于当前帧的包个数。在函数调用成功后,u32PackCount 返回实际填充 pstPack 的包的个数。
C、序列号 u32Seq
按帧获取时是帧序列号;按包获取时为包序列号。
D、码流特征信息
数据类型为联合体,包含了不同编码协议对应的码流特征信息stH264Info/ stJpegInfo/stMpeg4Info/stH265Info,码流特征信息的输出用于支持用户的上层应用。
我们看一下H.264码流特征信息结构体VENC_STREAM_INFO_H264_S。
typedef struct hiVENC_STREAM_INFO_H264_S
{ //编码当前帧的字节(BYTE)数HI_U32 u32PicBytesNum; /* the coded picture stream byte number */
//编码当前帧中采用跳跃(SKIP)编码模式的宏块数HI_U32 u32PSkipMbNum; /* the skip macroblock num */
//编码当前帧中采用 IPCM 编码模式的宏块数 HI_U32 u32IpcmMbNum; /* the ipcm macroblock num */
//编码当前帧中采用 Inter16x8 预测模式的宏块数HI_U32 u32Inter16x8MbNum; /* the inter16x8 macroblock num */HI_U32 u32Inter16x16MbNum; /* the inter16x16 macroblock num */HI_U32 u32Inter8x16MbNum; /* the inter8x16 macroblock num */HI_U32 u32Inter8x8MbNum; /* the inter8x8 macroblock num */HI_U32 u32Intra16MbNum; /* the intra16x16 macroblock num */HI_U32 u32Intra8MbNum; /* the intra8x8 macroblock num */HI_U32 u32Intra4MbNum; /* the inter4x4 macroblock num */
//编码当前帧属于何种跳帧参考模式下的参考帧H264E_REFSLICE_TYPE_E enRefSliceType; /*the reference type of H264E slice*/
//高级跳帧参考下的编码帧类型H264E_REF_TYPE_E enRefType; /*Type of encoded frames in advanced frame skipping reference mode*/
//通道属性或参数(包含 RC 参数)被设置的次数HI_U32 u32UpdateAttrCnt; /*Number of times that channel attributes or parameters (including RC parameters) are set*/
//编码当前帧的 startqp 值HI_U32 u32StartQp; /*StartQP Value*/
}VENC_STREAM_INFO_H264_S;
(4)码流包结构图
见第二季7:创建配置编码通道(step5:VENC部分)第一节的第5点内容描述。