H.264裸流文件中获取每一帧数据

    测试解码器性能时,最常用的无非是向解码器中推送码流。

    之前封装了一个avc的解码器,想做一个测试,读取H.264裸流文件将码流定期定时推送到解码器。

    测试其实很简单:

    1.了解H.264裸流文件的构成

    2.解析H.264裸流文件

    3.提取H.264码流调用接口推送数据


    1. 了解H.264逻辑文件

    根据H.264协议,avc码流主要包含I,B,P三种帧类型(裸流中没有B帧,暂时不予考虑),而IDR帧则有包含SPS,PPS,I三种帧类型(IDR帧一定是I帧,但I帧不一定是IDR帧)。

    任何一种帧都包含一个NAL单元,头部(前四个字节)为00 00 00 01,是帧的开始,也是帧的标识。而第五个字节则可识别是何种帧类型,公式为(arr[4] & 0x1f), arr表示包含一帧数据的首地址。

    IDR帧,又名关键帧,是H.264解码的关键,是码流的开始,该帧中包含完整的信息,可独立解码为一帧完整的YUV数据。其中IDR帧有包含SPS,PPS和I三种中类型。

    SPS和PPS为描述信息,通过解析可以得到视频的分辨率,码率等信息,SPS经过(arr[4] & 0x1f)计算可得7, PPS经过(arr[4] & 0x1f)计算可得8,I帧经过(arr[4] & 0x1f)计算可得5。

     由于H.264在编码时可以指定多slice编码,因此一个完整的IDR帧数据可能如下所示:

00 00 00 01 67 .... 00 00 00 01 68 ...... 00 00 00 01 66 ...... 00 00 00 01 65 ..... 00 00 00 01 65 .... 00 00 00 01 65 ..... 00 00 00 01 65 .....  00 00 00 01 65 ..... 

    首先IDR帧不一定包含[00 00 00 01 66 ....], 6 为冗余信息,在一些项目中是可有可无的。其次会发现这一帧信息中包含多个5,这就多slice编码的结果。

    那么何为多slice编码? 当一帧YUV数据过大时,单线程编码明显是不太现实,因此avc编码器通常将一帧YUV数据切割成几个大块,然后由多个线程同时编码。

    P帧又名前向参考帧,经过(arr[4] & 0x1f)计算可得1。这一帧是前面一帧图像的动态差值,通过前一帧数据加上当前帧的差值即可推到出当前的具体内容,这样的好处在于编码体积比较小,但带来的隐患就是帧和帧之间必须有严格的顺序,且不能缺少任何一帧,否则解码将会花屏。

    B帧又名双向参考帧,与P帧相似,不过B帧是前后两帧的动态差值,暂时未用到,不做赘述。

经过介绍,一个H.264裸流文件内可能是如下形式,多个00 00 00 01开头,包含不同数量的7 8 6 5 1等不同的帧:

00 00 00 01 67 .... 00 00 00 01 68 ...... 00 00 00 01 66 ...... 00 00 00 01 65 ..... 00 00 00 01 65 .... 00 00 00 01 65 ..... 00 00 00 01 65 .....  00 00 00 01 65 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 41 ..... 00 00 00 01 67 .... 00 00 00 01 68 ...... 00 00 00 01 66 ...... 00 00 00 01 65 ..... 


    2.解析H.264裸流文件

    了解了H.264裸流文件的构成,那么只需要将各个00 00 00 01在文件中的位置找到即可,有了每一帧的位置,就可以推算帧大小,并将数据提取出来,因此接口抽象为在指定的文件中查找所有与指定内容相同的内容,并记录其在文件中的位置,好处是代码通用,不仅局限于解析H.264文件,坏处是参数略显复杂,且数组长度不可变,可自行添加。示例代码如下:

/* 在指定的文件中查找所有与str内容相同的内容,并将内容在文件的位置记录在arr中。
 * @fp 指定查找的文件指针
 * @str 要查找的内容
 * @strLen 要查找的内容的长度
 * @arr 存放位置的数组,要求数组足够大
 * @len 两种含义,传入时len表示数组长度,函数结束后len表示数组中有效数据的个数
 **/
int getAllContent(FILE *fp, char *str, int strLen, unsigned *arr, unsigned *len)
{
    if(!fp || !arr || !len) return -1;

    unsigned arrLen = *len;
    long pos = 0;
    long posEnd = 0;
    char *buf = malloc(sizeof(char)*strLen);;
    if(!buf) return -2;

    fseek(fp, 0L, SEEK_END);
    posEnd = ftell(fp) - strLen;

    *len = 0;
    int res = 0;
    while(pos <= posEnd && *len < arrLen)
    {
        fseek(fp, pos, SEEK_SET);
        res = fread(buf, sizeof(char), strLen, fp);
        if(res != strLen) break;
        if(memcmp(str, buf, strLen*sizeof(char)) == 0)
        {
            arr[*len] = pos;
            (*len)++;
        }
        pos++;
    }

    fseek(fp, 0L, SEEK_SET);
    free(buf);
    return 0;
}

    3.提取H.264码流调用接口推送数据

#define MAX_LEN 10000
unsigned remoteArr[MAX_LEN] = {0};
unsigned remoteArrLen = MAX_LEN;
unsigned  char nal[4] = {0x00, 0x00, 0x00, 0x01};
getAllContent(fp, nal,4,remoteArr, &remoteArrLen);

    经过上一步之后,所有的00 00 00 01在文件中的位置存储数组remoteArr中。后面只需按位置读取并保持数据即可。下面这个函数设计就比较简单了,按图索骥即可。

假设调用如下:

SendAvcStream(fp, remoteArr, remoteArrLen);

那么实现如下:

/*根据arr中保持的各帧的位置读取文件
 * @fp 需要读取的文件
 * @arr 保持指定内容在文件中的位置的数组
 * @len 数组中可用数据的个数
 **/
int SendAvcStream(FILE *fp, unsigned *arr, unsigned len)
{
    static unsigned char *bufI = NULL;
    static unsigned char *buf2 = NULL;
    if(!bufI)
    {   
        bufI = malloc(1920 * 1080);
        if(!bufI) return -1;
    }
    if(!buf2)
    {   
        buf2 = malloc(1920 * 500);
        if(!buf2) return -1;
    }

    unsigned i = 1;
    int res = 0;
    unsigned frameSize = 0;
    while(i < len)
    {   
        unsigned size = arr[i] - arr[i-1];
        if(size == 0){i++; continue;}
        
        res = fread(buf2, 1, size, fp);
        if(res != (signed)size) break;
        
        int type = buf2[4] & 0x1f;
        printf("frame type is %d\n", type);
        if(type == 1)
        {   
            if(frameSize)// get a whole IDR frame which maybe include dual slices
            {   
                // TODO: to do something
                frameSize = 0;
            }
            
            // get a whole P frame
            // TODO: to do something
        }
        else // 7 8 6 5
        {
            // stroe 7 8 6 5 into an buffer
            memcpy(bufI + frameSize, buf2, size);
            frameSize += size;
        }

        usleep(30000);
        i++;
    }
    fseek(fp, 0L, SEEK_SET);
    return 0;
}

上述代码中,需要考虑多slice的情况,所以当拿到7 8 6 5并不意味着取得了完整的IDR帧,只有当拿到一个P帧时,方可认为已经获取到完整的IDR帧。

代码中TODO的地方,被隐去了,这里是将其推送的解码器中解码。另外函数其实略有缺陷,有些死板,应该将其设计为读取并返回一帧完整的数据,可能通用性会更好一些。

代码最终测试可用,解码器抗压能力不错,轮训推送码流并没有出现crash的情况,不过由于时间戳的问题,播放并不是很流畅。

猜你喜欢

转载自blog.csdn.net/xy_kok/article/details/81237361