这一篇笔记主要记录对 chunk 和 message 两个概念的理解。
一个不严谨的比喻
如果把一条 TCP 链接比喻成一条铁路,那么:
- 一个字节就是一份货物。
- 一个 chunk 就是一辆火车。
- 一条 chunk stream 就是一条轨道。
- 一份 message 就是包含若干货物的清单,这些货物被装载至一辆或多辆火车(chunk)上。
需要注意:
- 两个方向上的 chunk stream 数量无需相等,编号可同可不同。
- 同一个 message 产生的多个 chunk 只会在一条 chunk stream 串行发送。
- 先发送的 chunk 一定先到达。
- 多条 chunk stream 复用了一条 TCP 链接。
说的直接点:
- chunk 就是一个报文——预先约定清楚的 header 字段(ID,类型,时间戳等等),以及变长的负载数据(chunk data)。
- chunk stream 就是一串有相同 ID 的 chunk,借助 TCP 链接在网络上流动,从发送端流向接收端。
Chunk - 最小传输单元(这名是我胡诌的)
在 RTMP 层面,chunk 就是最小的传输单元,数据封装为 chunk 后方能通过 RTMP 协议进行发送。
Chunk 的字段可划分为四部分,如下图示:
Basic Header (1 - 3 bytes)
本部分包含两个字段,format 和 chunk stream id。
Format 仅占两个比特,其值决定了 message header 的长度。
Chunk stream id 是一个变长的整数,取值范围为 [2, 65599],可能占用 14,22 或 6 个比特。
CS ID 占用 14 个比特
RTMP规定当第 2 - 7 个比特的值为 0 时,chunk stream id 占用 14 个比特,其值减去 64 后存储在第 8 - 15 个比特中。取值范围为 [64, 319]。
CS ID 占用 22 个比特
RTMP规定当第 2 - 7 个比特的值为 1 时,chunk stream id 占用 22 个比特,其值减去 64 后存储在第 8 - 23 个比特中。取值范围为 [64, 65599]。
CS ID 占用 6 个比特
RTMP规定当第 2 - 7 个比特的值在区间 [2,63] 内时,这 6 比特数据就是 chunk stream id。
Message Header (0, 3, 7, or 11 bytes)
Meassage header 的格式依赖 basic header 中的 format 一起解释。
Format 0 (11 bytes)
此时 message header 占用 11 字节,包含四个字段,如下图示:
Timestamp (3 bytes)
这是一个很重要的字段,FFmpeg 会用其值计算帧的 DTS 和 PTS。
Message Length (3 bytes)
该字段用来描述 message 的长度。注意,是 message 的长度,而非 chunk data 的长度。
Message Type ID (1 byte)
该字段用来描述 message 的类型,后面再详细介绍。
Message Stream ID (4 bytes)
一个 message 可能被封装为多个 chunk,这些 chunk 的 message stream id 的值均相同。
在接收端,会按照先后次序,将具有相同 chunk stream id 和 message stream id 的 chunk 解封为一个 message。配合 message length 字段可判断是否完整接收了一个 message,具体判断逻辑下文有记录。
Format 1 (7 bytes)
此时 message header 占用 7 字节,各字段如下图所示:
相较于 format 0 的字段,这里有两处改动:
移除 Message Stream ID
没有 message stream id,那接收端如何确认该 chunk 应该解封至哪个 message 呢?
RTMP 规定,当 chunk 的 format 不等于 0 时,其 message stream id 的值等于最近一个具有相同 chunk stream id 的 chunk 的 message stream id。
这样做是为了复用了之前的数据,避免冗余传输。
修改 Timestamp 为 Timestamp Delta
RTMP 规定,当 chunk 的 format 不等于 0 时,其 timestamp 可由最近一个具有相同 chunk stream id 的 chunk 的 timestamp 加上 timestamp delta 获得。
一个隐藏的含义
不知大家是否注意到,format == 1 时,移除了 message stream id,但未移除 message type id 和 message type length。
这意味着,message stream 并没有和某一份具体的 message 配置(类型,长度)绑定。换言之,一个 message stream 可以传输不同类型和长度的 message。
Format 2 (3 bytes)
移除了 message length 和 message type id
在 format 1 的基础上又移除了 message length 和 message type id。同样的,RTMP 规定,复用最近一个 format 0 或 format 1 的,具有相同 chunk stream id 的对应字段。
Format 3 (0 bytes)
Umm… 所有字段都复用了,妙啊。
需要注意,若最近一个具有相同 chunk stream id 的 format :
- 为 0 时,将其 timestamp 的值作为当前 chunk 的 timestamp delta。
- 其他类型时,将其 timestamp delta 作为当前chunk 的 timestamp delta。
Extended Timestamp (0 or 4 bytes)
因为 timestamp / timestamp delta 字段仅有三个字节,所以当要传输的值不小于 0xFFFFFF
时可启用该字段。
RTMP 规定,当 timestamp 或 timestamp_delta 的值为 0xFFFFFF
时表示启动 extended timestamp 字段,该字段存储了完整的值。
需要注意的是,即使当 format 为 3 时,也不会复用该字段。换言之,当 format 为 3 时,若最近一个 format 0,1 或 2 类型的,具有相同 chunk stream id 的 timestamp / timestamp delta 为 0xFFFFFF
,则当前的 chunk 仍带有 extended timestamp。
Chunk Data
负载数据。比如音频的采样数据,视频的帧数据。
Chunk 中无指定 chunk data 长度的字段。Chunk data 的长度由两部分决定:
- maximum chunk size
- message length
Maximum Chunk Size
Maximum chunk size 可看做一个配置,RTMP规定默认大小时 128。可通过协议控制消息(Protocol Control Message,后续笔记记录) 中的 SetChunkSize 调整为 [0x1, 0xFFFFFF]
内的任意值。
注意:
- server → client 和 client → server 的 maximum chunk size 是独立的,互不影响。
- Maximum chunk size 只是一个上限,即 chunk data 的超度不能超过该值,但可以小于等于该值。
Message Length
RTMP 规定,一个 message 可以被封装为多个 chunk。设一个 message 被分成了 n ( n ≥ 1 ) n(n\ge 1) n(n≥1) 个 chunk,按照发送次序依次编号为 1 , 2 , 3 , . . . . , n 1,2,3,....,n 1,2,3,....,n。
RTMP 规定,除第 n n n 个 chunk 外,前 n − 1 n-1 n−1 个 chunk 的 chunk data 的长度均为 maximum chunk size。
第
n
n
n 个 chunk 的 chunk data 长度为
m
e
s
s
a
g
e
L
e
n
g
t
h
−
(
n
−
1
)
∗
(
m
a
x
i
m
u
m
C
h
u
n
k
S
i
z
e
)
messageLength - (n-1)*(maximumChunkSize)
messageLength−(n−1)∗(maximumChunkSize)
Message
一个 message 由 header 和 body 两部分组成。Header 的类型即为上文中的 message header,body 的长度由 message length 给出。
body 的解析方法由 message type id 指定。这一段落只记录 message type id 的含义。
Message Type ID
从《RTMP specification 1.0》的目录里可以看出,message 类型可细分如下:
- Protocol control message
- 控制 Chunk 层级行为的:
1
:Set Chunk Size2
:Abort Message3
:Acknowledgement5
:Windon Acknowledgement Size6
:Set Peer Bandwidth
- 控制 Message 层级行为的:
4
:User Control Message
- 控制 Chunk 层级行为的:
- RTMP command message,用于传输数据,RPC(Remote Procedure Calls)等
8
:Audio Message9
:Video Message17
,20
:Command Message15
,18
:Data Message16
,19
:Shared Object Message22
:Aggregate Message
有两个ID的类型,主要区别在于 AMF0 和 AMF3。可以粗略的理解为定义了两种 json
序列化和反序列化的标准。
这里给出两个链接,有需要的铁子自取:
- 《AMF0 spec 121207》
- 链接: https://pan.baidu.com/s/1_aZttaDbMbI80GZsPDK-iA
- 提取码: w66t
- 《Action Message Format – AMF3》
- 链接: https://pan.baidu.com/s/1OQeWcdbb1YlE9bTrOL9k_Q
- 提取码: 9pi7
Chunk 和 Message 转换示例
假设现在有两个如下的 audio message 要发送,header 分别如下表示:
Message Stream ID | Message Type ID | Timestamp | Length | |
---|---|---|---|---|
MSG # 1 | 10 | 8 | 1000 | 280 |
MSG # 2 | 10 | 8 | 1020 | 150 |
假设当前的 maximum chunk size 为 128,则将两个 message 依次封装为 5 个 chunk,如下:
Chunk Stream ID | Chunk Format | Timestamp(Delta) | Message Length | Message Type ID | Message Stream ID | Chunk Data Size | |
---|---|---|---|---|---|---|---|
CHK # 1 | 3 | 0 | 1000 | 280 | 8 | 10 | 128 |
CHK # 2 | 3 | 3 | \ | \ | \ | \ | 128 |
CHK # 3 | 3 | 3 | \ | \ | \ | \ | 24 |
CHK # 4 | 3 | 1 | 20 | 150 | 8 | 10 | 128 |
CHK # 5 | 3 | 3 | \ | \ | \ | \ | 22 |
如何计算 Timestmap 和 Timestamp Delta
每个 message 有且只有一个 timestamp 字段,但它有可能被封装为多个 chunk。那么一个 message 的 timestamp 和多个 chunk 的 timestamp( delta) 之间该如何换算呢?
// FFmpeg/libavformat/rtmppkt.c
// static int rtmp_packet_read_one_chunk(URLContext *h, RTMPPacket *p ...
// 该函数的功能是从网络读取 chunk。
// ts_field 为 timestamp 或 timestamp delta 的值。
// timestamp 为 extended timestamp 的值。
// 不难发现,当且仅当该 chunk 是 message 的第一个 chunk 时,才使用了 ts_filed 和 timestamp 字段;其他情况下,均直接丢弃了这两个字段。
238 if (!prev_pkt[channel_id].read) {
239 if ((ret = ff_rtmp_packet_create(p, channel_id, type, timestamp,
240 size)) < 0)
241 return ret;
242 p->read = written;
243 p->offset = 0;
244 prev_pkt[channel_id].ts_field = ts_field;
245 prev_pkt[channel_id].timestamp = timestamp;
246 }
上述代码是 FFmpeg 中的实现,从网络读取一个 chunk 并将其解封至一个 message。当且仅当读到 message 的第一个 chunk 时,才使用了 ts_filed 和 timestamp 字段,其他情况下,均直接丢弃了这两个字段。
参照 FFmpeg 的实现,可以愉快的得出如下结论~
一个 message 封装为多个 chunk :
- 若第一个 chunk 的 format 为 0,则 timestamp = message.timestamp。
- 反之,timestamp delta 即为当前 message 和前一个 message 的 timestamp 之差。
多个 chunk 解封为一个 message :
- 若第一个 chunk 的 format 为0,则 message.timestamp = chunk.timestamp。
- 反之,则将 timestamp delta 与前一个 message 的 timestamp 的和作为当前 message 的 timestamp。