一、简述
RTMP协议中基本的数据单元称为消息(Message)。当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为消息块(Chunk)。
每个块必须完整的发送后才能发送下一个块。接收端根据快流ID把块组装成完整的小消息。块允许把更高层协议的大消息分割成更小的消息分片,例如为了防止低优先级的大消息(如视频消息)阻碍高优先级的小消息(如音频和控制消息)。
二、Rtmp Chunk Header
+--------------+----------------+--------------------+--------------+
| Basic Header | Message Header | Extended Timestamp | Chunk Data |
+--------------+----------------+--------------------+--------------+
|<------------------- Chunk Header ----------------->|
Chunk Format
Chunk Header 分为4部分,
2.1基本头:Base Header(1-3字节)
RTMP 协议最多支持 65597 个流(chunk),CS ID(块流ID) 范围为: 3 ~ 65599。ID 0、1、2 被保留。
基本头分为三种情况由Base Header第一个字节后六个bits决定
- (后六个bits==0):基本头长度为2字节第二个字节表示块流ID
- (后六个bits==1):基本头长度为3字节,第二三个字节表示快流ID
- (1<后六个bits<=64):基本头长度为1字节,后6bits表示块流ID
1).第一个字节后六bits为0的基本头结构如下:
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 0 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk basic header 2
Basic Header 为 3字节的情况 (第一个字节后 6 位为 000001)
块流ID从64到65599。块流ID= 第三个byte*256+(第二个byte+64)
2). 第一个字节后六bits为1的基本头结构如下:
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 1 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk basic header 3
3).第一个字节后六bits为cs id的基本头结构如下:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|fmt| cs id |
+-+-+-+-+-+-+-+-+
Chunk basic header 1
Basic Header 为 1字节的情况 (第一个字节后 6 位为 cs id)
块流ID从2到63
CSID 保留值
0、1、2 被保留, 3 ~ 8 基本都是固定用途,所以 9 ~ 65599 才用于自定义 csid,但一般我们用不到。
- 0 表示 Basic Header 总共要占用 2 个字节
- 1 表示 Basic Header 总共要占用 3 个字节
- 2 代表该 chunk 是控制信息和一些命令信息
- 3 代表该 chunk 是客户端发出的 AMF0 命令以及服务端对该命令的应答
- 4 代表该 chunk 是客户端发出的音频数据,用于 publish
- 5 代表该 chunk 是服务端发出的 AMF0 命令和数据
- 6 代表该 chunk 是服务端发出的音频数据,用于 play;或客户端发出的视频数据,用于 publish
- 7 代表该 chunk 是服务端发出的视频数据,用于 play
- 8 代表该 chunk 是客户端发出的 AMF0 命令,专用来发送: getStreamLength, play, publish
2.2.消息头:Message Header(0-11字节)
2.2.1.消息头结构有4种类型
- 类型0:fmt == 0:Message Header 长度为 11
- 类型1:fmt == 1:Message Header 长度为 7
- 类型2:fmt == 2:Message Header 长度为 3
- 类型3:fmt == 3:Message Header 长度为 0
1).类型0
timestamp(4bytes)+message length(3bytes)+message type id(1byte)+message stream id(4bytes)
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp |message length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message length (cont) |message type id| msg stream id |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message stream id (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk Message Header - Type 0
注意: 此时 timestamp 为绝对时间戳
2).类型1
timestamp(4bytes)+message length(3bytes)+message type id(1byte)
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp delta |message length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message length (cont) |message type id|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk Message Header - Type 1
fmt为1的情况下省略了message stream id,这个id与上一个chunk message相同
注意: 此时 timestamp 为相对时间戳
3).类型2
timestamp(4bytes)
0 1 2 3
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| timestamp delta |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk Message Header - Type 3
fmt为2的情况下省略了message length、message type id和message stream id,省略的这个与上一个chunk message 相同
该类型的消息是固定长度与固定类型的消息,也就是说每次发的消息大小与格式相同,例如音频流格式,在第一个新的 chunk 以后使用该类型。
注意: 此时 timestamp 为相对时间戳
3).类型3
fmt为3的情况下Message Header全部省略了,跟上一个chunk Message完全相同
类型3使用的情况大多是因为消息太大比如视频消息,一个chunk中放不下,要分chunk发送,第一个chunk后的消息使用该类型,比如关键帧,动不动就上万字节的数据,而且时间戳类型都是固定的,所以使用该类型发送数据。
2.2.2.时间戳
只有在类型0的情况下时间戳是绝对时间戳其他类型的时间戳都是相对时间戳(时间戳增量)。从librtmp与srs中观察分析得到服务器与客户端推流的时间戳是相同的(本人都实现了,所以认为相同,如有其它情况请大神告知)
在此说下在rtmp中时间戳怎么打:
一般情况下这个时间戳都是从绝对时间戳开始也就是从0开始,音视频都有自己的时间戳(这一点很重要),音视频采集过来的时候会自带采集时间戳,伪代码如下:
private int GetVideoTimestamp(MediaFrame mediaFrame)
{
if (视频时间戳== 0)
{
_timestampVideo = mediaFrame.采集时间戳;
return 0;
}
else
{
return mediaFrame.采集时间戳 - 视频时间戳;
}
}
2.2.3.消息长度
消息长度指的是Header以后的所有字节数
2.2.4.消息类型
- 1 设置块大小
- 2 中断消息,丢弃旧数据
- 3 确认
- 4 用户控制消息
- 5 设置确认窗口大小
- 6 设置流带宽
- 7 音频数据
- 9 视频数据
- 15(0x0f). AMF3 数据
- 16(0x10) AMF3 共享对象事件
- 17(0x11) AMF3 命令
- 18(0x12) AMF0 数据
- 19(0x13) AMF0 共享对象事件
- 20(0x14) AMF0 命令,Invoke 方法调用
- 22(0x16) 聚合消息, H.264, 类似 FLV 文件存储格式,每个音视频包作为一个 Tag, 许多的 Tag 组成了这个 AMFType=0x16 的数据类型
2.2.5.消息ID
这个ID是四个字节的小端数据,这个ID默认置1即:0x01 0x00 0x00 0x00
2.3扩展时间戳:Extended Timestamp(0-4字节)
类型为1或2的块里,本字段代表当前块和上一个块的时间戳之差。如果时间间隔大于等于16777215(0xFFFFFF),此字段的取值必须为16777215,并且与扩展时间戳一起组成32比特的完整时间戳。如果时间戳小于16777215,那么此字段代表了完整的时间戳。
三、示例
3.1音频示例
一下是普通的音频帧发送方式,音频消息长度比较小,一般不用分包,直接全包发送即可,但是如下方式发送会导致部分消息的冗余。
+---------+-----------------+-----------------+-----------------+
| |Message Stream ID| Message Type ID | Time | Length |
+---------+-----------------+-----------------+-------+---------+
| Msg # 1 | 12345 | 8 | 0 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 2 | 12345 | 8 | 32 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 3 | 12345 | 8 | 64 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 4 | 12345 | 8 | 96 | 512 |
+---------+-----------------+-----------------+-------+---------+
如下方式解决了Chunk的消息冗余。消息按类型0、类型2、类型3、类型3... 的方式发送,第二包因为不确定时间戳增量因此用类型二来确定,从第三包以后因为时间戳增量也是相同的就可以用类型三来发送。
+---------+-----------------+-----------------+-----------------+
| |Message Stream ID| Message Type ID | Time | Length |
+---------+-----------------+-----------------+-------+---------+
| Msg # 1 | 12345 | 8 | 0 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 2 | 12345 | 8 | 32 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 3 | 12345 | 8 | 64 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 4 | 12345 | 8 | 96 | 512 |
+---------+-----------------+-----------------+-------+---------+
注意:本人在实现服务器的过程中,发现从各个其他组发来的流的时间戳是各式各样的,尤其音频的时间戳更是奇葩的厉害,根本不会出现使用类型3的情况,干脆就直接用类型1来传输音频,再者音视频都是相间交错发的。
3.2视频示例
视频消息由于太长即使你把Chunk Size设的再大也不行,一般Chunk Size都设成4096(0x1000)。
+-----------+-------------------+-----------------+-----------------+
| | Message Stream ID | Message Type ID | Time | Length |
+-----------+-------------------+-----------------+-----------------+
| Msg # 1 | 12346 | 9 (video) | 0 | 20000 |
+-----------+-------------------+-----------------+-----------------+
下面是分割后的Chunk。消息按类型0、类型3、类型3、类型3... 的方式发送,因为整个帧的时间戳都是一个时间戳。
+-------+------+-----+-------------+-----------+------------+
| |Chunk |Chunk|Header |No. of |Total No. of|
| |Stream| Type|Data |Bytes after| bytes in |
| | ID | | | Header | the chunk |
+-------+------+-----+-------------+-----------+------------+
|Chunk#1| 4 | 0 | delta: 0 | 4096 | 4096+12 |
| | | | length:9192 | | |
| | | | type: 9, | | |
| | | | stream ID: | | |
| | | | 12346 | | |
| | | | (11 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#2| 4 | 3 | none | 4096 | 4096+1 |
| | | | (0 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#3| 4 | 3 | none | 1000 | 1000+1 |
| | | | (0 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
注意:在类型0的的Chunk中一定要设定整个消息的长度,在本实例中的长度为9192(4096+4096+1000)这个别算错了。这里的视频帧是按RTMP发送视频帧格式拼装完后再整体进行Chunk 分块发送的。