视频编解码(二):海思VDEC模块视频解码代码解析

这里主要讲解海思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; 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);

根据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是如何区分两个帧的。

发布了40 篇原创文章 · 获赞 51 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/zh8706/article/details/100516575