FFmpeg学习(六)H264流媒体协议解析

一:H264了解(可跳过)

(一)H.264的主要目标

1.高的视频压缩比;2.良好的网络亲和性;

为了完成这些目标H264的解决方案是:

1.VCL   video coding layer    视频编码层;    视频编码层,H264编码/压缩的核心,主要负责将视频数据编码/压缩,再切分。
2.NAL   network abstraction layer   网络提取层;   网络抽象层,负责将VCL的数据组织打包。

其中,VCL层是对核心算法引擎,宏块及片的语法级别的定义,他最终输出编码完的数据 SODB;

1.压缩:预测(帧内预测和帧间预测)-> DCT变化和量化 -> 比特流编码;
2.切分数据,主要为了第三步。这里一点,网上看到的“切片(slice)”、“宏块(macroblock)”是在VCL中的概念,一方面提高编码效率和降低误码率、另一方面提高网络传输的灵活性。(这块本文将不展开)
3.输出编码完的数据 SODB

NAL层定义片级以上的语法级别(如序列参数集和图像参数集,针对网络传输),

同时支持以下功能:独立片解码,起始码唯一保证,SEI以及流格式编码数据传送,NAL层将SODB打包成RBSP然后加上NAL头,组成一个NALU(NAL单元);

NAL旨在提供“网络友好性”,以便为各种系统简单有效地定制VCL的使用。 NAL有助于将VCL数据映射到传输层,例如:
· RTP / IP适用于任何类型的实时有线和无线互联网服务。
· 文件格式,例如用于存储和MMS的ISO MP4。
· H.32X用于有线和无线会话服务。
· 用于广播服务等的MPEG-2系统。

(二)名词解释(和下面没关系,可以忽略)

H.264 中,句法元素共被组织成 序列、图像、片、宏块、子宏块五个层次。

H.264 的码流结构并没有大家想的那么复杂,编码后视频的每一组图像(GOP,图像组)都给予了传输中的序列(PPS)和本身这个帧的图像参数(SPS),所以,我们的整体结构,应该如此:

GOP (图像组)主要用作形容一个 i 帧 到下一个 i 帧之间的间隔了多少个帧,增大图片组能有效的减少编码后的视频体积,但是也会降低视频质量,至于怎么取舍,得看需求了。
注意: GOP是不包括SPS、PPS等NALU的
GOP 就是一段连续图像帧且画面之间变化不大。//当某个帧与前面的帧图像变化很大,无法参考前面帧生成时,就要开始一个新序列。

补充:
宏块:宏块大小通常为16×16像素,分为I、B、P宏块;编码处理的基本单元,由多个块组成。
:一个编码图像要划分成多个块才能进行处理,一个块是4X4像素

------1.一帧图片跟 NALU 的关联 :

一帧图片经过 H.264 编码器之后,就被编码为一个或多个片(slice)。<编码阶段,由VCL实现>

而装载着这些片(slice)的载体,就是 NALU 了,我们可以来看看 NALU 跟片的关系(slice)。<由NAL实现>

注意:实际中NAL还要对切片进行处理,然后形成NALU。

片(slice)的概念不同与帧(frame),帧(frame)是用作描述一张图片的,一帧(frame)对应一张图片,而片(slice),是 H.264 中提出的新概念,是通过编码图片后切分通过高效的方式整合出来的概念,一张图片至少有一个或多个片(slice)。

上图中可以看出,片(slice)都是又 NALU 装载并进行网络传输的,但是这并不代表 NALU 内就一定是切片,这是充分不必要条件,因为 NALU 还有可能装载着其他用作描述视频的信息,比如后面提到的SPS、PPS。

------2.什么是切片(slice)?

片的主要作用是用作宏块(Macroblock)的载体(ps:下面会介绍到宏块的概念)。片之所以被创造出来,主要目的是为限制误码的扩散和传输。

如何限制误码的扩散和传输?

每个片(slice)都应该是互相独立被传输的,某片的预测(片(slice)内预测和片(slice)间预测)不能以其它片中的宏块(Macroblock)为参考图像。

我们可以理解为一 张/帧 图片可以包含一个或多个分片(Slice),而每一个分片(Slice)包含整数个宏块(Macroblock),即每片(slice)至少一个 宏块(Macroblock),最多时每片包 整个图像的宏块。
上图结构中,我们不难看出,每个分片也包含着头和数据两部分:
1、分片头中包含着分片类型、分片中的宏块类型、分片帧的数量、分片属于那个图像以及对应的帧的设置和参数等信息。
2分片数据中则是宏块,这里就是我们要找的存储像素数据的地方。

------3.什么是宏块?

宏块是视频信息的主要承载者,因为它包含着每一个像素的亮度和色度信息视频解码最主要的工作则是提供高效的方式从码流中获得宏块中的像素阵列。
组成部分:
  一个宏块由一个16×16亮度像素附加的一个8×
8 Cb和一个 8×8 Cr 彩色像素块组成。
  每个图象中,若干宏块被排列成片的形式。

从上图中,可以看到,宏块中包含了宏块类型、预测类型、Coded Block Pattern 编码的块模式Quantization Parameter 量化参数、像素的亮度和色度数据集等等信息。

------4. 切片(slice)类型跟宏块类型的关系

I片:只包 I宏块,I 宏块利用从当前片中已解码的像素作为参考进行帧内预测(不能取其它片中的已解码像素作为参考进行帧内预测)。

P片:可包 P和I宏块,P 宏块利用前面已编码图象作为参考图象进行帧内预测,一个帧内编码的宏块可进一步作宏块的分割:即 16×16、16×8、8×16 或 8×8 亮度像素块(以及附带的彩色像素);如果选了 8×8 的子宏块,则可再分成各种子宏块的分割,其尺寸为 8×8、8×4、4×8 或 4×4 亮度像素块(以及附带的彩色像素)。

B片:可包 B和I宏块,B 宏块则利用双向的参考图象(当前和 来的已编码图象帧)进行帧内预测。

SP片(切换P):用于不同编码流之间的切换,包含 P 和/或 I 宏块

SI片:扩展档次中必须具有的切换,它包 了一种特殊类型的编码宏块,叫做 SI 宏块,SI 也是扩展档次中的必备功能。

二:H264的格式

详见更多:https://blog.csdn.net/qq_42024067/article/details/102292535 

H.264的两种打包/封装方法:字节流AnnexB格式 和 AVCC格式 (只有这两种)

其中在https://www.yuque.com/keith-an9fr/aab7xp/vng2pb中介绍的应该是有部分问题!!!

在H264用于网络发送时要封装成RTP格式!!!

(一)AnnexB格式---用于实时播放

------1.AnnexB流结构:使用start code分隔NAL(start code为三字节或四字节,0x000001或0x00000001,一般是四字节);SPS和PPS按流的方式写在头部

开始前缀(00000001或000001)+ NALU数据  绝大部分编码器的默认输出格式

NALU 就是 h264的实际数据部分。NALU = NALUHeader+EBSP 组成; EBSP = 防止竞争码+RBSP; RBSP = SODB + RBSP尾部 。

EBSP为扩展字节序列载荷(Encapsulated Byte Sequence Payload)  EBSP = RBSP插入防竞争字节0x03
RBSP为原始字节序列载荷(Raw Byte Sequence Payload)-------- RBSP = SODB + RBSP Trailing Bits(RBSP尾部补齐字节);引入RBSP Trailing Bits做8位字节补齐。
SODB为原始数据比特流 (String Of Data Bits)   -------   就是最原始的编码/压缩得到的数据。

H264码流结构:(下面两种表示都不错)

一共有两种起始码start_code:

   ①3字节0x000001  单帧多slice(即单帧多个NALU)之间间隔
   ②4字节0x00000001 帧之间,或者SPS、PPS等之前

4字节类型的起始码在连续的数据传输中非常有用,因为用字节来对齐、分割流数据,比如:用连续的31个bit 0 后接一个bit 1 来分割流数据,是很容易的。

如果接下来的bit是0(因为每个NALU都以bit0开始),那么这就是一个NALU包数据的起始位置了。4字节类型的开始码通常只用于标识流中的随机访问点,如SPS PPS AUD和IDR,然后其他地方都用3字节类型的开始码以减少数据量

防止竞争字节(0x03:前面讲到用StartCode的字节串来分割NALU,于是问题来了,如果RBSP中也包括了StartCode(0x000001或0x00000001)怎么办呢?所以,就有了防止竞争字节(0x03):

编码时,扫描RBSP,如果遇到连续两个0x00字节,就在后面添加防止竞争字节(0x03);
解码时,同样扫描EBSP,进行逆向操作即可。

编码如下:

解码操作:

在解码的时候如果在内部遇到0x000003序列时,就可以将其抛弃即可以恢复原始数据。EBSP 去除防止竞争码后就可以得到 RBSP。

------2.NALU Header

上面是截取的 NAL 层的语法头部部分。如果先不考虑语法,可以先如下理解,将第一个字节(1+2+5 正好是一个字节)按 bits 展开。

  • 第一位为 forbidden_zero_bit forbidden_zero_bit 禁止位,初始为0,当网络发现NAL单元有比特错误时可设置该比特为1,以便接收方纠错或丢掉该单元。
  • 后两位为 nal_ref_idc nal_ref_idc 代表 NALU 的重要性。值越大说明约重要。取值范围0~3,解码器在解码处理不过来的时候,可以丢掉重要性为0的NALU。当前的 NAL 是参考帧,序列集参数集或图像集重要数据时必须大于0。
  • 最后五位为 nal_unit_type 指的是当前 NAL 的类型。

1-4:I/P/B帧,合起来介绍的原因是,他们是依据VLC的slice区分的,这块因为本文不涉及,一方面是这个太过于细节,真要展开篇幅太长;另一个原因是就算不了解slice、macroblock也不影响对H264格式的理解。
5IDR帧。I帧的一种,告诉解码器,之前依赖的解码参数集合(接下来要出现的SPS\PPS等)可以被刷新了。
6:SEI,英文全称Supplemental Enhancement Information,翻译为“补充增强信息”,提供了向视频码流中加入额外信息的方法。
7:SPS,全称Sequence Paramater Set,翻译为“序列参数集”。SPS中保存了一组编码视频序列(Coded Video Sequence)的全局参数。因此该类型保存的是和编码序列相关的参数。
8: PPS,全称Picture Paramater Set,翻译为“图像参数集”。该类型保存了整体图像相关的参数。
9AU分隔符,AU全称Access Unit,它是一个或者多个NALU的集合,代表了一个完整的帧。

根据以上字段:判断类型

例如上面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帧)

其中0x41的二进制码为:0100 0001

4-8为00001,转为十进制1,参考第二幅图:1对应非IDR图像中的片(这里指的是P帧)

其中0x61的二进制码为:0110 0001

4-8为00001,转为十进制1,参考第二幅图:1对应非IDR图像中的片(同上,为P帧,仅仅是重要性不同)

其中0x06的二进制码为:0000 0100

4-8为00100,转为十进制6,参考第二幅图:6对应SEI

特殊的NALU类型:SPS和PPS

SPS和PPS存储了编解码需要一些图像参数。

AnnexB格式每个NALU都包含起始码,且通常会周期性的在关键帧之前重复SPS和PPS (在I帧之前)
  👉👉👉所以解码器可以从视频流随机点开始进行解码,实时的流格式

I/P/B帧:

I帧(帧内编码帧)是一种自带全部信息的独立帧无需参考其它图像便可独立进行解码。可以理解为这一帧画面的完整保留;解码时只需要本帧数据就可以完成(因为包含完整画面)

视频序列中的第一个帧始终都是I帧。
如果所传输的比特流遭到破坏,则需要将I帧用作新查看器的起始点或重新同步点
I帧可以用来实现快进、快退以及其它随机访问功能。
1.它是一个全帧压缩编码帧。它将全帧图像信息进行JPEG压缩编码及传输;
2.解码时仅用I帧的数据就可重构完整图像;
3.I帧描述了图像背景和运动主体的详情;
4.I帧不需要参考其他画面而生成;
5.I帧P帧和B帧的参考帧(其质量直接影响到同组中以后各帧的质量);
6.I帧是帧组GOP的基础帧(第一帧),在一组中只有一个I帧;
7.I帧不需要考虑运动矢量;
8.I帧所占数据的信息量比较大。

P帧(帧间预测编码帧)需要参考前面的I帧和/或P帧的不同部分才能进行编码。

与I帧相比,P帧通常占用更少的数据位,但其缺点是,由于P帧对前面的P和I参考帧有着复杂的依赖性,因此对传输错误非常敏感
P帧属于前向预测的帧间编码,它只参考 前面最靠近它 的I帧或者P帧
P帧是以I帧为参考帧,在I帧中找出P帧“某点”的预测值和运动矢量,取预测差值和运动矢量一起传送。
在接收端根据运动矢量从I帧中找出P帧“某点”的预测值并与差值相加以得到P帧“某点”样值,从而可得到完整的P帧。

1.P帧是I帧后面相隔1~2帧的编码帧;
2.P帧采用运动补偿的方法传送它与前面的IP帧的差值及运动矢量(预测误差);
3.解码时必须将I帧中的预测值与预测误差求和后才能重构完整的P帧图像;
4.P帧属于前向预测的帧间编码。它只参考前面最靠近它的I帧或P帧;
5.P帧可以是其后面P帧的参考帧,也可以是前后 的B帧的参考帧;
6.由于P帧是参考帧,它可能造成解码错误的扩散;
7.由于是差值传送,P帧的压缩比较高

B帧:双向预测内插编码帧。

B帧是双向差别帧,也就是B帧记录的是本帧与前后帧的差别(具体比较复杂,有4种情况,但我这样说简单些),
换言之,要解码B帧,不仅要取得之前的缓存画面,还要解码之后的画面,通过前后画面与本帧数据的叠加取得最终的画面。B帧压缩率高,但是解码时CPU会比较累
1.B帧是前面的I或P帧后面的P帧 来进行预测的;
2.B帧传送的是它与前面的I或P帧和后面的P帧之间预测误差运动矢量;
3.B帧是双向预测编码帧;
4.B帧压缩比最高,因为它只反映丙参考帧间运动主体的变化情况,预测比较准确;
5.B帧不是参考帧,不会造成解码错误的扩散。

I、B、P各帧是根据压缩算法的需要,是人为定义的,它们都是实实在在的物理帧。一般来说,I帧的压缩率是7(跟JPG差不多),P帧是20,B帧可以达到50。可见使用B帧能节省大量空间,节省出来的空间可以用来保存多一些I帧,这样在相同码率下,可以提供更好的画质。

AU分隔:

一个单独的NALU包、或者甚至一个VCL NALU包都不意味着是一个独立的帧,一帧数据可以被分割成几个NALU,一个或多个NALU组成了一个Access Units(AU),AU包含了一个完整的帧。把帧分割成几个独立的NALU需要耗费许多CPU资源,所以分割帧数据并不经常使用。实际上AU分隔不常用:http://www.360doc.com/content/13/0913/15/13084517_314201133.shtml

案例:分析一帧数据(查看了其他视频,好像都是以大端方式存放:高地址放低位,比如 0000 0001,按2字节存放时,就是高地址<越往后越高>放低位01,低地址存放高位00)

0000 0001 6764 000a ac72 8444 2684 0000
0300 0400 0003 00ca 3c48 9611 8000 0000
0168 e843 8f13 2130 0000 0165 8881 0005
4e7f 87df 61a5 8b95 eea4 e938 b76a 306a
71b9 5560 0b76 2eb5 0ee4 8059 27b8 67a9
6337 5e82 2055 fbe4 6ae9 3735 72e2 2291
9e4d ff60 86ce 7e42 b795 ce2a e126 be87
7384 26ba 1636 f4e6 9f17 dad8 6475 54b1
f345 0c0b 3c74 b39d bceb 5373 87c3 0e62
4748 62ca 59eb 863f 3afa 86b5 bfa8 6d06
1650 82c4 ce62 9e4e e64c c730 3ede a10b
d883 0bb6 b828 bca9 eb77 43fc 7a17 9485
21ca 376b 3095 b546 7730 60b7 12d6 8cc5
5485 29d8 69a9 6f12 4e71 dfe3 e2b1 6b6b
bf9f fb2e 5730 a969 76c4 46a2 dffa 91d9
5074 551d 4904 5a1c d686 687c b661 486c
96e6 124c 27ad bac7 5199 8ed0 f0ed 8ef6
6579 79a6 12a1 95db c8ae e3b6 35e6 8dbc
48a3 7faf 4a28 8a53 e27e 6808 9f67 7798
52db 5084 d65e 25e1 4a99 5834 c711 d643
ffc4 fd9a 4416 d1b2 fb02 dba1 8969 34c2
3255 98f9 9bb2 313f 4959 0c06 8cdb a5b2
9d7e 122f d087 9444 e40a 76ef 992d 9118
3950 3b29 3bf5 2c97 7348 9183 b0a6 f34b
702f 1c8f 3b78 23c6 aa86 4643 1dd7 2a23
5e2c d948 0af5 f52c d1fb 3ff0 4b78 37e9
45dd 72cf 8035 c395 07f3 d906 e54a 5876
036c 8120 6245 6544 73bc fec1 9f31 e5db
895c 6b79 d868 90d7 26a8 a188 8681 dc9a
4f40 a523 c7de be6f 76ab 7916 5121 6783
2ef3 d627 1a42 c294 d15d 6cdb 4a7a e2cb
0bb0 680b be19 5900 50fc c0bd 9df5 f5f8
a817 19d6 b3e9 74ba 50e5 2c45 7bf9 93ea
5af9 a930 b16f 5b36 241e 8d55 57f4 cc67
b265 6aa9 3626 d006 b8e2 e373 8bd1 c01c
5215 cab5 ac60 3e36 42f1 2cbd 9977 aba8
a9a4 8e9c 8b84 de73 f091 2997 aedb afd6
f85e 9b86 b3b3 03b3 ac75 6fa6 1169 2f3d
3ace fa53 8660 956c bbc5 4ef3 
pic.h264
with open("pic.h264","wb") as fw:
    with open("pic.txt","r") as fr:
        line = fr.readline();
        while line:
            for st in line.split(" "):
                if st=="" or st=="\n":
                    continue
                fw.write(int(st,16).to_bytes(1,"big"));
            line = fr.readline();
附送python代码字符串转字节

 

这是一个完整的访问单元(AU),包括3个NALU包,如你所见,数据序列以开始码开始,后面接了一个SPS(SPS 以0x67开始),在SPS中,你可以看到有2个防竞争字节。

没有这些字节那么非法的数据序列就会出现在这些位置。

然后可以看到一个开始码后面接着一个PPS(PPS 以0x68开始),然后是一个最后的开始码,后面跟着一个IDR包。

这是一个完整的H.264流,如果你把这些数据以16进制的方式保存到一个以.264为后缀名的文件中,可以把这些数据显示为上面的图片。

0000 0001 (start code) 67 (SPS) 64 000a ac72 8444 2684 0000
03 (防竞争字节) 00 0400 0003 (防竞争字节) 00ca 3c48 9611 8000 0000
01 (start code) 68 (PPS) e843 8f13 2130 0000 01 (start code) 65 (I帧) 8881 0005
4e7f 87df 61a5 8b95 eea4 e938 b76a 306a
71b9 5560 0b76 2eb5 0ee4 8059 27b8 67a9
6337 5e82 2055 fbe4 6ae9 3735 72e2 2291
9e4d ff60 86ce 7e42 b795 ce2a e126 be87
7384 26ba 1636 f4e6 9f17 dad8 6475 54b1
f345 0c0b 3c74 b39d bceb 5373 87c3 0e62
4748 62ca 59eb 863f 3afa 86b5 bfa8 6d06
1650 82c4 ce62 9e4e e64c c730 3ede a10b
d883 0bb6 b828 bca9 eb77 43fc 7a17 9485
21ca 376b 3095 b546 7730 60b7 12d6 8cc5
5485 29d8 69a9 6f12 4e71 dfe3 e2b1 6b6b
bf9f fb2e 5730 a969 76c4 46a2 dffa 91d9
5074 551d 4904 5a1c d686 687c b661 486c
96e6 124c 27ad bac7 5199 8ed0 f0ed 8ef6
6579 79a6 12a1 95db c8ae e3b6 35e6 8dbc
48a3 7faf 4a28 8a53 e27e 6808 9f67 7798
52db 5084 d65e 25e1 4a99 5834 c711 d643
ffc4 fd9a 4416 d1b2 fb02 dba1 8969 34c2
3255 98f9 9bb2 313f 4959 0c06 8cdb a5b2
9d7e 122f d087 9444 e40a 76ef 992d 9118
3950 3b29 3bf5 2c97 7348 9183 b0a6 f34b
702f 1c8f 3b78 23c6 aa86 4643 1dd7 2a23
5e2c d948 0af5 f52c d1fb 3ff0 4b78 37e9
45dd 72cf 8035 c395 07f3 d906 e54a 5876
036c 8120 6245 6544 73bc fec1 9f31 e5db
895c 6b79 d868 90d7 26a8 a188 8681 dc9a
4f40 a523 c7de be6f 76ab 7916 5121 6783
2ef3 d627 1a42 c294 d15d 6cdb 4a7a e2cb
0bb0 680b be19 5900 50fc c0bd 9df5 f5f8
a817 19d6 b3e9 74ba 50e5 2c45 7bf9 93ea
5af9 a930 b16f 5b36 241e 8d55 57f4 cc67
b265 6aa9 3626 d006 b8e2 e373 8bd1 c01c
5215 cab5 ac60 3e36 42f1 2cbd 9977 aba8
a9a4 8e9c 8b84 de73 f091 2997 aedb afd6
f85e 9b86 b3b3 03b3 ac75 6fa6 1169 2f3d
3ace fa53 8660 956c bbc5 4ef3 

由于是I帧,直接保存了一帧数据,不需要分隔符号。

------3.NALU Body       由NALU = NALUHeader+EBSP可以知道Body=EBSP;

EBSP = 防止竞争码+RBSP;       
RBSP
= SODB + RBSP尾部 。

去掉防竞争码后,我们得到了 RBSP。下一步就是如何从 RBSP 中获取原始的编码数据SODP(String Of Data Bits)。

对于 RBSP 尾部分成两种类型:

(1)RBSP尾部:其中大多数类型(非1-5)的NALU,使用这种尾部。

rbsp_stop_one_bit 停止位 占1个比特位,值为1

rbsp_alignment_zero_bit 值为0,目的是为了进行字节对齐,占据若干比特位,用若干个0bits对齐,用来补齐这个字节

所以RBSP就等于,SODB在它的最后一个字节的最后一个比特后,紧跟值为1的1个比特,然后增加若干比特的0,以补齐这个字节。

(2)条带RBSP尾部 :另一种尾部,就是当NALU类型为条带时,也即nal_unit_type等于1~5时,这时RBSP使用下面这种尾部

可以看到,rbsp_slice_trailing_bits()默认情况下,就是上面介绍的第一种尾部
只是当entropy_coding_mode_flag值为1,也即当前采用的熵编码为CABAC,而且more_rbsp_trailing_data()返回为true,也即RBSP中有更多数据时,添加一个或多个0x0000。

所以我们拿到RBSP,只需要按照上述语法,去掉RBSP的尾部,就可以得到SODB。然后就可以对照对应类型的NALU的句法,解析出语法元素的值。

(二)AVCC---用于存储

另一个存储H.264流的方式是AVCC格式,在这种格式中,每一个NALU包都加上了一个指定其长度(NALU包大小)的前缀(in big endian format大端格式),这种格式的包非常容易解析,但是这种格式去掉了Annex B格式中的字节对齐特性,而且前缀可以是1、2或4字节,这让AVCC格式变得更复杂了,指定前缀字节数(1、2或4字节)的值保存在一个头部对象中(流开始的部分),这个头通常称为’extradata’或者’sequence header’(重点)

------1.AVCC格式了解:使用NALU长度(固定字节,通常为4字节,取决于头部的NALULengthSizeMinusOne字段)分隔NAL;在头部包含extradata(或sequence header)的结构体。(extradata包含分隔的字节数、SPS和PPS)

解码器配置参数在一开始就配置好了(所以我们不能像视频网站中的实时播放一样可以在中间修改参数,比如:帧率,画面),系统可以很容易的识别NALU的边界,不需要额外的起始码,减少了资源的浪费,同时可以在播放时调到视频的中间位置。
这种格式通常被用于可以被随机访问多媒体数据。如存储在硬盘的文件:MP4、MKV通常用AVCC格式来存储

 AVCC格式不使用起始码(start code)作为NALU的分界,这种格式在每个NALU前都加上一个大端格式的前缀(1、2、4字节,代表NALU长度)

所以在解析AVCC格式的时候需要将指定的前缀字节数的值保存在一个头部对象中,这个都通常称为extradata或者sequence header。同时,SPS和PPS数据也需要保存在extradata或者叫’sequence header’中。

  • 视频开始有extradata,包含SPS,PPS
  • 每个NALU前有存储NALU的长度,

------2.先分析头部extradata,获取SPS、PPS以及NALULengthSizeMinusOne字段信息

1.其中 extradata 前4字节无用,跳过即可。

2.第5个字节:前6位保留,全部置为1,即('111111'b);后两位NALULengthSizeMinusOne字段用于告诉我们NALU前缀大小

  值=0 对应前缀1字节 对应每个NALU包最大长度255字节
  值=1 对应前缀2字节 对应每个NALU包最大长度64K
  值=3 对应前缀4字节 使用最多

3.第6个字节:前3位保留,全部置为1,即('111'b);后5位用于存放SPS NALU的个数(通常为1个)

4.取决于第6个字节中指定的SPS NALU个数,开始进行循环获取SPS数据:

  接下来获取两个字节采用两个字节,作为前缀指示接下来的一个SPS NALU的大小"N"(字节数)。每次获取一个SPS NALU单元数据,都需要先获取其前缀信息

  接下来获取N个字节获取SPS的数据

5.获取全部SPS数据后,开始获取PPS数据,获取1个字节,内部存放了PPS NALU单元的个数(通常为1个)

6.取决于前1个字节中指定的PPS NALU个数,开始进行循环获取PPS数据:

  接下来获取两个字节采用两个字节,作为前缀指示接下来的一个PPS NALU的大小"N"(字节数)。每次获取一个PPS NALU单元数据,都需要先获取其前缀信息

  接下来获取N个字节获取PPS的数据

SPS和PPS被存储在了非NALU包中(out of band带外),即独立于基本流数据。 这些数据的存储和传输是文件容器的任务,超出了本文的范畴。

------3.AVCC流结构

 

AVCC中的NALU格式,与AnnexB格式一致。当我们将AVCC转AnnexB时,如果检测到NALU Type = 5关键帧,那么在关键帧前面加上SPS NALU和PPS NALU即可。

虽然AVCC格式不使用起始码,防竞争字节还是有的。所以我们在转换AVCC与AnnexB格式的时候,不用考虑防竞争字节。因为NALU内部是一致的。

三:RTP格式---用于网络发送

RTP封装= 12字节固定RTP包头 + 载荷(NALU)

补充:针对IP网络的RTP打包方式。为原始的NAL打包格式,就是开始的若干字节(1,2,4字节)是NAL的长度,而不是start_code,此时必须借助某个全局的数据来获得编码器的profile,level,PPS,SPS等信息才可以解码。(AVCC格式)???

(一)RTP包头

前12字节固定 + (0~15)个32位的CSRC标识符

 

V (2bits):   RTP协议的版本号,当前协议版本号为2。
P (1bit):    填充标志,如果设置填充位P=1,在包尾将包含附加填充字节,它不属于有效载荷。填充的最后一个八进制包含应该忽略的八进制计数。某些加密算法需要固定大小的填充字节,或为在底层协议数据单元中携带几个RTP包。
X (1bit):    扩展标志,如果X=1,则在RTP报头后跟有一个扩展报头
CC(4bits):     CSRC计数器,指示CSRC 标识符的个数。

M (1bit):    标记位(不同载荷含义不同,视频标记一帧的最后一个分片slice则=1,其他=0)
PT (7bits):    载荷类型RTP_PAYLOAD_RTSP,记录后面资料使用哪种 Codec , receiver 端找出相应的 decoder 解碼出來。例如H264=96
序列号(16bits): 用于标识发送者所发送的 RTP 报文的序列号(初始值随机),每发送一个报文,序号增加 1

时间戳(32bits): 时间戳反映了该 RTP 报文的第一个八位组的采样时刻。 接受者使用时间戳来计算延迟和抖动, 并进行同步控制。
SSRC(32bits): 区分是在和谁通信。值随机选择,参加同一视频会议的两个同步信源的SSRC要相同。

贡献源(CSRC)标识符(32bits):每个CSRC标识符占32位,可以有0~15个。每个CSRC标识了包含在该RTP报文有效载荷中的所有特约信源。

RTP 协议实际上是由实时传输协议RTP(Real-time Transport Protocol)实时传输控制协议RTCP(Real-time Transport Control Protocol)两部分组成。

  RTP 协议基于多播或单播网络为用户提供连续媒体数据的实时传输服务;  
  RTCP 协议是 RTP 协议的控制部分,用于实时监控数据传输质量,为系统提供拥塞控制和流控制。

(二)回顾NALU类型

F: 1 个比特.

forbidden_zero_bit. 在 H.264 规范中规定了这一位必须为 0.

NRI: 2 个比特.

nal_ref_idc. 取 00 ~ 11, 似乎指示这个 NALU 的重要性, 如 00 的 NALU 解码器可以丢弃它而不影响图像的回放. 不过一般情况下不太关心这个属性

Type: 5 个比特.

nal_unit_type. 这个 NALU 单元的类型. 简述如下:

  0     没有定义
  1-23  NAL单元  单个 NAL 单元包.
  24    STAP-A   单一时间的组合包
  25    STAP-B   单一时间的组合包
  26    MTAP16   多个时间的组合包
  27    MTAP24   多个时间的组合包
  28    FU-A     分片的单元
  29    FU-B     分片的单元
  30-31 没有定义

(三)打包模式 : 拆包(1种) or 不拆包(2种)

RTP单次发送有上限👉2种RTP打包:拆包or不拆包

在IP网络中,当要传输的IP报文大小超过【最大传输单元MTU】时就会产生IP分片情况。(若交给底层协议拆包容易出问题→→→主动拆分NALU再打包成RTP包后发送

------1.SDP文件描述和封包的关联

H264的RTP中有三种不同的封包模式(Single NAL,Non-interleaved,Interleaved) 通过SDP参数中指定,如:

m=video 49170 RTP/AVP 98
a=rtpmap:98 H264/90000
a=fmtp:98 profile-level-id=42A01E; packetization-mode=1; sprop-parameter-sets=Z0IACpZTBYmI,aMljiA==

1、packetization-mode决定封包模式:

当 packetization-mode 的值为 0 时或不存在时, 必须使用单一 NALU 单元模式.(无此字段时,缺省为0) 单包
当 packetization-mode 的值为 1 时必须使用非交错(non-interleaved)封包模式.  FU-A
当 packetization-mode 的值为 2 时必须使用交错(interleaved)封包模式.     FU-B

2、sprop-parameter-sets: SPS,PPS

这个参数可以用于传输 H.264 的序列参数集和图像参数 NAL 单元. 这个参数的值采用 Base64 进行编码. 不同的参数集间用","号隔开。
//若不用Base64则可能会有数据丢失

3、profile-level-id:

这个参数用于指示 H.264 流的 profile 类型和级别. 由 Base16(十六进制) 表示的 3 个字节. 
  第一个字节表示 H.264 的 Profile 类型,
  第三个字节表示 H.264 的 Profile 级别

 

①single NAL unit packet 单包(1个RTP包:1个NALU)
②aggregation packets   聚合(组合)包(1个RTP包:多个NALU,提高传输效率),需要解包时在重组。
  ①STAP (Single-time aggregation packet)
    STAP-A
    STAP-B
  ② MTAP (Multi-time aggregation packet)
    MTAP16
    MTAP24
③Fragmentation Unit  拆包处理【一个NALU→多包 NALU>最大传输单元MTU】
    FU-A  //非交错模式
    FU-B  //交错模式

------2.单一NALU的RTP包

对于 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 包即可.

如有一个 H.264 的 NALU 是这样的:

[00 00 00 01 67 42 A0 1E 23 56 0E 2F ... ]       这是一个序列参数集 NAL 单元. [00 00 00 01] 是四个字节的开始码, 67 是 NALU 头, 42 开始的数据是 NALU 内容.

封装成 RTP 包将如下:

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

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

------3.组合NALU的RTP包

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

 这里只介绍STAP-A模式,如果是STAP-B的话会多加入一个DON域,另外还有MTAP16、MTAP24

例:如有一个 H.264 的 NALU 是这样的:

[00 00 00 01 67 42 A0 1E 23 56 0E 2F ... ]

[00 00 00 01 68 42 B0 12 58 6A D4 FF ... ]

封装成 RTP 包将如下:

[ RTP Header ] [78 (STAP-A头,占用1个字节)] [第一个NALU长度 (占用两个字节)] [ 67 42 A0 1E 23 56 0E 2F ] [第二个NALU长度 (占用两个字节)] [68 42 B0 12 58 6A D4 FF ... ]

------4.分片NALU的RTP包:FU_indicator和FU_head(RTP分包时的包头)

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

FU-A的分片格式:数据比较大的H264视频包,被RTP分片发送。12字节的RTP头后面跟随的就是FU-A分片

FU_indicator:
F  禁止位
NRI 重要标识位👈即拆包的nalu自身的NRI  ----   F与NRI 保存了 NALU的前3位
type RTP打包头类型,FU-A时type=28
FU_header:
S  开始位 1表示分片NAL单元的开始,反之=0
E  结束位 1表示分片NAL单元的结束,反之=0。
R  保留位 必须为0,接收者必须忽略该位。
type NALU数据类型 👈NALU_header  ----  保存了 NALU 类型(即NALU的后5位)

例:

0x7C85=01111100 10000101 (开始包)

0x7C05=01111100 00000101 (中间包)

0x7C45=01111100 01000101 (结束包)

如有一个 H.264 的 NALU 是这样的:

[00 00 00 01 65 42 A0 1E 23 56 0E 2F ...  02 17 C8 FD F1 B9 C7 53 59 72 ... CB FF FF F4 1A D5 C4 18 A8 ... F1 B9 C7 1D A5 FA 13 0B ...]  

封装成 RTP 包将如下(注意:下面去掉了开始码和NALU头部,但是在FU_header的type存放了NALU数据类型

[ RTP Header ] [ 7C 85 42 A0 1E 23 56 0E 2F ...]

[ RTP Header ] [ 7C 05 02 17 C8 FD F1 B9 C7 53 59 72 ...]

[ RTP Header ] [ 7C 05 CB FF FF F4 1A D5 C4 18 A8 ...]

[ RTP Header ] [ 7C 45 F1 B9 C7 1D A5 FA 13 0B ...]

拆包和解包:

发送端—拆包:NAL_header与分片后的FU的单元头有如下关系:
     NAL_header前三位为FU_indicator的前三位
     NAL_header后五位为FU_header的后五位
接收端—解包:将所有的分片包组合还原成原始的NAl包
     nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f) 

参考文章:

H264---封装格式:字节流格式(AnnexB)、AVCC 、RTP打包格式:https://blog.csdn.net/qq_42024067/article/details/102292535

H264格式 详细介绍:https://blog.csdn.net/shixin_0125/article/details/78940402

H264 编码分析:https://www.yuque.com/keith-an9fr/aab7xp/vng2pb

视频和视频帧:H264编码格式整理:https://zhuanlan.zhihu.com/p/71928833

I、P、B帧区别: https://blog.csdn.net/sjin_1314/article/details/40989173

切片与宏块:https://www.jianshu.com/p/9522c4a7818d

使用FFMPEG类库分离出多媒体文件中的H.264码流:https://blog.csdn.net/leixiaohua1020/article/details/11800877

 

posted @ 2021-04-25 21:25  山上有风景  阅读(6853)  评论(0编辑  收藏  举报