这里主要讲解海思sample\common\sample_comm_vdec.c文件中SAMPLE_COMM_VDEC_SendStream函数的代码。
SAMPLE_COMM_VDEC_SendStream是一个线程。
代码段1:
//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);
}
}
// 解码后一帧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; i0)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);
根据sample_vdec.c中的设定,解码H.264是采用了VIDEO_MODE_FRAME模式解码,即以帧方式发送码流。
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了一毫秒。
好了,目前就讲到这里,重点就是讲了VDEC是如何区分两个帧的。