流媒体传输协议之 RTMP

引言

本系列文章将整理各路流媒体传输协议,包括RTP/RTCP,RTMP,希望通过深入理解各个流媒体传输协议的设计细节,对今后流媒体部分的开发工作有一定的启发。

简介

RTMP在可靠流式传输(TCP)的基础上提供了双向的消息多路复用服务,在通讯双方之间传输与时间相关的并行流数据,如音频,视频和数据消息。协议实现方通常为不同的消息类型指定不同的优先级,这样在网络带宽受限时能改变底层传输顺序。

定义

  • 负载:包中所承载的数据。例如音频或视频数据。
  • 包:一个数据包由固定头部和所承载的数据组成。一些底层协议可能需要定义数据包的封装格式。
  • 端口:在一个计算机中用于区分不同目标的抽象定义。在TCP/IP协议中用一个小的正整数来表示端口。OSI传输层的传输选择器就相当于端口。
  • 传输地址:标识一个传输终端的网络地址和端口的组合,例如IP地址和TCP端口的组合。
  • 消息流:允许消息传播的逻辑通道。
  • 消息流ID:每个消息都会有一个对应的ID,用于标识其所在的消息流。
  • 块:消息的一个片段。消息在传输之前会被分割成更小的片段,因为每一块都很小,以至于可以给不同的块指定各自的优先级,通过这种方式保证多个流中数据可以按照时间戳的顺序传输。
  • 块流:块向某一确定方向传播的逻辑通道。可以是客户端到服务端,也可以是服务端到客户端。
  • 块流ID:每个块都会有一个对应的ID,用于标识其所在的块流。
  • 复用:将独立的音频/视频数据整合为统一的音视频流,可以使多个音视频流同步传输。
  • 复用分离:复用的逆向过程。将合并的音视频数据分离为原始的音频和视频数据。
  • 远程过程调用:客户端或服务端调用另一端的功能。
  • 元数据:媒体数据的描述信息。
  • 应用实例:服务器上可以和Client建立连接的应用。
  • 动作消息格式:一个可用于序列化ActionScript对象图的紧凑的二进制格式。
  • 字节序:字节的顺序,即多字节类型的数据在内存中的存放顺序。TCP/IP各层协议将字节序定义为大端字节序,因此TCP/IP协议中使用的字节序通常称之为网络字节序。
  • 大字节序:高位字节排放在内存的低地址,低位字节排放在内存的高地址。
  • 小字节序:低位字节排放在内存的低地址,高位字节排放在内存的高地址。

字节序,校准,时间格式

所有整数都是以网络字节序来表示的。除非另行说明,本文中的所有数字都是十进制数。
在没有特殊说明的情况下,RTMP中的数据都是字节对齐的。如果有填充的话,填充字节应该用0。
RTMP中的时间戳是用一个整数来表示的,代表相对于一个起始时间的毫秒数。通常每个流的时间戳都从0开始,但这不是必须的,只要通讯双方使用统一的起始时间就可以了。要注意的是,跨流的时间同步(不同主机之间)需要额外的机制来实现。
由于时间戳的长度只有32位,所以只能在50天内循环(49天17小时2分钟47.296秒)。而流是可以不断运行的,可能多年才会结束。所以RTMP应用在处理时间戳是应该使用连续的数字算法,并且应该支持回环处理。例如:一个应用可以假设所有相邻的时间戳间隔不超过2^31-1毫秒,在此基础上,10000在4000000000之后,3000000000在4000000000之前。
时间戳增量也是以毫秒为单位的无符号整数。时间戳增量可能会是24位长度也可能是32位长度。

RTMP块流

块流为上层流媒体协议提供复用和分包的功能。RTMP块流是为配合RTMP协议而设计,但它可以使用在任何发送消息流的协议中。每个消息包含时间戳和负载类型信息。RTMP块流和RTMP协议组合可以适用于多种音视频应用,从一对一或一对多直播到视频会议都能很好的满足。
当使用可靠传输协议(如TCP)时,RTMP块流为所有消息提供了可靠的跨流端对端按时间戳顺序发送的机制。RTMP块流不提供优先级控制,但是可以由上层协议提供这样的优先级。例如:当某个客户端网络比较慢时,可能会选择抛弃一些视频消息来保证声音消息能够及时接收。
RTMP块流除自身内置的协议控制消息外,还为上层协议提供了用户控制消息的机制。

消息格式

消息格式由上层协议定义,消息可以被分成多个块以支持多路复用。消息应该包含分块功能所需的所有字段,具体内容如下:

  • 时间戳(4-byte):消息的时间戳。
  • 长度(3-byte):消息有效负载的长度,如果消息头不能被省略,则消息头的长度也应该包含在长度中。
  • 类型ID(1-byte):消息类型ID。一些类型ID是为协议控制消息保留的,这些消息所表示的信息同时供RTMP块流协议和上层协议使用。所有其他类型ID都用于上层协议,RTMP块流对这些ID做不透明处理。实际上,RTMP块流不需要用这些值来区分类型,所有消息都可以是相同的类型,应用也可以用本字段来区分同步轨道而不是区分类型。
  • 消息流ID(4-byte):消息流ID可以是任意值。被复合到同一个块流的消息流,依据消息流ID进行分离。另外,就相关的块流而言,这个值是不透明的。这个字段使用小字节序。

握手

RTMP连接以握手开始,它的握手过程可能和其他协议不同,这里的握手由3个固定大小的块组成,而不是可变大小的块加上固定大小的头。

握手流程

握手由客户端发送C0和C1块开始。
客户端必须等接收到S1之后才可以发送C2。客户端必须等接收到S2之后才可以发送其他数据。
服务器必须等接收到C0之后才可以发送S0和S1,也可能接收到C1之后发送。服务器必须等接收到C1之后才可以发送S2。服务器必须等接收到C2之后才可以发送其他数据。

C0和S0格式

C0和S0是单独的一个字节,可以当做一个8bit的整数字段来对待。
rtmp-c0-s0
以下是CO和S0包的字段解释:

  • 版本号(8位): 在C0包中,该字段表示客户端请求的RTMP版本。在S0中,该字段表示服务器选择的RTMP版本。本规范所定义的版本是3。可选值中,0-2是早期版本所用的,已被丢弃,4-31保留在未来使用,32-255不允许使用(为了区分其他以某一可见字符开始的文本协议)。如果服务器不能识别客户端请求的版本,应该返回3,客户端可能选择降级到版本3,也可能放弃握手。

C1和S1格式

C1和S1包固定为1536字节,包含以下字段:
rtmp-c1-s1

  • 时间戳(4字节):该字段承载一个时间戳,该时间戳应该作为发送端点所有后续块的时间戳起始时间。可以是0,也可以是其他的任意值。为了同步多个块流,端点可能会发送其他块流的当前时间戳。
  • 零值(4字节):该字段所有值都必须为0。
  • 随机数据(1528字节):该字段可以是任意值。通过这个字段来区分自己和连接的另一方,所以此数据应该有充分的随机性,但是没必要使用加密安全的随机值或动态值。

C2和S2格式

C2和S2包的长度也为1536字节,基本上是S1和C1的回传,包含以下字段:
rtmp-c2-s2

  • 时间戳(4字节):该字段必须包含对端发来的时间戳(对C2来说是S1, 对S2来说是C1)。
  • 时间2(4字节):该字段必须包含先前发送的并被对端读取的包的时间戳。(对C2来说是C1,对S2来说是S1)。
  • 随机数据回显(1528字节):该字段必须包含对端发送过来的随机数据字段值(对C2来说是S1, 对S2来说是C1)。任何一端都可以用时间戳和时间戳2两个字段值和当前时间戳来快速的估算带宽和延迟,但这样可能并不实用。

握手流程示意图

rtmp-handshake
上图提到的状态的解释如下:

  • Uninitialized:未初始化状态。在该阶段发送协议版本。客户端在C0包中发送RTMP协议版本,如果服务器支持此版本,服务器将在响应中发送S0和S1。如果不支持,服务器采用适当的行为作为响应,在RTMP规范中是终止连接。
  • Version Send:版本已发送状态。在未初始化状态之后客户端和服务端都进入版本已发送状态。客户端等待接收S1包,服务端等待接收C1包。收到所等待的包后,客户端发送C2包,服务端发送S2包。之后状态进入发送确认状态。
  • Ack Send:客户端和服务端等待接收S2和C2包,收到后进入握手完成状态。
  • Handshake Done:握手完成, 客户端和服务端开始交换消息。

分块

握手完成后,一个或多个块流可能会复用同一个连接,每个块流承载来自同一个消息流的同一类消息。每个块都有一个唯一的块流ID,这些块通过网络进行传输。在传输过程中,必须一个块发送完毕之后再发送下一个块。在接收端,将所有块根据块中的块流ID组装成消息。
分块将上层协议的大消息分割成小的消息,保证大的低优先级消息(比如视频)不阻塞小的高优先级消息(比如音频或控制消息)。
分块还能降低消息发送的开销,它在块头中包含了压缩的原本需要在消息中所包含的信息。
块大小是可配置的,这个可以通过一个设置块大小控制消息进行设定修改。越大的块CPU使用率越低,但是在低带宽的情况下,大的写入会阻塞其他内容的写入。而小一些的块不适合高比特率的流。

块格式

每个块由块头和数据组成,块头包含3部分:基本头、消息头和扩展时间戳。
rtmp-chunk-structure

  • 基本头(1-3字节):块流ID和块类型,块类型决定了之后消息头的编码格式。基本头的长度取决于块流ID,当块流ID越大时所需要的字节数越多。
  • 消息头(0,3,7或11字节):所发送消息的描述信息。该部分的长度取决于基本头中指定的块类型。
  • 扩展时间戳(0或4字节):该部分只有在某些特殊情况下才会使用,是否使用取决于时间戳或时间戳增量是否超出了块消息头中相应字段的描述范围。
  • 块数据(变长):块承载的有效数据,最大长度为配置的块大小。
基本头

基本头包含块流ID和块类型(在下图中用fmt字段表示),块类型决定了消息头的编码格式,基本头长度可能是1,2或3字节,这取决于块流ID的长度。
协议实现方应该用能够用最短表示法来表示块流ID。
RTMP最多支持65597个流,ID在3-65599范围内,0,1,2为保留值。如果27位代表的值为0表示块基本头占2个字节,并且块流ID范围在64-319之间(第二个字节+64),如果27位代表的值为1表示块基本头占3个字节,并且ID范围在64-65599之间(第三个字节*256 + 第二个字节 + 64),当ID在3-63之间时直接使用2~7位的值来表示流ID。
2-63范围内的块流ID用1个字节来编码:
rtmp-basic-header-1
64-319范围内的块流ID用2个字节来编码,块流ID为计算所得,公式为:第二个字节值 + 64:
rtmp-basic-header-2
64-65599范围内的块流ID用3个字节来编码,块流ID为计算所得,公式为:第三个字节值*255 + 第二个字节值 + 64
rtmp-basic-header-3
上述图中各个部分的含义如下:

  • cs id(6位):该字段表示完整的块流ID,取值在2-63之间。0,1两个值是保留值,用来表示基本头是2字节还是3字节长度。
  • fmt:该字段表明了消息头使用的格式。
  • cs id - 64(8位或16位):该字段表示块流ID,取值在64-63399之间。

64-319范围内的块流ID可以用2字节来表示,也可以用3字节表示。

消息头

消息头共有4种不同的格式,根据基本头中的”fmt”字段值来确定。协议实现方应该用最紧凑的格式来表示块消息头。

类型0

0类型的块消息头占11个字节长度,该类型必须用在一个块流的开头,和每当块流时间戳回退的时候(例如视频回退的操作)。
rtmp-message-header-0

  • timestamp(3字节):对于0类型的消息块,消息的绝对时间戳在这里发送。 如果时间戳大于或等于16777215(0xFFFFFF),改字段值必须为16777215,并且必须设置扩展时间戳来共同编码32位的时间戳。否则该字段就是完整的时间戳。
  • message length(3字节): 消息长度,类型0和类型1的块包含此字段,表示消息的长度。要注意的是,通常消息长度与块长度并不相同。块长度除了最后一个块之外,都与块最大长度相同。
  • message type id(3字节): 消息类型id,类型0和类型1的块包含此字段,表示消息的类型。
  • message stream id(4字节): 消息流ID,类型0的块包含此字段,表示消息流ID。消息流ID以小字节序存储。通常,相同块流中的消息属于用一个消息流。虽然,不同的消息流复用相同的块流会导致消息头无法有效压缩,但是当一个消息流已关闭,准备打开另外一个消息流时,就可以通过发送一个新的0类型块来实现复用。
类型1

1类型的块消息头占用7个字节长度,不包含消息流ID,该块沿用上一个消息的消息流ID。对于传输大小可变消息的流(如多数视频格式),在发送第一个消息之后的每个消息都应该使用该类型格式。
rtmp-message-header-1

  • timestamp delta(3字节): 时间戳增量。类型1和类型2的块包含此字段,表示前一个块的timestamp字段和当前块timestamp间的差值。 如果时间戳增量大于或等于16777215(0xFFFFFF),该字段必须为16777215,并且必须设置扩展时间戳,来共同表示32位的时间戳增量,否则该字段值就是实际的时间戳增量。
类型2

2类型的块消息头占用3个字节长度,不包含消息流ID和消息长度,沿用上一个块的消息流ID和消息长度。对于传输固定大小消息的流(如音频和数据格式),在发送第一个消息之后的每一个消息都应该使用该类型格式。
rtmp-message-header-2

类型3

3类型的块没有消息头,消息流ID、消息长度和时间戳增量,该类型的块使用和上一个块相同的头数据。当一个消息被分割成块时,除了第一个块,其他块都应该使用该类型。由相同大小、消息流ID和时间间隔的消息组成的流,在类型2的块之后所有块都应该使用该类型格式。如果第一个消息和第二消息之间的时间增量与第一个消息的时间戳相同,则0类型的块之后可以马上发送3类型的块,而不必使用2类型的块来注册时间增量。如果类型3的块跟在类型0的块后面,那么3类型块的时间戳增量与0类型块的时间戳相同。

扩展时间戳

扩展时间戳用来辅助编码超过16777215(0xFFFFFF)的时间戳或时间戳增量,也就说消息头无法用24位数字来表示时间戳或时间戳增量时,既0类型块的时间戳字段或1,2类型的时间戳增量字段值为16777215(0xFFFFFF)。当最近的属于相同块流ID的0类型块、1类型块或2类型块有此字段时有此字段时,3类型块也应该有此字段。

示例

示例1

这是一个简单的音频流消息,这是示例示范了信息冗余。
rtmp-header-example-1
下图展示该消息流以块流形式发送。从3类型块开始了数据传输优化,之后的块只附加了一个字节。
rtmp-header-example-2

示例2

该示例展示了一个超过128字节长度的消息,消息被分割成了数个块。
rtmp-header-example-3
下图是被分割成的块:
rtmp-header-example-4
第一个块的头信息指明了消息总大小为307字节。
注意这两个示例,3类型块可以在两种情况下使用。第一种情况是消息拆分成多个块,另一种情况是新消息复用上一个消息的所有头部内容。

协议控制消息

RTMP块流用消息类型1,2,3,5和6来作为协议控制消息,这些消息包含RTMP块流协议所需要的信息。
这些协议控制消息必须用0作为消息流ID(控制流ID),并在ID为2的块流中发送。协议控制消息收到后立即生效,它们的时间戳信息是被忽略的。

设置块大小

协议控制消息类型1:设置块大小,用于通知另一端新的最大块大小。
最大块大小默认为128字节,客户端或服务端可以修改此值,并用该消息通知另一端。例如,假设一个客户端想要发送131字节的音频数据,而最大块大小为128。在这种情况下,客户端可以向服务端发送该消息,通知它最大块大小被设置为了131字节。这样客户端只用一个块就可以发送这些音频数据。
最大块大小不能小于1字节,通常应该不低于128字节。每个方向上的最大块大小是独立的。
rtmp-protocol-control-1

  • 0(1位): 该位必须为0.
  • chunk size(31位): 该字段以字节形式保存新的最大块大小,该值将用于后续的所有块的发送,直到收到新的通知。该值可取值范围为1-2147483647(0x7FFFFFFF),但是所有大于1677215(0xFFFFFF)的值都是视作是16777215,因为任何块不可能比消息大,而消息长度不能大于16777215字节。

终止消息

协议控制消息类型2:终止消息,通知正在等待消息后续块的另一端,可以丢弃指定块流接收到的数据,块流ID为该消息的载荷。应用可能在关闭的时候发送该消息,用来表明后面的消息没有必要继续处理了。
rtmp-protocol-control-2

  • chunk stream id(32字节): 指定消息的块流ID。

确认消息

客户端或服务器在收到数据总长和窗口大小相等时,通过它回复确认消息。在连接建立完成后,消息的发送方会通知接收方一个窗口的大小(指定一个长度),如果接收方收到指定长度的数据后没有发送回复消息,发送方就不会再发送任何内容了。
rtmp-protocol-control-3

  • sequence number(32字节): 到当前时刻为止接收到的字节总数。

确认窗口大小

客户端或服务端发送该消息来通知对端发送确认消息所使用的视窗大小,并等待接收端发送确认消息。接收端在接收到视窗大小后必须发送确认消息。
rtmp-protocol-control-4

设置对方传输带宽

客户端或服务端发送该消息来限制对端的输出带宽。接收端收到消息后,可以直接使用消息中指定的窗口大小,而不需要等待收到确认消息之后。如果视窗大小与上一个视窗大小不同,则该消息的接收端应该向该消息的发送端发送新的窗口大小消息。这个消息和上一个消息都是调整窗口大小的,不同的地方是,这个消息是接受者请求发送者,让它调整窗口大小,而上一个消息是发送者主动设置了窗口大小,通知数据接受者。
rtmp-protocol-control-5
Limit Type(限制类型)有以下值:

  • 0 - Hard: 应该将输出带宽限制为指定视窗大小。
  • 1 - Soft: 应该将输出带宽限制为指定视窗大小和当前视窗大小中较小的值。
  • 2 - Dynamic: 如果上一个消息的限制类型为Hard,则该消息同样为Hard,否则抛弃该消息。

RTMP消息格式

虽然RTMP被设计成使用RTMP块流传输,但是它也可以使用其他传输协议来发送消息,在这种情况下RTMP消息的格式如下所示。值得一提的是,RTMP块流协议和RTMP协议配合时,非常适合音视频应用,包括单播、一对多实时直播、视频点播和视频会议等。

格式

服务端和客户端通过在网络上发送RTMP消息实现之间的交互,消息包括音频、视频、数据等。
RTMP消息包含两部分,消息头和有效负载。

RTMP消息头

消息头包含以下信息:

  • Message Type: 消息类型,占用1个字节。1-6的消息类型ID是为协议控制消息保留的。
  • Length: 有效负载的字节数,占用3个字节。该字段是用大端序表示的。
  • Timestamp: 时间戳,占用4个字节,用大端序表示。
  • Message Stream Id: 消息流ID,标识消息所使用的流,用大端序表示。

rtmp-message-header

消息有效负载

消息的另一部分就是有效负载,也是消息包含的实际数据,可以是音频样本或者压缩的视频数据。

用户控制消息

RTMP协议将消息类型4作为用户控制消息ID,这些消息包含RTMP流所需的必要信息。消息类型1,2,3,5和6由RTMP块流协议使用。
用户控制消息应该使用ID为0的消息流(控制流),并且通过RTMP块流传输时使用ID为2的块流。用户控制消息收到后立即生效,它们的时间戳信息会被忽略。
客户端或服务端通过发送该消息告知对方用户控制事件。该消息携带事件类型和事件数据两部分。
rtmp-user-control-message
开头的2个字节用于指定事件类型,紧跟着是事件数据。事件数据字段长度可变,但是如果用RTMP块流传输,则消息总长度不能超过最大块大小,以使消息可以使用一个单独的块进行传输。

RTMP指令消息

各种类型的消息在客户端和服务端之间进行交换,包括用于发送音频数据的音频消息,用于发送视频数据的视频消息,用于发送任意用户数据的数据消息,共享对象消息和指令消息等。共享对象消息的主要用途是管理客户端和相同服务器的共享数据。指令消息发送的是客户端与服务端之间的AMF编码指令,客户端或服务端也可以通过指令消息来实现远程过程调用(RPC)。

消息类型

客户端和服务端通过在网络上发送消息来实现交互,消息可以是任意类型,包括音频消息、视频消息、指令消息、共享对象消息、数据消息和用户控制消息等。

指令消息

指令消息在客户端和服务端之间传递AMF编码的指令,消息类型20代表AMF0编码,消息类型17代表AMF3编码。发送这些消息来完成连接、创建流、发布、播放、暂停等操作。像状态、结果这样的指令消息,用于通知发送方请求的指令状态。一条指令消息由指令名、事务ID和包含相关参数的指令对象组成。客户端或服务端还可以通过指令消息来实现远程过程调用(RPC)。

数据消息

客户端或服务端通过该消息来发送元数据或其他用户数据。元数据包括数据(音频、视频)的创建时间、时长、主题等详细信息。消息类型18代表AMF0编码,消息类型15代表AMF3编码。

共享对象消息

共享对象是在多个客户端之间同步的Flash对象(键值对集合)。消息类型19代表AMF0编码,消息类型16代表AMF3编码。每个消息都可以包含多个事件。
rtmp-shared-object-message
支持以下事件类型:

  • 创建(1):客户端向服务端发送,请求创建指定名称的共享对象。
  • 释放(2):客户端通知服务端,共享对象已在本地删除。
  • 请求更新(3):客户端请求修改共享对象的属性值。
  • 更新(4):通知服务端向除自己外的其他客户端发送共享数据消息,通知它们有属性的值发生了变化。
  • 成功(5):“请求更新”事件被接受后,服务端向发送请求的客户端回复此事件。
  • 发送消息(6):客户端向服务端发送此事件,来广播一个消息。服务端收到此事件后向所有客户端广播一条消息,包括请求方客户端。
  • 状态(7):服务端发送此事件来通知客户端错误信息。
  • 清除(8):服务端向客户端发送此事件,通知客户端清除一个共享对象。服务端在回复客户端的“创建”事件时也会发送此事件。
  • 移除(9):服务端发送此事件,使客户端删除一个插槽。
  • 请求移除(10):客户端删除一个插槽时发送此事件。
  • 创建成功(11):当连接成功时服务端向客户端发送此事件。

音频消息

客户端或服务端通过发送此消息来发送音频数据给对方,消息类型8是为音频消息预留的。

视频消息

客户端或服务端通过发送此消息来发送视频数据给对方,消息类型9是为视频消息预留的。

组合消息

组合消息,是一个消息包含多个子RTMP消息,子消息符合RTMP消息格式。消息类型22用于组合消息。
rtmp-aggregate-message
组合消息的消息流ID会覆盖其中子消息的消息流ID。
组合消息的时间戳和其中第一个子消息的时间戳的差值,是用来将所有子消息的时间戳重整为流时间的位移量。位移量会加到每一个子消息的时间戳上来换算出正常的流时间。第一个子消息的时间戳应该与组合消息的时间戳相同,所以位移量应该为0。
Back Pointer(反向指针)包含前一个消息的长度(包括消息头),这样符合flv文件格式,可用于进行后退操作。
使用组合消息有以下好处:

  • 块流协议中,一个块最多只能发送一个消息,这样就使用组合消息,加大块大小,从而降低发送的块数量。
  • 子消息在内存中连续存放,这样系统调用网络发送数据的性能更高。

用户控制消息事件

客户端或服务器通过该消息发送用户控制事件。
rtmp-user-control-message
用户控制消息支持以下事件:

  • 流开始(0):服务端发送该事件,用来通知客户端一个流已经可以用来通讯了。默认情况下,该事件是在收到客户端连接指令并成功处理后发送的第一个事件。事件的数据使用4个字节来表示可用的流的ID。
  • 流结束(1):服务端发送该事件,用来通知客户端其在流中请求的数据已经结束了。如果没有额外的指令,将不会再发送任何数据,而客户端会丢弃之后从该流接收到的消息。事件数据使用4个字节来表示回放完成的流的ID。
  • 流枯竭(2):服务端发送该事件,用来通知客户端流中已经没有更多的数据了。如果服务端在一定时间后没有探测到更多数据,它就可以通知所有订阅该流的客户端,流已经枯竭。事件数据用4个字节来表示枯竭的流的ID。
  • 设置缓冲区大小(3):客户端发送该事件,用来告知服务端用来缓存流中数据的缓冲区大小(单位毫秒)。该事件在服务端开始处理流数据之前发送。事件数据中,前4个字节用来表示流ID,之后的4个字节用来表示缓冲区大小(单位毫秒)。
  • 流已录制(4):服务端发送该事件,用来通知客户端指定流是一个录制流。事件数据用4个字节表示录制流的ID。
  • ping请求(5):服务端发送该事件,用来探测客户端是否处于可达状态。事件数据是一个4字节的时间戳,表示服务端分发该事件时的服务器本地时间。客户端收到后用ping响应回复服务端。
  • ping响应(6):客户端用该事件回复服务端的ping请求,事件数据为收到的ping请求中携带的4字节的时间戳。

指令类型

客户端和服务器交换AMF编码的指令。发送端发送一条指令消息,其中包含了指令名称、处理ID、以及含有相关参数的指令对象。例如,连接指令消息包含了’app’参数,以告知服务器客户端希望连接的目标程序。接收端处理这条指令并回复含有同样处理ID的响应。回复的字符串可能为_result、_error或方法名。如verifyClient或contactExternalServer.
_result或_error的指令字符代表一条响应,处理ID则表明回复是针对哪条指令的,这在IMAP或其他协议中是完全相同的。指令字符串中的方法名表明发送端希望运行接收端上的一个方法。
指令消息可分为如下两类:

  • NetConnection:一个服务器和客户端之间连接的高层表现对象。
  • NetStream:一个音频流、视频流及其他相关数据传输流,我们会发送如播放、暂停等指令来控制数据流动。

NetConnection指令

NetConnection管理着一个客户端程序和服务器之间的双向连接,除此之外,它还提供了对异步远程方法调用的支持。
下列指令可通过NetConnection进行发送:

  • Connect
  • Call
  • Close
  • CreateStream
Connect

客户端发送connect指令至服务器端以请求连接至某一服务器程序实例。
指令结构如下:
rtmp-connect
Connect指令中会用到的键值对:
rtmp-connect-key-value
音频编码:
rtmp-connect-audio
视频编码:
rtmp-connect-video
视频功能:
rtmp-connect-video-function
对象编码:
rtmp-connect-object-encode
由服务器发送至客户端的指令结构如下:
rtmp-connect-command-server-to-client
指令执行流程:
rtmp-connect-flow
指令执行的消息流如下:

  • 客户端发送connect指令至服务器以请求连接至服务器端程序实例。
  • 在收到连接指令后,服务器端发送协议消息’Window Acknowledgement Size’给客户端。同时,服务器端还会连接connect指令中提到的应用。
  • 服务器端发送协议消息‘Set Peer Bandwidth’至客户端。
  • 客户端成功处理‘Set Peer Bandwidth’后发送协议消息‘Window Acknowledgement Size’给服务器端。
  • 服务器端发送用户控制消息(StreamBegin)协议消息给客户端。
  • 服务器端发送指令消息以通知客户端连接状态(success/fail)。该指令中含有处理ID(与1中收到相同),该消息同时还制定了部分属性,如Flash Media Server版本(string)。除此之外,它还指定了连接响应相关的信息如level(string),code(string),description(string),object-encoding(number)等。
Call

NetConnection对象的call方法用于远程调用接收端上的程序。需要远程调用的程序名称通过一个参数传递给call指令。
发送指令结构如下:
rtmp-call-send
响应指令结构如下:
rtmp-call-response

CreateStream

客户端发送该指令至服务器端以创建一条用于传递消息的逻辑通道,从而可以利用已创建的流通道发布音频、视频和元数据。
NetConnection是默认的通讯通道,流ID为0。协议和一些指令消息,包括createStream,使用默认通讯通道。
客户端发出的指令结构如下:
rtmp-create-stream-send
服务器发出的指令结构如下:
rtmp-create-stream-response

NetStream指令

基于NetConnection的客户端至服务器间连接,NetStream定义了一条可以传递音频流、视频流以及消息流的通道。NetConnection对象支持多个NetStreams以传输多个数据流。
客户端可在NetStream中发送下列指令至服务器:

  • Play
  • Play2
  • DeleteStream
  • CloseStream
  • ReceiveAudio
  • ReceiveVideo
  • Publish
  • Seek
  • Pause

服务器端通过“onStatus”将NetStream的状态更新至客户端:
rtmp-on-status

Play

客户端发送该指令值以播放一个流。多次调用该指令也可创建一个播放清单。
如果你希望创建一个在不同live或recorded流间切换的动态播放清单,需要多次调用play并传递false以避免每次reset。相反地,如果你希望立即播放某一指定流,传递true以清除等待播放队列中的所有其他流。
客户端发送的指令结构如下:
rtmp-play-send
流程图如下:
rtmp-play-flow
指令执行期间的消息流如下:

  • 客户端在接收到来自服务器的createStream指令的成功结果后发送play指令。
  • 在接收到play指令后,服务器发送协议数据来设置块大小。
  • 服务器发送一些另外一个协议数据(用户控制),在这个消息里包含事件“StreamIsRecord”和流ID。这个消息的前2个字节是事件类型随后的4字节是流ID。
  • 服务器向客户端发送另外一个协议消息(用户控制),这个消息指示了“StreamBegin” 事件,表示流开始了。
  • 如果客户端向服务器发送的play指令成功执行了,服务器会发送 onStatus 指令消息包含NetStream.Play.Start或NetStream.Play.Reset。仅当客户端发送的play指令中的设置了reset标志NetStream.Play.Reset才会被发送。如果播放的流不存在,服务器会在发送onStatus消息中包含NetStream.Play.StreamNotFound。随后,服务器就发送客户端播放的音频和视频数据。
Play2

不同于play指令,play2可以切换码率而不改变播放内容的时间轴。服务器为客户端可以在play2中请求的所有支持的码率维护多个字段。
客户端发送的指令结构如下:
rtmp-play2-send
该指令的消息流程如下图:
rtmp-play2-flow

DeleteStream

当NetStream对象将要被销毁时,它发送该deleteStream指令。
客户端发送的指令结构如下:
rtmp-delete-stream-send
服务器不需要发送任何应答。

ReceiveAudio

NetStream发送ReceiveAudio消息通知服务器是否发送或不发送音频到客户端。
客户端发送的指令结构如下:
rtmp-receive-audio-send
如果receiveAudio指令发送带有flase的bool flag,服务器不发送任何响应。如果这个标志被设置为true,服务器应答NetStream.Seek.Notify和NetStream.Play.Start的状态消息。

ReceiveVideo

NetStream发送ReceiveVideo消息通知服务器是否发送或不发送视频到客户端。
客户端发送的指令结构如下:
rtmp-receive-video-send
如果receiveVideo指令发送带有flase的bool flag,服务器不发送任何响应。如果这个标志被设置为true,服务器应答NetStream.Seek.Notify和NetStream.Play.Start的状态消息。

Publish

客户端发送publish指令将已命名的流发布到服务器上。使用这个名称,任何客户端都可以播放此流,并接收已发布的音频、视频和数据消息。
客户端发送的指令结构如下:
rtmp-publish-send
服务器应答onStatus指令,以标记发布的开始。

Seek

客户端发送seek指令以定位媒体文件内或者播放列表的某个位置(以毫秒为单位)。
客户端发送的指令结构如下:
rtmp-seek-send
当定位成功,服务器发送NetStream.Seek.Notify的状态消息。失败的时候,它返回一个_error的消息。

Pause

客户端发送pause指令以告诉服务器暂停或者开始播放。
客户端发送的指令结构如下:
rtmp-pause-send
当流被暂停,服务器发送一个NetStream.Pause.Notify的状态消息。当一个流变成未暂停状态,NetStream.Unpause.Notify被发送。失败的时候,它返回一个_error的消息。

消息交换例子

这里是一些样例,以解释使用RTMP的消息交换。

发布视频

这个例子说明了一个发布者如何可以发布一个流,然后将视频推流到服务器上。其他客户端可以订阅这个已发布的流,并播放视频。
rtmp-publish-video-example

广播一个共享对象消息

这个例子说明了在创建和更改共享对象时所交换的消息。它也说明了共享对象消息广播的过程。
rtmp-shared-object-example

发布媒体流元数据

这个例子描述了发布元数据的消息交换。
rtmp-publish-metadata-example

参考内容

[1] RTMP 规范
[2] RTMP协议规范翻译工作
[3] RTMP协议规范1.0中文版

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/protocol/rtmp/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。