这里主要讲解海思sample\common\sample_comm_vdec.c文件中SAMPLE_COMM_VDEC_SendStream函数的代码。
SAMPLE_COMM_VDEC_SendStream是一个线程。
代码段1:
HI_VOID * SAMPLE_COMM_VDEC_SendStream(HI_VOID *pArgs)
{
VDEC_THREAD_PARAM_S *pstVdecThreadParam =(VDEC_THREAD_PARAM_S *)pArgs;
HI_BOOL bEndOfStream = HI_FALSE;
HI_S32 s32UsedBytes = 0, s32ReadLen = 0;
FILE *fpStrm=NULL;
HI_U8 *pu8Buf = NULL;
VDEC_STREAM_S stStream;
HI_BOOL bFindStart, bFindEnd;
HI_U64 u64PTS = 0;
HI_U32 u32Len, u32Start;
HI_S32 s32Ret, i;
HI_CHAR cStreamFile[256];
prctl(PR_SET_NAME, "VideoSendStream", 0,0,0);
//cStreamFile为视频文件完整路径
snprintf(cStreamFile, sizeof(cStreamFile), "%s/%s", pstVdecThreadParam->cFilePath,pstVdecThreadParam->cFileName);
if(cStreamFile != 0)
{
fpStrm = fopen(cStreamFile, "rb"); // 打开视频文件
if(fpStrm == NULL)
{
SAMPLE_PRT("chn %d can't open file %s in send stream thread!\n", pstVdecThreadParam->s32ChnId, cStreamFile);
return (HI_VOID *)(HI_FAILURE);
}
}
printf("\n \033[0;36m chn %d, stream file:%s, userbufsize: %d \033[0;39m\n", pstVdecThreadParam->s32ChnId, pstVdecThreadParam->cFileName, pstVdecThreadParam->s32MinBufSize);
// 解码后一帧YUV图像的字节数
pu8Buf = malloc(pstVdecThreadParam->s32MinBufSize); //(1920X1080*3)/2
if(pu8Buf == NULL)
{
SAMPLE_PRT("chn %d can't alloc %d in send stream thread!\n", pstVdecThreadParam->s32ChnId, pstVdecThreadParam->s32MinBufSize);
fclose(fpStrm);
return (HI_VOID *)(HI_FAILURE);
}
fflush(stdout);
u64PTS = pstVdecThreadParam->u64PtsInit;
上面这段代码很简单,就是使用fopen打开视频文件,然后分配读取一帧图像的视频空间内存大小。这里补充,因为是YVU420空间,即每4个Y分量共用一个UV分量,因此UV分量的尺寸只有原来的四分之一,因此s32MinBufSize的大小为图像宽度x图像高度x3/2。
代码段2:
while (1)
{
if (pstVdecThreadParam->eThreadCtrl == THREAD_CTRL_STOP)
{
break;
}
else if (pstVdecThreadParam->eThreadCtrl == THREAD_CTRL_PAUSE)
{
sleep(1);
continue;
}
bEndOfStream = HI_FALSE;
bFindStart = HI_FALSE;
bFindEnd = HI_FALSE;
u32Start = 0;
fseek(fpStrm, s32UsedBytes, SEEK_SET); // 文件指针定位到s32UsedBytes位置
s32ReadLen = fread(pu8Buf, 1, pstVdecThreadParam->s32MinBufSize, fpStrm); // 读取一帧图像数据到pu8Buf
if (s32ReadLen == 0) // 读取文件完毕
{
if (pstVdecThreadParam->bCircleSend == HI_TRUE) // 循环读取
{
memset(&stStream, 0, sizeof(VDEC_STREAM_S) );
stStream.bEndOfStream = HI_TRUE;
HI_MPI_VDEC_SendStream(pstVdecThreadParam->s32ChnId, &stStream, -1); // 向视频解码通道发送码流数据
s32UsedBytes = 0;
fseek(fpStrm, 0, SEEK_SET); // 再定位到文件头
s32ReadLen = fread(pu8Buf, 1, pstVdecThreadParam->s32MinBufSize, fpStrm);
}
else
{
break; // 非循环读取,结束
}
}
我们再来看第2个代码段, 这是一个while循环,循环从文件中读取数据去做解码
fseek(fpStrm, s32UsedBytes, SEEK_SET)将文件指针位置定位到s32UsedBytes处,一开始为0位置,s32UsedBytes是变量随着whiile循环逐渐变大。
s32ReadLen = fread(pu8Buf, 1, pstVdecThreadParam->s32MinBufSize, fpStrm);读取一帧数据到pu8Buf,一帧数据大小就是上面说的s32MinBufSize。
这里有一个判断if (s32ReadLen == 0) ,即判断文件数据是否已读取完毕,如果读取完毕,再判断是否循环读取。不过不是,则break 出 while循环,结束。如果是循环读取,则将stStream清空后再发给到VDEC,之后再将文件指针定位到开头处,再从头开始读取一帧
代码段3:
// 解码264视频文件
if (pstVdecThreadParam->s32StreamMode==VIDEO_MODE_FRAME && pstVdecThreadParam->enType == PT_H264)
{
// 找到一个条带(slice)位置
for (i=0; i<s32ReadLen-8; i++)
{
int tmp = pu8Buf[i+3] & 0x1F;
if ( pu8Buf[i ] == 0 && pu8Buf[i+1] == 0 && pu8Buf[i+2] == 1 &&
(
((tmp == 0x5 || tmp == 0x1) && ((pu8Buf[i+4]&0x80) == 0x80)) ||
(tmp == 20 && (pu8Buf[i+7]&0x80) == 0x80)
)
)
{
bFindStart = HI_TRUE;
i += 8;
break;
}
}
// 找到下一帧数据流的开始位置
for (; i<s32ReadLen-8; i++)
{
int tmp = pu8Buf[i+3] & 0x1F;
if ( pu8Buf[i ] == 0 && pu8Buf[i+1] == 0 && pu8Buf[i+2] == 1 &&
(
tmp == 15 || tmp == 7 || tmp == 8 || tmp == 6 ||
((tmp == 5 || tmp == 1) && ((pu8Buf[i+4]&0x80) == 0x80)) ||
(tmp == 20 && (pu8Buf[i+7]&0x80) == 0x80)
)
)
{
bFindEnd = HI_TRUE;
break;
}
}
if(i>0)s32ReadLen = i;
if (bFindStart == HI_FALSE)
{
SAMPLE_PRT("chn %d can not find H264 start code!s32ReadLen %d, s32UsedBytes %d.!\n",
pstVdecThreadParam->s32ChnId, s32ReadLen, s32UsedBytes);
}
if (bFindEnd == HI_FALSE)
{
s32ReadLen = i+8;
}
}
这段代码的作用是找到视频文件数据流中一帧的数据长度,怎么找呢?根据H.264文件的视频流结构。这里有两个for循环,第1个for从零开始查到,直到符合条件,得到一个i值,第二个for在第1个for的基础上继续查找,再得到一个i值,i值赋值给s32ReadLen,这个就是要读取的数据长度。我们来看两个for查到的是什么东西。
for循环里有(pu8Buf[i+3] & 0x1F)接触过H.264的就很容易知道,这个就是nal_uint_type值,,
我们再次列出这个值的意义,如下:
代码中第一个for循环要求tmp等于0x5或0x1,也就是说是IDR或者非IDR,简单来说就是I 条带、P条带、B条带,这里不能说是I帧,因为SPS,PPS也属于I帧的一部分,但代码里不是从SPS开始,而是要从条带开始。
我们来看一段视频的二进制流,我们直接看I Slice:
(0x65&0x1F)等于0x5符合tmp的要求。第2个要求是((pu8Buf[i+4]&0x80) == 0x80),也就是说pu8Buf[i+4]要大于等于0x80,以上I Slice 符合要求。
我们可以再看下P Slice的开头,也是符合要求的。
我们在找一个有B帧的视频来看,如下,也是符合要求的。
因此以上第一个for循环的作用是找到I、P 、B的开始位置,即当前的i值(并自加8),这里第一个for循环结束。
第2个for循环,tmp的条件更多,第1个条件tmp == 15 || tmp == 7 || tmp == 8 || tmp == 6,也就是说,nal单元为15, SPS, PPS, SEI其中之一即可,很明显,除了15以外,这是I帧的开始位置。再来看后面的条件: ((tmp == 5 || tmp == 1) && ((pu8Buf[i+4]&0x80) == 0x80)) || (tmp == 20 && (pu8Buf[i+7]&0x80) == 0x80),与第1个for循环是一致的。因此第2个for循环就是要找到下一个NAL单元的开始位置,得到此时的位置值i,将i赋值给到s32ReadLen。
结合两个for循环来看,其实就是找到一帧的数据长度,假设视频文件流为SPS, PPS, I Slice,P Slice,那么i的位置就是P Slice的开始位置,因此i就包括了SPS, PPS, I Slice的长度,这三者组成了一个I帧。
代码段4:
stStream.u64PTS = u64PTS; //0
stStream.pu8Addr = pu8Buf + u32Start; // 码流包的地址
stStream.u32Len = s32ReadLen; // 一帧的长度
stStream.bEndOfFrame = (pstVdecThreadParam->s32StreamMode==VIDEO_MODE_FRAME)? HI_TRUE: HI_FALSE;
stStream.bEndOfStream = bEndOfStream;
stStream.bDisplay = 1; // 当前帧是否输出显示
SendAgain:
s32Ret=HI_MPI_VDEC_SendStream(pstVdecThreadParam->s32ChnId, &stStream, pstVdecThreadParam->s32MilliSec);
if( (HI_SUCCESS != s32Ret) && (THREAD_CTRL_START == pstVdecThreadParam->eThreadCtrl) )
{
usleep(pstVdecThreadParam->s32IntervalTime);
goto SendAgain;
}
else
{
bEndOfStream = HI_FALSE;
s32UsedBytes = s32UsedBytes +s32ReadLen + u32Start;
u64PTS += pstVdecThreadParam->u64PtsIncrease;
}
usleep(pstVdecThreadParam->s32IntervalTime);
u64PTS:这里的u64PTS虽然有pstVdecThreadParam->u64PtsIncrease,但pstVdecThreadParam->u64PtsIncrease为0(上图),解码器不会更改此值,其帧率控制控制由VO来控制。
u32Start:在解码JPEG才用到,解码H.264和H.265都为0。
s32ReadLen:就是之前两个for循环得到的帧长度。
bEndOfFrame:当前帧是否结束,仅 COMPAT 模式发送码流时有效,即当解码JPEG时才有效。
bEndOfStream :是否发完所有码流,根据之前的代码段,只有当读取到最后一帧时,bEndOfStream才被置为HI_TRUE,其他时候都为HI_FALSE,而且每次解码完后都被置为HI_FALSE。
s32UsedBytes:这个参数是定位文件指针的位置的,之前提到过,开头时该值为0,这里自增s32ReadLen + u32Start,即加上一帧的长度,下次就跳到s32ReadLe的位置开始读取数据。
s32IntervalTime:值为1000,即usleep了一毫秒。
根据sample_vdec.c中的设定,解码H.264是采用了VIDEO_MODE_FRAME模式解码,即以帧方式发送码流。
代码段5:
/************************************************
step8: send stream to VDEC
*************************************************/
for(i=0; i<u32VdecChnNum; i++)
{
snprintf(stVdecSend[i].cFileName, sizeof(stVdecSend[i].cFileName), "3840x2160_8bit.h264");
snprintf(stVdecSend[i].cFilePath, sizeof(stVdecSend[i].cFilePath), "%s", SAMPLE_STREAM_PATH);
stVdecSend[i].enType = astSampleVdec[i].enType;
stVdecSend[i].s32StreamMode = astSampleVdec[i].enMode;
stVdecSend[i].s32ChnId = i;
stVdecSend[i].s32IntervalTime = 1000;
stVdecSend[i].u64PtsInit = 0;
stVdecSend[i].u64PtsIncrease = 0;
stVdecSend[i].eThreadCtrl = THREAD_CTRL_START;
stVdecSend[i].bCircleSend = HI_TRUE;
stVdecSend[i].s32MilliSec = 0;
stVdecSend[i].s32MinBufSize = (astSampleVdec[i].u32Width * astSampleVdec[i].u32Height * 3)>>1;
}
SAMPLE_COMM_VDEC_StartSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]);
SAMPLE_COMM_VDEC_CmdCtrl(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]);
SAMPLE_COMM_VDEC_StopSendStream(u32VdecChnNum, &stVdecSend[0], &VdecThread[0]);
好了,目前就讲到这里,重点就是讲了VDEC是如何区分两个帧的。
————————————————
参考链接1:,参考链接2: