首先H.264是一种高度压缩数字视频压缩编码器的标准。H.264最大的优势是具有很高的数据压缩比率,在同等图像质量的条件下,H.264的压缩比是MPEG-2的2倍以上,是MPEG-4的1.5~2倍。这里说一下X264它是一个编码器,参照的标准是H.264标准。编码出来的数据就是H264数据。
NALU:H264编码数据存储或传输的基本单元,一般H264码流最开始的两个NALU是SPS和PPS,第三个NALU是IDR。SPS、PPS、SEI这三种NALU不属于帧的范畴。
SPS(Sequence Parameter Sets):序列参数集,作用于一系列连续的编码图像。
PPS(Picture Parameter Set):图像参数集,作用于编码视频序列中一个或多个独立的图像。
SEI(Supplemental enhancement information):附加增强信息,包含了视频画面定时等信息,一般放在主编码图像数据之前,在某些应用中,它可以被省略掉。
IDR(Instantaneous Decoding Refresh):即时解码刷新
HRD(Hypothetical Reference Decoder):假想码流调度器
序列 sequence:一段h264码流其实是由多个sequence组成的。一个sequence就是一秒如果FPS为20,则包含20帧图像(I/PB)帧。每个sequence均有固定结构单元:1sps+1pps+1sei+1I帧+若干p帧(加上B帧一共有6种单元情况)
分隔符:H.264在编码的时候,生成一个序列时,序列中每个单元前面就会加上00 00 00 01或者00 00 01作为分隔符。
1.分组:把几帧图像分为一组(GOP)为防止运动变化,不宜取多
2.定义帧:将每组内各帧图像定义为三种类型,即I\P\B帧
3.预测帧:以I帧为基础帧(关键帧),以I帧预测P帧,再以I帧P帧预测B帧
4.数据传输:最后将I帧数据与预测的差值信息进行存储和传输
帧内压缩,也称为空间压缩。当压缩一帧图像时候,仅考虑本帧的数据,而考虑相邻帧直接的冗余信息。这实际与静态帧压缩相似。帧内压缩一般使用有损压缩算法,由于帧内压缩是编码一个完整的图像,所以可以独立解码、显示。帧内压缩达不到很大的压缩程度,和JPEG相似
帧间压缩,它的原理是:相邻几帧关联性很大,或者说前后两帧信息变化很小的特点。即连续的视频相邻的帧具有冗余信息,利用这个特点,压缩相邻帧直接的冗余信息就可以进一步提高压缩量。帧间压缩也叫时间压缩,通过比较时间轴之间的数据进行压缩。帧间压缩一般是无损的。(帧差值算法是一种典型的时间压缩算法)
有损压缩与无损压缩:无损压缩即压缩前和解压后数据完全一致。多数无损压缩算法都用RLE行程编码算法。有损压缩意味着压缩后的数据与压缩前的不一致。在压缩过程中要丢失一些人眼和人耳所不敏感的图像或音频信息,而且丢失的信息不可恢复。几乎所有的高压缩算法都是有损压缩算法。
H264码流可以分为两层 VCL与NAL
VCL层存在三种封装格式:
SODB是编码收的原始数据,经过封装后为RBSP,RBSP是NAL单元的数据部分的封装格式。在NAL内部为了防止与起始码竞争,从而引入填充字节0x03,这样便形成了EBSP。
NAL保存了H264相关参数信息和图像信息。NAL层由多个NALU组成。
一个完整的NALU单元结构图如下。
一个原始的H.264 NALU 单元常由 [StartCode] [NALU Header] [NALU Payload] 三部分组成,其中 Start Code 用于标示这是一个NALU 单元的开始,必须是"00 00 00 01" 或"00 00 01"(如果NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001)
H264在网络传输的是NALU,NALU的结构是:NAL头(一个字节)+RBSP。
在上面我们已经讲了在NAL层传输的是NALU单元,而每个NALU单元都包含一个字节的NAL头。
NAL Header:
由上述八位来表示一个NAL头,NAL头标识NALU单元中RBSP的数据类型。
其中,nal_unit_type为1, 2, 3, 4, 5的NAL单元称为VCL的NAL单元,其他类型的NAL单元为非VCL的NAL单元。如NAL头为ox27二进制为
100111,取得后5位为 00111 十进制为7对于sps
H.264的SPS和PPS串包含了初始化H.264解码器所需要的信息参数,包括编码所用的profile、level、图像的宽高、deblock滤波器等。
I帧:帧内编码帧、I帧表示关键帧,可以理解为这一帧画面的完整保留。解码时只需要本帧数据既可以完成。
I帧特点:
P帧:前向预测编码帧。P帧表示的是这一帧和之前一个关键帧或P帧的差别。解码时需要用之前缓存的画面叠加上本帧定义的差别,生成最终画面(也就是说P帧没有完整画面数据,只有与前一帧的画面的差别数据)
P帧的预测与重构:P帧是以I帧为参考帧,在I帧中找到P帧“某点“的预测值和运动矢量。取预测差值和预测矢量一起传送。在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值。从而可以得到完整的P帧
P帧特点:
B帧:双向预测内插编码帧。B帧是双差别帧。即B帧记录的是本帧与前后帧的差别。要解码B帧。不仅要取得之前的缓存画面,还要取得之后的画面。通过前后画面与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累。
B帧的预测和重构:B帧以前面的I帧或P帧和后面的P帧作为参考帧,找出B帧“某点”的预测值和两个运动矢量,并取得差值和运动矢量传送。接收端根据差值和运动矢量在两个参考帧中“找出(算出)” 预测值并与差值求和。得到B帧“某点”的样值,从而得到完整的图像。
B帧的特点:
在直播中,或者网络视频的时候,我们打开会发现有时候会出现LOGING…黑屏的情况。这个好像最近几年不会出现了。造成这个问题就是因为我们刚好点进去的不是I帧,因我们解码一个I帧立马可以得到一个完整的图片。所以我们再接收端可以对接收的数据做缓冲处理。
#include
#include
#include
//shuju 类型,头的后五5位
typedef enum {
NALU_TYPE_SLICE = 1,
NALU_TYPE_DPA = 2,
NALU_TYPE_DPB = 3,
NALU_TYPE_DPC = 4,
NALU_TYPE_IDR = 5,
NALU_TYPE_SEI = 6,
NALU_TYPE_SPS = 7,
NALU_TYPE_PPS = 8,
NALU_TYPE_AUD = 9,
NALU_TYPE_EOSEQ = 10,
NALU_TYPE_EOSTREAM = 11,
NALU_TYPE_FILL = 12,
} nalu_type;
//找到startcode
int find_start_code(unsigned char* buffer)
{
//00000001
if(buffer[0] != 0x00 || buffer[1] != 0x00 || buffer[2] != 0x00 || buffer[3] != 0x01) return -1;
else return 1;
}
//还有一种H264头的开始是以000001开头的
int find_start_code2(unsigned char *buffer)
{
//00000001
if(buffer[0] != 0x00 || buffer[1] != 0x00 || buffer[2] != 0x01) return -1;
else return 1;
}
//H264头占8位1位禁止为,2位优先级 5位类型
typedef struct
{
int startcode_len; // h264数据的分隔符的长度NALU对应的Slice为一帧的开始,则用4字节表示,即0x00000001;否则用3字节表示,0x000001)
unsigned len; // 整个h264头的长度 不包括了startcode的长度
unsigned max_size; //! Nal Unit Buffer size
int forbidden_bit; //! 禁止位
int nal_reference_idc; //! 优先级
int nal_unit_type; //! 类型
char *buf; //! contains the first byte followed by the EBSP
} nalu_t;
/*
H264数据分析
*/
FILE *h264stream;
int info2 = 0;
int info3 = 0;
//得到头部信息
int get_annexb_nalu(nalu_t *nal)
{
int pos = 0;
int start_code;
int rewind;
unsigned char *buffer = (unsigned char *)malloc(nal->max_size);
//设开始的分隔符为ox000001
nal->startcode_len = 3;
if(3 != fread(buffer, 1, 3, h264stream))
{
printf("read 3 of startcoder err!\n");
free(buffer);
return -1;
}
//去找到分隔符 ox000001
info2 = find_start_code2(buffer);
if(info2 != 1)
{
//不是ox000001时再去读一个是不是ox00000001
if(1 != fread(buffer + 3, 1, 1, h264stream))
{
printf("fread 4 of startcode err!\n");
free(buffer);
return -1;
}
info3 = find_start_code(buffer);
if(info3 != 1)
{
printf("cant find start code!\n");
free(buffer);
return -1;
}
else
{
pos = 4;
nal->startcode_len = 4;
}
}
else
{
//startcode为0x000001
pos = 3;
nal->startcode_len = 3;
}
start_code = 0;
info2 = 0;
info3 = 0;
//开始去读取头部,当到达下一个分隔符的时候就退出
while(!start_code)
{
//当达到文件末尾
if(feof(h264stream))
{
nal->len = (pos - 1) - nal->startcode_len;
memcpy(nal->buf, &buffer[nal->startcode_len], nal->len);
nal->forbidden_bit = nal->buf[0] & 0x80;
nal->nal_reference_idc = nal->buf[0] & 0x60;
nal->nal_unit_type = nal->buf[0] & 0x1f;
free(buffer);
return pos - 1;
}
//读取头部信息
buffer[pos++] = fgetc(h264stream);
info3 = find_start_code(&buffer[pos - 4]);
if(info3 != 1)
{
info2 = find_start_code2(&buffer[pos - 3]);
}
start_code = (info2 == 1 || info3 == 1) ? 1 : 0;
}
//回到分隔符之前的距离
rewind = (info3 == 1) ? -4 : -3;
if(0 != fseek(h264stream, rewind, SEEK_CUR))
{
printf("feek startcode err!\n");
free(buffer);
return -1;
}
//nal长度以及数据
nal->len = (pos + rewind) - nal->startcode_len;
memcpy(nal->buf, &buffer[nal->startcode_len], nal->len);
nal->forbidden_bit = nal->buf[0] & 0x80;
nal->nal_reference_idc = nal->buf[0] & 0x60;
nal->nal_unit_type = nal->buf[0] & 0x1f;
free(buffer);
//返回读到下一个分隔符之前的长度包括了这一帧的分隔符长度
return (pos + rewind);
}
int main(int argc, char *argv[])
{
if(argc < 2)
{
printf("please input h264 file to ana!\n");
return -1;
}
int buffer_size = 10000;
h264stream = fopen(argv[1], "rb");
if(h264stream == NULL)
{
printf("oepn h264 file err!\n");
return -1;
}
nalu_t *nal = (nalu_t*)malloc(sizeof(nalu_t));
if(nal == NULL)
{
printf("malloc nal err!\n");
return -1;
}
nal->max_size = buffer_size;
nal->buf = (char *)malloc(buffer_size);
if(nal->buf == NULL)
{
printf("malloc nal buf err!\n");
return -1;
}
int offset = 0;
//开始读取文件分析
while(!feof(h264stream))
{
int data_length;
data_length = get_annexb_nalu(nal);
switch (nal->nal_unit_type)
{
case NALU_TYPE_SLICE:printf("pos :%d SLICE p or b\n", offset);break;
case NALU_TYPE_DPA:printf("pos :%d DPA\n", offset);break;
case NALU_TYPE_DPB:printf("pos :%d DPB\n", offset);break;
case NALU_TYPE_DPC:printf("pos :%d DPC\n", offset);break;
case NALU_TYPE_IDR:printf("pos :%d IDR\n", offset);break;
case NALU_TYPE_SEI:printf("pos :%d SEI\n", offset);break;
case NALU_TYPE_SPS:printf("pos :%d SPS\n", offset);break;
case NALU_TYPE_PPS:printf("pos :%d PPS\n", offset);break;
case NALU_TYPE_AUD:printf("pos :%d AUD\n", offset);break;
case NALU_TYPE_EOSEQ:printf("pos :%d EOSEQ\n", offset);break;
case NALU_TYPE_EOSTREAM:printf("pos :%d EOSTREAM\n", offset);break;
case NALU_TYPE_FILL:printf("pos :%d FILL\n", offset);break;
default:break;
}
offset += data_length;
}
fclose(h264stream);
return 0;
}