RTMP

RTMP,第1张

这一篇笔记主要记录对 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(n1) 个 chunk,按照发送次序依次编号为 1 , 2 , 3 , . . . . , n 1,2,3,....,n 123....n

RTMP 规定,除第 n n n 个 chunk 外,前 n − 1 n-1 n1 个 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(n1)(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 Size
      • 2:Abort Message
      • 3:Acknowledgement
      • 5:Windon Acknowledgement Size
      • 6:Set Peer Bandwidth
    • 控制 Message 层级行为的:
      • 4:User Control Message
  • RTMP command message,用于传输数据,RPC(Remote Procedure Calls)等
    • 8:Audio Message
    • 9:Video Message
    • 1720:Command Message
    • 1518:Data Message
    • 1619:Shared Object Message
    • 22: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 IDMessage Type IDTimestampLength
MSG # 11081000280
MSG # 21081020150

假设当前的 maximum chunk size 为 128,则将两个 message 依次封装为 5 个 chunk,如下:

Chunk Stream IDChunk FormatTimestamp(Delta)Message LengthMessage Type IDMessage Stream IDChunk Data Size
CHK # 1301000280810128
CHK # 233\\\\128
CHK # 333\\\\24
CHK # 43120150810128
CHK # 533\\\\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。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://www.outofmemory.cn/langs/758893.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-01
下一篇 2022-05-01

发表评论

登录后才能评论

评论列表(0条)

保存