Bootstrap

RTMP协议

背景介绍

RTMP(Real Time Messaging Protocol) 是由 Adobe 公司基于 Flash Player 播放器对应的音视频 flv 封装格式提出的一种,基于TCP 的数据传输协议。本身具有稳定、兼容性强、高穿透的特点。常被应用于流媒体直播、点播等场景。常用于推流方(主播)的稳定传输需求。RTMP协议的默认端口号是1935

RTMP流程

握手

RTMP是基于TCP的,在RTMP之前,传输层的TCP连接就已经建立好了。但除此之外,应用层的RTMP协议也是需要再建立RTMP连接的。

在建立连接之前,要经过RTMP握手。握手的主要目的为:交换RMTP协议版本信息、确立连通性、设置通信的起始时间等。握手的大致流程如下图所示:

在这里插入图片描述

建立连接

在握手成功之后,就可以正式建立连接了,大致流程如下:

在这里插入图片描述

  1. 客户端发送命令消息中的“连接”(connect)到服务器,请求与一个服务应用实例建立连接。
  2. 服务器接收到连接命令消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到客户端,同时连接到连接命令中提到的应用程序。
  3. 服务器发送设置带宽协议(Set Peer Bandwidth)消息到客户端。
  4. 客户端处理设置带宽协议消息后,发送确认窗口大小(Window Acknowledgement Size)协议消息到服务器端。
  5. 服务器发送用户控制消息中的“流开始”(Stream Begin)消息到客户端。
  6. 服务器发送命令消息中的“结果”(_result),通知客户端连接的状态。

创建流

建立连接后,就可以创建流了,大致流程如下:

在这里插入图片描述

  1. 客户端发送命令“创建流”给服务器
  2. 服务器接收到命令之后,发送“结果”给客户端。

推流或拉流

推流:(publish命令)

在这里插入图片描述

拉流:(play命令)

在这里插入图片描述

抓包分析

以推流为例,下图是RTMP推流的抓包分析

在这里插入图片描述

RTMP数据格式

RTMP Message

Message是RTMP协议中的基本数据单元。消息可以包含音频,视频,控制消息,以及其它数据。RTMP的消息由两部分构成,分别是header和payload。payload表示有效载荷。其中header的格式如下:

在这里插入图片描述

chunk分块

RTMP 协议为了维持稳定连续传递,避免单次传输数据量问题,采用了传输层封包,数据流切片的实现形式。被用来对当前带宽进行划分和复用的最小传输单位,被称为 Chunk 即消息块。通常情况下,一个Message,如果数据量超出当前 Chunk Size 的话,则会被拆分成多个chunk分块来分批传输。通过指定首个 Chunk 和后续 Chunk 类型,以及 Chunk Header 其他标志性数据,来使当前被切割的消息,能够在对端得到有效的还原和执行。可以参考下图来理解:

在这里插入图片描述

也就是说,除了RTMP握手期间的报文,其余的任何RTMP报文都是以chunk为单位进行发送的。

Chunk Header

和大多数协议一样,RTMP协议也是经典的 header+body 格式,详见下图:

在这里插入图片描述

ChunkData部分没什么好说的,就是携带的音视频数据或者协议的命令数据等,本文主要介绍的是ChunkHeader部分。

Chunk Header主要由三部分构成:BasicHeader + MessageHeader + ExtendedTimestamp

Basic Header

基础数据头(BasicHeader)有单字节、二字节、三字节这三种版本,但不论是哪一个版本,前两个比特位都固定为fmt字段,用以指示chunk message header的格式类型

RTMP 规格将BasicHeader分为3种:ID 在 2~63 范围内的 1-Byte 版;ID 在 64~319 范围内的 2-Byte 版;ID 在 64~65599 范围内的 3-Byte 版。基础数据头组成,也包含三个部分。

单字节格式:

在这里插入图片描述

二字节和三字节格式:

在这里插入图片描述

其中cs_id( Chunk stream ID )的范围 3 - 65599,0、1、2被保留。也就是说,如果第一个字节的后6位大于2,则表示是单字节模式,如果为0就表示二字节模式,为1就表示三字节模式。

需要注意的是,cs_id是用来区分消息信道的。因为 RTMP 协议,所有的通信都是通过同一个 TCP socket 来完成的,因此所有类型的通信信道需要由来进行区分,从而判断当前收到的消息所属的信道类型。也就是说,并不能通过cs_id来区分是音频还是视频。

MessageHeader

MessageHeader的有4种格式,由BasicHeader的fmt字段决定,
MessageHeader的有4种格式,由BasicHeader的fmt字段决定,具体如下所示:

  • Type 0:Full Header,这是最完整的头部格式,包含了所有的必要信息
    类型0块头的长度是 11 个字节。这一类型必须用在块流的起始位置,和流 timestamp 重来的时候
    在这里插入图片描述
  • Type 1:Full Header without Timestamp,与完整头部类似,但省略了时间戳字段
    类型1块头长为 7 个字节。不包含消息流 ID;这一块使用前一块一样的流 ID。可变长度消息的流 (例如,一些视频格式) 应该在第一块之后使用这一格式表示之后的每个新消息。
    在这里插入图片描述
  • Type 2:Message Stream ID Only,仅含消息流ID的头部
    类型2块头长度为 3 个字节。既不包含流 ID 也不包含消息长度;这一块具有和前一块相同的流 ID 和消息长度。具有不变长度的消息 (例如,一些音频和数据格式) 应该在第一块之后使用这一格式表示之后的每个新消息。
    在这里插入图片描述
  • Type 3:Type Only,最简化的头部形式,只包含了一个字节的类型标识符
    类型3的块没有消息头。流 ID、消息长度以及 timestamp delta 等字段都不存在;这种类型的块使用前面块一样的块流 ID。当单一一个消息被分割为多块时,除了第一块的其他块都应该使用这种类型。组成流的消息具有同样的大小,流 ID 和时间间隔应该在类型 2 之后的所有块都使用这一类型。如果第一个消息和第二个消息之间的 delta 和第一个消息的 timestamp 一样的话,那么在类型 0 的块之后要紧跟一个类型 3 的块,因为无需再来一个类型 2 的块来注册 delta 了。如果一个类型 3 的块跟着一个类型 0 的块,那么这个类型 3 块的 timestamp delta 和类型 0 块的 timestamp 是一样的。

对于这些Type,这里有一张便于理解的图:

在这里插入图片描述

块消息头中各字段的描述如下:

  • timestamp(3字节):时间戳,一般是绝对时间戳。
  • timestamp delta (3字节):对于一个类型 1 或者类型 2 的块,前一块的 timestamp 和当前块的 timestamp 的区别在这里发送。如果 delta 大于或者等于 16777215 (十六进制 0xFFFFFF),那么这一字段必须是为 16777215,表示具有扩展 timestamp 字段来对整个 32 位 delta 进行编码。否则的话,这一字段应该是为具体 delta。
  • message length (3字节):对于一个类型 0 或者类型 1 的块,消息长度在这里进行发送。注意这通常不同于块的有效载荷的长度。块的有效载荷代表所有的除了最后一块的最大块大小,以及剩余的 (也可能是小消息的整个长度) 最后一块。
  • message type id (消息类型,1字节):对于类型 0 或者类型 1 的块,消息的类型在这里发送。
  • message stream id (4字节):对于一个类型为 0 的块,保存消息流 ID。消息流 ID 以小端格式保存。所有同一个块流下的消息都来自同一个消息流。当可以将不同的消息流组合进同一个块流时,这种方法比头压缩的做法要好。但是,当一个消息流被关闭而其他的随后另一个是打开着的,就没有理由将现有块流以发送一个新的类型 0 的块进行复用了。

不同的消息类型对应不同的数据,类型说明如下:

在这里插入图片描述

ExtendedTimestamp

这个字段比较好理解,当MessageHeader中的timestamp字段的大小不够用时,即当其值为0xFFFFFF时,才会设置ExtendedTimestamp,否则就没有这个字段。

参考官方文档的说明:

扩展 timestamp 字段用于对大于 16777215 (0xFFFFFF) 的 timestamp 或者 timestamp delta 进行编码;也就是,对于不适合于在 24 位的类型 0、1 和 2 的块里的 timestamp 和 timestamp delta 编码。这一字段包含了整个 32 位的 timestamp 或者 timestamp delta 编码。可以通过设置类型 0 块的 timestamp 字段、类型 1 或者 2 块的 timestamp delta 字段 16777215 (0xFFFFFF) 来启用这一字段。当最近的具有同一块流的类型 0、1 或 2 块指示扩展 timestamp 字段出现时,这一字段才会在类型为 3 的块中出现。

内容补充

RTMP URL的格式

RTMP的url格式为:rtmp://server_address/application_instance_name/[stream_key]

  • server_address 是 RTMP 服务器的地址,可以是 IP 地址或域名。
  • application_instance_name 是应用实例的名称,例如 “live” 或 “vod”(视频点播)。
  • [stream_key] 是可选的,代表特定的流键,通常用于标识不同的直播流。

例如有一个 RTMP 服务器位于 rtmp.example.com,该服务器上有一个名为 “live” 的实例,用于处理直播流。如果要创建一个名为 “myStream” 的直播流,那么对应的RTMP URL为:rtmp://rtmp.example.com/live/myStream

ChunkStreamID、MessageTypeID、MessageStreamID的区别

  1. ChunkStreamID是用于区分不同信道的,官方并没有规定哪种信道具体要用什么ID,这完全是由用户自定义的,所以在不同的场景下同一种流的ChunkStreamID可能会不同。

  2. MessageTypeID则是专门用于区分消息类型的,官方有规定的具体ID与Type之间的对应细则。

  3. MessageStreamID则是为特定的消息流提供唯一的标识。当客户端与服务器之间有多个并发的消息流时,MessageStreamID帮助区分这些不同的流。例如,在多路复用的情况下,同一个连接上可以同时传输多个音轨或视频轨道,每个都会有一个独立的MessageStreamID。

简单来说,这三者之间并不存在强关联,仅仅作为不同信息层区分使用。

绝对时间戳和相对时间戳

RTMP的时间戳有绝对时间戳和相对时间戳之分,如果fmt为0,则第一个字段为timestamp,这就表示的是绝对时间戳。如果fmt不为零,那么其它格式的第一个字段都为timestamp delta,表示相对时间戳(时间戳差),表示相对于前一个绝对时间戳的差值。

官方文档

想要了解更多细节,可以参考官方文档:

  1. RTMP 协议规范(中文版) - 仲达超 - 博客园
  2. RTMP官方文档 - 英文原版

本文参考

  1. 流媒体:RTMP 协议完全解析 - 知乎
  2. RTMP 协议规范(中文版) - 仲达超 - 博客园
  3. RTMP协议推流交互流程 - 靑い空゛ - 博客园
  4. FFmpeg学习(八)RTMP与FLV协议 - 山上有风景 - 博客园
  5. RTMP协议学习——Message与Chunk解读
;