1、H264介绍

1.1 h264概述

H264是由ISO和ITU共同提出的继MPEG4之后的新一代数字视频压缩格式,高度压缩数字视频编解码器标准。

经过压缩后的帧分为:I帧,P帧和B帧:

  • I帧:关键帧,采用帧内压缩技术。(自身可以通过视频解压算法解压成一张单独的完整的图片)
  • P帧:向前参考帧,在压缩时,只参考前面已经处理的帧(只需要参考前面的I帧或P帧),采用帧内压缩技术。
  • B帧:双向参考帧,在压缩时,它既参考前面的帧,又参考后面的帧(需要同时参考前面和后面的I帧或P帧),采用帧间压缩技术。

I帧和IDR帧的关系:

        IDR帧就是I帧,但是I帧不一定是IDR帧。在一个完整的视频流单元中第一个图像帧是IDR帧,IDR帧是强制刷新帧,在解码过程中,当出现了IDR帧时,要更新sps、pps,原因是防止前面I帧错误,导致sps,pps参考I帧导致无法纠正。

除了I/P/B帧外,还有图像序列GOP:

  • GOP:两个I帧之间是一个图像序列,在一个图像序列中只有一个I帧。
  • GOP值越大,那么I帧率之间P帧和B帧数量越多,图像画质越精细,如果GOP是120,如果分辨率是720P,帧率是60,那么两I帧的时间就是120/60=2s.

(图出自H.264视频压缩标准白皮书)

在H.264基准类中,仅使用I帧和P帧以实现低延时,因此是网络摄像机和视频编码器的理想选择。

1.2 h264原始码流结构

H264分为两层,VCL(视频编码层)和 NAL(网络提取层)

  • VCL:包括核心压缩引擎和块,宏块和片的语法级别定义,设计目标是尽可能地独立于网络进行高效的编码。
  • NAL:负责将VCL产生的比特字符串适配到各种各样的网络和多元环境中,覆盖了所有片级以上的语法级别。

VCL数据传输或者存储之前,会被映射到一个NALU中,H264数据包含一个个NALU,如下图:

NALU

H.264 的编码视频序列包括一系列的NAL 单元,每个NAL 单元包含一个RBSP。编码片(包括数据分割片IDR 片)和序列RBSP 结束符被定义为VCL NAL 单元,其余为NAL 单元。典型的RBSP 单元序列如图2 所示,每个单元都按独立的NAL 单元传送。单元的信息头(一个字节)定义了RBSP 单元的类型,NAL 单元的其余部分为RBSP 数据。

一个NALU = 一组对应于视频编码的NALU头部信息 + 一个原始字节序列负荷(RBSP, Raw Byte Sequence Payload)


NAL单元格式如下图:

NALU结构

一个原始的NALU结构包含以下三部分:
[StartCode] + [NALU Header] + [NALU Payload]

StartCode 是一个NALU单元的开始,必须是00 00 00 01 或者 00 00 01

1.3 NAL单元

       每个NAL单元是一个一定语法元素的可变长字节字符串,包括包含一个字节的头信息(用来表示数据类型),以及若干整数字节的负荷数据。一个NAL单元可以携带一个编码片、A/B/C型数据分割或一个序列或图像参数集。

1.3.1 NAL Header

NALU头由一个字节组成,它的语法如下:

NAL单元按RTP序列号按序传送。其中,T为负荷数据类型,占5-bits;R为重要性指示位,占2-bits;最后的F为禁止位,占1bit。具体如下:

  • NALU类型位:可以表示NALU的32种不同类型特征,类型1~12是H.264定义的,类型24~31是用于H.264以外的,RTP负荷规范使用这其中的一些值来定义包聚合和分裂,其他值为H.264保留。
  • 重要性指示位:用于在重构过程中标记一个NAL单元的重要性,值越大,越重要。值为0表示这个NAL单元没有用于预测,因此可被解码器抛弃而不会有错误扩散;值高于0表示此NAL单元要用于无漂移重构,且值越高,对此NAL单元丢失的影响越大。
  • 禁止位:编码中默认值为0,当网络识别此单元中存在比特错误时,可将其设为1,以便接收方丢掉该单元,主要 用于适应不同种类的网络环境(比如有线无线相结合的环境)。

F:禁止位,0表示正常,1表示错误;H264定义此位必须为0

NRI:标识这个NALU单元的重要性(3最高)即:11表示非常重要

TYPE:表示该NALU单元的类型,见下表:

NALU类型是我们判断帧类型的利器,从官方文档中得出如下图:

nal_unit_type

由图可知:7为序列参数集(SPS),8为图像参数集(PPS),5代表关键帧(I帧)

1.3.2 RBSP序列
RBSP序列举例

 下面是RBSP序列的描述

RBSP序列

1.4 h264帧判断

H264常见的帧头数据为:

00 00 00 01 61 (P帧)

00 00 00 01 65 ( IDR帧)

00 00 00 01 67 (SPS)

00 00 00 01 68 (PPS)

上述的67、68、65、61,还有41等,都是NALU的识别级别

最上面图的码流对应的数据来层层分析,以00 00 00 01分割之后的下一个字节就是NALU类型,将其转为二进制数据后,

解读顺序为从左往右算,如下:

(1)第1位禁止位,值为1表示语法出错

(2)第2~3位为参考级别

(3)第4~8为是nal单元类型

例如上面00000001后有67、68以及65

其中0x67的二进制码为:

0110 0111

4-8位为00111,转为十进制为7,7对应序列参数集SPS

其中0x68的二进制码为:

0110 1000
4-8位为01000,转为十进制为8,8对应图像参数集PPS

其中0x65的二进制码为:

0110 0101

4-8位为00101,转为十进制为5,5对应IDR图像中的片(I帧)

所以判断是否为I帧的算法为:

(NALU类型 & 0001 1111)= 5 (即:NALU类型 & 31) = 5      比如0x65 & 31 = 5

即:int value = buf[4] & 0x0f;   // 5是I帧,7是sps,8是pps

2、RTP打包发送H264之封包详解

2.1 RTP头结构

0                   1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |V=2|P|X|  CC   |M|     PT      |       sequence number         |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                           timestamp                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |           synchronization source (SSRC) identifier            |
      +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
      |            contributing source (CSRC) identifiers             |
      |                             ....                              |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  • 负载类型 Payload type (PT): 7 bits
  • 序列号 Sequence number (SN): 16 bits
  • 时间戳 Timestamp: 32 bits

H.264 Payload 格式定义了三种不同的基本的负载(Payload)结构。接收端可能通过 RTP Payload的第一个字节来识别它们。这一个字节类似 NALU 头的格式,而这个头结构的 NAL 单元类型字段则指出了代表的是哪一种结构。

2.2 H264的码流结构

H.264流有两种格式:

  • 一种是annexb,也是传统模式,裸流一般都是annexb格式
  • annexb格式会在数据包前面加上startcode,然后在后面加上UALU包(NALU Header + RBSP)
  • 这个startcode用来做字节流对齐,以及分割流数据。
  • 将SPS和PPS都作为一个NALU进行封装,每一次遇到 I 帧之前,都是重复加上SPS和PPS,这个SPS的作用就是提供了序列信息,比如解码信息,而这个PPS提供的是图像信息,比如如何压缩等。
  • NALU封装了SPS、PPS、SEI、
  • annexb格式的好处,就是解码器可以从任意一个包开始解码。

  • 另外种就是AVCC模式,例如mp4、mkv都属于AVCC格式
  • 没有startcode,直接是一个个UALU包
  • 解码器配置参数在一开始就配置好了,使用NALU长度作为NALU的边界,不需要额外的起始码
  • SPS和PPS都封装在文件头部的extradata中;
  • 好处就是播放器直接能识别,去除了大量的startcode、sps、pps,缩小了文件大小;

 以annexb格式为例:

可能的结构类型分别有:

  • 单一 NAL 单元模式
    即一个 RTP 包仅由一个完整的 NALU 组成. 这种情况下 RTP NAL 头类型字段和原始的 H.264的
    NALU 头类型字段是一样的.

  • 组合封包模式
    即可能是由多个 NAL 单元组成一个 RTP 包. 分别有4种组合方式: STAP-A, STAP-B, MTAP16, MTAP24.
    那么这里的类型值分别是 24, 25, 26 以及 27.

  • 分片封包模式
    用于把一个 NALU 单元封装成多个 RTP 包. 存在两种类型 FU-A 和 FU-B. 类型值分别是 28 和 29.

2.2.1  单个 NAL 单元模式
  • 一个封装单个NAL单元包到RTP的NAL单元流的RTP序号必须符合NAL单元的解码顺序
  • 对于 NALU 的长度小于 MTU 大小的包,一般采用单一 NAL 单元模式
  • 对于一个原始的 H.264 NALU 单元常由 [Start Code] [NALU Header] [NALU Payload] 三部分组成,其中 [Start Code] 用于标示这是一个NALU 单元的开始,必须是 "00 00 00 01" 或 "00 00 01",NALU 头仅一个字节, 其后都是 NALU 单元内容
  • 打包时去除 "00 00 01" 或 "00 00 00 01" 的开始码,把其他数据封包的 RTP包即可
   0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |F|NRI|  type   |                                               |
  +-+-+-+-+-+-+-+-+                                               |
  |                                                               |
  |               Bytes 2..n of a Single NAL unit                 |
  |                                                               |
  |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                               :...OPTIONAL RTP padding        |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

例子:

如有一个 H.264 的 NALU 是这样的:[00 00 00 01 67 42 A0 1E 23 56 0E 2F … ]

这是一个序列参数集 NAL 单元。[00 00 00 01] 是4个字节的开始码,67 是 NALU 头,42 开始的数据是 NALU 内容。

封装成 RTP 包将如下:

[ RTP Header ] [ 67 42 A0 1E 23 56 0E 2F ]

即只要去掉 4 个字节的开始码就可以了

2.2.2  组合封包模式

当 NALU 的长度特别小时, 可以把几个 NALU 单元封在一个 RTP 包中。

0                   1                   2                   3
       0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                          RTP Header                           |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |STAP-A NAL HDR |         NALU 1 Size           | NALU 1 HDR    |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                         NALU 1 Data                           |
      :                                                               :
      +               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |               | NALU 2 Size                   | NALU 2 HDR    |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                         NALU 2 Data                           |
      :                                                               :
      |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      |                               :...OPTIONAL RTP padding        |
      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2.2.3  FU-A的分片封包模式

而当 NALU 的长度超过 MTU 时,就必须对 NALU 单元进行分片封包。也称为 Fragmentation Units (FUs)

   0                   1                   2                   3
   0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  | FU indicator  |   FU header  |                               |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+                               |
  |                                                               |
  |                         FU payload                            |
  |                                                               |
  |                               +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  |                               :...OPTIONAL RTP padding        |
  +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  Figure 14.  RTP payload format for FU-A

1) FU indicator有以下格式:

  +---------------+
  |0|1|2|3|4|5|6|7|
  +-+-+-+-+-+-+-+-+
  |F|NRI|  Type   |
  +---------------+

FU指示字节的类型域的28,29表示FU-A和FU-B。NRI域的值必须根据分片NAL单元的NRI域的值设置。(此处Type就是rtp分片类型) 见下表

  Type    Packet      Type name                       
  ---------------------------------------------------------
  0       undefined                                    -
  1-23    NAL unit     Single NAL unit packet per H.264  
  24      STAP-A       Single-time aggregation packet    
  25      STAP-B       Single-time aggregation packet    
  26      MTAP16       Multi-time aggregation packet     
  27      MTAP24       Multi-time aggregation packet     
  28      FU-A         Fragmentation unit                
  29      FU-B         Fragmentation unit                 
  30-31   undefined  

2 ) FU header的格式如下:

  +---------------+
  |0|1|2|3|4|5|6|7|
  +-+-+-+-+-+-+-+-+
  |S|E|R|  Type   |
  +---------------+

S: 1 bit

当设置成1,开始位指示分片NAL单元的开始。当跟随的FU荷载不是分片NAL单元荷载的开始,开始位设为0。

E: 1 bit

当设置成1,结束位指示分片NAL单元的结束,即荷载的最后字节也是分片NAL单元的最后一个字节。
当跟随的FU荷载不是分片NAL单元的最后分片,结束位设置为0。

R: 1 bit
保留位必须设置为0,接收者必须忽略该位。

Type: 5 bits
此处的Type就是NALU头中的Type,取1-23的那个值,表示 NAL单元荷载类型定义

2.3 解包过程

当接收端接收到一个一个的RTP包时,需要将其中的H264数据取出,并还原原始的H264帧,然后才能交给解码器解码。

单一NALU模式和组合封包模式解包过程其实比较简单,主要看一下分片封包模式:

步骤一:当接收到接收到FU-A类型的RTP Packet时,需要看FU Header中的S / E两位:

  • 如果时10,表示时一帧数据的开头部分;
  • 如果是00,表示一帧数据的中间部分,有可能多个00的分片;
  • 如果是01,表示一帧数据的结尾。

步骤二:把之前所有的数据都暂时保存、拼接成完整的一帧H264数据,然后再插入start code[00 00 00 01],便可以交给解码器解码;

步骤三:还原后的NAL头的八位是由FU indicator的前三位加FU header的后五位组成,即:

nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f)

简略代码如下:

auto nalType = data[0] & 0x1F;

switch(nalType)
{
    ...
    //Fragmentationunit
    case NAL_UNIT_TYPE_FU_A:
    {
        packFlag = data[1] & 0xC0;
        switch (packFlag) {
            //NAL Unit start packet
            case 0x80://一帧的开头
                NALUnit[4] = (byte) ((data[0] & 0xE0) | (data[1] & 0x1F));
                pktDatacopy(data, 2, NALUnit, 5, length - 2);
                tmpLen = length + 5 - 2;
                break;
            //NAL Unit middle packet
            case 0x00:
                pktDatacopy(data, 2, NALUnit, tmpLen, length - 2);
                tmpLen +=length - 2;
                break;
            //NAL Unit end packet
            case 0x40:
                pktDatacopy(data, 2, NALUnit, tmpLen, length - 2);
                tmpLen += length - 2;

                if (getH264Stream()) {            
                      decodeH264Stream(NALUnit, 0, tmpLen);//开始解码
                }
                tmpLen =0;
 
                break;
         }
    }
    break;
    ...
}

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐