测试解码器性能时,最常用的无非是向解码器中推送码流。
之前封装了一个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的情况,不过由于时间戳的问题,播放并不是很流畅。