流媒体协议--2:RTMP

"本文转载自:[^一二三^]的流媒体协议之RTMP详解"

1.概述

  RTMP(Real Time Messaging Protocol)实时消息传输协议是Adobe公司提出得一种媒体流传输协议,其提供了一个双向的通道消息服务,意图在通信端之间传递带有时间信息的视频、音频和数据消息流。其通过对不同类型得消息分配不同得优先级,进而在网传能力限制下确定各种消息得传输次序。

  RTMP最早是Adobe公司基于flash player播放器提出得一种音视频封装传输格式,在前期flash盛行时,得到了极其广泛得应用,当前flash基本被废弃,但是RTMP这种协议作为流媒体封装传输得方式,并没有预想中被冷落得情况,相反,在当下直播盛行得阶段,RTMP被经常用来向云端推流得流媒体协议。

  RTMP是TCP/IP协议模型中的应用层协议,其工作在TCP之上,默认端口为1935,RTMP协议是基于TCP协议进行传输,因此其需要TCP特性来保证消息传输的可靠性。TCP通过三次握手成功建立连接后,RTMP协议还需要客户端和服务端通过RTMP握手协议来建立RTMP Connection,RTMP握手协议主要目的是协商RTMP版本及时间对齐作用。RTMP Connection上会传输RTMP控制信息,比SetChunkSize,SetACKWindowSize,CreateStream等,其中CreateStream命令会创建一个Stream链接,用于传输具体的音视频数据和控制这些信息传输的命令信息。

  RTMP协议以RTMP Message格式传输,为了更好地实现多路复用、分包和信息的公平性,发送端把Message划分为带有MessageID的Chunk,每个Chunk可能是一个单独的Message,也可能是Message的一部分,在接受端会根据chunk中包含的data的长度,messageid和message的长度把chunk还原成完整的Message,从而实现信息的收发。

2.RTMP交互过程

2.1 握手协议

2.1.1 过程解析

  RTMP在建立好传输层TCP连接后,通过RTMP握手协议来完成RTMP的连接,RTMP握手协议由三个固定长度的块组成,客户端和服务端各发送相同的三个块,客户端发送C0、C1、C2,服务端发送S0、S1、S2,RTMP规范中没有详细规定各个块发送的顺序,只需要满足如下条件即可:

  • 握手以客户端发送 C0 和 C1 块开始;

  • 客户端必须等待接收到 S1 才能发送 C2;

  • 客户端必须等待接收到 S2 才能发送任何其他数据;

  • 服务器必须等待接收到 C0 才能发送S0和S1,也可能是接收到 C1 后发送;

  • 服务器必须等待接收到 C1 才能发送S2;

  • 服务器必须等待接收到 C2 才能发送其他数据。

  其握手示意图如下图所示:

+-------------+                           +-------------+
|    Client   |       TCP/IP Network      |    Server   |
+-------------+            |              +-------------+
      |                    |                     |
Uninitialized              |               Uninitialized
      |          C0        |                     |
      |------------------->|         C0          |
      |                    |-------------------->|
      |          C1        |                     |
      |------------------->|         S0          |
      |                    |<--------------------|
      |                    |         S1          |
 Version sent              |<--------------------|
      |          S0        |                     |
      |<-------------------|                     |
      |          S1        |                     |
      |<-------------------|                Version sent
      |                    |         C1          |
      |                    |-------------------->|
      |          C2        |                     |
      |------------------->|         S2          |
      |                    |<--------------------|
   Ack sent                |                  Ack Sent
      |          S2        |                     |
      |<-------------------|                     |
      |                    |         C2          |
      |                    |-------------------->|
 Handshake Done            |               Handshake Done
      |                    |                     |
          Pictorial Representation of Handshake
                     握手示意图

  客户端发送C0C1之前客户端和服务器都处于未初始化状态;在未初始化状态之后客户端和服务端都进入版本已发送状态,客户端等待接收 S1 包,服务端等待接收 C1 包,收到所等待的包后,客户端发送 C2 包,服务端发送 S2 包。之后状态进入发送确认状态;客户端和服务端等待接收S2和C2包,收到后进入握手完成状态,客户端和服务端开始交换消息。

  从规范上看只要满足以上条件,如何发送6个块的顺序都是可以的,但实际实现中为了在保证握手的身份验证功能的基础上尽量减少通信的次数,一般的发送顺序如下:

  • 客户端向服务端同时发送C0+C1;

  • 服务端确认版本号后,向客户端同时发送S0+S1+S2;

  • 客户端接收到S2后发送C2到服务端。

| client | Server |
|------C0+C1----->|
|<---S0+S1+S2-----|
|-------C2------->|

握手过程抓包

image.png

2.1.2 格式解析

(1)C0/S0格式

  C0和S0包都是单一的一个字节(8位),表示版本号,C0包中表示客户端请求的RTMP版本,在S0中表示服务器选择的RTMP版本,规范最新定义为3,如果服务器和客户端版本号不一致,则可能会终止交互或者降级。

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

(2)C1/S1格式

  C1/S1长度为1536字节,其格式如下:

 0                   1                   2                   3
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        time (4 bytes)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        zero (4 bytes)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        random bytes                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         random bytes                          |
|                            (cont)                             |
|                             ....                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                         C1 and S1 bits
  • 时间戳(Time 4bytes):时间戳,用于C/S发送所有后续块的时间起点,可以从0开始,或者其他值,主要用于多路流传输的时间同步。

  • 零值 (Zero 4bytes):规范中说必须为0,实际传输协议中并未对此进行校验,没啥意义,不为零也可正常传输。

  • 随机数据 (Random data 1528bytes):为随机数序列,用户区分出其响应C2/S2来自此RTMP连接发起的握手还是其他方发起的握手。

(3)C2/S2格式

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

 0                   1                   2                   3
 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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        time (4 bytes)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       time2 (4 bytes)                         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        random echo                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         random echo                           |
|                            (cont)                             |
|                             ....                              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                         C2 and S2 bits
  • 时间戳(Time 4bytes):必须是对方C1/S1发来的时间戳(对C2来说是S1的时间戳,对S2来说是C1的时间戳)。

  • 时间戳2(Time2 4bytes):必须是前面自己发送的C/S包里的时间戳。

  • 随机数据回显(Random echo 1528bytes):必须是对方发来的C1/S1包里携带的随机数据(对C2来说是S1,对S2来说是C1)

  在构建RTMP代码时一般只针对版本号进行校验,时间戳对其随机数对其等校验不严格,因为当前很多RTMP流媒体协议并没那么规范,如果严格校验,兼容性会差。

2.2 RTMP分块(chunk)

  RTMP 传输的数据称为Message,Message包含音视频数据信令,传输时不是以Message为单位的,而是把Message拆分成Chunk发送,而且必须在一个Chunk发送完成之后才能开始发送下一个Chunk,每个Chunk中带有msg stream id代表属于哪个Message,接受端也会按照这个id来将chunk组装成Message。每个Chunk的默认大小是 128 字节,可以通过Set Chunk Size的控制信息设置Chunk数据量的最大值,在发送端和接受端会各自维护一个Chunk Size,可以分别设置这个值来改变自己这一方发送的Chunk的最大值,其配置大小多少合适,需要我们去根据性能要求来调试合适的大小

  Chunk格式包含基本头、消息头、扩展时间戳和负载,如下图所示:

+--------------+----------------+--------------------+--------------+
| Basic Header | Message Header | Extended Timestamp |  Chunk Data  |
+--------------+----------------+--------------------+--------------+
|                                                    |
|<--------------------- Chunk Header --------------->|
                          Chunk Format

2.2.1 Basic Header(基本的头信息)

  Basic Header长度可能是1,2或3个字节,包含了chunk stream ID(流通道Id,CSID)和chunk type(chunk的类型,fmt),CSID用来唯一标识一个特定的流通道,同一个Chunk Stream ID必然属于同一个信道,chunk type决定了后面Message Header的格式。chunk type占最开始2bits,CSID的长度时可变的,其决定了基本头的长度,在足够表征流通道的前提下,最好用尽量少的字节来表示CSID,从而减少由于引入Header增加的数据量。

  RTMP最多可支持65597个流,CSID范围在3-65599 内,CSID辅助 0,1,2为保留值,其中CSID=0表示块基本头为2个字节,并且CSID范围在64-319 之间(第二个字节+64);CSID=1 表示块基本头为3个字节,并且ID范围在64-65599之间(第三个字节*256 + 第二个字节 + 64);3-63 范围内的值表示整个流ID;CSID=2 是为低版本协议保留的,用于协议控制消息和命令。

(1)Basic Header为1bytes时:csid为6bits,取值在[3~63]

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|fmt|   cs id   |
+-+-+-+-+-+-+-+-+
Chunk Basic Hedader 1

(2)Basic Header为2bytes时:第一个字节除了fmt外,其余6位表示数字0,csid范围是[64~319],即最大为(2^8 - 1) + 64 = 319

 0               1
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt|     0     |   cs id - 64  |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     Chunk Basic Hedader 2

(3)Basic Header为3bytes时:第一个字节除了fmt外,其余6位表示数字1,csid范围是[64~65599],最大值为 (2^16 - 1) + 64 = 65599

 0               1               2
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt|     1     |           cs id - 64          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             Chunk Basic Hedader 3

2.2.2 Message Header(消息头信息)

  Message Header包含了要发送的实际信息(可能是完整的,也可能是一部分)的描述信息。Message Header的格式和长度取决于Basic Header的chunk type(fmt)取值,共有4种不同的格式,由上fmt字段控制。其中第一种格式可以表示其他三种表示的所有数据,但由于其他三种格式是基于对之前chunk的差量化的表示,因此可以更简洁地表示相同的数据,实际使用的时候还是应该采用尽量少的字节表示相同意义的数据,以下按照字节数从多到少的顺序分别介绍这4种格式的。

(1)fmt=0,类型0的chunk消息头长度是11个字节,类型0必须用在块流的开头位置,或者每次当块流的时间戳后退的时候(例如向后拖动的操作),其格式如下:

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   timestamp                   |message length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     message length (cont)     |message type id| msg stream id |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|             msg stream id (cont)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             Chunk Message Header - Type 0 (11 bytes)
  • timestamp(时间戳):占用3个字节,最多能表示到16777215=0xFFFFF,如果时间戳大于或等于16777215(0xFFFFFF),该字段值必须为16777215,并且必须设置扩展时间戳Extended Timestamp来一起表示32位的时间戳,否则该字段就是完整的时间戳。接受端在判断timestamp值为0xFFFFFF时就会去Extended timestamp中解析实际的时间戳。

  • message length(消息数据的长度):占用3个字节,表示实际发送的消息的数据,如音频帧、视频帧等数据的长度,注意这里是Message的长度,也就是chunk属于的Message的总数据长度,而不是chunk本身Data的数据的长度。

  • message type id(消息的类型id):占用1个字节,表示实际发送的数据的类型,如8代表音频数据、9代表视频数据。

  • msg stream id(msid):占用4个字节,表示该chunk所在的流的ID,它采用小端存储的方式。

(2)fmt=1,Message Header占用7个字节,省去了表示msg stream id的4个字节,表示此chunk和上一次发的chunk所在的流相同,如果在发送端只和对端有一个流链接的时候可以尽量去采取这种格式,其格式如下:

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               timestamp delta                 |message length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|     message length (cont)     |message type id|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             Chunk Message Header - Type 1 (7 bytes)
  • timestamp delta:占用3个字节,这里和type=0时不同,表示上一个chunk的时间差,当它的值超过3个字节所能表示的最大值时,设置为0xFFFFFF,实际的时间戳差值就会转存到Extended Timestamp字段中,接受端在判断timestamp delta字段24个位都为1时就会去Extended timestamp中解析时机的与上次时间戳的差值。

(3)fmt=2,Message Header占用3个字节,相对于type=1格式又省去了表示消息长度的3个字节和表示消息类型的1个字节,表示此chunk和上一次发送的chunk所在的流、消息的长度和消息的类型都相同,格式如下:

 0               1               2              
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|               timestamp delta                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
     Chunk Message Header - Type 2 (3 bytes)

(4)fmt=3,它表示这个chunk的Message Header和上一个是完全相同的,不存在消息头,当它跟在Type=0的chunk后面时,表示和前一个chunk的时间戳都是相同的,就是一个Message拆分成了多个chunk,这个chunk和上一个chunk同属于一个Message;当它跟在Type=1或者Type=2的chunk后面时,表示和前一个chunk的时间戳的差是相同的。比如第一个chunk的Type=0,timestamp=3600,第二个chunk的Type=2,timestamp delta=3600,表示时间戳为3600+3600,第三个chunk的Type=3,表示timestamp delta=3600,时间戳为3600+3600+3600。

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

  • Chunk Data(块数据):用户层面上真正想要发送的与协议无关的数据,长度在[0,chunkSize]之间。

2.3 协议控制消息(Protocol Control Message)

  在RTMP的chunk会用一些特殊的值来代表协议的控制消息,控制信息的Message Stream ID必须为0(代表控制流信息),CSID必须为2,Message Type ID可以为1/2/3/5/6,控制消息的接收端会忽略掉chunk中的时间戳,收到后立即生效。

2.3.1 Set Chunk Size

  Set Chunk Size(Message Type ID=1),设置chunk中Data字段所能承载的最大字节数,默认为128bytes。通信过程中可以通过发送该消息来设置chunk Size的大小(不得小于128bytes),该值将作用于后续的所有块的发送,直到收到新的通知,而且通信双方会各自维护一个chunkSize,两端的chunkSize是独立的。其chunk data格式如下:

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0|                      chunk size (31 bits)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
          Payload for the 'Set Chunk Size' protocol message

  其中第一位必须为0,chunk Size占31个位,最大可配置为2147483647=0x7FFFFFFF,但实际上所有大于16777215=0xFFFFFF的值都用不上,因为chunk size不能大于Message的长度,表示Message的长度字段是用3个字节表示的,最大只能为0xFFFFFF。

2.3.2 Abort Message

  Abort Message(Message Type ID=2),当一个Message被切分为多个chunk,接收端只接收到了部分chunk时,发送该控制消息表示发送端不再传输同Message的chunk,接收端接收到这个消息后要丢弃这些不完整的chunk。

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                       chunk size (32 bits)                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
         Payload for the 'Abort Meddage' protocol message

payload数据中只需要一个CSID,表示丢弃该CSID的所有已接收到的chunk。

2.3.3 Acknowledgement

  Acknowledgement(Message Type ID=3),当收到对端的消息大小等于窗口大小(Window Size)时接收端要回馈一个ACK给发送端告知对方可以继续发送数据。窗口大小就是指收到接收端返回的ACK前最多可以发送的字节数量,返回的ACK中会带有从发送上一个ACK后接收到的字节数。

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                   sequence number (4 bytes)                   |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
       Payload for the 'Acknowledgement' protocol message

2.3.4 Window Acknowledgement Size

  Window Acknowledgement Size(Message Type ID=5),发送端在接收到接收端返回的两个ACK间最多可以发送的字节数,客户端或服务端发送该消息来通知对方发送确认消息(ACK)所使用的窗口大小,并等待对方发送回确认消息(ACK),对方(接收端)在接收到窗口大小确认信息后必须发送确认消息(ACK)。

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Acknowledgement Window size (4 bytes)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 Payload for the 'Window Acknowledgement Size' protocol message

2.3.5 Set Peer Bandwidth

  Set Peer Bandwidth(Message Type ID=6),限制对端的输出带宽。接收端接收到该消息后会通过设置消息中的Window ACK Size来限制已发送但未接受到反馈的消息的大小来限制发送端的发送带宽。如果消息中的Window ACK Size与上一次发送给发送端的size不同的话要回馈一个Window Acknowledgement Size的控制消息。

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|            Acknowledgement Window size (4 bytes)              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|   Limit Type  |
|    (1 byte)   |
+-+-+-+-+-+-+-+-+
        Payload for the 'Set Peer Bandwidth' protocol message
  • Hard(Limit Type=0):接收端应该将Window Ack Size设置为消息中的值;

  • Soft(Limit Type=1):接收端可以将Window Ack Size设为消息中的值,原有值如果小于此值,也可以保存原来的值;

  • Dynamic(Limit Type=2):如果上次的Set Peer Bandwidth消息中的Limit Type为0,本次也按Hard处理,否则忽略本消。

2.4 RTMP Message Format

  虽然RTMP被设计成使用RTMP块流传输,但是它也可以使用其他传输协议来发送消息。RTMP块流协议和RTMP协议配合时,非常适合音视频应用,包括一对一和一对多实时直播、视频点播和视频互动会议等。

  RTMP消息有两部分,消息头和有效负载。

2.4.1 消息头(Message Header)

  消息头格式如下:

 0               1               2               3
 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Type  |                  Payload length               |
|   (1 byte)    |                     (3 bytes)                 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                          Timestamp                            |
|                          (4 bytes)                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                  Stream ID                    |
|                  (3 bytes)                    |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                           Message Header (11 bytes)

  消息头包含以下信息:

  • Message Type:消息类型,1个字节。消息类型ID为 1 - 6 的是为协议控制消息保留的。

  • Payload Length:有效负载的字节数(长度),3个字节。该字段是用大字节序(big-endian)表示的。

  • Timestamp:时间戳,4个字节,用大字节序(big-endian)表示。

  • Message Stream ID:消息流ID,标识消息所使用的流,用大字节序(big-endian)表示。

  • Message Payload:消息的另一部分就是有效负载,也是消息包含的实际数据,比如说音频样本或者压缩的视频数据。

  这里注意RTMP消息的头(RTMP Message Header,不是chunk头中的 Message Header,两个不是同一个东西)有自己的统一格式,当然这部分也是会被切割到 Chunk 里传输的,不过,因为实际意义和 Chunk Header 内容重复,当前主流流媒体服务器在发送RTMP消息时,chunk data中不包含RTMP Message Header,只要双方约定好即可。

2.5 RTMP Message类型

  主要RTMP消息类型如下,其详细介绍可参照规范,RTMP协议规范《rtmp_specification_1.0》,这里不做详细描述。

  • Command Message(命令消息,Message Type ID=17或20):表示在客户端盒服务器间传递的在对端执行某些操作的命令消息,如connect表示连接对端,对端如果同意连接的话会记录发送端信息并返回连接成功消息,publish表示开始向对方推流,接受端接到命令后准备好接受对端发送的流信息,后面会对比较常见的Command Message具体介绍。当信息使用AMF0编码时,Message Type ID=20,AMF3编码时Message Type ID=17。

  • Data Message(数据消息,Message Type ID=15或18):传递一些元数据(MetaData,比如视频名,分辨率等等)或者用户自定义的一些消息。当信息使用AMF0编码时,Message Type ID=18,AMF3编码时Message Type ID=15.

  • Shared Object Message(共享消息,Message Type ID=16或19):表示一个Flash类型的对象,由键值对的集合组成,用于多客户端,多实例时使用。当信息使用AMF0编码时,Message Type ID=19,AMF3编码时Message Type ID=16.

  • Audio Message(音频信息,Message Type ID=8):音频数据。

  • Video Message(视频信息,Message Type ID=9):视频数据。

  • Aggregate Message (聚集信息,Message Type ID=22):多个RTMP子消息的集合。

  • User Control Message Events(用户控制消息,Message Type ID=4):告知对方执行该信息中包含的用户控制事件,比如Stream Begin事件告知对方流信息开始传输。和前面提到的协议控制信息(Protocol Control Message)不同,这是在RTMP协议层的,而不是在RTMP chunk流协议层的,这个很容易弄混。该信息在chunk流中发送时,Message Stream ID=0,Chunk Stream Id=2,Message Type Id=4。

2.6 RTMP Massage和chunk之间关系

  前文已经介绍了RTMP传输的单位不是massage,而是把massage拆分成一个或多个chunk来进行传输,可根据msg stream id判断是否属于同一个Massage,其拆分过程如下:

  这里采用通用的做法,RTMP Message Header不拆分到chunk data中,虽然规范上RTMP massage应该作为一个整体被拆分成chunk,但是由于RTMP massage header与chunk massage header信息重复,本着最小传输数据原则,一般做法是在chunk data中去掉此信息。

3.RTMP流媒体传输详解

  RTMP流媒体需要支持视频源端(视频发布端)通过rtmp推送采集的视频流,同时也要支持播放客户端通过RTMP地址拉流播放,这里详细讲解RTMP推流和拉流的过程。

3.1 RTMP 推流

  RTMP推流流程如下图所示:

image.png
  • 首先由发布客户端发起握手协议:handshaking done;

  • 发布端向服务器发送连接请求消息:Command Message(connect);

  • 服务端接收到连接命令后,发送窗口应答大小确认信息(Window Acknowledgement Size),配置对端带宽(Set Peer Bandwidth),发送用户控制协议(Stream Begin)告知流开始信息,并发送连接接收响应信息(_result-connect response);

  • 发布端发起创建流通道(createstream);

  • 服务器接收到创建流通到后,响应创建流(_result-creatStream response);

  • 发布端发起发布命令消息(public)并准备开始传输元数据消息(Metadata)、音频数据(Audio data);

  • 服务端接收到发布命令后,发送响应消息;

  • 发送端配置chunk size、开始发送视频数据;

  • 服务端返回发布结果信息,开始接收音视频流。

  其上为官方规范文档中描述的流程,实际过程可能稍有不同,发送端和接收端主要对创建流、发布、和数据传输的消息比较关注,解析时一般按照规范顺序和格式解析,其他消息发送顺序并无特殊规定,消息较小时,可一次发送多个RTMP消息。

推流抓包数据如下:

image.png

3.2 RTMP 拉流

  官方规范中给出的RTMP拉流流程如下图:

image.png
  • 首先和推流一样,由客户端发起握手协议,发送创建流命令;

  • 服务器端接收创建流命令后,发送响应命令;

  • 客户端发送命令消息(play);

  • 服务器端接收到播放命令play后,配置chunk大小,发送用户控制协议(StreamIsRecorded、StreamBegin)通知是否录制流,流已开启标志,之后发送播放命令响应消息(刷新当前状态、通知播放开始),这里如果play命令成功,服务端回复onStatus 命令消息 NetStream.Play.Start和NetStream.Play.Reset,其中NetStream.Play.Reset只有当客户端发送的play命令里设置了reset时才会发送,如果要播放的流没有找到,服务端会发送onStatus消息NetStream.Play.StreamNotFound;

  • 服务器端发送音视频消息到客户端,客户端开始播放。

拉流抓包数据如下:

image.png

4.参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,591评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,448评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,823评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,204评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,228评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,190评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,078评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,923评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,334评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,550评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,727评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,428评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,022评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,672评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,826评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,734评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,619评论 2 354

推荐阅读更多精彩内容