一、簡述
RTMP協議中基本的數據單元稱爲消息(Message)。當RTMP協議在互聯網中傳輸數據的時候,消息會被拆分成更小的單元,稱爲消息塊(Chunk)。
每個塊必須完整的發送後才能發送下一個塊。接收端根據快流ID把塊組裝成完整的小消息。塊允許把更高層協議的大消息分割成更小的消息分片,例如爲了防止低優先級的大消息(如視頻消息)阻礙高優先級的小消息(如音頻和控制消息)。
二、Rtmp Chunk Header
+--------------+----------------+--------------------+--------------+
| Basic Header | Message Header | Extended Timestamp | Chunk Data |
+--------------+----------------+--------------------+--------------+
|<------------------- Chunk Header ----------------->|
Chunk Format
Chunk Header 分爲4部分,
2.1基本頭:Base Header(1-3字節)
RTMP 協議最多支持 65597 個流(chunk),CS ID(塊流ID) 範圍爲: 3 ~ 65599。ID 0、1、2 被保留。
基本頭分爲三種情況由Base Header第一個字節後六個bits決定
- (後六個bits==0):基本頭長度爲2字節第二個字節表示塊流ID
- (後六個bits==1):基本頭長度爲3字節,第二三個字節表示快流ID
- (1<後六個bits<=64):基本頭長度爲1字節,後6bits表示塊流ID
1).第一個字節後六bits爲0的基本頭結構如下:
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|fmt| 0 | cs id - 64 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk basic header 2
Basic Header 爲 3字節的情況 (第一個字節後 6 位爲 000001)
塊流ID從64到65599。塊流ID= 第三個byte*256+(第二個byte+64)
2). 第一個字節後六bits爲1的基本頭結構如下:
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 header 3
3).第一個字節後六bits爲cs id的基本頭結構如下:
0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|fmt| cs id |
+-+-+-+-+-+-+-+-+
Chunk basic header 1
Basic Header 爲 1字節的情況 (第一個字節後 6 位爲 cs id)
塊流ID從2到63
CSID 保留值
0、1、2 被保留, 3 ~ 8 基本都是固定用途,所以 9 ~ 65599 才用於自定義 csid,但一般我們用不到。
- 0 表示 Basic Header 總共要佔用 2 個字節
- 1 表示 Basic Header 總共要佔用 3 個字節
- 2 代表該 chunk 是控制信息和一些命令信息
- 3 代表該 chunk 是客戶端發出的 AMF0 命令以及服務端對該命令的應答
- 4 代表該 chunk 是客戶端發出的音頻數據,用於 publish
- 5 代表該 chunk 是服務端發出的 AMF0 命令和數據
- 6 代表該 chunk 是服務端發出的音頻數據,用於 play;或客戶端發出的視頻數據,用於 publish
- 7 代表該 chunk 是服務端發出的視頻數據,用於 play
- 8 代表該 chunk 是客戶端發出的 AMF0 命令,專用來發送: getStreamLength, play, publish
2.2.消息頭:Message Header(0-11字節)
2.2.1.消息頭結構有4種類型
- 類型0:fmt == 0:Message Header 長度爲 11
- 類型1:fmt == 1:Message Header 長度爲 7
- 類型2:fmt == 2:Message Header 長度爲 3
- 類型3:fmt == 3:Message Header 長度爲 0
1).類型0
timestamp(4bytes)+message length(3bytes)+message type id(1byte)+message stream id(4bytes)
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| message stream id (cont) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk Message Header - Type 0
注意: 此時 timestamp 爲絕對時間戳
2).類型1
timestamp(4bytes)+message length(3bytes)+message type id(1byte)
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
fmt爲1的情況下省略了message stream id,這個id與上一個chunk message相同
注意: 此時 timestamp 爲相對時間戳
3).類型2
timestamp(4bytes)
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Chunk Message Header - Type 3
fmt爲2的情況下省略了message length、message type id和message stream id,省略的這個與上一個chunk message 相同
該類型的消息是固定長度與固定類型的消息,也就是說每次發的消息大小與格式相同,例如音頻流格式,在第一個新的 chunk 以後使用該類型。
注意: 此時 timestamp 爲相對時間戳
3).類型3
fmt爲3的情況下Message Header全部省略了,跟上一個chunk Message完全相同
類型3使用的情況大多是因爲消息太大比如視頻消息,一個chunk中放不下,要分chunk發送,第一個chunk後的消息使用該類型,比如關鍵幀,動不動就上萬字節的數據,而且時間戳類型都是固定的,所以使用該類型發送數據。
2.2.2.時間戳
只有在類型0的情況下時間戳是絕對時間戳其他類型的時間戳都是相對時間戳(時間戳增量)。從librtmp與srs中觀察分析得到服務器與客戶端推流的時間戳是相同的(本人都實現了,所以認爲相同,如有其它情況請大神告知)
在此說下在rtmp中時間戳怎麼打:
一般情況下這個時間戳都是從絕對時間戳開始也就是從0開始,音視頻都有自己的時間戳(這一點很重要),音視頻採集過來的時候會自帶採集時間戳,僞代碼如下:
private int GetVideoTimestamp(MediaFrame mediaFrame)
{
if (視頻時間戳== 0)
{
_timestampVideo = mediaFrame.採集時間戳;
return 0;
}
else
{
return mediaFrame.採集時間戳 - 視頻時間戳;
}
}
2.2.3.消息長度
消息長度指的是Header以後的所有字節數
2.2.4.消息類型
- 1 設置塊大小
- 2 中斷消息,丟棄舊數據
- 3 確認
- 4 用戶控制消息
- 5 設置確認窗口大小
- 6 設置流帶寬
- 7 音頻數據
- 9 視頻數據
- 15(0x0f). AMF3 數據
- 16(0x10) AMF3 共享對象事件
- 17(0x11) AMF3 命令
- 18(0x12) AMF0 數據
- 19(0x13) AMF0 共享對象事件
- 20(0x14) AMF0 命令,Invoke 方法調用
- 22(0x16) 聚合消息, H.264, 類似 FLV 文件存儲格式,每個音視頻包作爲一個 Tag, 許多的 Tag 組成了這個 AMFType=0x16 的數據類型
2.2.5.消息ID
這個ID是四個字節的小端數據,這個ID默認置1即:0x01 0x00 0x00 0x00
2.3擴展時間戳:Extended Timestamp(0-4字節)
類型爲1或2的塊裏,本字段代表當前塊和上一個塊的時間戳之差。如果時間間隔大於等於16777215(0xFFFFFF),此字段的取值必須爲16777215,並且與擴展時間戳一起組成32比特的完整時間戳。如果時間戳小於16777215,那麼此字段代表了完整的時間戳。
三、示例
3.1音頻示例
一下是普通的音頻幀發送方式,音頻消息長度比較小,一般不用分包,直接全包發送即可,但是如下方式發送會導致部分消息的冗餘。
+---------+-----------------+-----------------+-----------------+
| |Message Stream ID| Message Type ID | Time | Length |
+---------+-----------------+-----------------+-------+---------+
| Msg # 1 | 12345 | 8 | 0 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 2 | 12345 | 8 | 32 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 3 | 12345 | 8 | 64 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 4 | 12345 | 8 | 96 | 512 |
+---------+-----------------+-----------------+-------+---------+
如下方式解決了Chunk的消息冗餘。消息按類型0、類型2、類型3、類型3... 的方式發送,第二包因爲不確定時間戳增量因此用類型二來確定,從第三包以後因爲時間戳增量也是相同的就可以用類型三來發送。
+---------+-----------------+-----------------+-----------------+
| |Message Stream ID| Message Type ID | Time | Length |
+---------+-----------------+-----------------+-------+---------+
| Msg # 1 | 12345 | 8 | 0 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 2 | 12345 | 8 | 32 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 3 | 12345 | 8 | 64 | 512 |
+---------+-----------------+-----------------+-------+---------+
| Msg # 4 | 12345 | 8 | 96 | 512 |
+---------+-----------------+-----------------+-------+---------+
注意:本人在實現服務器的過程中,發現從各個其他組發來的流的時間戳是各式各樣的,尤其音頻的時間戳更是奇葩的厲害,根本不會出現使用類型3的情況,乾脆就直接用類型1來傳輸音頻,再者音視頻都是相間交錯發的。
3.2視頻示例
視頻消息由於太長即使你把Chunk Size設的再大也不行,一般Chunk Size都設成4096(0x1000)。
+-----------+-------------------+-----------------+-----------------+
| | Message Stream ID | Message Type ID | Time | Length |
+-----------+-------------------+-----------------+-----------------+
| Msg # 1 | 12346 | 9 (video) | 0 | 20000 |
+-----------+-------------------+-----------------+-----------------+
下面是分割後的Chunk。消息按類型0、類型3、類型3、類型3... 的方式發送,因爲整個幀的時間戳都是一個時間戳。
+-------+------+-----+-------------+-----------+------------+
| |Chunk |Chunk|Header |No. of |Total No. of|
| |Stream| Type|Data |Bytes after| bytes in |
| | ID | | | Header | the chunk |
+-------+------+-----+-------------+-----------+------------+
|Chunk#1| 4 | 0 | delta: 0 | 4096 | 4096+12 |
| | | | length:9192 | | |
| | | | type: 9, | | |
| | | | stream ID: | | |
| | | | 12346 | | |
| | | | (11 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#2| 4 | 3 | none | 4096 | 4096+1 |
| | | | (0 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
|Chunk#3| 4 | 3 | none | 1000 | 1000+1 |
| | | | (0 bytes) | | |
+-------+------+-----+-------------+-----------+------------+
注意:在類型0的的Chunk中一定要設定整個消息的長度,在本實例中的長度爲9192(4096+4096+1000)這個別算錯了。這裏的視頻幀是按RTMP發送視頻幀格式拼裝完後再整體進行Chunk 分塊發送的。