此文言简意赅 记录下
rtp over tcp的RTP/RTCP包格式的前四个字节说明
RTP/RTCP Socket和RTSP Socket共享TCP Socket,所以必须要有一个标识来区别三个数据。
RTP和RTCP数据会以 "$"符号 + 一个字节的通道编号 + 2个字节的数据长度,共四个字节的前缀开始,RTSP数据没有四个字节的前缀;RTP和RTCP数据的区别在于第二个字节的通道编号
所以第一个字节’$'用于与RTSP区分,第二个字节用于区分RTP和RTCP(RTP和RTCP的channel是在RTSP的SETUP过程中,客户端发送给服务端的------一般情况下 RTP通道编号是偶数,RTCP通道编号是奇数)。RTP/RTCP的前缀四个字节如下所示:(在rtp over tcp发送协议下)
字节 |
描述 |
第一个字节 |
‘$’,标识符,用于与RTSP区分 |
第二个字节 |
channel,用于区分RTP和RTCP |
第三和第四个字节 |
RTP包的大小 |
根据前面的说明,现在RTP的打包方式要在之前的每个RTP包前面加上四个字节,如下所示:
I帧、P帧、B帧、GOP、IDR 和PTS, DTS之间的关系 - 夜行过客 - 博客园 (cnblogs.com)
H.264的DTS、PTS、frame_num、poc_h264 pts_小葫芦写代码的博客-CSDN博客
流媒体基本要点简述:如何在H264数据中获取PTS?_13306945的技术博客_51CTO博客
注:可通过Elecard StreamEye Tools工具【例如 Elecard Stream Analyzer、Elecard StreamEye等】来分析H264码流
H264协议里定义了三种帧,完整编码的帧叫I帧,参考之前的I帧生成的只包含差异部分编码的帧叫P帧,还有一种参考前后的帧编码的帧叫B帧。
H264采用的核心算法是帧内压缩和帧间压缩,帧内压缩是生成I帧的算法,帧间压缩是生成B帧和P帧的算法。
序列的说明
在H264中图像以序列为单位进行组织,一个序列是一段图像编码后的数据流,以I帧开始,到下一个I帧结束。
一个序列的第一个图像叫做 IDR 图像(立即刷新图像),IDR 图像都是 I 帧图像。H.264 引入 IDR 图像是为了解码的重同步,当解码器解码到 IDR 图像时,立即将参考帧队列清空,将已解码的数据全部输出或抛弃,重新查找参数集,开始一个新的序列。这样,如果前一个序列出现重大错误,在这里可以获得重新同步的机会。IDR图像之后的图像永远不会使用IDR之前的图像的数据来解码。
一个序列就是一段内容差异不太大的图像编码后生成的一串数据流。当运动变化比较少时,一个序列可以很长,因为运动变化少就代表图像画面的内容变动很小,所以就可以编一个I帧,然后一直P帧、B帧了。当运动变化多时,可能一个序列就比较短了,比如就包含一个I帧和3、4个P帧。
在视频编码序列中,GOP即Group of picture(图像组),指两个I帧之间的距离,Reference(参考周期)指两个P帧之间的距离。两个I帧之间形成一组图片,就是GOP(Group Of Picture):
GOP说白了就是两个I帧之间的间隔:比较说GOP为120,如果是720p60的话,那就是2s一次I帧,GOP一般设置为编码器每秒输出的帧数,即每秒帧率,一般为25或30,当然也可设置为其他值。
在一个GOP中,P、B帧是由I帧预测得到的,当I帧的图像质量比较差时,会影响到一个GOP中后续P、B帧的图像质量,直到下一个GOP 开始才有可能得以恢复,所以GOP值也不宜设置过大。
由于P、B帧的复杂度大于I帧,所以过多的P、B帧会影响编码效率,使编码效率降低。另外,过长的GOP还会影响Seek操作的响应速度,由于P、B帧是由前面的I或P帧预测得到的,所以Seek操作需要直接定位,解码某一个P或B帧时,需要先解码得到本GOP内的I帧及之前的N个预测帧才可以,GOP值越长,需要解码的预测帧就越多,seek响应的时间也越长。
在H.264/AVC视频编码标准中,整个系统框架被分为了两个层面:视频编码层面(VCL)和网络抽象层面(NAL)。其中,前者负责有效表示视频数据的内容,而后者则负责格式化数据并提供头信息,以保证数据适合各种信道和存储介质上的传输。因此我们平时的每帧数据就是一个NAL单元(SPS与PPS除外)。在实际的H264数据帧中,往往帧前面带有00 00 00 01 或 00 00 01分隔符,一般来说编码器编出的首帧数据为PPS与SPS,接着为I帧……
H.264由一个一个的NALU组成,每个NALU之间使用00 00 00 01或00 00 01分隔开,每个NALU的第一次字节都有特殊的含义,其内容如下:
位 | 描述 |
---|---|
bit[7] | 必须为0 |
bit[5-6] | 标记该NALU的重要性 |
bit[0-4] | NALU单元的类型 |
常用Nalu_type:
0x67 (0 11 00111) SPS 非常重要 type = 7
0x68 (0 11 01000) PPS 非常重要 type = 8
0x65 (0 11 00101) IDR帧 关键帧 非常重要 type = 5
0x61 (0 11 00001) 非IDR的I帧 或 P帧 非常重要 type = 1 非IDR的I帧 不大常见 【这个大概率是P帧,通过工具查看某个H264的P slice,开头为00 00 00 01 61】
0x41 (0 10 00001) 非IDR的I帧 或 P帧 重要 type = 1 【非IDR的I帧和P帧都是有可能的,具体通过工具分析】
0x01 (0 00 00001) B帧 不重要 type = 1
0x06 (0 00 00110) SEI 不重要 type = 6
所以判断是否为I帧的算法为: (NALU类型 & 0001 1111) = 5
最简单的办法是找0x65或0x25(I frame启始位),
或者去找0x67或0x27(SPS)
和0x68或0x28(PPS)后面的完整包。
SPS和PPS后面必然跟着I frame。(68之后,出现 000001 开始就是关键帧数据 )
好,对于H.264格式了解这么多就够了,我们的目的是想从一个H.264的文件中将一个一个的NALU提取出来,然后封装成RTP包,下面介绍如何将NALU封装成RTP包。
NALU是H264用于网络传输的单元类型,一个完整的NALU单元一般是以0x000001或者0x00000001开始,其后跟的则是NALU头和NALU的数据;我们在网络传输的时候,会去掉开始的0x000001或者0x00000001的标志;一般需要将这些标志替换为RTP payload的头部(1个字节);
参考https://www. cnblogs.com/kimiway/p/4427310.html【H264 NAL RTP打包】
首先要明确,RTP包的格式是绝不会变的,永远都是RTP头+RTP载荷:
红色为RTP协议头,黄色是RTP载荷【H264码流,其中NALU数据就是RBSP数据】
4.1.1、RTP载荷第一个字节格式跟NALU头一样:【后面的格式与H264的RTP打包格式相关】
Python
+---------------+ |0|1|2|3|4|5|6|7| +-+-+-+-+-+-+-+-+ |F|NRI| Type | +---------------+
F:【1 bit】 forbidden_zero_bit, 占1位,在 H.264 规范中规定了这一位必须为 0
NRI:【2 bits】 nal_ref_idc, 占2位,取值从0到3,指示这个 NALU 的重要性,取值越大约重要。
Type:【5 bits】nalu是指包含在 NAL 单元中的 RBSP 数据结构的类型,其中0未定义,1-19在264协议中有定义,20-23为264协议指定的保留位。
(1)单一 NAL 单元模式,即一个 RTP 包仅由一个完整的 NALU 组成. 这种情况下 RTP NAL 头类型字段和原始的 H.264的 NALU 头类型字段是一样的。【1-23】
如有一个 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 个字节的开始码就可以了。
(2)组合封包模式 ,即可能是由多个 NAL 单元组成一个 RTP 包. 分别有4种组合方式: STAP-A, STAP-B, MTAP16, MTAP24。 【那么这里的类型值分别是 24, 25, 26 以及 27】
【STAP-A 单一时间的组合包 24
STAP-B 单一时间的组合包 25
MTAP16 多个时间的组合包 26
MTAP24 多个时间的组合包 27】
(3)分片封包模式,用于把一个 NALU 单元封装成多个 RTP 包. 存在两种类型 FU-A 和 FU-B.。【类型值分别是 28 和 29】
【FU-A 分片的单元 28
FU-B 分片的单元 29
没有定义 30-31 】
版本号(V):2Bit,用来标志使用RTP版本
填充位(P):1Bit,如果该位置位,则该RTP包的尾部就包含填充的附加字节
扩展位(X):1Bit,如果该位置位,则该RTP包的固定头部后面就跟着一个扩展头部
CSRC技术器(CC):4Bit,含有固定头部后面跟着的CSRC的数据
标记位(M):1Bit,该位的解释由配置文档来承担
载荷类型(PT):7Bit,标识了RTP载荷的类型
序列号(SN):16Bit,发送方在每发送完一个RTP包后就将该域的值增加1,可以由该域检测包的丢失及恢复包的序列。序列号的初始值是随机的
时间戳:32比特,记录了该包中数据的第一个字节的采样时刻
同步源标识符(SSRC):32比特,同步源就是RTP包源的来源。在同一个RTP会话中不能有两个相同的SSRC值
贡献源列表(CSRC List):0-15项,每项32比特,这个不常用
H.264可以由三种RTP打包方式:
(1)单NALU打包【RTP载荷第一个字节 type=1-23】
一个RTP包包含一个完整的NALU。
(2)组合打包【RTP载荷第一个字节 type=24-27】
对于较小的NALU,一个RTP包可包含多个完整的NALU。
(3)分片打包【RTP载荷第一个字节 type=28-29】
对于较大的NALU,一个NALU可以分为多个RTP包发送。
注意:这里要区分好概念,每一个RTP包都包含一个RTP头部和RTP荷载,这是固定的。而H.264发送数据可支持三种RTP打包方式。
比较常用的是单NALU打包和分片打包,本文也只介绍这两种:
所谓单NALU打包就是将一整个NALU的数据放入RTP包的载荷中,这是最简单的一种方式,无需过多的讲解。
每个RTP包都有大小限制的,因为RTP一般都是使用UDP发送,UDP没有流量控制,所以要限制每一次发送的大小,所以如果一个NALU的太大,就需要分成多个RTP包发送,如何分成多个RTP包,下面来好好讲一讲。
RTP头部是固定的,那么只能在RTP载荷中去添加额外信息来说明这个RTP包是表示同一个NALU,如果是分片打包的话,那么在RTP载荷开始有两个字节的信息,然后再是NALU的内容:
4.5.1、FU-A【type=28】分片打包类型的RTP载荷格式
VB
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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
注意,FU payload中并没有传送NALU的头部,
NALU的头部由FU indicator(前3位)和FU header(后五位)组成:nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f);
4.5.2、RTP载荷第一个字节位FU Indicator,其格式如下:【这个与NALU头第一个字节格式一样】
Python
+--FU indicator-+ |0|1|2|3|4|5|6|7| +-+-+-+-+-+-+-+-+ |F|NRI| Type | +---------------+
高三位:与NALU第一个字节的高三位相同,Type:28<----->0x1c,表示该RTP包一个分片,为什么是28?此处28为FU-A【H264的RTP打包格式:分片格式】因为H.264的规范中定义的,此外还有许多其他Type,这里不详讲,如果想弄清楚,可以参考https://www.cnblogs.com/kimiway/p/4427310.html【H264 NAL RTP打包】
F:【1 bit】 forbidden_zero_bit, 占1位,在 H.264 规范中规定了这一位必须为 0
NRI:【2 bits】 nal_ref_idc, 占2位,取值从0到3,指示这个 NALU 的重要性,取值越大约重要。
Type:【5 bits】nalu是指包含在 NAL 单元中的 RBSP 数据结构的类型,其中0未定义,1-19在264协议中有定义,20-23为264协议指定的保留位。
(1)单一 NAL 单元模式,即一个 RTP 包仅由一个完整的 NALU 组成. 这种情况下 RTP NAL 头类型字段和原始的 H.264的 NALU 头类型字段是一样的。【1-23】
(2)组合封包模式 ,即可能是由多个 NAL 单元组成一个 RTP 包. 分别有4种组合方式: STAP-A, STAP-B, MTAP16, MTAP24。 【那么这里的类型值分别是 24, 25, 26 以及 27】
【STAP-A 单一时间的组合包 24
STAP-B 单一时间的组合包 25
MTAP16 多个时间的组合包 26
MTAP24 多个时间的组合包 27】
(3)分片封包模式,用于把一个 NALU 单元封装成多个 RTP 包. 存在两种类型 FU-A 和 FU-B.。【类型值分别是 28 和 29】
【FU-A 分片的单元 28
FU-B 分片的单元 29
没有定义 30-31 】
4.5.3、RTP载荷第二个字节位FU Header,其格式如下
PowerShell
+---FU header---+ |0|1|2|3|4|5|6|7| +-+-+-+-+-+-+-+-+ |S|E|R| Type | +---------------+
S:占1位如果是1表示当前这个包是FU-A分片打包的起始包
E:占1位如果是1表示当前这个包是FU-A分片打包的结束包
R: 占1位,保留位,为0
Type:实际包含的nalu的类型,后续才是NALU data
注意,FU payload中并没有传送NALU的头部,NALU的头部由FU indicator(前3位)和FU header(后五位)组成:nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f);
4.5.4、FU-A分包方式注意点
①关于时间戳,需要注意的是h264的采样率为90000HZ(被标准固定死的,为了方便转换成npt时间,见维基百科:https://en.wikipedia.org/wiki/RTP_audio_video_profile),因此时间戳的单位为1(秒)/90000,因此如果当前视频帧率为25fps,那时间戳间隔或者说增量应该为3600,如果帧率为30fps,则增量为3000,以此类推。
②关于h264拆包,按照FU-A方式说明:
1)第一个FU-A包的FU indicator:F应该为当前NALU头的F,而NRI应该为当前NALU头的NRI,Type则等于28,表明它是FU-A包。FU header生成方法:S = 1,E = 0,R = 0,Type则等于NALU头中的Type。
2)后续的N个FU-A包的FU indicator和第一个是完全一样的,如果不是最后一个包,则FU header应该为:S = 0,E = 0,R = 0,Type等于NALU头中的Type。
3)最后一个FU-A包FU header应该为:S = 0,E = 1,R = 0,Type等于NALU头中的Type。
③FU payload中并没有传送NALU的头部,NALU的头部由FU indicator(前3位)和FU header(后五位)组成:nal_unit_type = (fu_indicator & 0xe0) | (fu_header & 0x1f);
④FU-A 还原的时候,也是0x00 00 00 01 开始,不需要自己额外添加0x00 00 00 01
⑤ FU-A 的的解析,start end等数据要解析好
⑥single nal unit 也是以0x00 00 00 01开始,也不需要自己添加分隔符
编解码标准-H.264