zl程序教程

您现在的位置是:首页 >  系统

当前栏目

海思Hi3559A Sample_comm_vdec模块解码 视频解码解析

模块 解析 视频 解码 海思 Sample
2023-09-14 09:13:10 时间

海思Hi3559A Sample_comm_vdec 解码模块讲解

1.sample_comm_vdec.c文件中的代码

这里主要讲解海思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了一毫秒。

2.sample_vdec.c文件中的代码

根据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: