x264是根据h264编译协议写出来的一个编码库,两者就是这样关系。so,什么是h264编码协议?首先我们从简单的说起:
在H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。
以上概念大家应该都有所了解,然后我们再从宏观理解入手:
H264协议的数据码流分为两层:
(1) VCL(Video Coding Layer)视频编码层:负责高效的视频内容表示,VCL 数据即编码处理的输出,它表示被压缩编码后的视频数据序列。(就是以上说的IPB帧)
(2) NAL(Network Abstraction Layer)网络提取层:负责以网络所要求的恰当的方式对数据进行打包和传送,是传输层,不管是在本地播放还是在网络播放的传输,都要通过这一层来传输。(打包VCL进行有规范的传输)
然后yuv->h264的编码工作之前,编码器必须根据实际的视频参数做好初始化准备,那么就需要一个字段来说明你需要初始化一个怎样的解码器,怎样的解码方式。其中H264协议中的的Level(级别)是用来约束分辨率、帧率。Profile(档次)是用来说明编解码方式和码率。(下表就是level和profile简要说明)
level Max macroblocks Max video bit rate (kbit/s) Examples for high resolution @ frame rate (max stored frames) per second
per frame
BP、XP、MP HiP Hi10P Hi422P, Hi444PP 1 1,485 99 64 80 192 256 128×[email protected] (8)
176×[email protected] (4)1.1 3,000 396 192 240 576 768 176×[email protected] (9)
320×[email protected] (3)
352×[email protected] (2)1.2 6,000 396 384 480 1,152 1,536 320×[email protected] (7)
352×[email protected] (6)1.3 11,880 396 768 960 2,304 3,072 320×[email protected] (7)
352×[email protected] (6)2 11,880 396 2,000 2,500 6,000 8,000 320×[email protected] (7)
352×[email protected] (6)2.1 19,800 792 4,000 5,000 12,000 16,000 352×[email protected] (7)
352×[email protected] (6)2.2 20,250 1,620 4,000 5,000 12,000 16,000 352×[email protected](10)
352×[email protected] (7)
720×[email protected] (6)
720×[email protected] (5)3 40,500 1,620 10,000 12,500 30,000 40,000 352×[email protected] (12)
352×[email protected] (10)
720×[email protected] (6)
720×[email protected] (5)3.1 108,000
3,600
14,000
17,500
42,000
56,000
720×[email protected] (13)
720×[email protected] (11)
1280×[email protected] (5)3.2 216,000
5,120
20,000
25,000
60,000
80,000
1,280×[email protected] (5)
1,280×1,[email protected] (4)4 245,760
8,192
20,000
25,000
60,000
80,000
1,280×[email protected] (9)
1,920×1,[email protected] (4)
2,048×1,[email protected] (4)4.1
245,760
8,192
50,000
62,500
150,000
200,000
1,280×[email protected] (9)
1,920×1,[email protected] (4)
2,048×1,[email protected] (4)5 589,824
22,080
135,000
168,750
405,000
540,000
1,920×1,[email protected] (13)
2,048×1,[email protected] (13)
2,048×1,[email protected] (12)
2,560×1,[email protected] (5)
3,680×1,[email protected] (5)5.1 983,040
36,864
240,000
300,000
720,000
960,000
1,920×1,[email protected] (16)
4,096×2,[email protected] (5)
4,096×2,[email protected] (5)
Max macroblocks:最大宏块数。注:宏块尺寸是16x16的。
per second:每秒(的最大宏块数)。可用于约束帧率。
per frame:每帧(的最大宏块数)。可用于约束分辨率。
Max video bit rate (kbit/s):最大视频码率。不同档次(Profile)下会有区别。
BP:Baseline Profile,基线档次。提供I/P帧,仅支持Progressive(逐行扫描)和CAVLC。多应用于“视频会话”
XP:Extended Profile,进阶档次。提供I/P/B/SP/SI帧,仅支持Progressive和CAVLC。多应用于流媒体领域
MP:Main Profile,主要档次。提供I/P/B帧,支持Progressive和Interlaced(隔行扫描),提供CAVLC和CABAC。多应用于数字电视广播、数字视频存储等领域;
HiP:High Profile,高级档次。(Fidelity Range Extensions,FRExt)在Main profile基础上新增8*8帧内预测,Custom Quant,Lossless Video Coding,更多YUV格式(4:2:2,4:4:4),像素精度提高到10位或14位。多应用于对高分辨率和高清晰度有特别要求的领域。
Hi10P:High 10 Profile,高级10位档次。
Hi422P:High 4:2:2 Profile,高级4:2:2档次。
Hi444PP:High 4:4:4 Predictive Profile,高级4:4:4(实验性)档次。
说那么多理论,可以总结知道只有这个level和profile设置正确了,编码器才能正常地进行yuv->h264的编码工作。接下来我们继续学习编码之后的NAL Unit。
在 VCL 数据传输或存储之前,这些编码的 VCL 数据,先被映射或封装进NAL 单元中。每个 NAL 单元包括一个原始字节序列负荷( RBSP, Raw Byte Sequence Payload)和一组对应于视频编码的 NAL 头信息。RBSP 的基本结构是:在原始编码数据的后面填加了结尾比特。一个 bit“1”若干比特“0”,以便字节对齐。(这就是网络上很多人说的,每个NALU都是以0x00 00 00 01或者0x00 00 01作为分界,俗称起始码)
NAL头由一个字节组成(1个byte、8个bit)NAL头信息的每一位都有自己的意义。如下所示(这个是NAL-Header,不要和起始码搞混,这个nal-header只占1个byte,而起始码一般是占4个byte)
F代表禁止位(1bit)、NRI代表重要性位(2bit)、Type代表NALU类型(5bit)。
位位置 | 0 | 1-2 | 3-7 |
简称 | F | NRI |
TYPE |
全称 | forbidden_zero_bit | nal_ref_idc | nal_unit_type |
中文 | 禁止位 | 重要性指示位 | NALU类型 |
作用 |
网络发现NAL单元有比特错误时可设置该比特为1,以便接收方丢掉该单元 | 标志该NAL单元用于重建时的重要性,值越大,越重要。取 00 ~ 11 | 1 ~ 23表示单个NAL包,24 ~ 31需要分包或者组合发送,具体含义需要参考下面的表格 |
nal_unit_type取值的含义如下:
0 没有定义
1-23 NAL单元,单个 NAL 单元包
1 不分区,非IDR图像的片
2 片分区A
3 片分区B
4 片分区C
5 IDR图像中的片
6 补充增强信息单元(SEI)
7 SPS(Sequence Parameter Set序列参数集,作用于一串连续的视频图像,即视频序列)
8 PPS(Picture Parameter Set图像参数集,作用于视频序列中的一个或多个图像)
9 序列结束
10 序列结束
11 码流结束
12 填充
13-23 保留
24 STAP-A单一时间组合包
25 STAP-B单一时间组合包
26 MTAP16 多个时间的组合包
27 MTAP24 多个时间的组合包
28 FU-A 分片的单元
29 FU-B 分片的单元
30-31 还没有定义
h264协议的解析就先讲到这里。承接上篇文章的prepareVideoEncoder。我已经选择合适的level和profile得到x264_encoder,所以还是讲讲YUV的理论吧(笑哭.jpg)
YUV的存储格式根据排列格式分为:planar和packed。
1.对于planar的YUV格式,先连续存储所有像素点的Y,紧接着存储所有像素点的U,随后是所有像素点的V。
2.对于packed的YUV格式,每个像素点的Y,U,V是连续交*存储的。YUV的存储格式其实又与其采样的方式密切相关,主流的采样方式有三种,YUV4:4:4,YUV4:2:2,YUV4:2:0
1. YUV 4:4:4采样,每一个Y对应一组UV分量,8+8+8 = 24bits,3个字节。
2. YUV 4:2:2采样,每两个Y共用一组UV分量,一个YUV占8+4+4 = 16bits 2个字节。
3. YUV 4:2:0采样,每四个Y共用一组UV分量,一个YUV占8+2+2 = 12bits 1.5个字节。
因为YUV420比较常用, 在这里就重点介绍YUV420。YUV420分为两种:YUV420p和YUV420sp(这两种都是属于planar)
YUV420sp格式如下图: YUV420p数据格式如下图:
我们先看YUV420sp,根据u和v的存储顺序不一样,又细分为:
NV12:存储顺序是先存Y,再UV交替存储。YYYY YYYY UV UV
NV21:存储顺序是先存Y,再VU交替存储。YYYY YYYY VU VU
再看YUV420p,根据u和v的存储顺序不一样,又细分为:
YU12又叫I420:存储顺序是先存Y,再存U,最后存V。YYYYYYYY UU VV
YV12:存储顺序是先存Y,再存V,最后存U。YYYYYYYY VV UU
简单的理论介绍完毕,承接上篇文章的prepareVideoEncoder是时候把摄像头传入的NV21数据编码成h264字节流。代码如下:
JNIEXPORT void JNICALL Java_org_zzrblog_ffmp_RtmpPusher_feedVideoData
(JNIEnv *env, jobject jobj, jbyteArray array)
{
if(gRtmpPusher == NULL )
return;
int y_len = gRtmpPusher->width * gRtmpPusher->height;
int u_len = y_len / 4;
int v_len = y_len / 4;
//NV21->YUV420P
jbyte* nv21_buffer = (*env)->GetByteArrayElements(env,array,NULL);
uint8_t* y = gRtmpPusher->pic_in.img.plane[0];
uint8_t* u = gRtmpPusher->pic_in.img.plane[1];
uint8_t* v = gRtmpPusher->pic_in.img.plane[2];
memcpy(y, nv21_buffer, (size_t) y_len);
for (int i = 0; i < u_len; i++) {
//NV21 先v后u
*(v + i) = (uint8_t) *(nv21_buffer + y_len + i * 2);
*(u + i) = (uint8_t) *(nv21_buffer + y_len + i * 2 + 1);
}
x264_nal_t *nal = NULL; //h264编码得到NALU数组
int n_nal = -1; //NALU的个数
//进行h264编码
if(x264_encoder_encode(gRtmpPusher->x264_encoder,&nal, &n_nal,
&(gRtmpPusher->pic_in), &(gRtmpPusher->pic_out)) < 0){
LOGE("%s","编码失败");
return;
} else {
// 初始化编码器的时候,pic_in->i_pts = 0
// 每编码一帧 i_pts累加1
gRtmpPusher->pic_in.i_pts += 1;
}
//使用rtmp协议将h264编码的视频数据发送给流媒体服务器
//帧分为关键帧和普通帧,为了提高画面的纠错率,关键帧应都包含SPS和PPS数据
int sps_len=0, pps_len=0;
unsigned char sps[256];
unsigned char pps[256];
memset(sps,0,256);
memset(pps,0,256);
//遍历NALU数组,根据NALU的类型判断
for(int i=0; i < n_nal; i++){
if(nal[i].i_type == NAL_SPS) {
//复制SPS数据
sps_len = nal[i].i_payload - 4;
memcpy(sps,nal[i].p_payload + 4, (size_t) sps_len); //不复制四字节起始码
if(sps_len>0 && pps_len>0) {
//发送序列信息
//h264关键帧会包含SPS和PPS数据
add_param_sequence(pps,sps,pps_len,sps_len);
sps_len=0;pps_len=0;
}
}else if(nal[i].i_type == NAL_PPS){
//复制PPS数据
pps_len = nal[i].i_payload - 4;
memcpy(pps,nal[i].p_payload + 4, (size_t) pps_len); //不复制四字节起始码
if(sps_len>0 && pps_len>0) {
//发送序列信息
//h264关键帧会包含SPS和PPS数据
add_param_sequence(pps,sps,pps_len,sps_len);
sps_len=0;pps_len=0;
}
}else{
//发送普通帧信息
add_common_frame(nal[i].p_payload, nal[i].i_payload);
}
}
(*env)->ReleaseByteArrayElements(env, array, nv21_buffer, 0);
}
我们从初始化编码器的输入图像pic_in(结构体x264_picture_t)获取帧对象img(结构体x264_image_t)再获取缓冲区。这样的数据结构设计和ffmpeg是一样的,因为我们初始化编码器的时候指定的是I420的模式,也就是YU12,就是YYYYYYYY UU VV的存储格式,所以很方便对应img.plane[0,1,2]三个缓冲区,方便操作。这就是为啥我选择I420的模式,不选用NV21的模式原因。如果选择NV21的模式,就不知道怎样的写入img.plane的缓冲区了,所以还是以I420的格式分批写入Y、U、V。
然后我们根据NV21 和 I420两者存储格式的差别,分别写入UV分量。之后就可以扔给x264_encoder_encode进行编码了。编码出来的结果是一组x264_nal_t的数组,其个数就是第三个传入参数的返回。编码成功之后记得把输入的图像pic_in.i_pts += 1; 确保编码的顺序性,也方便解码的同步。
这一组编码后的NAL Unit 数组,其中包含了关键帧(I)和普通帧(P)两种图像类型的NALUnit,以及每一帧关键帧前都附带的SPS和PPS这两种参数类型的NALUnit,共4种类型的NALUnit,这4种类型的NALUnit都需要分别的进行处理,才能打包发送流媒体服务器。
一个for循环遍历这个NALUnit数组,根据结构体x264_nal_t 当中的 i_type 字段可以判断其类型。如果是SPS和PPS,我们就缓存到相应的栈变量上(记得内存复制的时候,要去掉开头的起始码的4个字节长度)。等待SPS和PPS构成一组对应参数集之后,我们就可以根据自行封装,最后通过rtmp发送到流媒体服务器。
so,怎么封装呢?一样是根据协议来呗。先上代码:
/**
* 打包h264的SPS与PPS->NALU
*/
void add_param_sequence(unsigned char* pps, unsigned char* sps, int pps_len, int sps_len)
{
int body_size = 16 + sps_len + pps_len; //按照H264标准配置SPS和PPS,共使用了16字节
unsigned char* body = malloc(sizeof(char)*body_size);
memset(body, 0, body_size);
int i = 0;
//二进制表示:00010111
body[i++] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
body[i++] = 0x00;//AVCPacketType=0(AVC sequence header)表示设置AVCDecoderConfigurationRecord
//composition time 0x000000 24bit ?
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
/*AVCDecoderConfigurationRecord*/
body[i++] = 0x01;//configurationVersion,版本为1
body[i++] = sps[1];//AVCProfileIndication
body[i++] = sps[2];//profile_compatibility
body[i++] = sps[3];//AVCLevelIndication
body[i++] = 0xFF;//lengthSizeMinusOne,H264视频中NALU的长度,计算方法是 1 + (lengthSizeMinusOne & 3),实际测试时发现总为FF,计算结果为4.
/*sps*/
body[i++] = 0xE1;//numOfSequenceParameterSets:SPS的个数,计算方法是 numOfSequenceParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1.
body[i++] = (unsigned char) ((sps_len >> 8) & 0xff);//sequenceParameterSetLength:SPS的长度
body[i++] = (unsigned char) (sps_len & 0xff);//sequenceParameterSetNALUnits
memcpy(&body[i], sps, (size_t) sps_len);
i += sps_len;
/*pps*/
body[i++] = 0x01;//numOfPictureParameterSets:PPS 的个数,计算方法是 numOfPictureParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1.
body[i++] = (unsigned char) ((pps_len >> 8) & 0xff);//pictureParameterSetLength:PPS的长度
body[i++] = (unsigned char) ((pps_len) & 0xff);//PPS
memcpy(&body[i], pps, (size_t) pps_len);
i += pps_len;
// ... 未完待续...
}
根据h264协议,需要正确的追加H264 header的信息才能正确的被认为是这个NALU是h264的编码。怎么理解这句话?画一个简单的示意图帮助大家理解。
这下应该明白了吧?那么h264配置头信息的规则是怎样的呢?以下就给出了。
一开始是VideoTagHeader,只占一个字节,含着视频帧类型及视频CodecID最基本信息。
(1) FrameType,4bit,帧类型
1 = key frame (for AVC, a seekable frame)
2 = inter frame (for AVC, a non-seekable frame)
3 = disposable inter frame (H.263 only)
4 = generated key frame (reserved for server use only)
5 = video info/command frame
H264的一般为1或者2.
(2)CodecID ,4bit,编码类型
1 = JPEG(currently unused)
2 = Sorenson H.263
3 = Screen video
4 = On2 VP6
5 = On2 VP6 with alpha channel
6 = Screen video version 2
7 = AVC
VideoTagHeader之后跟着的就是VIDEODATA数据了,也就是AVCPacketType 。如果视频的格式是AVC(H.264)的话,VideoTagHeader会多出4个字节的信息。根据类型设置AVCPacketType后,其它都填0。
(3) AVCPacketType 8bit
IF AVCPacketType == 0 AVCDecoderConfigurationRecord(AVC sequence header)(此时FrameType必为1)
IF AVCPacketType == 1 One or more NALUs (Full frames are required)
IF AVCPacketType == 2 AVC end of sequence (lower level NALU sequence ender is not required or supported)
Composition Time,24bit
接下来设置AVCDecoderConfigurationRecord,它包含的是H.264解码相关比较重要的SPS和PPS信息,在给AVC解码器送数据流之前一定要把SPS和PPS信息送出,否则的话解码器不能正常解码。而且在解码器stop之后再次start之前,如seek、快进快退状态切换等,都需要重新送一遍SPS和PPS的信息。这部分配置也是最复杂难理解的一部分,慢慢品尝。
(4) AVCDecoderConfigurationRecord(AVCPacketType == 0,FrameType==1)
1.configurationVersion,8bit
2.AVCProfileIndication,8bit
3.profile_compatibility,8bit
4.AVCLevelIndication,8bit
5.lengthSizeMinusOne,8bit (H.264 视频中 NALU 的长度,计算方法是 1 + (lengthSizeMinusOne & 3),实际测试时发现总为ff,计算结果为4.)
6.numOfSequenceParameterSets,8bit (SPS 的个数,计算方法是 numOfSequenceParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1)
7.sequenceParameterSetLength,16bit (SPS 的长度)
8.sequenceParameterSetNALUnits ,sps数据包。长度为sequenceParameterSetLength。
9.numOfPictureParameterSets,8bit (PPS 的个数,计算方法是 numOfPictureParameterSets & 0x1F,实际测试时发现总为E1,计算结果为1。)
10.pictureParameterSetLength,16bit。PPS的长度。
11.pictureParameterSetNALUnits 长度为pictureParameterSetLength。
SPS和PPS的h264头信息配置就是这些了,那么I帧P帧的呢,也是大同小异,这里贴出部分代码进行参考:
/**
* 打包h264的图像(IPB)帧数据->NALU
*/
void add_common_frame(unsigned char *buf ,int len)
{
// 每个NALU之间通过startcode(起始码)进行分隔,起始码分成两种:0x000001(3Byte)或者0x00000001(4Byte)。
// 如果NALU对应的Slice为一帧的开始就用0x00000001,否则就用0x000001。
//去掉起始码(界定符)
if(buf[2] == 0x00){ //00 00 00 01
buf += 4;
len -= 4;
}else if(buf[2] == 0x01){ // 00 00 01
buf += 3;
len -= 3;
}
int body_size = len + 9;
unsigned char* body = malloc(sizeof(char)*body_size);
memset(body, 0, body_size);
//buf[0] NAL Header与运算,获取type,根据type判断关键帧和普通帧
//当NAL头信息中,type(第一个字节的前5位)等于5,说明这是关键帧NAL单元
int type = buf[0] & 0x1f;
//Inter Frame 帧间压缩 普通帧
body[0] = 0x27;//VideoHeaderTag:FrameType(2=Inter Frame)+CodecID(7=AVC)
//IDR I帧图像
if (type == NAL_SLICE_IDR) {
body[0] = 0x17;//VideoHeaderTag:FrameType(1=key frame)+CodecID(7=AVC)
}
//AVCPacketType = 1
body[1] = 0x01; /*nal unit,NALUs(AVCPacketType == 1)*/
body[2] = 0x00; //composition time 0x000000 24bit
body[3] = 0x00;
body[4] = 0x00;
//写入NALU信息,右移8位,一个字节的读取?
body[5] = (len >> 24) & 0xff;
body[6] = (len >> 16) & 0xff;
body[7] = (len >> 8) & 0xff;
body[8] = (len) & 0xff;
/*copy data*/
memcpy(&body[9], buf, (size_t) len);
// ... 未完待续
}
到AVCPacketType 的前部分都一样的,我们根据上面说的协议规则填对应位数就可以了。这里插入一点NALU的小知识,每个NALU第一个字节(NAL-Header)的前5位标明的是该NAL包的类型,即NAL nal_unit_type,所以我们需要取第一个字节的内容,(与位操作& 0x1f) 即 (& 0001 1111)获取其帧类型。
此时我们的AVCPacketType == 1 One or more NALUs (Full frames are required)
之后的信息就变成了NALU的长度和具体的数据包就完成了。1.nal_length,32bit。每个nal包长度前面4个字节为计算结果。
2.nal 具体的数据包
至此,编码协议h264也就分析到这,再复杂的需求也得是根据协议进行操作。所以大家还是好好掌握这次文章的内容吧。