NDK学习笔记:RtmpPusher之深度学习编码协议h264

NDK学习笔记:RtmpPusher之深度学习编码协议h264

 

认识编码协议 h264

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)

NDK学习笔记:RtmpPusher之深度学习编码协议h264_第1张图片

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数据格式如下图:

NDK学习笔记:RtmpPusher之深度学习编码协议h264_第2张图片    NDK学习笔记:RtmpPusher之深度学习编码协议h264_第3张图片

我们先看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的编码。怎么理解这句话?画一个简单的示意图帮助大家理解。

NDK学习笔记:RtmpPusher之深度学习编码协议h264_第4张图片

这下应该明白了吧?那么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也就分析到这,再复杂的需求也得是根据协议进行操作。所以大家还是好好掌握这次文章的内容吧。 

你可能感兴趣的:(NDK学习笔记,深度学习编码协议h264)