RTMP协议是一个互联网五层体系结构中应用层的协议。RTMP协议中基本的数据单元称为消息(Message)。当RTMP协议在互联网中传输数据的时候,消息会被拆分成更小的单元,称为块(Chunk)。
一.定义
Payload(载荷):包含于一个数据包中的数据,例如音频采样或者视频压缩数据。
Packet(数据包):一个数据包由固定头和载荷数据构成。一些底层协议可能会要求对数据包进行封装。
Port(端口):TCP/IP使用小的正整数对端口进行标识。OSI传输层使用的运输选择器 (TSEL) 相当于端口。
Transport address(传输地址):用以识别传输层端点的网络地址和端口的组合,例如一个IP地址和一个TCP端口。
Message stream(消息流):通信中消息流通的一个逻辑通道。
Message stream ID(消息流ID):每个消息有一个关联的ID,使用ID可以识别出该消息属于哪个消息流。
Chunk(块):消息的一段。消息在网络发送之前被拆分成很多小的部分。块按照时间戳的顺序进行端到端的传输。
Chunk stream(块流):通信中允许块流向一个特定方向的逻辑通道。块流可以从客户端流向服务器,也可以从服务器流向客户端。
Chunk stream ID(块流 ID):每个块有一个关联的ID,使用ID可以识别出该块属于哪个块流。
Multiplexing(合成):将独立的音频/视频数据合成为一个连续的音频/视频流,这样就可以同时发送视频和音频了。
DeMultiplexing(分解):Multiplexing 的逆向处理,将交叉的音频和视频数据还原成原始音频和视频数据的格式。
Remote Procedure Call(RPC 远程方法调用):允许客户端或服务器调用对端的一个子程序或者程序的请求。
Metadata(元数据):关于数据的描述。比如电影的 metadata 包括电影标题、持续时间、创建时间等等。
Application Instance (应用实例):应用实例运行于服务器上,客户端可连接这个实例并发送连接请求,连接服务器。
Action Message Format (AMF,操作消息格式):AMF是Adobe独家开发出来的通信协议,它采用二进制压缩,序列化、反序列化、传输数据,从而为Flash 播放器与Flash Remoting网关通信提供了一种轻量级的、高效能的通信方式。如下图所示。
AMF的初衷只是为了支持Flash ActionScript的数据类型,目前有两个版本:AMF0和AMF3。AMF从Flash MX时代的AMF0发展到现在的AMF3。AMF3用作Flash Playe 9的ActionScript 3.0的默认序列化格式,而AMF0则用作旧版的ActionScript 1.0和2.0的序列化格式。在网络传输数据方面,AMF3比AMF0更有效率。AMF3能将int和uint对象作为整数(integer)传输,并且能序列化 ActionScript 3.0才支持的数据类型, 比如ByteArray,XML和Iexternalizable。
二.握手
握手以客户端发送C0和C1块开始。
客户端必须等待接收到S1才能发送C2。
客户端必须等待接收到S2才能发送任何其他数据。
服务器端必须等待接收到C0才能发送S0和S1,也可以等待接收到C1再发送S0和S1。服务器端必须等待接收到C1才能发送S2。服务器端必须等待接收到C2才能发送任何其他数据。
1.C0和S0格式
C0和S0都是八位,即一个字节,如下所示:
Version(8bits):在C0中,它表示客户端的RTMP版本;在S0中,它表示服务器端的RTMP版本。RTMP规范目前将它定义为3。0—2用于早期的产品,已经被废弃。4—31保留,用于RTMP未来版本。32—255禁止使用。
2.C1和S1格式
C1和S1都是1536个字节,如下所示:
time(4字节):包含时间戳,该时间戳应该被用做本终端发送的块的起点。该字段可以为0或者其他任意值。
zero(4字节):该字段必须为0。
random data(1528字节):该字段可以包含任意值。终端需要区分出是它发起的握手还是对端发起的握手,所以这该字段应该发送一些足够随机的数。
3.C2和S2格式
C2和S2也都是1536个字节,几乎是C1和S1的副本,如下所示:
time(4字节):包含时间戳,必须与C1或S1中的时间戳相同。
time2(4字节):包含时间戳,必须与前一个C1或S1中的时间戳相同。
random echo(1528字节):该字段必须与S1或者S2中的随机数相同。
4.握手示意图三.块
握手之后,连接开始对一个或多个块流进行合并。每个块都有一个唯一ID对其进行关联,这个ID叫做chunk stream ID(块流ID)。这些块通过网络进行传输,在发送端,每个块必须被完全发送才可以发送下一块。在接收端,这些块根据块流ID被组装成消息。
每个块都是由块头和块数据体组成,而块头自身也是由三部分组成,块格式如下所示:
Basic Header(基本头,1—3字节):该字段编码了块流ID和块类型。块类型决定了Message Header(消息头)的编码格式。该字段长度完全取决于块流ID,因为块流ID是一个可变长度的字段。
Message Header(消息头,0、3、7或11字节):该字段编码了消息的相关信息,标识了块数据所属的消息。该字段的长度由块类型决定。
Extended Timestamp(扩展时间戳,0或4字节):该字段只在特定情况下出现。
Chunk Data(块数据,可变大小):块的载荷部分,取决于配置的最大块尺寸,一般为128字节。
1.Basic Header
块基本头对块类型(用fmt 字段表示,参见下图) 和块流ID(chunk stream ID)进行编码。fmt字段占2bits,取值范围时0—3。RTMP协议最多支持65597个流,流的ID范围是3—65599。ID值0、1和2被保留,0表示两字节形式,1表示三字节形式,2的块流ID被保留,用于下层协议控制消息和命令。
☆一字节形式
ID取值范围2—63,0和1用于标识多字节形式。
☆两字节形式
第一个字节中cs id为0,表示有两个字节
ID取值范围64—319,即第二个字节+64。
☆三字节形式
第一个字节中cs id为1,表示有三个字节
ID取值范围64—65599,即第三个字节*256+第二个字节+64。
2.Message Header
有四种类型的块消息头,由块基本头中的“fmt”字段决定。
☆类型0
由11个字节组成,必须用于块流的起始块或者流时间戳重置的时候。
timestamp(3字节):消息的绝对时间戳,如果大于等于16777215(0xFFFFFF),该字段仍为16777215,此时Extend Timestamp(扩展时间戳)字段存在,用于对溢出值进行扩展。否则,该字段标识整个时间戳,不需要扩展。
message length(3字节):通常与块载荷的长度不同,块载荷长度通常表示块的最大长度128字节(除了最后一个块)和最后一个块的剩余空间。
message type id(1字节):消息类型。
message stream id(4字节):该字段用小端模式保存。
☆类型1
由7个字节组成,不包括message stream ID(消息流ID),此时块与之前的块取相同的消息流ID。可变长度消息的流(例如,一些视频格式)应该在第一块之后使用这一格式表示之后的每个新块。
timestamp delta(3字节):前一个块时间戳与当前块时间戳的差值,即相对时间戳,如果大于等于16777215(0xFFFFFF),该字段仍为16777215,此时Extend Timestamp(扩展时间戳)字段存在,用于对溢出值进行扩展。否则,该字段标识整个差值,不需要扩展。
message length(3字节):通常与块载荷的长度不同,块载荷长度通常表示块的最大长度(除了最后一个块)和最后一个块的剩余空间。该长度是指为块载荷AMF编码后的长度。
message type id(1字节):消息类型。
☆类型2
由3个字节组成,既不包括message stream ID(消息流ID),也不包括message length(消息长度),此时块与之前的块取相同的消息流ID和消息长度。固定长度消息的流(例如,一些音频格式)应该在第一块之后使用这一格式表示之后的每个新块。
timestamp delta(3字节):前一个块时间戳与当前块时间戳的差值,如果大于等于16777215(0xFFFFFF),该字段仍为16777215,此时Extend Timestamp(扩展时间戳)字段存在,用于对溢出值进行扩展。否则,该字段标识整个差值,不需要扩展。
☆类型3
没有消息头,从之前具有相同块流ID的块中取相应的值。当一条消息被分割成多个块时,所有的块(除了第一个块)应该使用这种类型。
3.Extended Timestamp(4字节)
只有当块消息头中的普通时间戳设置为0x00ffffff时,本字段才被传送。如果普通时间戳的值小于0x00ffffff,那么本字段一定不能出现。当最近的具有同一块流ID,类型 0、1 或 2 块的Extended Timestamp 字段出现时,这一字段才会在类型为 3 的块中出现。当扩展时间戳启用时,timestamp字段或者timestamp delta要全置为1,表示应该去扩展时间戳字段来提取真正的时间戳或者时间戳差。注意扩展时间戳存储的是完整值,而不是减去时间戳或者时间戳差的值。
4.例子
☆不分割消息
从上面两个表中可以看出,从消息3开始,数据处理得到优化,此时Chunk除了载荷以外,只多了一个块基本头。
☆分割消息
当消息的载荷长度超过128字节时,需要将消息分割为若干个块进行传输。
从上面两个例子可以看出,块类型3有两种用法。一个是指定一个新消息的消息头可以派生自已经存在的状态数据(例一),另一个是指定消息的连续性(例二)。
四.消息
消息是RTMP协议中基本的数据单元。不同种类的消息包含不同的Message Type ID,代表不同的功能。RTMP协议中一共规定了十多种消息类型,分别发挥着不同的作用。例如,Message Type ID在1-6的消息用于协议控制,这些消息一般是RTMP协议自身管理要使用的消息,用户一般情况下无需操作其中的数据。Message Type ID为8,9的消息分别用于传输音频和视频数据。Message Type ID为15-20的消息用于发送AMF编码的命令,负责用户与服务器之间的交互,比如播放,暂停等等。
1.消息头
消息头(Message Header)有四部分组成:标志消息类型的Message Type ID,标志载荷长度的Payload Length,标识时间戳的Timestamp,标识消息所属媒体流的Stream ID。消息的格式如下所示。
2.载荷
载荷是消息包含的实际数据,它们可能是音频采样数据或者是视频压缩数据。
由于端与端之间实际传输的是块,所以只需要将载荷加上块头封装成块。实际应用中,无扩展时间戳,一字节形式的块基本头就能满足要求,整个块头满足以下四种长度:
fmt=0:Basic Head+Message Head=1+11=12
fmt=1:Basic Head+Message Head=1+7=8
fmt=2:Basic Head+Message Head=1+3=4
fmt=3:Basic Head+Message Head=1+0=1
需要注意的是,当载荷为H.264数据时,要使用AMF3进行编码(即序列化),关于AMF3可以参考:AMF3中文版
五.协议控制消息
RTMP块流使用消息类型ID 1、2、3、5、6作为协议控制消息。这些消息包含了必要的RTMP块流协议信息。这些协议控制消息必须使用0作为消息流ID(这些流也被称为控制流),同时使用2作为块流ID。协议控制消息接收立即生效;解析时,时间戳字段被忽略。
1.设置块大小消息(Message Type=1)
设置块大小,被用来通知对方新的最大块大小。
默认最大块大小为128字节,客户端和服务器可以使用此消息来修改默认的块大小。例如,假设客户端想要发送的音频数据大小为131字节,而块大小为128字节。在这种情况下,客户端可以通知服务器新的块大小为131字节,然后就可以使用一个块来发送完整的音频数据了。
最大的块大小建议至少为128字节,但必须至少为1字节。通信的每一个方向(例如从客户端到服务器)拥有独立的块大小设置。最大的块大小由通信双方 (服务器或者客户端) 自行维护。
0:此位必须为 0。
chunk size (块大小,31 位):这一字段保存新的最大块大小值,以字节为单位,发送端之后将使用此值作为最大的块大小,直到收到 (关于最大块大小的) 通知。有效值为1到 2147483647(0x7FFFFFFF,1和2147483647都可取); 但是所有大于16777215(0xFFFFFF) 值是等价的,因为没有一个块比一整个消息大,并且没有一个消息会大于16777215字节。
2.终止消息(Message Type=2)
终止消息,用于通知对端,如果正在等待一条消息的部分块(已经接收了一部分),那么可以丢弃之前已经接收到的块。对端将接收到的块流ID作为当前协议控制消息的有效负载。应用程序可能会在关闭的时候发送这个消息以指示不需要进一步对这个消息的处理了。
chunk stream ID (块流 ID,32 位):这一字段保存块流 ID,该流的当前消息会被丢弃。
3.确认消息(Message Type=3)
客户端或者服务器在接收到等同于窗口大小的字节之后必须发送给对端一个确认消息。窗口大小是指发送者在没有收到接收者确认消息之前发送的最大字节数。这个消息定义了序列号,也就是到目前为止接收到的字节数。
sequence number (序列号,32 位):这一字段保存到目前为止接收到的字节数。
4.确认窗口大小消息(Message Type=5)
在发送确认消息(Message Type ID=4)的时候,客户端或者服务器端发送这条消息来通知对端要使用的窗口大小(也就是说这条消息可以重置窗口大小)。发送者在发送等于窗口大小的数据之后,等待对端的确认。接收端必须发送一个确认消息,在会话开始时,或从上一次发送确认消息之后接收到了等于窗口大小的数据。
5.设置对端带宽消息(Message Type=6)
客户端或者服务器端发送这一消息来限制其对端的输出带宽。对端接收到这一消息后限制其输出带宽,这是通过限制已发送但未被确认的数据量不超过本消息所给出的窗口大小来实现的。接收到这一消息的对端应该回复一个确认窗口大小消息(Message Type ID=5)—如果这个窗口大小不同于上一条它发送给“发送者”的设置对端带宽消息。
限制类型取以下列值之一:
0 - Hard:对端应该限制其输出带宽不超过指定的窗口大小。
1 - Soft:对端应该限制其输出带宽不超过指定的窗口大小,或者已经有限制在起作用的话,就取两个窗口大小之间的较小值。
2 - Dynamic:如果先前的限制类型为 Hard,则这条消息的也被视为Hard消息,否则的话忽略这条消息。
六.用户控制消息(Message Type=4)
RTMP使用消息类型ID 4表示用户控制消息。这些消息包含RTMP流传输层所使用的信息。协议控制消息使用的ID为 1、2、3、5 和 6 (前面已经介绍过了)。
用户控制消息应该使用消息流ID 0 (以被认为是控制流),并且以RTMP块流发送时以块流ID为2。协议控制消息接收立即生效;解析时,时间戳字段被忽略。
客户端或者服务器端发送这个消息来通知对端一些用户控制事件。这一消息携带有事件类型和事件数据。
负载的前两个字节用于指示事件类型。事件类型后面紧跟着事件数据。事件数据字段的大小是可变的。但是,如果这条消息必须通过 RTMP 块流层传输时,最大块大小 (前面已经介绍了) 应该足够大以允许这些消息填充在一个单一块中。
支持的事件类型如下所示:
事件 | 描述 |
Stream Begin (=0) | 服务器发送这个事件来通知客户端一个流已就绪并可以用来通信。默认情况下,这一事件在成功接收到客户端的连接命令之后以ID=0发送。事件数据为4字节,代表了已就绪流的流ID。 |
Stream EOF (=1) | 服务器发送这一事件来通知客户端请求的流的数据回放已经结束。在发送额外的命令之前不再发送任何数据。客户端将丢弃接收到的这个流的消息。事件数据为4字节,代表了回放已结束的流的流 ID。 |
StreamDry (=2) | 服务器发送这一事件来通知客户端当前流中已没有数据。当服务器在一段时间内没有检测到任何消息,它可以通知相关客户端当前流已经没数据了。这一事件数据为4字节,代表了已没数据的流的流 ID。 |
SetBuffer Length (=3) | 客户端发送这一事件来通知服务器缓冲区大小 (以毫秒为单位),这个缓冲区用于缓存来自流的任何数据。此事件在服务器开始处理流之前就发送。事件数据的前4个字节代表了流ID,紧接其后的4个字节代表了以毫秒为单位的缓冲区的长度。 |
Streams Recorded (=4) | 服务器发送这一事件来通知客户端当前流是一个录制流。事件数据为4字节,代表了录制流的流 ID。 |
PingRequest (=6) | 服务器端发送这一事件用于测试客户端是否可达。事件数据是为一个4字节的时间戳,代表了服务器端发送这一命令时的服务器本地时间。客户端在接收到这一消息后会立即发送 PingResponse 回复。 |
PingResponse (=7) |
客户端发送这一事件用于回复服务器的PingRequest。事件数据是为一个4字节的时间戳,该时间戳是从接收到的PingRequest的事件数据中获取的。 |
七.RTMP命令消息
这一节介绍了在服务端和客户端通信交互时使用的不同类型消息和命令。
服务端和客户端交换的不同消息类型包括用于发送音频数据的音频消息、用于发送视频数据的视频消息、用于发送任意用户数据的数据消息、共享对象消息以及命令消息。共享对象消息提供了一个通用的方法来管理多用户和一台服务器之间的分布式数据。命令消息在客户端和服务端传输 AMF 编码的命令。客户端或者服务端可以使用命令消息向对端请求远程过程调用 (RPC)。
1.数据消息(Message Type=18或15)
客户端和服务端使用该消息向对端发送metadata,metadata包括数据(音频、视频等)的详细信息,比如创建时间,时长,主题等等。AMF0 编码时,消息类型为18;AMF3编码时,消息类型为15。
2.共享对象消息 (Message Type=19或16)
所谓共享对象其实是一个 Flash 对象 (一个名值对的集合),这个对象在多个不同客户端、应用实例中保持同步。AMF0 编码时,消息类型为19;AMF3编码时,消息类型为16。每条消息可以包含多个事件。
支持的事件类型如下所示:
事件 | 描述 |
---|---|
Use(=1) | 客户端发送这一事件以通知服务器一个命名共享对象被创建。 |
Release(=2) | 当共享对象在客户端被删除时客户端发送这一事件到服务器。 |
Request Change (=3) | 客户端发送给服务器这一事件以请求共享对象命名参数值的改变。 |
Change (=4) | 服务器发送这一事件已通知发起这一请求之外的所有客户端,一个命名参数值的改变。 |
Success (=5) | 如果请求被接受,服务器发送这一事件给请求的客户端,以作为RequestChange事件的响应。 |
SendMessage (=6) | 客户端发送这一事件到服务器以广播一条消息。一旦接收到这一事件,服务器端将会给所有的客户端广播这一消息,包括这一消息的发起者。 |
Status (=7) | 服务器端发送这一事件以通知客户端异常情况。 |
Clear (=8) | 服务器端发送这一消息到客户端以清理一个共享对象。服务器端也会对客户端发送的Use事件使用这一事件进行响应。 |
Remove (=9) | 服务器端发送这一事件通知客户端删除一个slot。 |
Request Remove (=10) | 客户端发送这一事件通知客户端删除一个slot。 |
Use Success (=11) | 服务器端发送给客户端这一事件表示连接成功。 |
3.音频消息 (Message Type=8)
客户端和服务端使用该消息向对端发送音频数据。
4.视频消息 (Message Type=9)
客户端和服务端使用该消息向对端发送视频数据。
5.聚合消息(Message Type=22)
聚合消息是包含一系列子消息的单个消息。
聚合消息的消息流ID将覆盖消息聚合内的子消息的流ID。
聚合消息与第一条子消息时间戳的区别是偏移量,它用于将子消息的时间戳重新归一到流时间表。偏移量被添加到每个子消息的时间戳以达到归一化流时间。 第一个子消息的时间戳应该与聚合消息的时间戳相同,所以偏移应该为零。
返回指针包含前一个消息的大小,包括它的消息头。 包含消息头是为了与FLV文件的格式相匹配和用于向后查找(backward seek)。
使用聚合消息具有以下性能优势:
☆可以在通过一个块发送一条完整的消息。因此,增加块大小并使用聚合消息可以减少发送的块数。
☆子消息能在内存中连续存储。这样在调用系统函数通过网络发送数据时,会更加高效。
6.命令消息(Message Type=20或17)
命令消息在客户端和服务器传递 AMF 编码的命令。AMF0 编码时,消息类型为20;AMF3编码时,消息类型为17。发送这些消息是为了进行一些操作,比如,连接,创建流,发布,播放,暂停。命令消息,像 onstatus、result 等等,用于通知发送者请求命令的状态。一个命令消息由命令名、业务 ID 和包含相关参数的命令对象组成。客户端或者服务可以使用命令消息向对端请求远程过程调用 (RPC)。
客户端和服务器交换 AMF 编码的命令。发送端发送一个命令消息,它由命令名、业务ID以及包含有相关参数的命令对象组成。例如,包含有 'app' 参数的连接命令,这个参数指明了客户端将要连接的服务器应用名字。接收者处理这一命令并回发一个同样业务ID的回复。回复字符串可以是 _result、_error 或者 一个方法名,比如,verifyClient 或者 contactExternalServer。
以下类对象可用于发送多种不同的命令:
NetConnection 代表服务器和客户端之间连接的对象。
NetStream 代表发送音频流、视频流和其他相关数据的通道的对象。当然,我们也会发送控制数据流的命令,比如 play、pause 等等。
6.1.NetConnection相关命令
NetConnection 管理着一个客户端应用和服务器之间的双相连接。此外,它还提供远程方法的异步调用。
NetConnection 可用于发送以下命令:
☆connect
☆call
☆close
☆createStream
6.1.1.connect
客户端发送connect命令到服务器,请求连接到服务器的应用实例。
由客户端发送到服务器的connect命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为 "connect"。 |
Transaction ID | 数字 | 业务ID,总是设置为 1。 |
Command Object | 对象 | 具有名值对的命令对象。 |
Optional User Arguments | 对象 | 任意可选信息。 |
属性 |
类 型 |
描述 | 范例 |
app | 字符串 | 客户端连接到的服务器应用的名字。 | testapp |
flashver | 字符串 | Flash Player版本号。和ApplicationScript getversion()方法返回的是同一个字符串。 | FMSc/1.0 |
swfUrl | 字符串 | 进行当前连接的SWF文件的URL。 | file://C:/FlvPlayer.swf |
tcUrl | 字符串 | 服务器的URL。具有以下格式:protocol://servername:port/appName/appInstance | rtmp://localhost:1935/testapp/instance1 |
fpad | 布尔 | 如果使用了代理,为 true。 | true或者false。 |
audioCodecs | 数字 | 指明客户端所支持的音频编码。 | SUPPORT_SND_MP3 |
videoCodecs | 数字 | 指明支持的视频编码。 | SUPPORT_VID_SORENSON |
videoFunction | 数字 | 指明所支持的特殊视频功能。 | SUPPORT_VID_CLIENT_SEEK |
pageUrl | 字符串 | SWF文件所加载网页的URL。 | http://somehost/sample.html |
objectEncoding | 数字 | AMF编码方式。 | AMF3 |
audioCodecs属性的标记值
videoCodecs属性的标记值
videoFunction属性的标记值
objectEncoding属性的值
由服务器发送到客户端的connect命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,_result或_error。指明回复的是result还是error |
Transaction ID | 数字 | 业务ID,总是设置为 1。 |
Properties | 对象 | 名值对,描述连接的属性(fmsver等)。 |
Information | 对象 | 名值对,描述来自服务器的回复。‘code’、‘level’、‘description’等是信息的名字 |
命令执行时的消息流如下:
1. 客户端发送connect命令到服务器以请求对服务器应用实例的连接。
2. 收到 connect命令后,服务器发送协议控制消息 '确认窗口大小' 到客户端。服务器端也会连接到connect命令中提到的应用。
3. 服务器端发送协议控制消息 '设置对端带宽' 到客户端。
4. 在处理完协议控制消息 '设置对端带宽' 之后,客户端发送协议控制消息 '窗口确认大小' 到服务器端。
5. 服务器端发送用户控制消息 (StreamBegin) 到客户端。
6. 服务器端发送结果( _result)命令消息告知客户端连接状态 (success/fail)。这一消息指定了业务 ID (connect 命令通常将其设置为1)。这一消息还定义了一些属性,比如 FMS 服务器版本 (字符串)。此外,它定义了其他与连接响应相关的信息,比如 level (字符串)、code (字符串)、description (字符串)、objectencoding (数字) 等等。
6.1.2.Call
NetConnection对象的call方法在接收端执行远程过程调用 (PRC)。被调用的PRC名字作为一个参数传给调用命令。
发送端发送给接收端的命令结构如下:
字段名 | 类型 | 描述 |
Procedure Name | 字符串 | 调用的远程过程的名字。 |
Transaction ID | 数字 | 如果期望回复我们要给一个业务ID。否则我们传0值即可。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Optional Arguments | 对象 | 任意可选参数。 |
回复的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名。 |
Transaction ID | 数字 | 业务ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Response | 对象 | 被调用方法的回复。 |
6.1.3.createStream
客户端发送这一命令到服务器,为消息连接创建一个逻辑通道。音频、视频和元数据的发布使用createStream命令创建的流通道。
NetConnection是默认的通信通道,流ID为 0。协议和一些命令消息,包括createStream,使用默认的通信通道。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名。设置为"createStream"。 |
Transaction ID | 数字 | 业务ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
字段名 | 类型 | 描述 |
Command Name | 字符串 | _result 或 _error;指明回复的是result还是error。 |
Transaction ID | 数字 | 业务ID。 |
Command Object | 对象 | 如果存在一些命令信息要设置这个对象,否则置空。 |
Stream ID | 数字 | 返回值要么是一个流ID要么是一个错误信息对象。 |
NetStream 定义了通道,使用这个通道,音频流、视频流以及数据消息流可以通过连接客户端到服务器的 NetConnection传输。NetConnection对象支持多个NetStream,因此支持多路数据流。
以下命令可以由客户端使用NetStream发送到服务端:
☆play
☆play2
☆deleteStream
☆closeStream
☆receiveAudio
☆receiveVideo
☆publish
☆seek
☆pause
服务器使用"onStatus"命令向客户端发送NetStream状态:
字段名 | 类 型 | 描述 |
Command Name | 字符串 | 命令名,设置为“onStatus”。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | onStatus消息无命令对象。 |
Info Object | 对象 | 一个AMF对象至少要有一些三个属性。‘level’(字符串):消息等级,取‘warning’、‘status’、‘error’中的某个值;‘code’(字符串):消息码,例如“NetStream.Play.Start”;‘description’(字符串):关于消息的描述。Info Object可以在‘code’中包含其他合适的属性。 |
6.2.1.play
客户端发送这一命令到服务器以播放流。也可以多次使用这一命令以创建一个播放列表。
如果你想要创建一个动态的播放列表—在不同的直播流或者录制流之间进行切换,需多次调用 play 方法,并在每次调用时将‘reset’设置为false。相反的,如果你想要立即播放指定流,将其他等待播放的流清空,将‘reset’设为 true。
客户端发送到服务器的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为 "play"。 |
Transaction ID | 数字 | 事务ID,设置为 0。 |
Command Object | Null | 命令对象不存在。设为 null 。 |
Stream Name | 字符串 | 要播放流名。播放视频 (FLV) 文件,指定流名时不要加文件扩展名 (例如,"sample")。重播MP3或者ID3,你必须在流名前加上 mp3:例如,"mp3:sample"。播放H.264/AAC文件,你必须在流名前加上mp4:并指定文件扩展名。例如,要播放 sample.m4v 文件,定义 "mp4:sample.m4v"。 |
Start | 数字 | 一个可选的参数,以秒为单位指定开始时间。默认值为-2,表示用户首先尝试播放Stream Name字段中定义的直播流。如果那个名字的直播流没有找到,它将播放同名的录制流。如果没有那个名字的录制流,客户端将等待一个新的那个名字的直播流,并当其有效时进行播放。如果你将Start 字段设置为-1,那么就只播放Stream Name字段中定义的那个名字的直播流。如果你将Start字段设置为0或正整数,那么将从 Start 字段定义的时间开始播放流名中定义的那个录制流。如果没有找到录制流,那么将播放播放列表中的下一项。 |
Duration | 数字 | 一个可选的参数,以秒为单位定义了回放的持续时间。默认值为-1。-1值意味着一个直播流会一直播放直到它不再可用或者一个录制流一直播放直到结束。如果设置为0值,它从记录流开始时从Start字段中指定的时间起播放单个帧。假定定义在 Start 字段中的值大于或者等于0。如果设置为正数,将播放Duration字段定义的一段直播流。之后,变为可播放状态,或者播放Duration字段定义的一段录制流。(如果流在Duration字段定义的时间段内结束,那么流结束时回放也结束)。如果设置为-1以外的负数的话,效果与-1相同。 |
Reset | 布尔 | 一个可选的布尔值或者数字定义了是否对以前的播放列表进行重置。 |
命令执行时的消息流如下:
1.当客户端从服务器接收到createStream命令的结果是为success时,发送play命令。
2.一旦接收到play命令,服务器发送一个协议控制消息来设置块大小。
3.服务器发送一个用户控制消息,这个消息中定义了'StreamsRecorded'事件和流ID。消息在前两个字节中保存事件类型,在后四个字节中保存流ID。
4.服务器发送另一个用户控制消息,这一消息中定义了'StreamBegin' 事件,来指明发送给客户端的流的起点。
5.如果客户端发送的play命令成功,服务器发送一个onStatus命令消息NetStream.Play.Start & NetStream.Play.Reset。只有当客户端发送的play命令设置了reset时,服务器才会发送NetStream.Play.Reset。如果要播放的流没有找到,服务器端发送onStatus消息NetStream.Play.StreamNotFound。
6.之后,服务器发送视频和音频数据,客户端对其进行播放。
6.2.2.play2
不同于play命令的是,play2可以在不改变播放内容时间轴的情况下切换到不同的比特率。服务器维护了多个文件,支持客户端在play2中请求的所有比特率。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为"play2"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null 。 |
Parameters | 对象 | 一个AMF编码的对象,该对象的属性是为公开,被用于描述flash.net.NetStreamPlayOptions ActionScript对象。 |
6.2.3.deleteStream
当NetStream对象消亡时,NetStream发送deleteStream 命令。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为"deleteStream"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设为Null 。 |
Stream ID | 数字 | 服务器端消亡的流ID。 |
服务器不发送任何回复。
6.2.4.receiveAudio
NetStream通过发送receiveAudio消息来通知服务器是否发送音频到客户端。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
Command Name | 字符串 | 命令名,设置为"receiveAudio"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null。 |
Bool Flag | 布尔 | true 或false ,指明是否接受音频。 |
6.2.5.receiveVideo
NetStream通过发送receiveVideo消息来通知服务器是否发送视频到客户端。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为"receiveVideo"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null。 |
Bool Flag | 布尔 | true或false,指明是否接受视频。 |
如果发送来的receiveVideo命令布尔字段被设为false,服务器不发送任何回复。如果这一标识被设为true,服务器以状态消息NetStream.Seek.Notify和NetStream.Play.Start进行回复。
6.2.6.publish
客户端发送publish命令,以发布一个命名流到服务器。使用这个名字,任意客户端都可以播放这个流,并接受发布的音频、视频以及数据消息。
客户端发送给服务器的命令结构如下:
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为"publish"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null。 |
Publishing Name | 字符串 | 发布的流的名字。 |
Publishing Type | 字符串 | 发布类型。可以设置为"live"、"record" 或"append"。record:流被发布,数据被录制到一个新的文件。新文件被存储在服务器上服务应用目录下的子目录。如果文件已存在,将覆盖。append:流被发布,数据被添加到一个文件。如果该文件没找着,将新建一个。live:直播数据只被发布,并不对其进行录制。 |
服务器用onStatus命令回复,并标记发布的起始位置。
6.2.7.seek
客户端发送seek命令以查找媒体文件或者播放列表的偏移量(以毫秒为单位)。
客户端发送到服务器的命令结构如下:
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为"seek"。 |
Transaction ID | 数字 | 业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null。 |
milliSeconds | 数字 | 播放列表查找的毫秒数。 |
6.2.8.pause
客户端发送pause命令,通知服务器暂停或开始播放。
客户端发送到服务器的命令结构如下:
字段名 | 类型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,设置为"pause"。 |
Transaction ID | 数字 | 此命令没有业务ID,设置为0。 |
Command Object | Null | 命令对象不存在,设置为Null。 |
Pause/Unpause Flag | 布尔 | true 或false,指明暂停或重新播放。 |
milliSeconds | 数字 | 流暂停或者重新播放的毫秒数。这个是客户端暂停的当前流时间。当回放被恢复时,服务器端只发送时间戳比这字段值大的消息。 |
八.消息交互实例
如这里有几个例子,演示了如何使用RTMP协议进行消息交互。
1.发布视频
这个例子说明了客户端是如何发布一个视频流并传递到服务器的。其他客户端可以对发布的流进行订阅并播放视频。
2.广播共享对象消息
这个例子说明了在共享对象创建和改变时,消息是如何交互的。同时说明了共享对象消息广播的过程。
3.发布metadata
这个例子描述了发布metadata时的消息交互
九.打包H.264
如果整个打包过程都自己弄,是非常繁琐的,还好网上有大神开源了RTMP库,这里使用librtmp进行H.264数据的打包推送。
librtmp的编译可以参考:Win7(Windows 7)下用VS2012(Visual Studio 2012)编译librtmp
使用librtmp时,解析RTMP地址、握手、建立流媒体链接和AMF编码这块我们都不需要关心,但是数据是如何打包并通过int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue) 函数推送的还是得学习一下。
RTMPPacket类型的结构体定义如下,一个RTMPPacket对应RTMP协议规范里面的一个块(Chunk)。
typedef struct RTMPPacket
{
uint8_t m_headerType;//块消息头的类型(4种)
uint8_t m_packetType;//消息类型ID(1-7协议控制;8,9音视频;10以后为AMF编码消息)
uint8_t m_hasAbsTimestamp; //时间戳是绝对值还是相对值
int m_nChannel; //块流ID
uint32_t m_nTimeStamp; //时间戳
int32_t m_nInfoField2; //last 4 bytes in a long header,消息流ID
uint32_t m_nBodySize; //消息载荷大小
uint32_t m_nBytesRead; //暂时没用到
RTMPChunk *m_chunk; //暂时没用到
char *m_body; //消息载荷,可分割为多个块载荷
} RTMPPacket;
一些定义
#define RTMP_DEFAULT_CHUNKSIZE 128//默认块大小
#define RTMP_BUFFER_CACHE_SIZE (16*1024)//开辟16K字节空间
#define RTMP_PACKET_TYPE_AUDIO 0x08//音频的消息类型
#define RTMP_PACKET_TYPE_VIDEO 0x09//视频的消息类型
#define RTMP_MAX_HEADER_SIZE 18//块基本头+块消息头+扩展时间戳=3+11+4=18
#define RTMP_PACKET_SIZE_LARGE 0//块消息头类型0
#define RTMP_PACKET_SIZE_MEDIUM 1//块消息头类型1
#define RTMP_PACKET_SIZE_SMALL 2//块消息头类型2
#define RTMP_PACKET_SIZE_MINIMUM 3//块消息头类型3
RTMP_SendPacket函数
//queue:TRUE为放进发送队列,FALSE是不放进发送队列,直接发送
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
{
const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
uint32_t last = 0;//上一个块的时间戳
int nSize;//消息载荷大小,可分割为多个块载荷大小
int hSize;//块头大小
int cSize;//块基本头大小增量
char *header;//指向块头起始位置
char *hptr;
char *hend;//指向块头结束位置
char hbuf[RTMP_MAX_HEADER_SIZE];
char c;
uint32_t t;//相对时间戳
char *buffer;//指向消息载荷
char *tbuf = NULL;
char *toff = NULL;
int nChunkSize;//块载荷大小
int tlen;
//不是完整块消息头(即不是11字节的块消息头)
if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
{
//前一个块和这个块对比
//原理参考 例子—不分割消息
if (prevPacket->m_nBodySize == packet->m_nBodySize
&& prevPacket->m_packetType == packet->m_packetType
&& packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)
packet->m_headerType = RTMP_PACKET_SIZE_SMALL;
//原理参考 例子—分割消息
if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp
&& packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;
//上一个块的时间戳
last = prevPacket->m_nTimeStamp;
}
//非法
if (packet->m_headerType > 3)
{
RTMP_Log(RTMP_LOGERROR, "sanity failed!! trying to send header of type: 0x%02x.",
(unsigned char)packet->m_headerType);
return FALSE;
}
//nSize暂时设置为块头大小;packetSize[] = { 12, 8, 4, 1 }
nSize = packetSize[packet->m_headerType];
//块头大小初始化
hSize = nSize;
cSize = 0;
//相对时间戳,当块时间戳与上一个块时间戳的差值
t = packet->m_nTimeStamp - last;
if (packet->m_body)
{
//m_body是指向载荷数据首地址的指针,“-”号用于指针前移
//header:块头起始位置
header = packet->m_body - nSize;
//hend:块头结束位置
hend = packet->m_body;
}
else
{
header = hbuf + 6;
hend = hbuf + sizeof(hbuf);
}
//当块流ID大于319时
if (packet->m_nChannel > 319)
//块基本头是3个字节
cSize = 2;
//当块流ID大于63时
else if (packet->m_nChannel > 63)
//块基本头是2个字节
cSize = 1;
if (cSize)
{
//header指针指块头起始位置,“-”号用于指针前移
header -= cSize;
//当cSize不为0时,块头需要进行扩展,默认的块基本头为1字节,但是也可能是2字节或3字节
hSize += cSize;
}
//如果块消息头存在,且相对时间戳大于0xffffff,此时需要使用ExtendTimeStamp
if (nSize > 1 && t >= 0xffffff)
{
header -= 4;
hSize += 4;
}
hptr = header;
//把块基本头的fmt类型左移6位。
c = packet->m_headerType << 6;
switch (cSize)
{
//把块基本头的低6位设置成块流ID
case 0:
c |= packet->m_nChannel;
break;
//同理,但低6位设置成000000
case 1:
break;
//同理,但低6位设置成000001
case 2:
c |= 1;
break;
}
//可以拆分成两句*hptr=c;hptr++,此时hptr指向第2个字节
*hptr++ = c;
//cSize>0,即块基本头大于1字节
if (cSize)
{
//将要放到第2字节的内容tmp
int tmp = packet->m_nChannel - 64;
//获取低位存储于第2字节
*hptr++ = tmp & 0xff;
//块基本头是最大的3字节时
if (cSize == 2)
//获取高位存储于第三个字节(注意:排序使用大端序列,和主机相反)
*hptr++ = tmp >> 8;
}
//块消息头一共有4种,包含的字段数不同,nSize>1,块消息头存在。
if (nSize > 1)
{
//块消息头的最开始三个字节为时间戳,返回值hptr=hptr+3
hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);
}
//如果块消息头包括MessageLength+MessageTypeID(4字节)
if (nSize > 4)
{
//消息长度,为消息载荷AMF编码后的长度
hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
//消息类型ID
*hptr++ = packet->m_packetType;
}
//消息流ID(4字节)
if (nSize > 8)
hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);
//如果块消息头存在,且相对时间戳大于0xffffff,此时需要使用ExtendTimeStamp
if (nSize > 1 && t >= 0xffffff)
hptr = AMF_EncodeInt32(hptr, hend, t);
//消息载荷大小
nSize = packet->m_nBodySize;
//消息载荷指针
buffer = packet->m_body;
//块大小,默认128字节
nChunkSize = r->m_outChunkSize;
RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket,
nSize);
//使用HTTP
if (r->Link.protocol & RTMP_FEATURE_HTTP)
{
//nSize:消息载荷大小;nChunkSize:块载荷大小
//例nSize:307,nChunkSize:128;
//可分为(307+128-1)/128=3个
//为什么减1?因为除法会只取整数部分!
int chunks = (nSize+nChunkSize-1) / nChunkSize;
//如果块的个数超过一个
if (chunks > 1)
{
//消息分n块后总的开销:
//n个块基本头,1个块消息头,1个消息载荷,这里是没有扩展时间戳的情况
//实际中只有第一个块是完整的,剩下的只有块基本头
tlen = chunks * (cSize + 1) + nSize + hSize;//这里其实多算了一个块基本头
//分配内存
tbuf = (char *) malloc(tlen);
if (!tbuf)
return FALSE;
toff = tbuf;
}
}
while (nSize + hSize)
{
int wrote;
//消息载荷小于块载荷(不用分块)
if (nSize < nChunkSize)
nChunkSize = nSize;
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)header, hSize);
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)buffer, nChunkSize);
if (tbuf)
{
memcpy(toff, header, nChunkSize + hSize);
toff += nChunkSize + hSize;
}
else
{
wrote = WriteN(r, header, nChunkSize + hSize);
if (!wrote)
return FALSE;
}
//消息载荷长度块载荷长度
nSize -= nChunkSize;
//Buffer指针前移1个块载荷长度
buffer += nChunkSize;
hSize = 0;
//如果消息没有发完
if (nSize > 0)
{
header = buffer - 1;
hSize = 1;
if (cSize)
{
header -= cSize;
hSize += cSize;
}
//块基本头第1个字节
*header = (0xc0 | c);
//如果块基本头大于1字节
if (cSize)
{
int tmp = packet->m_nChannel - 64;
header[1] = tmp & 0xff;
if (cSize == 2)
header[2] = tmp >> 8;
}
}
}
if (tbuf)
{
int wrote = WriteN(r, tbuf, toff-tbuf);
free(tbuf);
tbuf = NULL;
if (!wrote)
return FALSE;
}
/* we invoked a remote method */
if (packet->m_packetType == 0x14)
{
AVal method;
char *ptr;
ptr = packet->m_body + 1;
AMF_DecodeString(ptr, &method);
RTMP_Log(RTMP_LOGDEBUG, "Invoking %s", method.av_val);
/* keep it in call queue till result arrives */
if (queue)
{
int txn;
ptr += 3 + method.av_len;
txn = (int)AMF_DecodeNumber(ptr);
AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
}
}
if (!r->m_vecChannelsOut[packet->m_nChannel])
r->m_vecChannelsOut[packet->m_nChannel] = (RTMPPacket *) malloc(sizeof(RTMPPacket));
memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));
return TRUE;
}
现在要解决的是如何给结构体RTMPPacket中的消息载荷m_body赋值,即如何将H.264的NALU打包进消息载荷。
1.sps和pps的打包
sps和pps是需要在其他NALU之前打包推送给服务器。由于RTMP推送的音视频流的封装形式和FLV格式相似,向FMS等流媒体服务器推送H264和AAC直播流时,需要首先发送"AVC sequence header"和"AAC sequence header"(这两项数据包含的是重要的编码信息,没有它们,解码器将无法解码),因此这里的"AVC sequence header"就是用来打包sps和pps的。
AVC sequence header其实就是AVCDecoderConfigurationRecord结构,该结构在标准文档“ISO/IEC-14496-15:2004”的5.2.4.1章节中有详细说明,如下所示:
用表格表示如下:
FLV 是一个二进制文件,简单来说,其是由一个文件头(FLV header)和很多 tag 组成(FLV body)。tag 又可以分成三类: audio, video, script,分别代表音频流,视频流,脚本流,而每个 tag 又由 tag header 和 tag data 组成。
然后参照“Video File Format Specification Version 10”中The FLV File Format的Video tags章节,如下所示:
上表中tag header为两个4bits,即一个字节,其他的是tag data。inter frame即P frame。
AVC时,3字节CompositionTime无意义,通常设置为0。
AVCDecoderConfigurationRecord结构的表格中可以看出,由于NALUnitLength-1=3,因此每个NALU包都有NALUnitLength=4个字节来描述它的长度。这4个字节需要添加到每个NALU的前面,因此当AVCPacketType==1时,上表中Data的结构实际上如下所示:
一个典型的打包示例如下所示:
body = (unsigned char *)packet->m_body;
i = 0;
body[i++] = 0x17;// 1:Iframe 7:AVC ,元数据当做keyframe发送
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
//AVCDecoderConfigurationRecord
body[i++] = 0x01;
body[i++] = sps[1];
body[i++] = sps[2];
body[i++] = sps[3];
body[i++] = 0xff;
/*sps*/
body[i++] = 0xe1;
body[i++] = (sps_len >> 8) & 0xff;
body[i++] = sps_len & 0xff;
memcpy(&body[i],sps,sps_len);
i += sps_len;
/*pps*/
body[i++] = 0x01;
body[i++] = (pps_len >> 8) & 0xff;
body[i++] = (pps_len) & 0xff;
memcpy(&body[i],pps,pps_len);
i += pps_len;
2.其它NALU的打包
一个典型的打包示例如下所示:
int i = 0;
if(bIsKeyFrame)
{
body[i++] = 0x17;// 1:Iframe 7:AVC
body[i++] = 0x01;// AVC NALU
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
// NALU size
body[i++] = size>>24 &0xff;
body[i++] = size>>16 &0xff;
body[i++] = size>>8 &0xff;
body[i++] = size&0xff;
// NALU data
memcpy(&body[i],data,size);
}
else
{
body[i++] = 0x27;// 2:Pframe 7:AVC
body[i++] = 0x01;// AVC NALU
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
// NALU size
body[i++] = size>>24 &0xff;
body[i++] = size>>16 &0xff;
body[i++] = size>>8 &0xff;
body[i++] = size&0xff;
// NALU data
memcpy(&body[i],data,size);
}
一个具体的例子:Qt基于librtmp推送H.264
参考链接:
http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf
http://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf