RTMP 是一个多媒体数据传播协议,相比于HTTP这些超文本协议,多媒体传输的音频和视频信息都相对较大。对于单个较大的信息可能会阻塞连接,导致优先级更高的信息无法传递,因此较大的信息一般都需要分包发送。
为了满足分包发送需求,RTMP 在传输的时候会对较大的数据进行分块(chunking
)操作,简而言之,就是将较大的信息分割成一个个的子信息,且不会超过配置的固定大小。发送端需要保证当前完整的“分包”信息发送完成再发送下一个“分包”,那么接收端就可以利用TCP有序到达的优势在对端进行重组。
“分包”由于是都来自同一个大信息,因此在包头的描述处会存在大量的冗余数据。为了解决传输过程中的冗余数据,RTMP设计出一个特殊的数据格式——块(chunk
),对需要传输的信息进行了块包装,在有序可靠的TCP的基础下利用不同类型的块尽可能消除冗余数据。从而可以令RTMP用较小的开支发送更多的小消息。
块大小利弊
- 更大的块可以降低 CPU 开销, 但在低带宽连接时会因其大量的写入导致延迟其他内容的传递;
- 更小的块确不利于高比特率的流化。
虽然RTMP提供了分块(chunking
)操作与块(chunk
)包装的设计,然而在块大小的选择上我们需要根据实际情况权衡利弊。
块流与信息流
-
块流(
Chunk Stream
):指块组成的数据流,是 RTMP 协议传输的数据流。 -
信息流(
Message Stream
):指信息组成的数据流,是需要传输的主要信息体构成的数据流。
RTMP 握手完成之后,连接开始对一个或多个块流进行多路传输。通过块流ID(Chunk Stream ID
)可以将同一个块流(Chunk Stream
)的块(Chunk
)整理在一起,然后将块(Chunk
)中信息(message
)根据信息流ID(Messsage Stream ID
)重新连接在一起。当一个完整的音视频信息接收完成后,接收端就可以继续做解码等一系列工作了。
块格式
每个块包含一个块头和块数据体,其中块头包含三个部分:
+--------------+----------------+--------------------+--------------+
| Basic Header | Message Header | Extended Timestamp | Chunk Data |
+--------------+----------------+--------------------+--------------+
| |
|<------------------- Chunk Header ----------------->|
Chunk Format
-
Basic Header(基本头,1~3字节):包含
chunk stream ID
(块流ID)和chunk type
(块类型),长度由chunk stream ID
决定。 -
Message Header(消息头,0,3,7,11字节):包含被发送的消息信息(全部或部分),长度由块头中的
chunk type
来决定。 -
Extended Timestamp(扩展时间戳,0,4字节):这个字段是否存在取决于块消息头中的
timestamp
或者timestamp delta
字段。 - Chunk Data(块数据,可变大小):当前块的有效数据,即信息的主体部分,上限为配置的最大块大小减去块头。
Basic Header
Basic Header(基本头信息)用于存放整个块头的基本信息,包括chunk stream ID
(块流ID)和chunk type
(块类型)。同时,Basic Header 应该使用最小的容量存放chunk type
和chunk stream ID
,进而减少引入 Header 增加的数据量。
-
chunk type(块类型):决定消息头的后续的编码格式
- 一般使用
fmt
表示,用来标识一个chunk
的类型 -
chunk type
的长度固定为2 bits
- 一般使用
-
chunk stream ID(块流ID):决定 Basic Header 的字节数
- 一般简写为
CSID
,用来标识一个特定的块流通道 -
chunk stream ID
的长度由其真实值确定,取值为 6、14、22 bits - RTMP 最多支持 65597 个流,
CSID
的范围为3 ~ 65599
,其中0、1、2
被保留用作 Basic Header 的版本标记:-
0
:表示二字节版本 -
1
:表示三字节版本 -
2
:保留值,用于下层协议控制消息和命令 -
3 ~ 65599
:用于表示不同的块流
-
- 一般简写为
Basic Header 字节数形式与CSID
的表示范围:
-
一字节形式:
CSID
范围为3~63
(63 = 2^6 - 1) -
二字节形式:
CSID
范围为64~319
(319 = (2^8 - 1) + 64 = 255 + 64) -
三字节形式:
CSID
范围为64~65599
(65599 = (2^16 - 1) + 64 = 65535 + 64)
一字节版本 - CSID
范围为3~63
一字节中没有辅助的字段,而是直接将一字节剩下的6bits用作存放CSID
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|fmt| cs id |
+-+-+-+-+-+-+-+-+
CSID = <第一个字节的值(fmt=0)>
二字节版本 - CSID
范围为64~319
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 0 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
条件: <第一个字节的值(fmt=0)> == 0
CSID = <第二个字节的值> + 64
三字节版本 - CSID
范围为64~65599
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
条件: <第一个字节的值(fmt=0)> == 1
CSID = <第三个字节的值> * 256 + <第二个字节的值> + 64
64~319 既可以用二字节形式也可用三字节形式表示,为了数据大小最好用二字节,不过具体还是看实际情况
Message Header
Message Header(块信息头)用于存放实际信息的描述信息。它有四种不同的格式,由 Basic Header 中的chunk type
即fmt
进行区分。其中,第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk
的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据。根据chunk type
即fmt
取值为0
、1
、2
、3
,我们将其命名为类型 0、类型 1、类型 2、类型 3。
类型 0(fmt = 0, 11 bytes)
类型 0 的块头信息长度为 11 个字节,它能表示其他三种类型的数据,并且该类型必须用在chunk stream
的起始位置和流时间戳回退或重置的时候。
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 (coutinue) |message type id| msg stream id |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message stream id (coutinue) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
timestamp(时间戳):占用 3 个字节,因此它最多能表示到
16777215=0xFFFFFF=2^24-1
,当它的值超过这个最大值时,这三个字节都置为1,这样实际的timestamp
会转存到Extended Timestamp
字段中,接收端在判断timestamp
字段 24 bits 都为1时就会去Extended Timestamp
中解析实际的时间戳。 -
message length(消息数据长度):占用 3 个字节,表示实际发送的消息的数据(如音频帧、视频帧等数据)的长度,单位是字节。注意这里是整个
chunk
的长度,而不是chunk
本身data的长度。 -
message type id(消息的类型id):占用 1 个字节,表示实际发送的数据的类型
-
1
、2
、3
、5
、6
:用于协议控制信息 -
8
:代表音频数据 -
9
:代表视频数据
-
-
message stream id(消息的流id):占用 4 个字节,表示该
chunk
所搭载的信息所在的信息流的ID(Basic Header的CSID
一样,采用小端存储方式)。
类型 1(fmt = 1, 7 bytes)
类型 1 的块头信息长度为 7 个字节,它省去了message stream id
的 4 个字节,表示此chunk
d的信息和上一次发的chunk
所在的信息流相同。该块通常跟随在类型 0 的块后面,表示其信息流不变,但是其信息长度是可变的(例如一些视频格式数据)。
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 (coutinue) |message type id|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
-
timestamp delta(时间差):占用 3 个字节,存储的是和上一个
chunk
的时间差。类似上面提到的timestamp
,当它的值超过 3 个字节所能表示的最大值时,都会像timestamp
采取一致的方式转存该值到Extended Timestamp
。 - 其他字段与上面的解释相同
类型 2(fmt = 2, 3 bytes)
类型 2 的块头信息长度为 3 个字节,它又省去了message length
的 3 个字节和message type id
的 1 个字节,只使用 3 个字节表示timestamp delta
。该块通常用于类型 0 或类型 1 的块后面,表示其所在的信息流、信息长度和信息类型都是不变的(例如一些音频和数据格式)。
0 1 2
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
类型 3(fmt = 3, 0 byte)
类型 3 的块头信息长度为 0 个字节,它把所有描述字段都省略了,表示这个chunk
的 Message Header 和上一个chunk
的完全相同。下面举例说明类型 3 的使用场景:
场景一:相同信息流中,信息的间隔相同
- 第一个
chunk
是类型 0 的,其timestamp
为100; - 第二个
chunk
是类型 2 的,其timestamp delta
为20,即timestamp
为 100 + 20 = 120; - 第三个
chunk
是类型 3 的,其timestamp
就为 120 + 20 = 140。
列子二:较大信息被分块后,每一个块中的信息处于同一时间戳
- 第一个
chunk
是类型 0 的,其timestamp
为200; - 第二个
chunk
是类型 3 的,即timestamp
也为200; - 第三个
chunk
是类型 3 的,即timestamp
也为200;
Extended Timestamp
Extended Timestamp(扩展时间戳)用于扩展 Message Header 中时间戳的表示。
- 该字段占4个字节(即32bits),最大能表示的数值为
4294967295
(0xFFFFFFFF
)。 - 该字段用于对大于3个字节能表示的
16777215
(0xFFFFFF
) ,即对不适合于在 24 bit 的类型 0、1 和 2 的chunk
的timestamp
或者timestamp delta
进行编码。 - 在时间戳表示正常的时候,这个字段的数值全为
0
;当该字段启用时,timestamp
或者timestamp delta
必须全置为1
- 当最近的具有同一块流的类型 0、1 或 2
chunk
指示 Extended Timestamp 字段出现时,这一段才会在类型 3 的chunk
中出现。
举例
上面的介绍相对比较枯燥,通过下面的例子,我们就可以切身体会到块与分块的好处。
例子 1:块包装信息流的效果 - 解决信息冗余问题
本例展示的一个简单的音频信息流的信息冗余情况:
+---------+-----------------+-----------------+-----------------+------------------+
| |Message Stream ID| Message Type ID | Time | Length | Estimated Total |
+---------+-----------------+-----------------+-------+---------+------------------+
| Msg # 1 | 12345 | 8 | 1000 | 32 | 4+1+3+32=40 |
+---------+-----------------+-----------------+-------+---------+------------------+
| Msg # 2 | 12345 | 8 | 1020 | 32 | 4+1+3+32=40 |
+---------+-----------------+-----------------+-------+---------+------------------+
| Msg # 3 | 12345 | 8 | 1040 | 32 | 4+1+3+32=40 |
+---------+-----------------+-----------------+-------+---------+------------------+
| Msg # 4 | 12345 | 8 | 1060 | 32 | 4+1+3+32=40 |
+---------+-----------------+-----------------+-------+---------+------------------+
Sample audio messages to be made into chunks
Message Stream ID
假设用 4 个字节表示,Message Type ID
假设用 1 个字节表示,Time
假设用 3 个字节表示,那么信息流分成的每一个信息的估算长度为 40 个字节。并且用这中信息表示方式,后续的信息中会越来越多冗余的信息。
下图表示RTMP中对这个信息流进行块包装的效果:
+--------+---------+-----+------------+------- ---+------------+
| | Chunk |Chunk|Header Data |No.of Bytes|Total No.of |
| |Stream ID|Type | | After |Bytes in the|
| | | | |Header |Chunk |
+--------+---------+-----+------------+-----------+------------+
|Chunk#1 | 3 | 0 | delta: 1000| 32 | 44 |
| | | | length: 32,| | |
| | | | type: 8, | | |
| | | | stream ID: | | |
| | | | 12345 (11 | | |
| | | | bytes) | | |
+--------+---------+-----+------------+-----------+------------+
|Chunk#2 | 3 | 2 | 20 (3 | 32 | 36 |
| | | | bytes) | | |
+--------+---------+-----+----+-------+-----------+------------+
|Chunk#3 | 3 | 3 | none (0 | 32 | 33 |
| | | | bytes) | | |
+--------+---------+-----+------------+-----------+------------+
|Chunk#4 | 3 | 3 | none (0 | 32 | 33 |
| | | | bytes) | | |
+--------+---------+-----+------------+-----------+------------+
Format of each of the chunks of audio messages
由于Chunk Stream ID
为 3,小于 64,因此 Basic Header 只占用 1 个字节。由于timestamp
和timestamp detal
只为 1000 和 20,因此不需要使用 Extended Timestamp 字段。综上,我们在计算 Chunk Header 大小的时候只需要计算 Message Header 的大小加 1 即可。以下的字节计算式均为:Basic Header + Message Header + Extended Timestamp + Chunk Data。
-
Chunk#1
:作为整个块流的第一个,该chunk
必须为类型 0 的块,因此该块的大小为 44 字节。- 44 = 1 + 11+ 0 + 32
-
Chunk#2
:由Chunk#1
的message type
为8
所知,该信息为一个音频信息流。由于音频流信息分割后都是等分的,因此这里能不使用类型 1 直接采用类型 2 省略相同的信息类型、信息流ID和信息长度。可知 1020 与 1000 相差 20,因此timestamp
的位置直接存储timestamp detal
时间差。这样能避免时间戳太大,因此块的时间戳的字节也只采用 3 个字节,这样能有效压缩数据,也不会影响使用。因此该块大小为 36 字节。- 36 = 1 + 3 + 0 + 32
-
Chunk#3
:由于Chunk#3
的时间差与Chunk#2
是一致的,因此可以在类型 2 的基础上再省略掉timestamp detal
直接使用类型 3,即省略掉整个 Message Header 部分。因此该快的大小只为 33 字节。- 33 = 1 + 0 + 0 + 32
-
Chunk#4
:由于后续的信息类型、信息流ID、信息长度和时间差都没有变化,因此依然能完全省略 Message Header,该快大小也为 33 字节
综上情况,从第 3 个
chunk
开始,数据传输达到最优化,只用一个字节就能标识信息。
例子 2:信息流分块效果 - 避免单个包过大
本例展示了一条长消息,该信息流单独封包相对比较大,可能会因为单独传输这个包而延后了优先级更高的信息
+-----------+-------------------+-----------------+-----------------+
| | Message Stream ID | Message Type ID | Time | Length |
+-----------+-------------------+-----------------+-----------------+
| Msg # 1 | 12346 | 9 (video) | 1000 | 307 |
+-----------+-------------------+-----------------+-----------------+
Sample Message to be broken to chunks
由于消息的长度超过了块的最大长度(128字节),此消息传输时需要对其做分块操作,即将大的消息分割成若干个块,效果如下图所示:
+-------+------+-----+-------------+-----------+------------+
| |Chunk |Chunk|Header |No. of |Total No. of|
| |Stream| Type|Data |Bytes after| bytes in |
| | ID | | | Header | the chunk |
+-------+------+-----+-------------+-----------+------------+
|Chunk#1| 4 | 0 | delta: 1000 | 128 | 140 |
| | | | length: 307 | | |
| | | | type: 9, | | |
| | | | stream ID: | | |
| | | | 12346 (11 | | |
| | | | bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#2| 4 | 3 | none (0 | 128 | 129 |
| | | | bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#3| 4 | 3 | none (0 | 51 | 52 |
| | | | bytes) | | |
+-------+------+-----+-------------+-----------+------------+
Format of each of the chunks
由于Chunk Stream ID
为 4,小于 64,因此 Basic Header 也只占用 1 个字节。由于timestamp
为 1000,因此也不需要使用 Extended Timestamp 字段。因此该例的块字节大小计算与上一例相同。
-
Chunk#1
:作为块流第一个,它的类型必须是 0,因此该块的大小为 140 字节。- 140 = 1 + 11 + 0 + 128
-
Chunk#2
:这个块是从同一个大消息中分割出来的,因此他的时间戳和上一个块一致,所以这里能直接使用类型 3 把所有 Message Header 都省略。因此该块的大小为 129 字节。- 129 = 1 + 0 + 0 + 128
-
Chunk#3
:假设这个视频格式编码出来的信息长度是固定的,这里把剩余的信息封块,因此可以直接使用类型 3 表示。因此块的大小为 52 字节。- 52 = 1 + 0 + 0 + 51
综上两个例子,类型 3 的块有两种使用方式:
- 说明消息的继续
- 说明新消息的头信息可以由前面存在的消息中推导出来
网友评论