rtmp协议规范详解

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/HandsomeHong/article/details/79084967

转载请注明出处:http://blog.csdn.net/handsomehong

本篇文章大部分是rtmp协议1.0版本的翻译,翻译参考了网上的版本,加了一些抓包分析。

Adobe公司的实时消息传输协议

摘要

  此备忘录描述了 Adobe公司的实时消息传输协议(RTMP),此协议从属于应用层,被用来在适合的传输协议(如TCP)上复用和打包多媒体传输流(如音频、视频和互动内容)。

目录

1.简介 
1.1.术语 
2.贡献者 
3.定义 
4.字节序,对齐,和时间格式 
5.RTMP块流 
5.1.消息格式 
5.2.握手 
5.2.1.握手序列 
5.2.2.C0和S0格式 
5.2.3.C1和S1格式 
5.2.4.C2和S2格式 
5.2.5.握手流程图 
5.3.组块 
5.3.1.块格式 
5.3.1.1.块的基本头 
5.3.1.2.块的消息头 
5.3.1.2.1.类型0 
5.3.1.2.2.类型1 
5.3.1.2.3.类型2 
5.3.1.2.4.类型3 
5.3.1.2.5.常见的头字段 
5.3.1.3.扩展的时间戳 
5.3.2.示例 
5.3.2.1.示例1 
5.3.2.2.示例2 
5.4.协议控制消息 
5.4.1.设置块大小 (1) 
5.4.2.中断消息 (2) 
5.4.3.应答 (3) 
5.4.4.应答窗口大小 (5) 
5.4.5.设置流带宽 (6) 
6.RTMP消息格式 
6.1.RTMP消息格式 
6.1.1.消息头 
6.1.2.消息有效数据 (音视频数据)
6.2.用户控制消息 (4) 
7.RTMP命令消息 
7.1.消息类型 
7.1.1.命令消息 (20, 17) 
7.1.2.数据消息 (18, 15) 
7.1.3.共享对象消息 (19, 16) 
7.1.4.音频消息 (8) 
7.1.5.视频消息 (9) 
7.1.6.集合消息 (22) 
7.1.7.用户控制消息事件 
7.2.命令类型 
7.2.1.网络连接命令 
7.2.1.1.连接 
7.2.1.2.调用 
7.2.1.3.创建流 
7.2.2.网络流命令 
7.2.2.1.播放 
7.2.2.2.播放2 
7.2.2.3.删除流 
7.2.2.4.接收音频 
7.2.2.5.接收视频 
7.2.2.6.发布 
7.2.2.7.定位 
7.2.2.8.暂停 
7.3.消息交互示例 
7.3.1.发布录制的视频 
7.3.2.广播共享对象消息 
7.3.3.从录制的流发布元数据 
8.引用 
作者的联系方式

1.简介

Adobe公司的实时消息传输协议(RTMP)通过可靠的流传输,比如TCP协议[RFC0793],提供了一套全双工的消息多路传输服务,用来在一对结 点之间并行传输带时间戳的音频流,视频流,数据流。通常情况下,不同类型的消息会被分配不同的优先级,当网络传输能力受限时,优先级会影响消息在底层流传 输的入队顺序。

  这篇文章描述了实时消息传输协议(RTMP)的语法和操作。

1.1术语

当前文章中的这些关键字“MUST”“MUST NOT”“REQUIRED”“SHALL”“SHALL NOT”“SHOULD”“SHOULD NOT”“RECOMMENDED”“NOT RECOMMENDED”“MAY”“OPTIONAL”按照[RFC2119]中的描述来解读。

2.贡献者

Rajesh Mallipeddi,Adobe公司前员工,是这份规范的原始作者,并且提供了大部分的原始文本。 
Mohit Srivastava,Adobe公司员工,为这份规范的完善做出了很大贡献。

3.定义

有效数据(直接翻译是 载荷) 
包含在包中的数据,例如音频采样、压缩后的视频数据。有效数据的格式超出了本文的范围。 

 
一个数据包包括固定的包头和有效数据。一些底层协议可能需要封装包的定义。

端口 
传输协议使用端口来区分同一个主机上的多个目标地址。TCP/IP协议根据小正整数(端口号)来识别端口。OSI传输层使用的传输选择器(TSEL)相当于端口。

传输地址 
一个网络地址和端口的组合,用来标识传输层的一端,例如IP地址和TCP端口。数据包从源地址传输到目的地址。

消息流 
消息流动的逻辑通信信道。

消息流ID (备注:msid)
每条消息都有一个与之相关联的ID,用来区分这条消息属于哪个流。

 
消息的一个分片。消息在被发送到网络之前,被分割成更小的部分。块可以确保跨越多个流,按时间戳顺序,端到端的交付所有消息。

块流 
一个逻辑通信信道,允许块在一个特定的方向上流动。块流可以从客户机传输到服务器,也可以反向传输。

块流ID (备注:csid)
每个块都有一个与之相关联的ID,用来区分块属于哪个流。

多路复用 
将独立的音频或视频数据组合为连贯的音视频数据流,使同时传输多个视频和音频成为可能。

多路分解 
多路复用的逆向过程,将混合的音视频数据拆分成独立的音频和视频数据。

远程过程调用(RPC) 
允许客户端或服务器请求对端调用程序或子程序。

元数据 
关于数据的描述。电影的元数据包括标题、时间、创建日期、等等。

应用实例 
当客户端发送连接请求到服务器时,会在服务器端建立一个应用实例。

动作消息格式(AMF) 
一种紧凑的二进制格式,用来序列化ActionScript object graphs。AMF有两个版本:AMF0、AMF3。 
注: AMF0代表早期的flex对象,AMF3代表flash对象;

4.字节序,对齐,和时间格式

所有的整形字段使用网络字节序的方式传输,第零字节位于第一位,第零比特是双字节或字段的标志位。这种字节顺序通常称为大端。传输顺序的描述详见IP协议[RFC0791]。除非特别声明,本文中的数字一律为十进制。 
注: 网络字节序为大端排序方式。

除非特别说明,RTMP协议中的数据都以字节对齐。例如,一个16比特数字可能位于奇数偏移字节。涉及到填补数据的,填补字节应该为零。

RTMP的时间戳,是以毫秒为单位的相对于不特定的某一时刻的整型数。通常,每个流以零作为时间戳的起始,但这不是必需的,只要他们的基准时间一样即可。注意,这意味着,任何同步传输的多个流(特别是来自不同的主机)的时间戳需要在RTMP协议外做一些额外处理。

时间戳是一个32位整型数,每隔49天17时2分47.296秒翻转。由于,流允许连续传输,可能连续数年,RTMP应用程序在处理时间戳时,应该使用序 列号算法[RFC1982],and SHOULD be capable of handling wraparound。例如,应用程序假设所有相邻的时间戳都在1到2^32毫秒之间,比如4000000000后面跟着 10000,3000000000在4000000000的前面。

时间戳增量是相对于前一个时间戳的无符号整型数。时间戳增量可以是24位或者32位。


5.RTMP块流

本节讲述了实时消息协议块流(RTMP块流)。它为更高层多媒体流协议提供了多路复用和打包服务。RTMP块流被设计用于实时消息协议(第6章),它可以 处理发送消息流的任何协议。每个消息都包含时间戳和有效数据类型标识。RTMP块流和RTMP适用于各种视听传播的应用程序,包括一对一的,和一对多的视 频直播、视频点播服务、互动会议应用程序。

当使用一个可靠的传输协议如TCP[RFC0793]时,RTMP块流提供了一种可以在多个流中,基于时间戳顺序的端到端交付所有消息的方法。RTMP块 流不提供任何优先级或类似形式的控制,但可以使用更高层的协议来提供这样的优先级。例如,一个视频服务器可以根据发送的时间或确认每个消息的时间,来决定 为一个网络差的用户丢弃视频信息,以确保音频信息的及时接收。

RTMP块流不仅包含了自己的协议控制信息,同时也为更高层的协议提供了嵌入用户自定义控制信息的机制。

5.1.消息格式

消息格式可以被分割成多个块,用来在更高层的协议中支持多路复用。消息格式应包含以下在创建块时所必须的字段:

时间戳 
消息的时间戳。这个字段占用4字节。

长度 
消息的有效数据长度。如果消息头不能被忽略,它应该被包括进长度。这个字段在块头中占用3字节。

类型ID 
A range of type IDs are reserved for protocol control messages.这些消息使用RTMP块流协议和更高层的协议来传输信息。所有其他类型的ID可以用在更高层的协议,这对于RTMP块流来说,是不透明 的。事实上,RTMP块流中没有要求使用这些值作为类型;所有(无协议的)消息可能是相同的类型,或者应用程序使用这个字段来区分多个连接,而不是类型。 这个字段在块头中占用1字节。

消息流ID 
消息流ID可以是任意值。不同的消息流会复用到相同的块流,可以通过消息流ID来解复用。另外,对于RTMP块流而言,这是一个不透明值。该字段在块头中占用4字节,使用小端格式。

5.2.握手

RTMP连接从握手开始。它包含三个固定大小的块,不像其他的协议,是由大小可变包含头部的块组成。

客户端(初始化连接的一端)和服务端发送同样的三个块。为了方便描述,客户端发送的三个块命名为C0,C1,C2;服务端发送的三个块命名为S0,S1,S2。

握手序列

客户端通过发送C0和C1块来启动握手过程。客户端必须接收到S1块,然后才能发送C2块。客户端必须接收到S2块,然后发送其他数据。

服务端必须接收到C0或者C1块,然后才能发送S0和S1块。服务端必须接收到C1块,然后才能发送S2块。服务端必须接收到C2块,然后发送其他数据。

C0和S0格式

C0和S0包由一个字节组成,下面是C0/S0包内的字段:

  1. 0 1 2 3 4 5 6 7
  2. +-+-+-+-+-+-+-+-+
  3. | version |
  4. +-+-+-+-+-+-+-+-+
  5. C0 and S0 bits

版本(8比特) 
在C0包内,这个字段代表客户端请求的RTMP版本号。在S0包内,这个字段代表服务端选择的RTMP版本号。此文档使用的版本是3。版本0-2用在早期 的产品中,现在已经被弃用;版本4-31被预留用于后续产品;版本32-255(为了区分RTMP协议和文本协议,文本协议通常以可打印字符开始)不允许 使用。如果服务器无法识别客户端的版本号,应该回复版本3。客户端可以选择降低到版本3,或者中止握手过程。

C1和S1格式

C1和S1包长度为1536字节,包含以下字段:

  1. 0 1 2 3
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. | time (4 bytes) |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. | zero (4 bytes) |
  7. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  8. | random bytes |
  9. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  10. | random bytes |
  11. | (cont) |
  12. | .... |
  13. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  14. C1 and S1 bits

时间(4字节) 
本字段包含一个时间戳,以后所有从该终端发送的块都应该以该时刻为参考点。时间戳取值可以为零或其他任意值。为了同步多个块流,终端可能希望发送其他块流的时间戳。

零(4字节) 
本字段必须为零。

随机数据(1528字节) 
本字段可以包含任意数据。由于握手的双方需要区分另一端,此字段填充的数据必须足够随机(以防止与其他握手端混淆)。不过没必要为此使用加密安全随机数据或动态数据。

C2和S2格式

C2和S2包长度为1536字节,作为C1和S1的回应,包含以下字段:

  1. 0 1 2 3
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. | time (4 bytes) |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. | time2 (4 bytes) |
  7. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  8. | random echo |
  9. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  10. | random echo |
  11. | (cont) |
  12. | .... |
  13. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  14. C2 and S2 bits

时间(4字节) 
本字段必须包含对端发送的S1(对于C2)或者C1(对于S2)时间戳。

时间2(4字节) 
本字段必须包含时间戳,取值为接收对端发送过来的握手包(s1或c1)的时刻。

随机数据(1528字节) 
本字段必须包含对端发送过来的随机数据(对于C2来说是S1,对于S2来说是C1)。握手的双方可以使用时间1和时间2字段来估算网络连接的带宽和/或延迟,但是不一定有用。

握手过程示意图

  1. +-------------+ +-------------+
  2. | Client | TCP/IP Network | Server |
  3. +-------------+ | +-------------+
  4. | | |
  5. Uninitialized | Uninitialized
  6. | C0 | |
  7. |------------------->| C0 |
  8. | |-------------------->|
  9. | C1 | |
  10. |------------------->| S0 |
  11. | |<--------------------|
  12. | | S1 |
  13. Version sent |<--------------------|
  14. | S0 | |
  15. |<-------------------| |
  16. | S1 | |
  17. |<-------------------| Version sent
  18. | | C1 |
  19. | |-------------------->|
  20. | C2 | |
  21. |------------------->| S2 |
  22. | |<--------------------|
  23. Ack sent | Ack Sent
  24. | S2 | |
  25. |<-------------------| |
  26. | | C2 |
  27. | |-------------------->|
  28. Handshake Done | Handshake Done
  29. | | |
  30. Pictorial Representation of Handshake
  31. [译]握手示意图

下面是握手示意图中提到的状态:

未初始化 
协议版本号在此阶段发送。客户端和服务器均处于未初始化状态。客户端发送携带协议版本号的C0包。如果服务器支持此版本,回复S0和S1包。如果服务器不支持此版本,使用适当的动作回复。在RTMP协议中,此动作是中止连接。 
注: 在”C0和S0格式”章节中提及,如果服务器不支持客户端的版本号,可以选择降到版本3或中止。

发送版本 
客户端和服务器双方在未初始化状态后,会进入发送版本状态。之后,客户端等待S1包,服务器等待C1包。待接收到数据包,客户端发送C2包,服务器发送S2包。然后,双方都进入答复状态。

答复状态

客户端等待S2包,服务器等待C2包。

握手完成 
客户端和服务器交换消息。

下面结合抓包软件来分析。打开抓包软件,用软件播放一个rtmp直播流。

由于rtmp协议基于tcp协议,所以必有tcp的三次握手建立连接。


下面先学习以下tcp四层模型的各层协议。

学习以太网的帧结构:

以太网帧主要有两种:以太网V2类型和802.3类型。以太网帧的格式如下(以太网V2类型):

以太网帧是物理层的概念,它的数据部分叫MAC帧,是MAC层的概念,MAC帧的数据部分叫IP报文,是IP层的概念,IP层的数据部分是传输层的概念, 根据传输层使用的协议的不同,又可分为UDP报文和TCP报文,传输层之上是最后一层,应用层。应用层的协议非常多,比如本篇文章的rtmp协议就是应用 层的协议。在应用层之上可以还有进一步的封装,那就属于用户的事了。




 其中以太网帧的数据部分就是上层的“IP报文”,在MAC层封装时,在IP报文(比如ARP报文)的基础上加了12个字节的的源目MAC地址,2个字节的报文类型,4个字节的FCS校验位,一共18个字节。

2个字节的类型字段主要是用来标志上一层使用的是什么协议,主要方便于收到MAC帧的主机在解包的过程中,知道把数据字段(IP报文)交给上一层的哪个协议,例如ARP的报文类型是 0X0806 。常用的类型时PPP(点对点协议,工作于链路层)

数据字段的大小在46-1500字节之间,也就说MAC帧的大小不能小于46+18=64字节,否则被丢弃


物理层之上是链路层。链路层我以PPP协议为例子。

版本和类型1字节

会话数据1字节

会话ID2字节

有效数据长度2字节


链路层之上是网络层,对应于IP报文

IP报文格式如下图


版本:IP协议的版本,目前的IP协议版本号为4,下一代IP协议版本号为6。

首部长度:IP报头的长度。固定部分的长度(20字节)和可变部分的长度之和。共占4位。最大为1111,即10进制 的15,代表IP报头的最大长度可以为15个32bits(4字节),也就是最长可为15*4=60字节,除去固定部分的长度20字节,可变部分的长度最 大为40字节。首部长度通常为20。

服务类型:Type Of Service。通常为0,有些地方叫区分服务字段。

总长度:IP报文的总长度。报头的长度和数据部分的长度之和。

标识:唯一的标识主机发送的每一分数据报。通常每发送一个报文,它的值加一。当IP报文长度超过传输网络的MTU(最大传输单元)时必须分片,这个标识字段的值被复制到所有数据分片的标识字段中,使得这些分片在达到最终目的地时可以依照标识字段的内容重新组成原先的数据。

标志:共3位。R、DF、MF三位。R表示保留位,目前只有后两位有效,DF位:为1表示不分片,为0表示分片。MF:为1表示“更多的片”,为0表示这是最后一片。

片位移本分片在原先数据报文中相对首位的偏移位。(需要再乘以8)

生存时间:IP报文所允许通过的路由器的最大数量。每经过一个路由器,TTL减1,当为0时,路由器将该数据报丢弃。 TTL 字段是由发送端初始设置一个 8 bit字段.推荐的初始值由分配数字 RFC 指定,当前值为 64。发送 ICMP 回显应答时经常把 TTL 设为最大值 255。

协议:指出IP报文携带的数据使用的是那种协议,以便目的主机的IP层能知道要将数据报上交到哪个进程(不同的协议有专门不同的进程处理)。和端口号类似,此处采用协议号,TCP的协议号为6,UDP的协议号为17。ICMP的协议号为1,IGMP的协议号为2.

首部校验和:计算IP头部的校验和,检查IP报头的完整性。

源IP地址:标识IP数据报的源端设备。

目的IP地址:标识IP数据报的目的地址。



网络层之上是传输层。传输层的协议有TCP和UDP。

首先来看一下TCP报文的首部。

tcp首部
各字段介绍:
1、源端口:是应用层进程所使用的端口,16位
2、目的端口:通信另一端的进程所使用的端口,16位
3、序号:发送分组时,将报文按字节编号,报文的第一个字节的序号就是序号,每一次发送报文的序号时由双方决定,也就是发送方报文的第一个字节序号。32位
4、确认序号:是接收方期望发送方下一个报文的字节序号,假如A向B发送了一个报文,序号是x,如果B收到了这个报文,那么B发送了一个确认报文给A,将确认序号设置为x+1,表示前x个字节的数据我已经收到,你下次给我发从x+1开始的数据。
5、首部长度:注意单位为4字节,所以可表示15*4=60字节的首部。一般首部长度为20字节。
6、保留:6bit, 均为0
下面是6个标志位。
备注:网上的资料一致是6位保留,6位标志位,但是我用最新的wireshark抓包却显示是3位保留,9位标志位。

多出来的三个是Nonce、CWR(Congestion Window Reduced)、ECN-Echo

7、紧急URG:当URG=1时,表示报文段中有紧急数据,应尽快传送。
8、确认比特ACK:ACK = 1时代表这是一个确认的TCP包,取值0则不是确认包。
9、 推送比特PSH:当发送端PSH=1时,接收端尽快的交付给应用进程。
10、 复位比特(RST):当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接。
11、同步比特SYN:在建立连接是用来同步序号。SYN=1, ACK=0表示一个连接请求报文段。SYN=1,ACK=1表示同意建立连接。
12、 终止比特FIN:FIN=1时,表明此报文段的发送端的数据已经发送完毕,并要求释放传输连接。
13、 窗口:用来控制对方发送的数据量。16位。单位是4字节。
14、 检验和:该字段检验的范围包括首部和数据这两部分。由发端计算和存储,并由收端进行验证。
15、紧急指针:紧急指针在URG=1时才有效,它指出本报文段中的紧急数据的字节数。通常为0
16、 选项:长度可变,最长可达40字节
关于tcp选项:最大分片大小(MSS)2字节、window scale(WS)、SACK_PERM
        
常见的有:

tcp的三次握手

1、客户端发送syn标志位=1(表明是个连接请求),序列号为0的tcp包

2、服务器端收到之后发送syn标志位=1,ack标志位=1,序列号为0的tcp包

3、客户端收到之后发送ack标志位=1,序列号为1的tcp包

握手时tcp的数据部分都是0。

tcp握手成功,按照协议rtmp协议也要握手。

客户端要向服务器发送C0和C1rtmp块,大小分别为1和1535字节,显然是超过了前面介绍的以太网帧的数据部分最多只能是1500字节,而且这1500字节还是ip报文。所以需要分批发送,抓包发现:

客户端向服务器发送了ack标志位=1,序列号=1,附加有1300字节数据的tcp包。

客户端继续向服务器发送了ack标志位=1,序列号=1301,附加有237字节数据的tcp包。

发现两者加起来刚好是1357。按道理一个以太网帧可以发送除去ip、tcp头1500-40=1460字节的应用层数据,至于这里为啥是1300呢,不得而知

ok,C0和C1已经发送完毕,可以看到,在实际实现时,C0和C1是绑在一起发送的。C1的时间戳是0a 67 bb c0

服务器收到两个tcp包自然要回应两次:

一次ACK=1301,表示告诉客户端前面1300自己我已经收到了。

一次ACK=1358,表示高数客户端前面1357字节我已经收到了。接下来就站在应用层(即rtmp)的角度来看,不考虑tcp应答包这些。

至于如果第一次包没有收到,只收到了第二次包,会怎样呢?这是tcp协议的事了,不在本文讨论范围之内。

下面服务器收到了C0和C1后,开始向客户端发送S0、S1、S2rtmp块了,可以看到,为了方便,这三个块是绑在一起发的的。

又是发序列号=1,1300字节数据(应用层)

发序列号=1301,1300字节数据(应用层)

发序列号=2601,473字节数据(应用层)

S1的时间戳时01 86 4f 7f,S2的时间戳是0a 67 bb c0 等于C1时间戳,符合协议。

接着是客户端收到数据的回应。

最后客户端向服务器发送C2块。同样要分两次发送。C2的时间戳是01 86 4f 7f,发现等于S1的时间戳,符合协议。

接着客户端向服务器发送一个Push=1,ack=1的tcp包,该包是一个包含AMF命令(connect)的包,格式如下:

基本头(格式0,块流ID3)

消息头(时间戳00 00 00 ,有效数据长度,消息类型0x14,表示是AMF命令,流ID0)

无扩展时间戳

有效数据部分如下:


分号后面就是数据。简单来讲就是 : 键 键的长度 键值类型(系统已经定义了几种类型供选择)键值

对于字符串可以直接是:字符串长度 字符串

整个包的数据整体来看就是:

connect 1 一堆值对(请求的视频地址,音视频编码器等)

接着服务器向客户端发:


接着客户端向服务器告知窗口大小:


接着客户端发:


event type后面跟着buffer长度。

又是客户端发:

接着客户端发:

服务器回应:



接着客户端发播放命令

客户端又向服务器发set buffer length

服务端发:

开始真正的音视频数据传输:
第一个块的时间戳是0,块流ID5,类型0,流ID1,消息类型ID0x08(表示音频数据),有效数据部分是音频的参数(1字节),音频数据
第二个块的时间戳增量是21,类型1,消息类型ID0x08(表示音频数据),有效数据部分是音频的参数(1字节),音频数据


音频数据:

视频数据:







先提前介绍AMF编码。

rtmp包默认的最大长度为128字节(或通过chunksize改变rtmp包最大长度), 当AMF数据超过128Byte的时候就可能由多个rtmp包组成,如果需要发送的rtmp包太长则被TCP协议分割成多个TCP包。那么解码的时候需要先将包含rtmp包的tcp封包合并,再把合并的数据解码,解码后可得到AMF格式的数据,将这些AMF数据取出来就可以对AMF数据解码了。
RTMP封包包括包头和AMF数据2部分,AMF数据里面可以是命令也可以是音视频数据。组成服务器和Flash客户端之间的所有数据都是用AMF格式的数据在传送,例如connect() publish()等命令。

AMF数据由2部分组成: ObjType + ObjValue。ObjType的大小为一个字节。ObjValue的大小不固定,和ObjType相关。 常用的ObjType类型和对应的ObjValue大小整理如下,详细的ObjType的数据在本文的最下面列出:

类型说明(ObjType)    ObjValueSize
CORE_String    0x02  2字节 (2字节的数据纪录了String的实际长度)
CORE_Object   0x03   0字节(开始嵌套0x000009表示嵌套结束)
NULL     0x05  0字节 空字节无意义
CORE_NUMBER   0x00    8字节
CORE_Map   0x08       4字节(开始嵌套)
CORE_BOOLEAN   0x01      1字节

ObjValue不一定是一个固定的大小,他可以包含另外一个AMF数据,这另外一个AMF数据里面又有ObjType 加上 ObjValue,也就是AMF数据的嵌套关系。

  AMF0数据的嵌套关系如下:
Object={ObjType + ObjValue}
CORE_BOOLEAN={Value(1 Byte)}
CORE_NUMBER={Value(8 Byte)}
CORE_String={StringLen(2 Byte) + StringValue(StringLen Byte)}
CORE_DATE={value(10 Byte)}
CORE_Array={ArrayLen(4 Byte) + Object}
CORE_Map={MapNum(4 Byte) + CORE_Object}
CORE_Object={CORE_String + Object}




5.3.组块

握手完成之后,此连接可以复用于一到多个块流。每个块流携带一个消息流的某种类型的消息。块通过网络进行传输。传输过程中,每个块必须被完整的发送后,才能发送下一个块。接收端接收完成之后,根据块流ID把块组装成完整的消息。

块允许把更高层协议的大消息分割成更小的消息分片,例如为了防止低优先级的大消息(如视频消息)阻碍高优先级的小消息(如音频和控制消息)。

块允许以更少的开销来发送小消息,减少开销的方法是压缩必须携带的块头的数据。

块的大小是可配置的。配置的方法详见5.4.1章节的设置块大小控制消息。大的块消息可以减少CPU使用率,但是在带宽比较小时会导致其他内容的延迟。小的块消息不利于高码率流的传输。流的每一个方向都可以配置独立的块大小。

5.3.1.块格式

每一个块包含了头和数据。块头包含了三个部分:

  1. +--------------+----------------+--------------------+--------------+
  2. | Basic Header | Message Header | Extended Timestamp | Chunk Data |
  3. +--------------+----------------+--------------------+--------------+
  4. | |
  5. |<------------------- Chunk Header ----------------->|
  6. Chunk Format

基本头(1-3字节) 
这个字段包含块流ID和块类型。块类型决定了编码过的消息头的格式。这个字段是一个变长字段,长度取决于块流ID。

消息头(0,3,7,11字节) 
这个字段包含被发送的消息信息(无论是全部,还是部分)。字段长度由块头中的块类型来决定。

扩展时间戳(0,4字节) 
这个字段是否存在取决于块消息头中编码的时间戳。更多详情参考5.3.1.3章节。

块数据(可变大小) 
当前块的有效数据,上限为配置的最大块大小。

5.3.1.1.块的基本头

块的基本头包含块流ID和块类型(下面的fmt字段)。块类型决定了编码过的消息头的格式。此字段根据块流ID的不同,长度可能为1,2或3字节。

在实现协议时,此字段应该使用可以容纳ID的最小长度。

此协议支持最多65597个流,ID从3到65599。0,1,2这三个为保留ID。ID为0表示块基本头长度为2字节,id从64到319(第二字 节+64)。ID为1表示块基本头的长度为3字节,ID从64到65599(第三字节*256+第二字节+64,说白点,就是2的16次方+64)。ID 从3到63表示完整流ID。块流ID2是为低层协议控制消息和命令保留。

块基本头中的第0到第五位(least significant)表示块流ID。

块流ID从2到63可以编码进1字节的块基本头。

  1. 0 1 2 3 4 5 6 7
  2. +-+-+-+-+-+-+-+-+
  3. |fmt| cs id |
  4. +-+-+-+-+-+-+-+-+
  5. Chunk basic header 1

块流ID从64到319可以编码进2字节的块基本头。ID计算方法为(第二字节+64)。

  1. 0 1
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. |fmt| 0 | cs id - 64 |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. Chunk basic header 2

块流ID从64到65599可以编码进3字节得到块基本头。ID计算方法为(第三字节*256+第二字节+64,说白了就是2的16次方+64)

  1. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
  2. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  3. |fmt| 1 | cs id - 64 |
  4. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  5. Chunk basic header 3

块流ID(6比特) 
本字段包含了从2到63的块流ID。当块的基本头长度为2字节时,此字段取值为0。当块的基本头长度为3字节时,此字段取值为1。

类型(2比特) 
本字段标识了块消息头的类型。块消息头的4种类型,会在下一小节中说明。

块流ID-64(8或16比特) 
本字段的取值为块流ID减去64。例如,块流ID为365的存储方式为,第3-8比特为1,第9-24比特为301。

64-319之间的块流ID,既可以使用2字节头长度,也可以使用3头长度。不过前面曾经提到过“在实现协议时,此字段应该使用可以容纳ID的最小长度”,因此应该选择使用2字节头长度。

5.3.1.2.块的消息头

根据块的基本头中类型字段的取值不同,块的消息头有4种类型。

在实现协议时,此字段应该使用可以容纳消息头的最小长度。

5.3.1.2.1.类型0

类型为0的消息头长度为11字节。块流必须以这种类型的块开始,当块流的时间戳回退时,也必须以这种类型的消息开始(比如回放)。

  1. 0 1 2 3
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. | timestamp |message length |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. | message length (cont) |message type id| msg stream id |
  7. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  8. | message stream id (cont) |
  9. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  10. Chunk Message Header - Type 0

时间戳(3字节) 
类型为0的块,此字段必须为绝对时间戳。如果时间戳大于等于16777215(0xFFFFFF),此字段的取值必须为16777215,并且与扩展时间戳一起组成32比特的完整时间戳。如果时间戳小于16777215,那么此字段代表了完整的时间戳。

5.3.1.2.2.类型1

类型为1的消息头长度为7字节。不包含消息流ID;当前块和上一个块使用相同的流ID。拥有可变大小消息(比如多种视频格式)的流应该在首个消息之后的每个消息的第1个块使用这种类型。

  1. 0 1 2 3
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. | timestamp delta |message length |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. | message length (cont) |message type id|
  7. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  8. Chunk Message Header - Type 1
5.3.1.2.4.类型2

类型为2的消息头长度为3字节。不包含消息流ID或消息长度;当前块和上一个块使用相同的流ID和流长度。拥有固定大小消息(音频或其他数据)的流应该在首个消息之后的第一个消息的第1个块使用这种类型。

  1. 0 1 2
  2. 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
  3. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  4. | timestamp delta |
  5. +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
  6. Chunk Message Header - Type 2
5.3.1.2.4.类型3

类型为3的块没有消息头(0字节)。消息头内的相关字段都不存在。当前块和上一个块使用相同的流ID。当一个独立的消息被分割成块时,第一个块以后的所有 开始使用这种类型。第5.3.2.2章节的示例2展示了这种情况。如果流由大小,流ID,间隔时间完全相同的消息组成时,应该在类型为2的块之后的所有块 使用这种类型。第5.3.2.1章节的示例1展示了这种情况。如果第一个和第二个消息之间的时间间隔和第一个消息的时间戳相同,那么类型为0的块后面跟随 类型为3的块,因为没有必要再使用类型为2的块来标识时间间隔。如果类型为0的块后面跟随着类型为3的块,那么类型为3的块的时间间隔与类型为1的块的时 间戳相同。

5.3.1.2.5.常见的头字段

块消息头里面的每个字段的描述:

时间戳增量(3字节) 
类型为1或2的块里,本字段代表当前块和上一个块的时间戳之差。如果时间间隔大于等于16777215(0xFFFFFF),此字段的取值必须为 16777215,并且与扩展时间戳一起组成32比特的完整时间戳。如果时间戳小于16777215,那么此字段代表了完整的时间戳。

消息长度(3字节) 
类型为0或1的块里,本字段代表消息的长度。这个值通常情况下跟块的有效数据长度是不相等的。块的有效数据长度是块大小的最大值(除了最后一个块)。

The chunk payload length is the maximum chunk size for all but the last chunk, and the remainder (which may be the entire length, for small messages) for the last chunk. 

消息类型ID(1字节) 
类型为0或1的块里(因为其他类型的块没有这个字段,注意区分消息类型和块类型),本字段代表消息的类型。

消息流ID(4字节) 
类型为0的块里,本字段代表消息流的ID。并使用小端方式存储。通常情况下,相同块流里面的消息来自于相同的消息流。虽然可以将不同的消息流复用到相同的块流中,但这却违背了头压缩的好处。(???)无论如何,如果关闭了一个消息流,紧接着又打开了另一个,那么没有理由已经存在的块流不能被用于发送新的类型为0的块。(???)

5.3.1.3.扩展的时间戳

扩展的时间戳字段用来表示时间戳或时间间隔取值大于16777215(0xFFFFFF)的。也就是说,类型为0或1或2的块中,时间戳或时间间隔的取值 超出了24比特。本字段与之前的时间字段一起构成完整的32比特时间戳或时间间隔。完整时间值的前一部分(24比特)位于类型为0的块的时间戳字段,或类 型为1或2的时间间隔字段,取值为16777215(0xFFFFFF)。类型为3的块中,扩展时间戳字段与上一个块中(可能为类型0或1或2)的扩展时 间戳相等。(前面不是说类型3的块消息头为空吗?这里岂不是自相矛盾???)

5.3.2.示例

5.3.2.1.示例1

本示例展示了一个音频消息流。例子说明信息的冗余。

  1. +---------+-----------------+-----------------+-----------------+
  2. | |Message Stream ID| Message Type ID | Time | Length |
  3. +---------+-----------------+-----------------+-------+---------+
  4. | Msg # 1 | 12345 | 8 | 1000 | 32 |
  5. +---------+-----------------+-----------------+-------+---------+
  6. | Msg # 2 | 12345 | 8 | 1020 | 32 |
  7. +---------+-----------------+-----------------+-------+---------+
  8. | Msg # 3 | 12345 | 8 | 1040 | 32 |
  9. +---------+-----------------+-----------------+-------+---------+
  10. | Msg # 4 | 12345 | 8 | 1060 | 32 |
  11. +---------+-----------------+-----------------+-------+---------+
  12. Sample audio messages to be made into chunks

下面的表格展示了由此音频流产生的块。从第3条消息开始,数据传输达到最大优化。每条消息的头部只增加了1字节长度(基本头的大小)。

本例子一条消息用一个块来传输。

  1. +--------+---------+-----+------------+------- ---+------------+
  2. | | Chunk |Chunk|Header Data |No.of Bytes|Total No.of |
  3. | |Stream ID|Type | | After |Bytes in the|
  4. | | | | |Header |Chunk |
  5. +--------+---------+-----+------------+-----------+------------+
  6. |Chunk#1 | 3 | 0 | delta: 1000| 32 | 44 |
  7. | | | | length: 32,| | |
  8. | | | | type: 8, | | |
  9. | | | | stream ID: | | |
  10. | | | | 12345 (11 | | |
  11. | | | | bytes) | | |
  12. +--------+---------+-----+------------+-----------+------------+
  13. |Chunk#2 | 3 | 2 | 20 (3 | 32 | 36 |
  14. | | | | bytes) | | |
  15. +--------+---------+-----+----+-------+-----------+------------+
  16. |Chunk#3 | 3 | 3 | none (0 | 32 | 33 |
  17. | | | | bytes) | | |
  18. +--------+---------+-----+------------+-----------+------------+
  19. |Chunk#4 | 3 | 3 | none (0 | 32 | 33 |
  20. | | | | bytes) | | |
  21. +--------+---------+-----+------------+-----------+------------+
  22. Format of each of the chunks of audio messages

本例子,每条消息的大小固定,块类型顺序为0233,印证了类型0、2、3的使用场合:即每条块流应从类型0的块开始,对于固定长度的消息,第二条消息的第一个块应用类型2,对于流中所有消息的大小、流id、时间戳增量都相等时,类型2之后的所有块都用类型3

类型3的应用场合是:
一条消息被切分成许多块时,第一个块之后的所有块都使用类型3

对于流中所有消息的大小、流id、时间戳增量都相等时,类型2之后的所有块都用类型3

类型2的块在某场合可以被类型3替代,就是当时间戳增量等于类型0块的时间戳时。

示例2

本示例展示了一条长消息,由于消息的长度超过了块的最大长度(128字节),此消息在传输时将被分割成若干个块。

  1. +-----------+-------------------+-----------------+-----------------+
  2. | | Message Stream ID | Message Type ID | Time | Length |
  3. +-----------+-------------------+-----------------+-----------------+
  4. | Msg # 1 | 12346 | 9 (video) | 1000 | 307 |
  5. +-----------+-------------------+-----------------+-----------------+
  6. Sample Message to be broken to chunks

下面是消息分割后产生的块:

  1. +-------+------+-----+-------------+-----------+------------+
  2. | |Chunk |Chunk|Header |No. of |Total No. of|
  3. | |Stream| Type|Data |Bytes after| bytes in |
  4. | | ID | | | Header | the chunk |
  5. +-------+------+-----+-------------+-----------+------------+
  6. |Chunk#1| 4 | 0 | delta: 1000 | 128 | 140 |
  7. | | | | length: 307 | | |
  8. | | | | type: 9, | | |
  9. | | | | stream ID: | | |
  10. | | | | 12346 (11 | | |
  11. | | | | bytes) | | |
  12. +-------+------+-----+-------------+-----------+------------+
  13. |Chunk#2| 4 | 3 | none (0 | 128 | 129 |
  14. | | | | bytes) | | |
  15. +-------+------+-----+-------------+-----------+------------+
  16. |Chunk#3| 4 | 3 | none (0 | 51 | 52 |
  17. | | | | bytes) | | |
  18. +-------+------+-----+-------------+-----------+------------+
  19. Format of each of the chunks

第一个块的头数据显示了消息的长度为307字节。

在这两个示例中,类型为3的块有两种使用方式。第一种是说明消息的继续。第二种是说明新消息的头信息可以由前面已经存在的消息推导出来。

5.4.Protocol Control Messages

RTMP Chunk Stream使用消息类型ID 1,2,3,5,和6来做协议控制消息。这些消息包含了RTMP Chunk Stream协议所需的信息。

这些协议控制消息必须有message stream ID 0(被称为控制流),并以chunk stream ID 2被发送。协议控制信息一经被收到后立马生效,它们的时间戳是被忽略的。

5.4.1.Set Chunk Size (1)

协议控制消息1,Set Chunk Size,用于通知对端一个新的最大chunk大小。

最大chunk大小默认为128字节,但客户端或服务器可以更改此值,并使用此消息更新其对端的状态。例如,假设一个客户端要发送131个字节的音频数 据,并且chunk大小为128字节。在这种情况下,客户端可以将此消息发送到服务器,通知它,chunk大小现在为131个字节。然后,客户端可以在一 个chunk中发送该音频数据。

最大chunk大小应该至少有128个字节,并且必须至少是1个字节。最大chunk大小是独立于每个方向保持的。

\

0:这个位必须是0。

chunk size(31位):此字段保存了新的最大chunk大小,以字节为单位,它将用于所有的发送端的后续chunk,直到进一步通知。有效大小为1到 2147483647(0x7FFFFFFF),包括2147483647本身;然而,所有大小大于16777215(0xFFFFFF)是等价的,因为 没有chunk大于一个消息,没有消息大于16777215个字节。

5.4.2.Abort Message (2)

协议控制消息2,Abort Message,用于通知对端,如果对端在等待chunk完成一个消息,就让它丢弃在chunk stream上收到的部分消息。简单地理解就是告诉对端我不继续发送块了,请你把之前在这个块流上收到的信息丢到吧。对端接收chunk stream ID作为该协议消息的payload。应用程序可以在关闭时发送此消息,以指示该消息不需要再进一步处理。

\

chunk stream ID(32位):这个字段保存了当前将被丢弃消息的chunk stream ID。

5.4.3.Acknowledgement (3)

客户端或服务器必须在接收到等于窗口大小的字节后必须向对端发送一个acknowledgment确认。窗口大小是发送端没有从接收端接收到确认前发送的最大字节数。此消息指定了序列号,它是到目前为止接收到的字节数。

\

sequence number(32个字节):这个字段保存到目前为止接收到的字节数。

5.4.4.Window Acknowledgement Size (5)

客 户端或服务器发送此消息通知对端发送Acknowledgement之间的窗口大小。发送端希望在它发送了窗口大小的字节数之后,从对端收到 Acknowledgement。自上次Acknowledgement发送后,接收端接收指定的字节数后必须发送一个 Acknowledgement(第5.4.3节),否则如果Acknowledgement还没有被发送,会话得从新开始。

The client or the server sends this message to inform the peer of the window size to use between sending acknowledgments. The sender expects acknowledgment from its peer after the sender sends window size bytes. The receiving peer MUST send an Acknowledgement (Section 5.4.3) after receiving the indicated number of bytes since the last Acknowledgement was sent, or from the beginning of the session if no Acknowledgement has yet been sent.

\

5.4.5.Set Peer Bandwidth (6)

客户端或服务器发送此消息限制对端的输出带宽。对端接收到这条消息后,会限制它自己的已经发送但还未得到确认的数据总量最大为消息中的窗口大小,以此来限制自己的输出带框。对端收到此消息后,如果窗口大小与发送端上次发送该消息的不一样,它应该应答一个Window Acknowledgement Size的消息。

The peer receiving this message SHOULD respond with a Window Acknowledgement Size message if the window size is different from the last one sent to the sender of this message.

\

Limit Type是下列值之一:

0 - Hard:对端应该限制它的输出带宽为指定的窗口大小。

1 - Soft:对端应该限制它的输出带宽为此消息指定的窗口大小或者直到该限制已经有效果为止,无论哪一个的窗口更小

2 - Dynamic:如果前一个是硬限制类型,那么此消息被认为是硬限制类型,否则忽略此消息。

6.RTMP Message Formats

此节说明RTMP消息的格式。它基于使用更低级的传输层网络在实体之间传输,例如RTMP Chunk Stream。

虽然RTMP被设计于用RTMP Chunk Stream工作,但它可以使用任何其他的传输协议发送消息。RTMP Chunk Stream和RTMP是适合多种音视频应用,包括一对一和一对多的直播点播服务,交互式视频会议应用。

6.1.RTMP Message Format

服务器和客户端通过网络发送RTMP消息来彼此通信。该消息可以包括音频、视频、数据或任何其他消息。

RTMP消息有两部分,一个头部和有效载荷。

6.1.1.Message Header

消息头包含以下部分:

Message Type:1个字节字段以表示消息类型。范围1-6的ID类型被保留用作协议控制消息(前面有叙述)。

Length:3个字节的字段,表示payload的字节大小。它被制定为大端格式。

Timestamp:4个字节的字段,它包含该消息的时间戳。这4个字节以大端字节序封包。

Message Stream Id:3个字节的字段,标识消息流。这几个字节被制定为大端格式。

\

6.1.2.Message Payload

消息的另一部分是有效负载payload,这是消息中包含的实际数据。例如,它可以是一些音频采样或压缩的视频数据。payload的格式和解释超出了本文档的范围。

6.2.User Control Messages (4)

RTMP使用ID为4的消息类型作为用户控制消息。这些消息包含了RTMP streaming层使用到的信息。ID 1,2,3,5和6的协议消息被用于RTMP Chunk Stream协议(第5.4节)。

用户控制信息应该使用message stream ID 0(称为控制流),当基于RTMP Chunk Stream发送时,它被用chunk stream ID 2来发送。用户控制信息在流中被收到时立马生效;它们的时间戳被忽略。

客户端或服务器发送此消息以通知对端用户控制事件。此消息传输事件类型Event type和事件数据Event data。

\

前2个字节的消息数据用于标识事件类型。事件类型之后就是事件数据。事件数据字段的大小是可变的。然而,在消息通过RTMP Chunk Stream层传输的情况下,最大chunk大小(第5.4.1节)应该足够大,以让这些消息放在一个chunk中。

事件类型和事件数据在第7.1.7节中列举。

7.RTMP Command Messages

本节介绍了服务器和客户端之间相互通信的不同类型的消息和命令。

服务器和客户端之间交换的不同类型的消息包括用于发送音频数据的音频消息audio messages、用于发送视频数据的视频消息video messages、用于发送任何用户数据的数据消息data messages、共享对象消息shared object messages和命令消息command messages。共享对象消息提供了一个通用的方式来管理多个客户端和一个服务器之间的分布式数据。命令消息携带在客户端和服务器之间的AMF编码命 令。客户端或服务器可以使用命令消息请求基于流的远程过程调用(RPC)与对方交流。

7.1.Types of Messages

服务器和客户端通过网络上的消息互相通信。该消息可以是任何类型,其中包括音频消息、视频消息、命令消息、共享对象消息、数据消息和用户控制消息。

7.1.1.Command Message (20, 17)

命令消息携带客户端和服务器之间的AMF编码命令。这些消息的消息类型值为20(AMF0编码)或者17(AMF3编码)。这些消息被发送到对端执行一些 操作,如连接,创建流、发布、播放,暂停。命令消息类型像状态和结果,被用来通知发送者的请求命令的状态。命令消息由命令名称command name、事务标识transaction ID和包含相关参数的命令对象command object组成。客户端或服务器可以使用命令消息请求基于流的远程过程调用(RPC)与对方交流。

7.1.2.Data Message (18, 15)

客户端或服务器通过此消息发送元数据或任何用户数据给对端。元数据包括数据(音频,视频等)的详细信息,如创建时间,持续时间,主题等。这些消息为被分配消息类型值18(AMF0)和15(AMF3)。

7.1.3.Shared Object Message (19, 16)

共享对象是一个在跨多个客户端,实例等同步的Flash对象(名称-值对的集合)。消息类型19(对于AMF0)和16(对于AMF3)被保留用作共享对象事件shared object events。每个消息可以包含多个事件。

\

下面是支持的事件类型:

事件

描述

Use(=1)

客户端发送此事件通知服务器一个共享对象的创建。The client sends this event to inform the server |about the creation of a named shared object.

Release(=2)

当在客户端删除共享对象时,客户端将此事件发送到服务器上。

Request Change(=3)

客户端发送此事件以请求更改与共享对象命名参数关联的值。

Change (=4)

服务器发送此事件通知除了正在发起请求之外的所有客户端命名参数值的变化。

Success (=5)

如果请求被受理,服务器发送此事件给正在请求的客户端,以响应RequestChange事件。

SendMessage(=6)

客户端将此事件发送到服务器以广播一个消息。在接收此事件时,服务器向所有客户端广播消息,包括该发送者。

Status (=7)

服务器发送此事件以通知客户端相关的错误状况。

Clear (=8)

服务器将此事件发送给客户端,以清除共享对象。服务器还发送此事件以响应客户端在连接时发送Use event。

Remove (=9)

服务器发送此事件让客户端删除一个插槽。

Request Remove(=10)

客户端发送此事件让服务器删除一个插槽。

Use Success(=11)

成功连接时,服务器将此事件发送给客户端。

 

7.1.4.Audio Message (8)

客户端或服务器通过发送此消息发送音频数据到对端。消息类型值8被保留用作音频消息。

7.1.5.Video Message (9)

客户端或服务器通过发送此消息发送视频数据到对端。消息类型值9被保留用作视频消息。

7.1.6.Aggregate Message (22)

一个聚合消息是一个单一的包含了一系列RTMP子消息的消息,它们的格式在6.1节描述。消息类型22用于聚合消息。

\

聚合消息的stream ID覆盖(override)聚合体内子消息的stream ID。

聚合消息和第一子消息的时间戳之间的差异是时间偏移,它被用于将子消息的时间戳归一化成流的时标。偏移量被加到每个子消息的时间戳,以变成归一化后的流时间。第一次子消息的时间戳应该与聚合消息的时间戳相同,所以偏移量应为0。

后面的指针包含在它头部中前一消息的大小。它被包含以匹配FLV文件格式并用于向后定位。

使用聚合消息有几个性能优势:

lchunk stream可以在一个chunk中至多发送一个完整的消息。因此,增加chunk的大小并且使用聚合消息减少了chunk发送的数量。

l子消息可以连续存储在内存中。当产生系统调用在网络上发送数据时,它更有效的。

7.1.7.User Control Message Events

客户端和服务器发送此消息把用户控制事件通知对端。有关该消息的格式,请参考第6.2节。

以下是支持的用户控制事件:

事件

描述

Stream Begin (=0)

服务器发送此事件通知客户端流已成为功能,并可用于通信。默认情况下,在从客户端成功收到应用程序连接命令后,此事件以ID 0来发送。

Stream EOF(=1)

服务器发送此事件通知客户端该流请求的数据回放结束了。若不发出额外的命令,就没有更多的数据被发送了。客户端丢弃该流收到的消息。该4个字节的事件数据代表哪个stream ID的流播放已结束。

StreamDry(=2)

服务器发送此事件通知客户端,在流上没有更多的数据。如果服务器在一段时间内没有检测到任何的消息,它可以通知订阅的客户端流是结束的。该4个字节的事件数据代表结束流的stream ID。

SetBufferLength (=3)

客户端发送此事件通知服务器用于缓冲从流过来的任何数据的缓冲区大小(以毫秒为单位)。此事件在服务器开始处理流之前被发送。事件数据的前4个字节表示stream ID,而下面4个字节代表每毫秒缓冲区的长度。

StreamIsRecorded (=4)

服务器发送此事件来通知客户端,该流是一个记录流。4个字节的事件数据代表记录流的stream ID。

PingRequest(=6)

服务器发送此事件来测试客户端是否是可到达的。事件数据是4个字节的时间戳,代表当服务器发出命令时,本地服务器的时间。客户收到PingRequest时响应PingResponse。

PingResponse(=7)

客户端向服务器发送此事件响应ping请求。事件数据是当收到PingRequest时的4个字节时间戳。

 

7.2.Types of Commands

客户端和服务器交换AMF编码的命令。发送者发送一个命令消息,该命令消息包含命令名称command name、事务标识transaction ID和包含相关参数的命令对象command object。例如,连接命令包含“app”参数,它告诉服务器该客户端要连接的应用程序名称。接收端处理命令并使用相同的transaction ID回应。响应字符串要么是_result,要么是_error,要么是方法名method name,例如,方法名是verifyClient或contactExternalServer。

命令字符串_result或_error表示一个应答。transaction ID指示应答相关的重要命令。它与在IMAP等多种协议的标签tag是相同的。命令字符串中的方法名表示发送端正在试图在接收端运行一个方法。

下面类型的对象用于发送各种命令:

NetConnection:一个服务器和客户端之间的连接的一个更高级别表示的对象。

NetStream:一个表示发送音频流、视频流和其他相关数据的信道对象。我们还发送如播放,暂停等控制数据流的命令。

7.2.1.NetConnection Commands

NetConnection管理客户端和服务器之间的双向连接。此外,它为异步远程方法调用提供了支持。

下面的命令可以在NetConnection上发送:

connect

call

close

createStream

7.2.1.1.Connect

客户端向服务器发送连接命令,请求连接到服务器应用程序实例。

从客户端到服务器的命令结构如下:

字段名

类型

描述

Command Name

String

命令名称,设置为“connect”。

Transaction ID

Number

通常设置为1。

Command Object

Object

包含名称-值对的命令对象。

Optional User Arguments

Object

任何可选的信息。

 

以下是连接命令对象中使用的名称-值对的描述:

属性

类型

描述

样例值

app

String

客户端要连接的服务器应用程序名

testapp

flashver

String

Flash Player的版本号。它与ApplicationScript中getversion()方法返回的字符串是一样的。

FMSc/1.0

swfUrl

String

发起连接的SWF源文件的URL

file://C:/FlvPlayer.swf

tcUrl

String

服务器URL。它具有以下的格式。

protocol://servername:port/appName/appInstance

 

rtmp://localhost:1935/testapp/instance1

fpad

Boolean

如果使用代理,值为true。

true或者false

audioCodecs

Number

指示客户端支持的音频编码。

SUPPORT_SND_MP3

videoCodecs

Number

指示支持什么视频编码。

SUPPORT_VID_SORENSON

videoFunction

Number

指示支持什么专用的视频方法。

SUPPORT_VID_CLIENT_SEEK

pageUrl

String

SWF文件从哪里加载的网页URL。

http://somehost/sample.html

objectEncoding

Number

AMF编码方法。

AMF3

 

audioCodecs属性的标志值:

编解码标志

用法

SUPPORT_SND_NONE

原始音频,无压缩

0x0001

SUPPORT_SND_ADPCM

ADPCM压缩

0x0002

SUPPORT_SND_MP3

mp3压缩

0x0004

SUPPORT_SND_INTEL

未使用

0x0008

SUPPORT_SND_UNUSED

未使用

0x0010

SUPPORT_SND_NELLY8

8-kHz采样速率的NellyMoser压缩

0x0020

SUPPORT_SND_NELLY

NellyMoser压缩(5,11,22,和44 kHz采样速率)

0x0040

SUPPORT_SND_G711A

G711A声音压缩(仅Flash Media Server支持)

0x0080

SUPPORT_SND_G711U

G711U声音压缩(仅Flash Media Server支持)

0x0100

SUPPORT_SND_NELLY16

16-kHz采样速率的NellyMoser压缩

0x0200

SUPPORT_SND_AAC

高级音频编码(AAC)编解码

0x0400

SUPPORT_SND_SPEEX

Speex音频

0x0800

SUPPORT_SND_ALL

所有RTMP支持的音频编解码

0x0FFF

 

videoCodecs属性的标志值:

编解码标志

用法

SUPPORT_VID_UNUSED

废弃值

0x0001

SUPPORT_VID_JPEG

废弃值

0x0002

SUPPORT_VID_SORENSON

Sorenson Flash视频

0x0004

SUPPORT_VID_HOMEBREW

V1屏幕共享

0x0008

SUPPORT_VID_VP6 (On2)

On2视频(Flash 8+)

0x0010

SUPPORT_VID_VP6ALPHA(On2 with alphachannel)

带有alpha通道的On2视频

0x0020

SUPPORT_VID_HOMEBREWV(screensharing v2)

屏幕共享版本2(Flash 8+)

0x0040

SUPPORT_VID_H264

H264视频

0x0080

SUPPORT_VID_ALL

所有的RTMP支持的视频编解码

0x00FF

 

videoFunction属性的标志值:

方法标志

用法

SUPPORT_VID_CLIENT_SEEK

指示客户端可以进行帧精确控制查找

1

 

对象编码属性的值:

编码类型

用法

AMF0

AMF0对象编码,Flash 6及更新版本支持

0

AMF1

来自于Flash 9的AMF3编码(AS3)

3

 

从服务器到客户端的命令结构如下所示:

字段名

类型

描述

Command Name

String

_result或者_error;表示响应是否为结果或者错误。

Transaction ID

Number

对于connect的应答来说,Transaction ID是1

Properties

Object

该连接的描述属性(fmsver等)的名称-值对。

Information

Object

来自于服务器的描述应答的名称-值对。“code”,“level”,“description”是这些信息的几个名称。

 

\

在该命令执行期间的消息流程是:

1.客户端发送connect命令给服务器,请求与服务器应用程序实例连接。

2.在收到connect命令之后,服务器给客户端发送“Window Acknowledgement Size”的协议消息。服务器也会连接connect命令提到的应用程序。

3.服务器给客户端发送“Set Peer Bandwidth”协议消息。

4.在处理“Set Peer Bandwidth”协议消息之后,客户端给服务器发送“Window Acknowledgement Size”的协议消息。

5.服务器给客户端发送另一个类型的协议消息,用户控制消息(StreamBegin)。

6.服务器发送result命令消息通知客户端该连接的状态(成功或失败)。该命令指定transaction ID(对于connect命令来说,它总是等于1)。该消息也会指定一些属性,如Flash Media Server的版本(字符串)。另外,它指定了其他连接应答相关的信息,比如级别(字符串),编码(字符串),描述(字符串),对象编码(数字)等。


7.2.1.2.Call

NetConnection对象的调用方法在接收端运行远程过程调用(RPC)。RPC调用的名称作为该call命令的参数传递。

发送端到接收端的命令结构如下所示:

字符名

类型

描述

Procedure Name

String

远程过程调用的名称。

Transaction ID

Number

如果需要一个应答,我们给出一个transaction Id。否则,我们传一个0的值。

Command Object

Object

如果存在任何的命令信息,它就会被设置,否则,它被设置为null类型。

Optional Arguments

Object

任何将要提供的可选参数。

 

应答的命令结构如下所示:

字段名

类型

描述

Command Name

String

命令名称。

Transaction ID

Number

命令的ID,标识它属于哪个应答的。

Command Object

Object

如果存在任何的命令信息,它就会被设置,否则,它被设置为null类型。

Response

Object

该调用方法的应答。

 

7.2.1.3.createStream

客户端发送命令到服务器创建消息通信的逻辑通道。发布的音频、视频和元数据通过使用createStream命令创建的流通道传输。

NetConnection是默认的通信通道,它的stream ID为0。协议和少量命令消息,包括createStream,使用该默认的通信通道。

客户端到服务端的命令结构如下所示:

字段名

类型

描述

Command Name

String

命令名称。设置为“createStream”。

Transaction ID

Number

命令的会话ID。

Command Object

Object

如果存在任何的命令信息,它就会被设置,否则,它被设置为null类型。

 

服务器到客户端的命令结构如下所示:

字段名

类型

描述

Command Name

String

_result或者_error;表示响应是否为结果或者错误。

Transaction ID

Number

命令的ID,标识它属于哪个应答的。

Command Object

Object

如果存在任何的命令信息,它就会被设置,否则,它被设置为null类型。

Stream ID

Number

该返回值要么是stream ID,要么是一个错误信息对象。

 

7.2.2.NetStream Commands

NetStream定义了音频,视频和数据信息流可以在连通客户端到服务器的NetConnection上流通的通道。一个NetConnection对象可以支持多种数据流的多个NetStream。

以下命令可以在NetStream上从客户端发送到服务器:

play

play2

deleteStream

closeStream

receiveAudio

receiveVideo

publish

seek

pause

 

服务器使用“onStatus”命令给客户端发送NetStream状态更新:

字段名

类型

描述

Command Name

String

命令名“onStatus”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

onStatus消息没有命令对象。

Info Object

Object

一个AMF对象至少有下面3种属性:

“level”(字符串):此消息的级别,“warning”、“status”或者“error”其中之一;

“code”(字符串):消息代码,比如“NetStream.Play.Start”;

“description”(字符串):一个可读的消息描述。

Info对象可能包含其他适合编码的属性。

 

7.2.2.1.play

客户端发送此命令让服务器播放流。也可以多次使用此命令创建一个播放列表。

如果你想创建一个在不同直播或录制流之间切换的动态播放列表,多次调用play并将reset字段设置为false。相反,如果你想立即播放指定的流,将reset字段设置为true。

从客户端到服务端的命令结构如下所示:


字段名

类型

描述

Command Name

String

命令的名称,设置为“play”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Stream Name

String

待播放流的名称。如果要播放视频(FLV)文件,指定不带文件扩展名(比如,“sample”)的流名称。如果要回放MP3或者ID3标签,你必须要在流 名称前加上mp3:的前缀(比如,“mp3:sample”)。如果要播放H.264/AAC文件,你必须在流名称前加上mp4:的前缀,并且指定文件名 后缀。比如,要播放文件sample.m4v,指定“mp4:sample.m4v”。

Start

Number

一个可选的参数,指定以秒为单位的起始时间。默认值为-2,意思是,该订阅者第一次尝试播放流名称字段指定的直播流。如果该名称的直播流没有被找到,它会 播放同名的录播流。如果没有该名称的录播流,该订阅者等到该名称的直播流可用时再播放。如果你在Start字段中传递-1,那么只有流名称字段指定的直播 流才会被播放。如果你在Start字段传递0或者一个正数,一个在流名称字段指定的录播流会被播放,起始时间是Start字段指定的时间。如果找不到该记 录流,播放列表的下一个条目会被播放。

Duration

Number

一个可选的参数,指定以秒为单位的回放时间段。默认值是-1。-1值意味着,直播流被播放直到它不可用,或者录播流被播放直到结束。如果你传递0,它会播 放录播流中Start字段指定开始时间的一帧。它假定Start字段的值大于等于0。如果你指定一个正数,它会播放Duration字段指定该段时间的直 播流。之后,它能够播放Duration字段指定该段时间的录播流。(如果流在Duration字段指定的时间前结束,回放随着流的结束而结束。)如果你 传递一个不同于-1的负数,它会把该值解析为-1。

Reset

Boolean

一个可选的布尔值或数字,它指定是否清除之前的所有播放列表。

\

 

在该命令执行期间的消息流程是:

1.客户端在收到来自于服务器的createStream命令的成功结果后,发送play命令。

2.在收到play命令的同时,服务器发送一个协议消息设置chunk的大小。

3.服务器发送另一个协议消息(用户控制)指定“StreamIsRecorded”的事件,并且在该消息中带上stream ID。该消息在头2个字节带上事件类型并且在最后4个字节包含stream ID。

4.服务器发送另一个协议消息(用户控制)指定“StreamBegin”事件,以向客户端指示流的开始。

5.如果客户端发送的play命令成功的话,该服务器发送一个onStatus的命令消息NetStream.Play.Start和 NetStream.Play.Reset。只有客户端发送的play命令带有reset标志时,服务器才会发送 NetStream.Play.Reset。如果要播放的流没在被找到,服务器发送NetStream.Play.StreamNotFound的 onStatus消息回应。

 

之后,服务器发送在客户端播放的音频和视频数据。

7.2.2.2.play2

不同于play命令,play2可以切换一个不同的码率而不改变播放内容的时间轴。服务器为客户端可以在play2中请求的所有支持的码率维护多个字段。

客户端到服务器的命令结构如下:

字段名

类型

描述

Command Name

String

命令的名称,设置为“play2”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Parameters

Object

一个AMF编码的对象,它的属性是描述flash.net.NetStreamPlayOptions ActionScript对象的公有属性。

NetStreamPlayOptions对象的公共属性在ActionScript 3语言参考手册中描述。

该命令的消息流程如下图所示:

\

 

7.2.2.3.deleteStream

当NetStream对象将要被销毁时,它发送该deleteStream命令。

客户端到服务器的命令结构如下所示:

字段名

类型

描述

Command Name

String

命令的名称,设置为“deleteStream”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Stream ID

Number

在服务器上销毁流的ID。

该服务器不需要发送任何应答。

7.2.2.4.receiveAudio

NetStream发送receiveAudio消息通知服务器是否发送或不发送音频到客户端。

客户端到服务端的命令结构如下:

字段名

类型

描述

Command Name

String

命令的名称,设置为“receiveAudio”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Bool Flag

Boolean

true或者false表示是否接收音频。

如果receiveAudio命令发送带有flase的bool flag,服务器不发送任何响应。如果这个标志被设置为true,服务器应答NetStream.Seek.Notify和NetStream.Play.Start的状态消息。

7.2.2.5.receiveVideo

NetStream发送receiveVideo消息通知服务器是否发送或不发送视频到客户端。

客户端到服务端的命令结构如下:

字段名

类型

描述

Command Name

String

命令的名称,设置为“receiveVideo”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Bool Flag

Boolean

true或者false表示是否接收视频。

如果receiveVideo命令发送带有flase的bool flag,服务器不发送任何响应。如果这个标志被设置为true,服务器应答NetStream.Seek.Notify和NetStream.Play.Start的状态消息。

7.2.2.6.publish

客户端发送publish命令将已命名的流发布到服务器上。使用这个名称,任何客户端都可以播放此流,并接收已发布的音频、视频和数据消息。

客户端到服务端的命令结构如下:

字段名

类型

描述

Command Name

String

命令的名称,设置为“publish”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

PublishingName

String

发布流的名称。

PublishingType

String

发布的类型。设置为“live”,“record”或者“append”。

record:该流已被发布并且数据被记录到一个新的文件。该文件存储在服务器的一个包含服务器应用程序目录的子目录。如果文件已经存在,则它被覆盖。

append:流已经被发布,并且该数据被追加到一个文件。如果找不到文件,则创建它。

live:直播数据被发布,而没有记录到文件。

服务器应答onStatus命令,以标记发布的开始。

7.2.2.7.seek

客户端发送seek命令以定位媒体文件内或者播放列表的某个位置(以毫秒为单位)。

客户端到服务端的命令结构如下:

字段名

类型

描述

Command Name

String

命令的名称,设置为“seek”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

milliSeconds

Number

定位到播放列表的毫秒数。

当定位成功,服务器发送NetStream.Seek.Notify的状态消息。失败的时候,它返回一个_error的消息。

7.2.2.8.pause

客户端发送pause命令以告诉服务器暂停或者开始播放。

客户端到服务端的命令结构如下:


字段名

 型

     描述

Command Name

String

命令的名称,设置为“pause”。

Transaction ID

Number

Transaction ID设置为0。

Command Object

Null

不存在命令信息,设置为null类型。

Pause/Unpause Flag

Boolean

true或者false,以表示暂停或者恢复播放。

milliSeconds

Number

毫秒数,在流的哪个位置被暂停或者恢复播放的。当流被暂停,这就是客户端的当前流的时间。当播放被恢复,该服务器仅会发送带有大于此值的时间戳的消息。

当流被暂停,服务器发送一个NetStream.Pause.Notify的状态消息。当一个流变成未暂停状态,NetStream.Unpause.Notify被发送。失败的时候,它返回一个_error的消息。

7.3.Message Exchange Examples

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

7.3.1.Publish Recorded Video

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

\

7.3.2.Broadcast a Shared Object Message

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

\

7.3.3.Publish Metadata from Recorded Stream

这个例子描述了发布元数据的消息交换。

\

8.References

[RFC0791] Postel, J., "Internet Protocol", STD 5, RFC 791,

September 1981.

[RFC0793] Postel, J., "Transmission Control Protocol", STD 7,

RFC 793, September 1981.

[RFC1982] Elz, R. and R. Bush, "Serial Number Arithmetic", RFC 1982,

August 1996.

[RFC2119] Bradner, S., "Key words for use in RFCs to Indicate

Requirement Levels", BCP 14, RFC 2119, March 1997.

[AS3] Adobe Systems, Inc., "ActionScript 3.0 Reference for the

Adobe Flash Platform", 2011,

actionscript/documentation.html>.

[AMF0] Adobe Systems, Inc., "Action Message Format -- AMF 0",

December 2007,

attachments/1114283/amf0_spec_121207.pdf>.

[AMF3] Adobe Systems, Inc., "Action Message Format -- AMF 3",

May 2008,

attachments/1114283/amf3_spec_05_05_08.pdf>.

 

 

Authors’ Addresses

Hardeep Singh Parmar (editor)

Adobe Systems Incorporated

345 Park Ave

San Jose, CA 95110-2704

US

Phone: +1 408 536 6000

Email: hparmar@adobe.com

URI: http://www.adobe.com/

 

Michael C. Thornburgh (editor)

Adobe Systems Incorporated

345 Park Ave

San Jose, CA 95110-2704

US

Phone: +1 408 536 6000

Email: mthornbu@adobe.com

URI: http://www.adobe.com/


首先来看一下TCP报文的首部。

tcp首部
各字段介绍:
1、源端口:是应用层进程所使用的端口,16位2、目的端口:通信另一端的进程所使用的端口,16位3、序号:发送分组时,将报文按字节编号,报文的第一个字节的序号就是序号,每一次发送报文的序号时由双方决定,也就是发送方报文的第一个字节序号。32位4、确认序号:是接收方期望发送方下一个报文的字节序号,假如A向B发送了一个报文,序号是x,如果B收到了这个报文,那么B发送了一个确认报文给A,将确认序号设置为x+1,表示前x个字节的数据我已经收到,你下次给我发从x+1开始的数据。5、首部长度:注意单位为4字节,所以可表示15*4=60字节的首部。一般首部长度为20字节。6、保留:6bit, 均为0
下面是6个标志位。7、紧急URG:当URG=1时,表示报文段中有紧急数据,应尽快传送。8、确认比特ACK:ACK = 1时代表这是一个确认的TCP包,取值0则不是确认包。9、 推送比特PSH:当发送端PSH=1时,接收端尽快的交付给应用进程。10、 复位比特(RST):当RST=1时,表明TCP连接中出现严重差错,必须释放连接,再重新建立连接。11、同步比特SYN:在建立连接是用来同步序号。SYN=1, ACK=0表示一个连接请求报文段。SYN=1,ACK=1表示同意建立连接。12、 终止比特FIN:FIN=1时,表明此报文段的发送端的数据已经发送完毕,并要求释放传输连接。13、 窗口:用来控制对方发送的数据量。16位。14、 检验和:该字段检验的范围包括首部和数据这两部分。由发端计算和存储,并由收端进行验证。15、紧急指针:紧急指针在URG=1时才有效,它指出本报文段中的紧急数据的字节数。16、 选项:长度可变,最长可达40字节

2月起,科技园有种学历叫免考本科,一年课程,正规本科,学信网可查 爱华学历中心 · 顶新
2月起,科技园有种学历叫免考本科,一年课程,正规本科,学信网可查 爱华学历中心 · 顶新
zwg流泪关注
zwg流泪

87篇文章

排名:千里之外

NoahDSun关注
NoahDSun

8篇文章

排名:千里之外

边缘998关注
边缘998

195篇文章

排名:2000+

weixin_34109408关注
weixin_34109408

0篇文章

排名:千里之外

一个长期喝土蜂蜜的人,竟然变成了这样,再忙也要看一下 顺端 · 鹓鶵
千万不要再乱喝蜂蜜了,记者亲赴深山,揭露背后惊人黑幕! 树米 · 鹓鶵
科技园25岁美女手机做这个,1年存款吓呆父母!! 赛尔网络科技 · 鹓鶵
一个长期喝土蜂蜜的人,竟然变成了这样,再忙也要看一下 科技 · 鹓鶵
一个长期喝土蜂蜜的人,后来竟然会变成这样?看到一定要告诉家人 热点 · 顶新
千万不要再乱喝蜂蜜了,美女潜伏工厂,揭露蜂蜜市场黑幕! 新钥 · 顶新

没有更多推荐了,返回首页

猿学习

不良信息举报

rtmp协议规范详解 - HandsomeHong的博客

最多只允许输入30个字