譯序:
本文是為截至發稿時止最新 Adobe 官方公布的 RTMP 規范。本文包含 RTMP 規范的全部內容。是第一個比較全面的 RTMP 規范的中譯本。由於成文時間倉促,加上作者知識面所限,翻譯錯誤之處在所難免,懇請各位朋友熱心指出,可以直接在博客后面留言,先行謝過。rtmp_specification_1.0.pdf 官方下載地址http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf,國內下載地址http://download.csdn.net/detail/defonds/6312051。請隨時關注官方文檔更新:http://www.adobe.com/cn/devnet/rtmp.html。以下內容來自 rtmp_specification_1.0.pdf。
1. 簡介
Adobe 公司的實時消息傳輸協議 (RTMP) 通過一個可靠地流傳輸提供了一個雙向多通道消息服務,比如 TCP [RFC0793],意圖在通信端之間傳遞帶有時間信息的視頻、音頻和數據消息流。實現通常對不同類型的消息分配不同的優先級,當運載能力有限時,這會影響等待流傳輸的消息的次序。
本文檔將對實時流傳輸協議 (Real Time Messaging Protocol) 的語法和操作進行描述。
1.1. 術語
本文檔中出現的關鍵字,"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY" 、"OPTIONAL",都將在 [RFC2119] 中進行解釋。
2. 貢獻者
Rajesh Mallipeddi,Adobe Systems 原成員,起草了本文檔原始規范,並提供大部分的原始內容。
Mohit Srivastava,Adobe Systems 成員,促成了本規范的開發。
3. 名詞解釋
Payload (有效載荷):包含於一個數據包中的數據,例如音頻采樣或者壓縮的視頻數據。payload 的格式和解釋,超出了本文檔的范圍。
Packet (數據包):一個數據包由一個固定頭和有效載荷數據構成。一些個底層協議可能會要求對數據包定義封裝。
Port (端口):"傳輸協議用以區分開指定一台主機的不同目的地的一個抽象。TCP/IP 使用小的正整數對端口進行標識。" OSI 傳輸層使用的運輸選擇器 (TSEL) 相當於端口。
Transport address (傳輸地址):用以識別傳輸層端點的網絡地址和端口的組合,例如一個 IP 地址和一個 TCP 端口。數據包由一個源傳輸地址傳送到一個目的傳輸地址。
Message stream (消息流):通信中消息流通的一個邏輯通道。
Message stream ID (消息流 ID):每個消息有一個關聯的 ID,使用 ID 可以識別出流通中的消息流。
Chunk (塊):消息的一段。消息在網絡發送之前被拆分成很多小的部分。塊可以確保端到端交付所有消息有序 timestamp,即使有很多不同的流。
Chunk stream (塊流):通信中允許塊流向一個特定方向的邏輯通道。塊流可以從客戶端流向服務器,也可以從服務器流向客戶端。
Chunk stream ID (塊流 ID):每個塊有一個關聯的 ID,使用 ID 可以識別出流通中的塊流。
Multiplexing (合成):將獨立的音頻/視頻數據合成為一個連續的音頻/視頻流的加工,這樣可以同時發送幾個視頻和音頻。
DeMultiplexing (分解):Multiplexing 的逆向處理,將交叉的音頻和視頻數據還原成原始音頻和視頻數據的格式。
Remote Procedure Call (RPC 遠程方法調用):允許客戶端或服務器調用對端的一個子程序或者程序的請求。
Metadata (元數據):關於數據的一個描述。一個電影的 metadata 包括電影標題、持續時間、創建時間等等。
Application Instance (應用實例):服務器上應用的實例,客戶端可以連接這個實例並發送連接請求。
Action Message Format (AMF 動作消息格式協議):一個用於序列化 ActionScript 對象圖的緊湊的二進制格式。AMF 有兩個版本:AMF 0 [AMF0] 和 AMF 3 [AMF3]。
4. 字節序、對齊和時間格式
所有整數型屬性以網絡字節順序傳輸,字節 0 代表第一個字節,零位是一個單詞或字段最常用的有效位。字節序通常是大端排序。關於傳輸順序的更多細節描述參考 IP 協議 [RFC0791]。除非另外注明,本文檔中的數值常量都是十進制的 (以 10 為基礎)。
除非另有規定,RTMP 中的所有數據都是字節對准的;例如,一個十六位的屬性可能會在一個奇字節偏移上。填充后,填充字節應該有零值。
RTMP 中的 Timestamps 以一個整數形式給出,表示一個未指明的時間點。典型地,每個流會以一個為 0 的 timestamp 起始,但這不是必須的,只要雙端能夠就時間點達成一致。注意這意味着任意不同流 (尤其是來自不同主機的) 的同步需要 RTMP 之外的機制。
因為 timestamp 的長度為 32 位,每隔 49 天 17 小時 2 分鍾和 47.296 秒就要重來一次。因為允許流連續傳輸,有可能要多年,RTMP 應用在處理 timestamp 時應該使用序列碼算法 [RFC1982],並且能夠處理無限循環。例如,一個應用假定所有相鄰的 timestamp 都在 2^31 - 1 毫秒之內,因此 10000 在 4000000000 之后,而 3000000000 在 4000000000 之前。
timestamp 也可以使用無符整數定義,相對於前面的 timestamp。timestamp 的長度可能會是 24 位或者 32 位。
5. RTMP 塊流
本節介紹實時消息傳輸協議的塊流 (RTMP 塊流)。 它為上層多媒體流協議提供合並和打包的服務。
當設計 RTMP 塊流使用實時消息傳輸協議時,它可以處理任何發送消息流的協議。每個消息包含 timestamp 和 payload 類型標識。RTMP塊流和RTMP一起適合各種音頻-視頻應用,從一對一和一對多直播到點播服務,到互動會議應用。
當使用可靠傳輸協議時,比如 TCP [RFC0793],RTMP 塊流能夠對於多流提供所有消息可靠的 timestamp 有序端對端傳輸。RTMP 塊流並不提供任何優先權或類似形式的控制,但是可以被上層協議用來提供這種優先級。例如,一個直播視頻服務器可能會基於發送時間或者每個消息的確認時間丟棄一個傳輸緩慢的客戶端的視頻消息以確保及時獲取其音頻消息。
RTMP 塊流包括其自身的帶內協議控制信息,並且提供機制為上層協議植入用戶控制消息。
5.1 消息格式
可以被分割為塊以支持組合的消息的格式取決於上層協議。消息格式必須包含以下創建塊所需的字段。
Timestamp:消息的 timestamp。這個字段可以傳輸四個字節。
Length:消息的有效負載長度。如果不能省略掉消息頭,那它也被包括進這個長度。這個字段占用了塊頭的三個字節。
Type Id:一些類型 ID 保留給協議控制消息使用。這些傳播信息的消息由 RTMP 塊流協議和上層協議共同處理。其他的所有類型 ID 可用於上層協議,它們被 RTMP 塊流處理為不透明值。事實上,RTMP 塊流中沒有任何地方要把這些值當做類型使用;所有消息必須是同一類型,或者應用使用這一字段來區分同步跟蹤,而不是類型。這一字段占用了塊頭的一個字節。
Message Stream ID:message stream (消息流) ID 可以使任意值。合並到同一個塊流的不同的消息流是根據各自的消息流 ID 進行分解。除此之外,對 RTMP 塊流而言,這是一個不透明的值。這個字段以小端格式占用了塊頭的四個字節。
5.2 簡單握手(simple handshake)
這里主要討論的是簡單握手協議。simple handshake是rtmp spec 1.0定義的握手方式。而complex handshake是后來變更的方式,Adobe沒有公開。在此暫不討論。
一個 RTMP 連接以握手開始。RTMP 的握手不同於其他協議;RTMP 握手由三個固定長度的塊組成,而不是像其他協議一樣的帶有報頭的可變長度的塊。
客戶端 (發起連接請求的終端) 和服務器端各自發送相同的三塊。便於演示,當發送自客戶端時這些塊被指定為 C0、C1 和 C2;當發送自服務器端時這些塊分別被指定為 S0、S1 和 S2。
5.2.1. 握手順序
握手以客戶端發送 C0 和 C1 塊開始。
客戶端必須等待接收到 S1 才能發送 C2。
客戶端必須等待接收到 S2 才能發送任何其他數據。
服務器端必須等待接收到 C0 才能發送 S0 和 S1,也可以等待接收到 C1 再發送 S0 和 S1。服務器端必須等待接收到 C1 才能發送 S2。服務器端必須等待接收到 C2 才能發送任何其他數據。
5.2.2. C0 和 S0 的格式
C0 和 S0 包都是一個單一的八位字節,以一個單獨的八位整型域進行處理:

以下是 C0/S0 包中的字段:
版本號 (八位):在 C0 中,這一字段指示出客戶端要求的 RTMP 版本號。在 S0 中,這一字段指示出服務器端選擇的 RTMP 版本號。本文檔中規范的版本號為 3。0、1、2 三個值是由早期其他產品使用的,是廢棄值;4 - 31 被保留為 RTMP 協議的未來實現版本使用;32 - 255 不允許使用(以區分開 RTMP 和其他常以一個可打印字符開始的文本協議)。無法識別客戶端所請求版本號的服務器應該以版本 3 響應,(收到響應的) 客戶端可以選擇降低到版本 3,或者放棄握手。
如上圖,是一個c0+c1一起發送的抓包例子(complex handshake的)。其中選中的字節為c0字段,代表版本號。后面的數據是c1字段。整體包長1537.
5.2.3. C1 和 S1 的格式
C1 和 S1 數據包的長度都是 1536 字節,包含以下字段:

Time (四個字節):這個字段包含一個 timestamp,用於本終端發送的所有后續塊的時間起點。這個值可以是 0,或者一些任意值。要同步多個塊流,終端可以發送其他塊流當前的 timestamp 的值。
Zero (四個字節):這個字段必須都是 0。
Random data (1528 個字節):這個字段可以包含任意值。終端需要區分出響應來自它發起的握手還是對端發起的握手,這個數據應該發送一些足夠隨機的數。這個不需要對隨機數進行加密保護,也不需要動態值。
5.2.4. C2 和 S2 的格式
C2 和 S2 數據包長度都是 1536 字節,基本就是 S1 和 C1 的副本 (分別),包含有以下字段:

Time (四個字節):這個字段必須包含終端在 S1 (給 C2) 或者 C1 (給 S2) 發的 timestamp。
Time2 (四個字節):這個字段必須包含終端先前發出數據包 (s1 或者 c1) timestamp。
Random echo (1528 個字節):這個字段必須包含終端發的 S1 (給 C2) 或者 S2 (給 C1) 的隨機數。兩端都可以一起使用 time 和 time2 字段再加當前 timestamp 以快速估算帶寬和/或者連接延遲,但這不太可能是有多大用處。
5.2.5. 握手示意圖

下面描述了握手示意圖中提到的狀態:
Uninitialized (未初始化):協議的版本號在這個階段被發送。客戶端和服務器都是 uninitialized (未初始化) 狀態。之后客戶端在數據包 C0 中將協議版本號發出。如果服務器支持這個版本,它將在回應中發送 S0 和 S1。如果不支持呢,服務器會才去適當的行為進行響應。在 RTMP 協議中,這個行為就是終止連接。
Version Sent (版本已發送):在未初始化狀態之后,客戶端和服務器都進入 Version Sent (版本已發送) 狀態。客戶端會等待接收數據包 S1 而服務器在等待 C1。一旦拿到期待的包,客戶端會發送數據包 C2 而服務器發送數據包 S2。(客戶端和服務器各自的)狀態隨即變為 Ack Sent (確認已發送)。
Ack Sent (確認已發送):客戶端和服務器分別等待 S2 和 C2。
Handshake Done (握手結束):客戶端和服務器可以開始交換消息了。
5.3 復雜握手(complex handshake)
rtmp 1.0規范中,指定了RTMP的握手協議:
- c0/s0:一個字節,說明是明文還是加密。
- c1/s1: 1536字節,4字節時間,4字節0x00,1528字節隨機數
- c2/s2: 1536字節,4字節時間1,4字節時間2,1528隨機數和s1相同。這個就是srs以及其他開源軟件所謂的simple handshake,簡單握手,標准握手,FMLE也是使用這個握手協議。
Flash播放器連接服務器時,若服務器只支持簡單握手,則無法播放h264和aac的流,可能是adobe的限制。adobe將簡單握手改為了有一系列加密算法的復雜握手(complex handshake)。下表是總結:
由上表可知,當服務器和客戶端的握手是按照rtmp協議進行,是不支持h264/aac的,有數據,就是沒有視頻和聲音。原因是adobe變更了握手的數據結構,標准rtmp協議的握手的包是隨機的1536字節(S1S2C1C2),變更后的是需要進行摘要和加密。rtmp協議定義的為simple handshake,變更后加密握手可稱為complex handshake。
本文詳細分析了rtmpd(ccrtmpserver)中的處理邏輯,以及rtmpdump的處理邏輯,從一個全是魔法數字的世界找到他們的數據結構和算法。
5.3.1. complex handshake C1 S1結構(此處參照了winlin博客的相關文章)
complex handshake將C1S1分為4個部分,它們的順序(schema)一種可能是:
- // c1s1 schema0
- time: 4bytes
- version: 4bytes
- key: 764bytes
- digest: 764bytes
其中,key和digest可能會交換位置,即schema可能是:
- // c1s1 schema1
- time: 4bytes
- version: 4bytes
- digest: 764bytes
- key: 764bytes
客戶端來決定使用哪種schema,服務器端則需要先嘗試按照schema0解析,失敗則用schema1解析。如下圖所示:
無論key和digest位置如何,它們的結構是不變的:
- // 764bytes key結構
- random-data: (offset)bytes
- key-data: 128bytes
- random-data: (764-offset-128-4)bytes
- offset: 4bytes
- // 764bytes digest結構
- offset: 4bytes
- random-data: (offset)bytes
- digest-data: 32bytes
- random-data: (764-4-offset-32)bytes
備注:發現FMS只認識digest-key結構。
如下圖所示:
crtmp中這些全是魔法數字。
5.3.2.complex handshake C2 S2結構:
c2 s2主要是用來提供對C1 S1的驗證,結構如下:
- // 1536bytes C2S2結構
- random-data: 1504bytes
- digest-data: 32bytes
C2S2的結構相對比較簡單。如下圖所示:
下面介紹C1S1C2S2的生成以及驗證算法。
5.3.3.complex handshake C1 S1算法
C1S1中都是包含32字節的digest,而且digest將C1S1分成兩部分:
- // C1S1被digest分成兩部分
- c1s1-part1: n bytes
- digest-data: 32bytes
- c1s1-part2: (1536-n-32)bytes
如下圖所示:
在生成C1時,需要用到c1s1-part1和c1s1-part2這兩個部分的字節拼接起來的字節,定義為:
- c1s1-joined = bytes_join(c1s1-part1, c1s1-part2)
也就是說,把1536字節的c1s1中的32字節的digest拿剪刀剪掉,剩下的頭和尾加在一起就是c1s1-joined。用到的兩個常量FPKey和FMSKey:
- u_int8_t FMSKey[] = {
- 0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
- 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
- 0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
- 0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
- 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
- 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
- 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
- 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
- }; // 68
- u_int8_t FPKey[] = {
- 0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
- 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
- 0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
- 0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
- 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
- 0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
- 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
- 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
- }; // 62
生成C1的算法如下:
- calc_c1_digest(c1, schema) {
- get c1s1-joined from c1 by specified schema
- digest-data = HMACsha256(c1s1-joined, FPKey, 30)
- return digest-data;
- }
- random fill 1536bytes c1 // also fill the c1-128bytes-key
- time = time() // c1[0-3]
- version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
- schema = choose schema0 or schema1
- digest-data = calc_c1_digest(c1, schema)
- copy digest-data to c1
生成S1的算法如下:
- /*decode c1 try schema0 then schema1*/
- c1-digest-data = get-c1-digest-data(schema0)
- if c1-digest-data equals to calc_c1_digest(c1, schema0) {
- c1-key-data = get-c1-key-data(schema0)
- schema = schema0
- } else {
- c1-digest-data = get-c1-digest-data(schema1)
- if c1-digest-data not equals to calc_c1_digest(c1, schema1) {
- switch to simple handshake.
- return
- }
- c1-key-data = get-c1-key-data(schema1)
- schema = schema1
- }
- /*generate s1*/
- random fill 1536bytes s1
- time = time() // c1[0-3]
- version = [0x04, 0x05, 0x00, 0x01] // s1[4-7]
- DH_compute_key(key = c1-key-data, pub_key=s1-key-data)
- get c1s1-joined by specified schema
- s1-digest-data = HMACsha256(c1s1-joined, FMSKey, 36)
- copy s1-digest-data and s1-key-data to s1.
C1S1的算法完畢。
5.3.4.complex handshake C2 S2
C2S2的生成算法如下:
- random fill c2s2 1536 bytes
- // client generate C2, or server valid C2
- temp-key = HMACsha256(s1-digest, FPKey, 62)
- c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
- // server generate S2, or client valid S2
- temp-key = HMACsha256(c1-digest, FMSKey, 68)
- s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
驗證的算法是一樣的。
5.3.5.解析補充
讀rtmpd(ccrtmpserver)代碼發現,handshake確實變更了。BTW:rtmpd的代碼可讀性要強很多,很快就能知道它在做什么;不過,它是部分C++部分C的混合體;譬如,使用了BaseRtmpProtocol和InboundRtmpProtocol這種C++的解決方式;以及在解析complex handshake時對1536字節的包直接操作,offset=buf[772]+buf[773]+buf[774]+buf[775],這個就很難看明白在做什么了,其實1536是由4字節的time+4字節的version+764字節的key+764字節的digest,key的offset在后面,digest的offset在前面,若定義兩個結構再讓它們自己去解析,就很明白在做什么了。
sources\thelib\src\protocols\rtmp\inboundrtmpprotocol.cpp: 51
- InboundRTMPProtocol::PerformHandshake
- // 沒有完成握手。
- case RTMP_STATE_NOT_INITIALIZED:
- // buffer[1537]
- // 第一個字節,即c0,表示握手類型(03是明文,06是加密,其他值非法)
- handshakeType = GETIBPOINTER(buffer)[0];
- // 刪除第一個字節,buffer[1536] 即c1
- buffer.Ignore(1);
- // 第5-8共4個字節表示FPVersion,這個必須非0,0表示不支持complex handshake。
- _currentFPVersion = ENTOHLP(GETIBPOINTER(buffer) + 4);
- // 進行明文握手(false表示明文)
- PerformHandshake(buffer, false);
- InboundRTMPProtocol::PerformHandshake
- // 先驗證client,即驗證c1
- // 先嘗試scheme0
- valid = ValidClientScheme(0);
- // 若失敗,再嘗試scheme1
- valid = ValidClientScheme(1)
- // 若驗證成功
- if(valid)
- // 復雜的handshake:PerformComplexHandshake,主要流程如下:
- S0 = 3或6
- 隨機數初始化S1S2
- 根據C1的public key生成S1的128byets public key
- 生成S1的32bytes digest
- 根據C1和S2生成S2的32bytes digest
- else
- // rtmp spec 1.0定義的握手方式
- PerformSimpleHandshake();
其實到后面看明白了,scheme1和scheme2這兩種方式,是包結構的調換。
- <strong>complex的包結構如下:</strong>C1/S1 1536bytes
- time: 4bytes 開頭是4字節的當前時間。(u_int32_t)time(NULL)
- peer_version: 4bytes 為程序版本。C1一般是0x80000702。S1是0x04050001。
- 764bytes: 可能是KeyBlock或者DigestBlock
- 764bytes: 可能是KeyBlock或者DigestBlock
- 其中scheme1就是KeyBlock在前面DigestBlock在后面,而scheme0是DigestBlock在前面KeyBlock在后面。
- <strong>子結構KeyBlock定義:</strong>
- 760bytes: 包含128bytes的key的數據。
- key_offset: 4bytes 最后4字節定義了key的offset(相對於KeyBlock開頭而言)
- <pre name="code" class="cpp"><strong>子結構DigestBlock定義:</strong>
- digest_offset: 4bytes 開頭4字節定義了digest的offset(相對於第DigestBlock的第5字節而言,offset=3表示digestBlock[7~38]為digest
- 760bytes: 包含32bytes的digest的數據。
其中,key和digest的主要算法是:C1的key為128bytes隨機數。C1_32bytes_digest = HMACsha256(P1+P2, 1504, FPKey, 30) ,其中P1為digest之前的部分,P2為digest之后的部分,P1+P2是將這兩部分拷貝到新的數組,共1536-32長度。S1的key根據C1的key算出來,算法如下:
- DHWrapper dhWrapper(1024);
- dhWrapper.Initialize()
- dhWrapper.CreateSharedKey(c1_key, 128)
- dhWrapper.CopyPublicKey(s1_key, 128)
S1的digest算法同C1。注意,必須先計算S1的key,因為key變化后digest也也重新計算。
S2/C2沒有key,只有一個digest,是根據C1/S1算出來的:
- 先用隨機數填充S2
- s2data=S2[0-1504]; 前1502字節為隨機的數據。
- s2digest=S2[1505-1526] 后32bytes為digest。
- // 計算s2digest方法如下:
- ptemphash[512]: HMACsha256(c1digest, 32, FMSKey, 68, ptemhash)
- ps2hash[512]: HMACsha256(s2data, 1504, ptemphash, 32, ps2hash)
- 將ps2hash[0-31]拷貝到s2的后32bytes。
5.3.6.代碼實現
- /*
- The MIT License (MIT)
- Copyright (c) 2013 winlin
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
- #ifndef SRS_CORE_COMPLEX_HANDSHKAE_HPP
- #define SRS_CORE_COMPLEX_HANDSHKAE_HPP
- /*
- #include <srs_core_complex_handshake.hpp>
- */
- #include <srs_core.hpp>
- class SrsSocket;
- /**
- * rtmp complex handshake,
- * @see also crtmp(crtmpserver) or librtmp,
- * @see also: http://blog.csdn.net/win_lin/article/details/13006803
- * @doc update the README.cmd
- */
- class SrsComplexHandshake
- {
- public:
- SrsComplexHandshake();
- virtual ~SrsComplexHandshake();
- public:
- /**
- * complex hanshake.
- * @_c1, size of c1 must be 1536.
- * @remark, user must free the c1.
- * @return user must:
- * continue connect app if success,
- * try simple handshake if error is ERROR_RTMP_TRY_SIMPLE_HS,
- * otherwise, disconnect
- */
- virtual int handshake(SrsSocket& skt, char* _c1);
- };
- #endif
- /*
- The MIT License (MIT)
- Copyright (c) 2013 winlin
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
- #include <srs_core_complex_handshake.hpp>
- #include <time.h>
- #include <stdlib.h>
- #include <srs_core_error.hpp>
- #include <srs_core_log.hpp>
- #include <srs_core_auto_free.hpp>
- #include <srs_core_socket.hpp>
- // 68bytes FMS key which is used to sign the sever packet.
- u_int8_t SrsGenuineFMSKey[] = {
- 0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
- 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
- 0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
- 0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
- 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
- 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
- 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
- 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
- }; // 68
- // 62bytes FP key which is used to sign the client packet.
- u_int8_t SrsGenuineFPKey[] = {
- 0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
- 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
- 0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
- 0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
- 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
- 0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
- 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
- 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
- }; // 62
- #include <openssl/evp.h>
- #include <openssl/hmac.h>
- int openssl_HMACsha256(const void* data, int data_size, const void* key, int key_size, void* digest) {
- HMAC_CTX ctx;
- HMAC_CTX_init(&ctx);
- HMAC_Init_ex(&ctx, (unsigned char*) key, key_size, EVP_sha256(), NULL);
- HMAC_Update(&ctx, (unsigned char *) data, data_size);
- unsigned int digest_size;
- HMAC_Final(&ctx, (unsigned char *) digest, &digest_size);
- HMAC_CTX_cleanup(&ctx);
- if (digest_size != 32) {
- return ERROR_OpenSslSha256DigestSize;
- }
- return ERROR_SUCCESS;
- }
- #include <openssl/dh.h>
- #define RFC2409_PRIME_1024 \
- "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" \
- "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" \
- "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" \
- "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" \
- "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" \
- "FFFFFFFFFFFFFFFF"
- int __openssl_generate_key(
- u_int8_t*& _private_key, u_int8_t*& _public_key, int32_t& size,
- DH*& pdh, int32_t& bits_count, u_int8_t*& shared_key, int32_t& shared_key_length, BIGNUM*& peer_public_key
- ){
- int ret = ERROR_SUCCESS;
- //1. Create the DH
- if ((pdh = DH_new()) == NULL) {
- ret = ERROR_OpenSslCreateDH;
- return ret;
- }
- //2. Create his internal p and g
- if ((pdh->p = BN_new()) == NULL) {
- ret = ERROR_OpenSslCreateP;
- return ret;
- }
- if ((pdh->g = BN_new()) == NULL) {
- ret = ERROR_OpenSslCreateG;
- return ret;
- }
- //3. initialize p, g and key length
- if (BN_hex2bn(&pdh->p, RFC2409_PRIME_1024) == 0) {
- ret = ERROR_OpenSslParseP1024;
- return ret;
- }
- if (BN_set_word(pdh->g, 2) != 1) {
- ret = ERROR_OpenSslSetG;
- return ret;
- }
- //4. Set the key length
- pdh->length = bits_count;
- //5. Generate private and public key
- if (DH_generate_key(pdh) != 1) {
- ret = ERROR_OpenSslGenerateDHKeys;
- return ret;
- }
- // CreateSharedKey
- if (pdh == NULL) {
- ret = ERROR_OpenSslGenerateDHKeys;
- return ret;
- }
- if (shared_key_length != 0 || shared_key != NULL) {
- ret = ERROR_OpenSslShareKeyComputed;
- return ret;
- }
- shared_key_length = DH_size(pdh);
- if (shared_key_length <= 0 || shared_key_length > 1024) {
- ret = ERROR_OpenSslGetSharedKeySize;
- return ret;
- }
- shared_key = new u_int8_t[shared_key_length];
- memset(shared_key, 0, shared_key_length);
- peer_public_key = BN_bin2bn(_private_key, size, 0);
- if (peer_public_key == NULL) {
- ret = ERROR_OpenSslGetPeerPublicKey;
- return ret;
- }
- if (DH_compute_key(shared_key, peer_public_key, pdh) == -1) {
- ret = ERROR_OpenSslComputeSharedKey;
- return ret;
- }
- // CopyPublicKey
- if (pdh == NULL) {
- ret = ERROR_OpenSslComputeSharedKey;
- return ret;
- }
- int32_t keySize = BN_num_bytes(pdh->pub_key);
- if ((keySize <= 0) || (size <= 0) || (keySize > size)) {
- //("CopyPublicKey failed due to either invalid DH state or invalid call"); return ret;
- ret = ERROR_OpenSslInvalidDHState;
- return ret;
- }
- if (BN_bn2bin(pdh->pub_key, _public_key) != keySize) {
- //("Unable to copy key"); return ret;
- ret = ERROR_OpenSslCopyKey;
- return ret;
- }
- return ret;
- }
- int openssl_generate_key(char* _private_key, char* _public_key, int32_t size)
- {
- int ret = ERROR_SUCCESS;
- // Initialize
- DH* pdh = NULL;
- int32_t bits_count = 1024;
- u_int8_t* shared_key = NULL;
- int32_t shared_key_length = 0;
- BIGNUM* peer_public_key = NULL;
- ret = __openssl_generate_key(
- (u_int8_t*&)_private_key, (u_int8_t*&)_public_key, size,
- pdh, bits_count, shared_key, shared_key_length, peer_public_key
- );
- if (pdh != NULL) {
- if (pdh->p != NULL) {
- BN_free(pdh->p);
- pdh->p = NULL;
- }
- if (pdh->g != NULL) {
- BN_free(pdh->g);
- pdh->g = NULL;
- }
- DH_free(pdh);
- pdh = NULL;
- }
- if (shared_key != NULL) {
- delete[] shared_key;
- shared_key = NULL;
- }
- if (peer_public_key != NULL) {
- BN_free(peer_public_key);
- peer_public_key = NULL;
- }
- return ret;
- }
- // the digest key generate size.
- #define OpensslHashSize 512
- /**
- * 764bytes key結構
- * random-data: (offset)bytes
- * key-data: 128bytes
- * random-data: (764-offset-128-4)bytes
- * offset: 4bytes
- */
- struct key_block
- {
- // (offset)bytes
- char* random0;
- int random0_size;
- // 128bytes
- char key[128];
- // (764-offset-128-4)bytes
- char* random1;
- int random1_size;
- // 4bytes
- int32_t offset;
- };
- // calc the offset of key,
- // the key->offset cannot be used as the offset of key.
- int srs_key_block_get_offset(key_block* key)
- {
- int max_offset_size = 764 - 128 - 4;
- int offset = 0;
- u_int8_t* pp = (u_int8_t*)&key->offset;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- return offset % max_offset_size;
- }
- // create new key block data.
- // if created, user must free it by srs_key_block_free
- void srs_key_block_init(key_block* key)
- {
- key->offset = (int32_t)rand();
- key->random0 = NULL;
- key->random1 = NULL;
- int offset = srs_key_block_get_offset(key);
- srs_assert(offset >= 0);
- key->random0_size = offset;
- if (key->random0_size > 0) {
- key->random0 = new char[key->random0_size];
- for (int i = 0; i < key->random0_size; i++) {
- *(key->random0 + i) = rand() % 256;
- }
- }
- for (int i = 0; i < (int)sizeof(key->key); i++) {
- *(key->key + i) = rand() % 256;
- }
- key->random1_size = 764 - offset - 128 - 4;
- if (key->random1_size > 0) {
- key->random1 = new char[key->random1_size];
- for (int i = 0; i < key->random1_size; i++) {
- *(key->random1 + i) = rand() % 256;
- }
- }
- }
- // parse key block from c1s1.
- // if created, user must free it by srs_key_block_free
- // @c1s1_key_bytes the key start bytes, maybe c1s1 or c1s1+764
- int srs_key_block_parse(key_block* key, char* c1s1_key_bytes)
- {
- int ret = ERROR_SUCCESS;
- char* pp = c1s1_key_bytes + 764;
- pp -= sizeof(int32_t);
- key->offset = *(int32_t*)pp;
- key->random0 = NULL;
- key->random1 = NULL;
- int offset = srs_key_block_get_offset(key);
- srs_assert(offset >= 0);
- pp = c1s1_key_bytes;
- key->random0_size = offset;
- if (key->random0_size > 0) {
- key->random0 = new char[key->random0_size];
- memcpy(key->random0, pp, key->random0_size);
- }
- pp += key->random0_size;
- memcpy(key->key, pp, sizeof(key->key));
- pp += sizeof(key->key);
- key->random1_size = 764 - offset - 128 - 4;
- if (key->random1_size > 0) {
- key->random1 = new char[key->random1_size];
- memcpy(key->random1, pp, key->random1_size);
- }
- return ret;
- }
- // free the block data create by
- // srs_key_block_init or srs_key_block_parse
- void srs_key_block_free(key_block* key)
- {
- if (key->random0) {
- srs_freepa(key->random0);
- }
- if (key->random1) {
- srs_freepa(key->random1);
- }
- }
- /**
- * 764bytes digest結構
- * offset: 4bytes
- * random-data: (offset)bytes
- * digest-data: 32bytes
- * random-data: (764-4-offset-32)bytes
- */
- struct digest_block
- {
- // 4bytes
- int32_t offset;
- // (offset)bytes
- char* random0;
- int random0_size;
- // 32bytes
- char digest[32];
- // (764-4-offset-32)bytes
- char* random1;
- int random1_size;
- };
- // calc the offset of digest,
- // the key->offset cannot be used as the offset of digest.
- int srs_digest_block_get_offset(digest_block* digest)
- {
- int max_offset_size = 764 - 32 - 4;
- int offset = 0;
- u_int8_t* pp = (u_int8_t*)&digest->offset;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- return offset % max_offset_size;
- }
- // create new digest block data.
- // if created, user must free it by srs_digest_block_free
- void srs_digest_block_init(digest_block* digest)
- {
- digest->offset = (int32_t)rand();
- digest->random0 = NULL;
- digest->random1 = NULL;
- int offset = srs_digest_block_get_offset(digest);
- srs_assert(offset >= 0);
- digest->random0_size = offset;
- if (digest->random0_size > 0) {
- digest->random0 = new char[digest->random0_size];
- for (int i = 0; i < digest->random0_size; i++) {
- *(digest->random0 + i) = rand() % 256;
- }
- }
- for (int i = 0; i < (int)sizeof(digest->digest); i++) {
- *(digest->digest + i) = rand() % 256;
- }
- digest->random1_size = 764 - 4 - offset - 32;
- if (digest->random1_size > 0) {
- digest->random1 = new char[digest->random1_size];
- for (int i = 0; i < digest->random1_size; i++) {
- *(digest->random1 + i) = rand() % 256;
- }
- }
- }
- // parse digest block from c1s1.
- // if created, user must free it by srs_digest_block_free
- // @c1s1_digest_bytes the digest start bytes, maybe c1s1 or c1s1+764
- int srs_digest_block_parse(digest_block* digest, char* c1s1_digest_bytes)
- {
- int ret = ERROR_SUCCESS;
- char* pp = c1s1_digest_bytes;
- digest->offset = *(int32_t*)pp;
- pp += sizeof(int32_t);
- digest->random0 = NULL;
- digest->random1 = NULL;
- int offset = srs_digest_block_get_offset(digest);
- srs_assert(offset >= 0);
- digest->random0_size = offset;
- if (digest->random0_size > 0) {
- digest->random0 = new char[digest->random0_size];
- memcpy(digest->random0, pp, digest->random0_size);
- }
- pp += digest->random0_size;
- memcpy(digest->digest, pp, sizeof(digest->digest));
- pp += sizeof(digest->digest);
- digest->random1_size = 764 - 4 - offset - 32;
- if (digest->random1_size > 0) {
- digest->random1 = new char[digest->random1_size];
- memcpy(digest->random1, pp, digest->random1_size);
- }
- return ret;
- }
- // free the block data create by
- // srs_digest_block_init or srs_digest_block_parse
- void srs_digest_block_free(digest_block* digest)
- {
- if (digest->random0) {
- srs_freepa(digest->random0);
- }
- if (digest->random1) {
- srs_freepa(digest->random1);
- }
- }
- /**
- * the schema type.
- */
- enum srs_schema_type {
- srs_schema0 = 0, // key-digest sequence
- srs_schema1 = 1, // digest-key sequence
- srs_schema_invalid = 2,
- };
- void __srs_time_copy_to(char*& pp, int32_t time)
- {
- // 4bytes time
- *(int32_t*)pp = time;
- pp += 4;
- }
- void __srs_version_copy_to(char*& pp, int32_t version)
- {
- // 4bytes version
- *(int32_t*)pp = version;
- pp += 4;
- }
- void __srs_key_copy_to(char*& pp, key_block* key)
- {
- // 764bytes key block
- if (key->random0_size > 0) {
- memcpy(pp, key->random0, key->random0_size);
- }
- pp += key->random0_size;
- memcpy(pp, key->key, sizeof(key->key));
- pp += sizeof(key->key);
- if (key->random1_size > 0) {
- memcpy(pp, key->random1, key->random1_size);
- }
- pp += key->random1_size;
- *(int32_t*)pp = key->offset;
- pp += 4;
- }
- void __srs_digest_copy_to(char*& pp, digest_block* digest, bool with_digest)
- {
- // 732bytes digest block without the 32bytes digest-data
- // nbytes digest block part1
- *(int32_t*)pp = digest->offset;
- pp += 4;
- if (digest->random0_size > 0) {
- memcpy(pp, digest->random0, digest->random0_size);
- }
- pp += digest->random0_size;
- // digest
- if (with_digest) {
- memcpy(pp, digest->digest, 32);
- pp += 32;
- }
- // nbytes digest block part2
- if (digest->random1_size > 0) {
- memcpy(pp, digest->random1, digest->random1_size);
- }
- pp += digest->random1_size;
- }
- /**
- * copy whole c1s1 to bytes.
- */
- void srs_schema0_copy_to(char* bytes, bool with_digest,
- int32_t time, int32_t version, key_block* key, digest_block* digest)
- {
- char* pp = bytes;
- __srs_time_copy_to(pp, time);
- __srs_version_copy_to(pp, version);
- __srs_key_copy_to(pp, key);
- __srs_digest_copy_to(pp, digest, with_digest);
- if (with_digest) {
- srs_assert(pp - bytes == 1536);
- } else {
- srs_assert(pp - bytes == 1536 - 32);
- }
- }
- void srs_schema1_copy_to(char* bytes, bool with_digest,
- int32_t time, int32_t version, digest_block* digest, key_block* key)
- {
- char* pp = bytes;
- __srs_time_copy_to(pp, time);
- __srs_version_copy_to(pp, version);
- __srs_digest_copy_to(pp, digest, with_digest);
- __srs_key_copy_to(pp, key);
- if (with_digest) {
- srs_assert(pp - bytes == 1536);
- } else {
- srs_assert(pp - bytes == 1536 - 32);
- }
- }
- /**
- * c1s1 is splited by digest:
- * c1s1-part1: n bytes (time, version, key and digest-part1).
- * digest-data: 32bytes
- * c1s1-part2: (1536-n-32)bytes (digest-part2)
- */
- char* srs_bytes_join_schema0(int32_t time, int32_t version, key_block* key, digest_block* digest)
- {
- char* bytes = new char[1536 -32];
- srs_schema0_copy_to(bytes, false, time, version, key, digest);
- return bytes;
- }
- /**
- * c1s1 is splited by digest:
- * c1s1-part1: n bytes (time, version and digest-part1).
- * digest-data: 32bytes
- * c1s1-part2: (1536-n-32)bytes (digest-part2 and key)
- */
- char* srs_bytes_join_schema1(int32_t time, int32_t version, digest_block* digest, key_block* key)
- {
- char* bytes = new char[1536 -32];
- srs_schema1_copy_to(bytes, false, time, version, digest, key);
- return bytes;
- }
- /**
- * compare the memory in bytes.
- */
- bool srs_bytes_equals(void* pa, void* pb, int size){
- u_int8_t* a = (u_int8_t*)pa;
- u_int8_t* b = (u_int8_t*)pb;
- for(int i = 0; i < size; i++){
- if(a[i] != b[i]){
- return false;
- }
- }
- return true;
- }
- /**
- * c1s1 schema0
- * time: 4bytes
- * version: 4bytes
- * key: 764bytes
- * digest: 764bytes
- * c1s1 schema1
- * time: 4bytes
- * version: 4bytes
- * digest: 764bytes
- * key: 764bytes
- */
- struct c1s1
- {
- union block {
- key_block key;
- digest_block digest;
- };
- // 4bytes
- int32_t time;
- // 4bytes
- int32_t version;
- // 764bytes
- // if schema0, use key
- // if schema1, use digest
- block block0;
- // 764bytes
- // if schema0, use digest
- // if schema1, use key
- block block1;
- // the logic schema
- srs_schema_type schema;
- c1s1();
- virtual ~c1s1();
- /**
- * get the digest key.
- */
- virtual char* get_digest();
- /**
- * copy to bytes.
- */
- virtual void dump(char* _c1s1);
- /**
- * client: create and sign c1 by schema.
- * sign the c1, generate the digest.
- * calc_c1_digest(c1, schema) {
- * get c1s1-joined from c1 by specified schema
- * digest-data = HMACsha256(c1s1-joined, FPKey, 30)
- * return digest-data;
- * }
- * random fill 1536bytes c1 // also fill the c1-128bytes-key
- * time = time() // c1[0-3]
- * version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
- * schema = choose schema0 or schema1
- * digest-data = calc_c1_digest(c1, schema)
- * copy digest-data to c1
- */
- virtual int c1_create(srs_schema_type _schema);
- /**
- * server: parse the c1s1, discovery the key and digest by schema.
- * use the c1_validate_digest() to valid the digest of c1.
- */
- virtual int c1_parse(char* _c1s1, srs_schema_type _schema);
- /**
- * server: validate the parsed schema and c1s1
- */
- virtual int c1_validate_digest(bool& is_valid);
- /**
- * server: create and sign the s1 from c1.
- */
- virtual int s1_create(c1s1* c1);
- private:
- virtual int calc_s1_digest(char*& digest);
- virtual int calc_c1_digest(char*& digest);
- virtual void destroy_blocks();
- };
- /**
- * the c2s2 complex handshake structure.
- * random-data: 1504bytes
- * digest-data: 32bytes
- */
- struct c2s2
- {
- char random[1504];
- char digest[32];
- c2s2();
- virtual ~c2s2();
- /**
- * copy to bytes.
- */
- virtual void dump(char* _c2s2);
- /**
- * create c2.
- * random fill c2s2 1536 bytes
- *
- * // client generate C2, or server valid C2
- * temp-key = HMACsha256(s1-digest, FPKey, 62)
- * c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
- */
- virtual int c2_create(c1s1* s1);
- /**
- * create s2.
- * random fill c2s2 1536 bytes
- *
- * // server generate S2, or client valid S2
- * temp-key = HMACsha256(c1-digest, FMSKey, 68)
- * s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
- */
- virtual int s2_create(c1s1* c1);
- };
- c2s2::c2s2()
- {
- for (int i = 0; i < 1504; i++) {
- *(random + i) = rand();
- }
- for (int i = 0; i < 32; i++) {
- *(digest + i) = rand();
- }
- }
- c2s2::~c2s2()
- {
- }
- void c2s2::dump(char* _c2s2)
- {
- memcpy(_c2s2, random, 1504);
- memcpy(_c2s2 + 1504, digest, 32);
- }
- int c2s2::c2_create(c1s1* s1)
- {
- int ret = ERROR_SUCCESS;
- char temp_key[OpensslHashSize];
- if ((ret = openssl_HMACsha256(s1->get_digest(), 32, SrsGenuineFPKey, 62, temp_key)) != ERROR_SUCCESS) {
- srs_error("create c2 temp key failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate c2 temp key success.");
- char _digest[OpensslHashSize];
- if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
- srs_error("create c2 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate c2 digest success.");
- memcpy(digest, _digest, 32);
- return ret;
- }
- int c2s2::s2_create(c1s1* c1)
- {
- int ret = ERROR_SUCCESS;
- char temp_key[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1->get_digest(), 32, SrsGenuineFMSKey, 68, temp_key)) != ERROR_SUCCESS) {
- srs_error("create s2 temp key failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate s2 temp key success.");
- char _digest[OpensslHashSize];
- if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
- srs_error("create s2 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate s2 digest success.");
- memcpy(digest, _digest, 32);
- return ret;
- }
- c1s1::c1s1()
- {
- schema = srs_schema_invalid;
- }
- c1s1::~c1s1()
- {
- destroy_blocks();
- }
- char* c1s1::get_digest()
- {
- srs_assert(schema != srs_schema_invalid);
- if (schema == srs_schema0) {
- return block1.digest.digest;
- } else {
- return block0.digest.digest;
- }
- }
- void c1s1::dump(char* _c1s1)
- {
- srs_assert(schema != srs_schema_invalid);
- if (schema == srs_schema0) {
- srs_schema0_copy_to(_c1s1, true, time, version, &block0.key, &block1.digest);
- } else {
- srs_schema1_copy_to(_c1s1, true, time, version, &block0.digest, &block1.key);
- }
- }
- int c1s1::c1_create(srs_schema_type _schema)
- {
- int ret = ERROR_SUCCESS;
- if (_schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("create c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- destroy_blocks();
- time = ::time(NULL);
- version = 0x02070080; // client c1 version
- if (_schema == srs_schema0) {
- srs_key_block_init(&block0.key);
- srs_digest_block_init(&block1.digest);
- } else {
- srs_digest_block_init(&block0.digest);
- srs_key_block_init(&block1.key);
- }
- schema = _schema;
- char* digest = NULL;
- if ((ret = calc_c1_digest(digest)) != ERROR_SUCCESS) {
- srs_error("sign c1 error, failed to calc digest. ret=%d", ret);
- return ret;
- }
- srs_assert(digest != NULL);
- SrsAutoFree(char, digest, true);
- if (schema == srs_schema0) {
- memcpy(block1.digest.digest, digest, 32);
- } else {
- memcpy(block0.digest.digest, digest, 32);
- }
- return ret;
- }
- int c1s1::c1_parse(char* _c1s1, srs_schema_type _schema)
- {
- int ret = ERROR_SUCCESS;
- if (_schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- destroy_blocks();
- time = *(int32_t*)_c1s1;
- version = *(int32_t*)(_c1s1 + 4); // client c1 version
- if (_schema == srs_schema0) {
- if ((ret = srs_key_block_parse(&block0.key, _c1s1 + 8)) != ERROR_SUCCESS) {
- srs_error("parse the c1 key failed. ret=%d", ret);
- return ret;
- }
- if ((ret = srs_digest_block_parse(&block1.digest, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
- srs_error("parse the c1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("parse c1 key-digest success");
- } else if (_schema == srs_schema1) {
- if ((ret = srs_digest_block_parse(&block0.digest, _c1s1 + 8)) != ERROR_SUCCESS) {
- srs_error("parse the c1 key failed. ret=%d", ret);
- return ret;
- }
- if ((ret = srs_key_block_parse(&block1.key, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
- srs_error("parse the c1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("parse c1 digest-key success");
- } else {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- schema = _schema;
- return ret;
- }
- int c1s1::c1_validate_digest(bool& is_valid)
- {
- int ret = ERROR_SUCCESS;
- char* c1_digest = NULL;
- if ((ret = calc_c1_digest(c1_digest)) != ERROR_SUCCESS) {
- srs_error("validate c1 error, failed to calc digest. ret=%d", ret);
- return ret;
- }
- srs_assert(c1_digest != NULL);
- SrsAutoFree(char, c1_digest, true);
- if (schema == srs_schema0) {
- is_valid = srs_bytes_equals(block1.digest.digest, c1_digest, 32);
- } else {
- is_valid = srs_bytes_equals(block0.digest.digest, c1_digest, 32);
- }
- return ret;
- }
- int c1s1::s1_create(c1s1* c1)
- {
- int ret = ERROR_SUCCESS;
- if (c1->schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("create s1 failed. invalid schema=%d, ret=%d", c1->schema, ret);
- return ret;
- }
- destroy_blocks();
- schema = c1->schema;
- time = ::time(NULL);
- version = 0x01000504; // server s1 version
- if (schema == srs_schema0) {
- srs_key_block_init(&block0.key);
- srs_digest_block_init(&block1.digest);
- } else {
- srs_digest_block_init(&block0.digest);
- srs_key_block_init(&block1.key);
- }
- if (schema == srs_schema0) {
- if ((ret = openssl_generate_key(c1->block0.key.key, block0.key.key, 128)) != ERROR_SUCCESS) {
- srs_error("calc s1 key failed. ret=%d", ret);
- return ret;
- }
- } else {
- if ((ret = openssl_generate_key(c1->block1.key.key, block1.key.key, 128)) != ERROR_SUCCESS) {
- srs_error("calc s1 key failed. ret=%d", ret);
- return ret;
- }
- }
- srs_verbose("calc s1 key success.");
- char* s1_digest = NULL;
- if ((ret = calc_s1_digest(s1_digest)) != ERROR_SUCCESS) {
- srs_error("calc s1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("calc s1 digest success.");
- srs_assert(s1_digest != NULL);
- SrsAutoFree(char, s1_digest, true);
- if (schema == srs_schema0) {
- memcpy(block1.digest.digest, s1_digest, 32);
- } else {
- memcpy(block0.digest.digest, s1_digest, 32);
- }
- srs_verbose("copy s1 key success.");
- return ret;
- }
- int c1s1::calc_s1_digest(char*& digest)
- {
- int ret = ERROR_SUCCESS;
- srs_assert(schema == srs_schema0 || schema == srs_schema1);
- char* c1s1_joined_bytes = NULL;
- if (schema == srs_schema0) {
- c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
- } else {
- c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
- }
- srs_assert(c1s1_joined_bytes != NULL);
- SrsAutoFree(char, c1s1_joined_bytes, true);
- digest = new char[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFMSKey, 36, digest)) != ERROR_SUCCESS) {
- srs_error("calc digest for s1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("digest calculated for s1");
- return ret;
- }
- int c1s1::calc_c1_digest(char*& digest)
- {
- int ret = ERROR_SUCCESS;
- srs_assert(schema == srs_schema0 || schema == srs_schema1);
- char* c1s1_joined_bytes = NULL;
- if (schema == srs_schema0) {
- c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
- } else {
- c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
- }
- srs_assert(c1s1_joined_bytes != NULL);
- SrsAutoFree(char, c1s1_joined_bytes, true);
- digest = new char[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFPKey, 30, digest)) != ERROR_SUCCESS) {
- srs_error("calc digest for c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("digest calculated for c1");
- return ret;
- }
- void c1s1::destroy_blocks()
- {
- if (schema == srs_schema_invalid) {
- return;
- }
- if (schema == srs_schema0) {
- srs_key_block_free(&block0.key);
- srs_digest_block_free(&block1.digest);
- } else {
- srs_digest_block_free(&block0.digest);
- srs_key_block_free(&block1.key);
- }
- }
- SrsComplexHandshake::SrsComplexHandshake()
- {
- }
- SrsComplexHandshake::~SrsComplexHandshake()
- {
- }
- int SrsComplexHandshake::handshake(SrsSocket& skt, char* _c1)
- {
- int ret = ERROR_SUCCESS;
- ssize_t nsize;
- static bool _random_initialized = false;
- if (!_random_initialized) {
- srand(0);
- _random_initialized = true;
- srs_trace("srand initialized the random.");
- }
- // decode c1
- c1s1 c1;
- // try schema0.
- if ((ret = c1.c1_parse(_c1, srs_schema0)) != ERROR_SUCCESS) {
- srs_error("parse c1 schema%d error. ret=%d", srs_schema0, ret);
- return ret;
- }
- // try schema1
- bool is_valid = false;
- if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
- if ((ret = c1.c1_parse(_c1, srs_schema1)) != ERROR_SUCCESS) {
- srs_error("parse c1 schema%d error. ret=%d", srs_schema1, ret);
- return ret;
- }
- if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
- ret = ERROR_RTMP_TRY_SIMPLE_HS;
- srs_info("all schema valid failed, try simple handshake. ret=%d", ret);
- return ret;
- }
- }
- srs_verbose("decode c1 success.");
- // encode s1
- c1s1 s1;
- if ((ret = s1.s1_create(&c1)) != ERROR_SUCCESS) {
- srs_error("create s1 from c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("create s1 from c1 success.");
- c2s2 s2;
- if ((ret = s2.s2_create(&c1)) != ERROR_SUCCESS) {
- srs_error("create s2 from c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("create s2 from c1 success.");
- // sendout s0s1s2
- char* s0s1s2 = new char[3073];
- SrsAutoFree(char, s0s1s2, true);
- // plain text required.
- s0s1s2[0] = 0x03;
- s1.dump(s0s1s2 + 1);
- s2.dump(s0s1s2 + 1537);
- if ((ret = skt.write(s0s1s2, 3073, &nsize)) != ERROR_SUCCESS) {
- srs_warn("complex handshake send s0s1s2 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("complex handshake send s0s1s2 success.");
- // recv c2
- char* c2 = new char[1536];
- SrsAutoFree(char, c2, true);
- if ((ret = skt.read_fully(c2, 1536, &nsize)) != ERROR_SUCCESS) {
- srs_warn("complex handshake read c2 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("complex handshake read c2 success.");
- return ret;
- }
5.4. 分塊
握手之后,連接開始對一個或多個塊流進行合並。創建的每個塊都有一個唯一 ID 對其進行關聯,這個 ID 叫做 chunk stream ID (塊流 ID),即csid。這些塊通過網絡進行傳輸。傳遞時,每個塊必須被完全發送才可以發送下一塊。在接收端,這些塊被根據塊流 ID 被組裝成消息。
分塊允許上層協議將大的消息分解為更小的消息,例如,防止體積大的但優先級小的消息 (比如視頻) 阻礙體積較小但優先級高的消息 (比如音頻或者控制命令)。
分塊也讓我們能夠使用較小開銷發送小消息,因為塊頭包含包含在消息內部的信息壓縮提示。
塊的大小是可以配置的。它可以使用一個設置塊大小的控制消息進行設置 (參考 5.4.1)。更大的塊大小可以降低 CPU 開銷,但在低帶寬連接時因為它的大量的寫入也會延遲其他內容的傳遞。更小的塊不利於高比特率的流化。所以塊的大小設置取決於具體情況。
5.4.1. 塊格式
每個塊包含一個頭和數據體。塊頭包含三個部分:

Basic Header (基本頭,1 到 3 個字節):這個字段對塊流 ID 和塊類型進行編碼。塊類型決定了消息頭的編碼格式。(這一字段的) 長度完全取決於塊流 ID,因為塊流 ID 是一個可變長度的字段。
Message Header (消息頭,0,3,7,或者 11 個字節):這一字段對正在發送的消息 (不管是整個消息,還是只是一小部分) 的信息進行編碼。這一字段的長度可以使用塊頭中定義的塊類型進行決定。
Extended Timestamp (擴展 timestamp,0 或 4 字節):這一字段是否出現取決於塊消息頭中的 timestamp 或者 timestamp delta 字段。更多信息參考 5.4.1.3 節。
Chunk Data (可變的大小):當前塊的有效負載,大小由配置決定。
5.4.1.1. 塊基本頭
塊基本頭對塊流 ID (chunk stream id=csid)和塊類型 (由下圖中的 fmt 字段表示) 進行編碼。塊基本頭字段可能會有 1,2 或者 3 個字節,取決於塊流 ID。
一個 (RTMP) 實現應該使用能夠容納這個 ID 的最小的容量進行表示。
RTMP 協議最多支持 65597 個流,流 ID 范圍 3 - 65599。ID 0、1、2 被保留。0 值表示二字節形式,並且 ID 范圍 64 - 319 (第二個字節 + 64)。1 值表示三字節形式,並且 ID 范圍為 64 - 65599 ((第三個字節) * 256 + 第二個字節 + 64)。3 - 63 范圍內的值表示整個流 ID。帶有 2 值的塊流 ID 被保留,用於下層協議控制消息和命令。
塊基本頭中的 0 - 5 位 (最低有效) 代表塊流 ID。
塊流 ID 2 - 63 可以編進這一字段的一字節版本中。

塊流 ID 64 - 319 可以以二字節的形式編碼在頭中。ID 計算為 (第二個字節 + 64),這個時候第一字節的fmt后面的6個bit為000000:

塊流 ID 64 - 65599 可以編碼在這個字段的三字節版本中。ID 計算為 ((第三個字節) * 256 + (第二個字節) + 64),這個時候第一字節的fmt后面的6個bit為000001。

例如:如果csid=288=224+64,那么應該表示成fmt00000011100000(2-byte)或者fmt0000010000000011100000(3-byte)。
csid (六位):這一字段包含有塊流 ID,值的范圍是 2 - 63。fmt后面緊跟着的6位的值, 0 和 1 用於指示這一字段是 2- 或者 3- 字節版本。
fmt (兩個字節):這一字段指示 'chunk message header' 使用的四種格式之一。每種塊類型的 'chunk message header' 會在下一小節解釋。
csid - 64 (8 或者 16 位):這一字段包含了塊流 ID 減掉 64 后的值塊流 ID,即csid - 64為塊流 ID在字節中的表現形式。
塊流 ID 64 - 319 可以使用 2-byte 或者 3-byte 的形式在頭中表示,塊流 ID320-65599只能使用3-byte的形式表示。
5.4.1.2. 塊消息頭(fmt的值)
塊消息頭又四種不同的格式,由塊基本頭中的 "fmt" 字段進行選擇。
一個 (RTMP) 實現應該為每個塊消息頭使用最緊湊的表示。
5.4.1.2.1. 類型 0
類型 0 塊頭的長度是 11 個字節。即AMF0包頭為11字節。這一類型必須用在塊流的起始位置,和流 timestamp 重來的時候 (比如,重置)。

圖例1:符合AMF0類型的協議控制包(play&set buffer length)
例如圖例1,是一個fmt=0的rtmp的協議控制包(play&set buffer length),csid=8。圖中選中字段為包含消息流ID一字節的AMF0包頭(12字節,去掉第一個字節的0x08,即為AMF0的11字節包頭)。其中timestamp(3個字節,00 01 8d) 為397,message length(body size字段,3個字節,00 00 18)為24.type id(stream id,1個字節)代表body的幀類型為0x11(AMF3 command類型,1字節 0x11),代表是指令幀。msg stream id為1(四個字節,01 00 00 00).

timestamp (三個字節):對於 type-0 塊,當前消息的絕對 timestamp 在這里發送。如果 timestamp 大於或者等於 16777215 (十六進制 0xFFFFFF),這一字段必須是 16777215,表明有擴展 timestamp 字段來補充完整的 32 位 timestamp。否則的話,這一字段必須是整個的 timestamp。
5.4.1.2.2. 類型 1
類型 1 塊頭長為 7 個字節。即AMF1包頭為7字節。不包含消息流 ID;這一塊使用前一塊一樣的流 ID。可變長度消息的流 (例如,一些視頻格式) 應該在第一塊之后使用這一格式表示之后的每個新消息。
圖例2:符合AMF1類型的視頻包
例如圖例2,是一個fmt=1的rtmp的視頻包。csid=4,圖中選中字段為包含消息流ID一字節的AMF1包頭(8字節,去掉第一個字節的0x44,即為AMF1的包頭)。其中timestamp delta為6,message length(body size字段)為628.type id代表body的幀類型為9,是視頻幀。

5.4.1.2.3. 類型 2
類型 2 塊頭長度為 3 個字節。即AMF2包頭為3字節。既不包含流 ID 也不包含消息長度;這一塊具有和前一塊相同的流 ID 和消息長度。具有不變長度的消息 (例如,一些音頻和數據格式) 應該在第一塊之后使用這一格式表示之后的每個新消息。
圖例3:符合AMF2類型的包
例如圖例3,是一個fmt=2的rtmp的固定長度的數據包。csid=37,圖中選中字段為不包含消息流ID 1字節的AMF2包頭(3字節,消息流ID為0xa5)。其中timestamp delta為4357388,無message length字段,因為默認與前以對應csid包的數據大小相同。沒有幀類型,緊跟數據。

5.4.1.2.4. 類型 3
類型 3 的塊沒有消息頭。流 ID、消息長度以及 timestamp delta 等字段都不存在;這種類型的塊使用前面塊一樣的塊流 ID。即AMF3無包頭。當單一一個消息被分割為多塊時,除了第一塊的其他塊都應該使用這種類型。參考例 2 (5.3.2.2 小節)。組成流的消息具有同樣的大小,流 ID 和時間間隔應該在類型 2 之后的所有塊都使用這一類型。參考例 1 (5.3.2.1 小節)。如果第一個消息和第二個消息之間的 delta 和第一個消息的 timestamp 一樣的話,那么在類型 0 的塊之后要緊跟一個類型 3 的塊,因為無需再來一個類型 2 的塊來注冊 delta 了。如果一個類型 3 的塊跟着一個類型 0 的塊,那么這個類型 3 塊的 timestamp delta 和類型 0 塊的 timestamp 是一樣的。
5.4.1.2.5. 通用頭字段
塊消息頭中各字段的描述如下:
timestamp delta (三個字節):對於一個類型 1 或者類型 2 的塊,前一塊的 timestamp 和當前塊的 timestamp 的區別在這里發送。如果 delta 大於或者等於 16777215 (十六進制 0xFFFFFF),那么這一字段必須是為 16777215,表示具有擴展 timestamp 字段來對整個 32 位 delta 進行編碼。否則的話,這一字段應該是為具體 delta。
message length (三個字節):對於一個類型 0 或者類型 1 的塊,消息長度在這里進行發送。注意這通常不同於塊的有效載荷的長度。塊的有效載荷代表所有的除了最后一塊的最大塊大小,以及剩余的 (也可能是小消息的整個長度) 最后一塊。
message type id (消息類型 id,一個字節):對於類型 0 或者類型 1 的塊,消息的類型在這里發送。
message stream id (四個字節):對於一個類型為 0 的塊,保存消息流 ID。消息流 ID 以小端格式保存。所有同一個塊流下的消息都來自同一個消息流。當可以將不同的消息流組合進同一個塊流時,這種方法比頭壓縮的做法要好。但是,當一個消息流被關閉而其他的隨后另一個是打開着的,就沒有理由將現有塊流以發送一個新的類型 0 的塊進行復用了。
5.4.1.3. 擴展 timestamp
擴展 timestamp 字段用於對大於 16777215 (0xFFFFFF) 的 timestamp 或者 timestamp delta 進行編碼;也就是,對於不適合於在 24 位的類型 0、1 和 2 的塊里的 timestamp 和 timestamp delta 編碼。這一字段包含了整個 32 位的 timestamp 或者 timestamp delta 編碼。可以通過設置類型 0 塊的 timestamp 字段、類型 1 或者 2 塊的 timestamp delta 字段 16777215 (0xFFFFFF) 來啟用這一字段。當最近的具有同一塊流的類型 0、1 或 2 塊指示擴展 timestamp 字段出現時,這一字段才會在類型為 3 的塊中出現。
5.4.2. 例子
5.4.2.1. 例子 1
這個例子演示了一個簡單地音頻消息流。這個例子演示了信息的冗余。

下一個表格演示了這個流所產生的塊。從消息 3 起,數據傳輸得到了最佳化利用。每條消息的開銷在這一點之后都只有一個字節。

5.4.2.2. 例子 2
這一例子闡述了一條消息太大,無法裝在一個 128 字節的塊里,被分割為若干塊。

這是傳輸的塊:

塊 1 的數據頭說明了整個消息長度是為 307 個字節。
由以上倆例子可以得知,塊類型 3 可以被用於兩種不同的方式。第一種是用於定義一條消息的配置。第二種是定義一個可以從現有狀態數據中派生出來的新消息的起點。
5.5. 協議控制消息
RTMP 塊流使用消息類型 ID(stream id) 為 1、2、3、5 和 6 用於協議控制消息。這些消息包含有 RTMP 塊流協議所需要的信息。
這些協議控制消息必須使用消息流 ID 0 (作為已知控制流) 並以流 ID 為 2 的塊發送。協議控制消息一旦被接收到就立即生效;協議控制消息的 timestamp 被忽略。
5.5.1. 設置塊類型 (1=set chunk size)
協議控制消息 1,設置塊大小,以通知對端一個新的最大塊大小(set chunk size)。
默認的最大塊大小是為 128 字節,但是客戶端或者服務器可以改變這個大小,並使用這一消息對對端進行更新。例如,假定一個客戶端想要發送一個 131 字節的音頻數據,當前塊大小是默認的 128。在這種情況下,客戶端可以發送這種消息到服務器以通知它塊大小現在是 131 字節了。這樣客戶端就可以在單一塊中發送整個音頻數據了。
最大塊大小設置的話最少為 128 字節,包含內容最少要一個字節。最大塊大小由每個方面 (服務器或者客戶端) 自行維護。

0:這個位必須為 0。
chunk size (塊大小,31 位):這一字段保存新的最大塊大小值,以字節為單位,這將用於之后發送者發送的塊,直到有更多 (關於最大塊大小的) 通知。有效值為 1 到 2147483647 (0x7FFFFFFF,1 和 2147483647 都可取); 但是所有大於 16777215 (0xFFFFFF) 的大小值是等價的,因為沒有一個塊比一整個消息大,並且沒有一個消息大於 16777215 字節。
5.5.2. 終止消息(2,)
協議控制消息 2,終止消息,用於通知對端,如果對端在等待去完成一個消息的塊的話,然后拋棄一個塊流中已接受到的部分消息。對端接收到塊流 ID 作為當前協議消息的有效負載。一些程序可能會在關閉的時候使用這個消息以指示不需要進一步對這個消息的處理了。

chunk stream ID (塊流 ID,32 位):這一字段保存塊流 ID,該流的當前消息會被丟棄。
5.5.3. 確認 (3=acknowledgement)
客戶端或者服務器在接收到等同於窗口大小的字節之后必須要發送給對端一個確認。窗口大小是指發送者在沒有收到接收者確認之前發送的最大數量的字節。這個消息定義了序列號,也就是目前接收到的字節數。

sequence number (序列號,32 位):這一字段保存有目前接收到的字節數。
5.5.4. 窗口確認大小 (5=window acknowledgement size)
客戶端或者服務器端發送這條消息來通知對端發送和應答之間的窗口大小。發送者在發送完窗口大小字節之后期待對端的確認。接收端在上次確認發送后接收到的指示數值后,或者會話建立之后尚未發送確認,必須發送一個確認 (5.4.3 小節)。

5.5.5. 設置對端帶寬 (6=set peer bandwidth)
客戶端或者服務器端發送這一消息來限制其對端的輸出帶寬。對端接收到這一消息后,將通過限制這一消息中窗口大小指出的已發送但未被答復的數據的數量以限制其輸出帶寬。如果這個窗口大小不同於其發送給 (設置對端帶寬) 發送者的最后一條消息,那么接收到這一消息的對端應該回復一個窗口確認大小消息。

限制類型取以下值之一:
0 - Hard:對端應該限制其輸出帶寬到指示的窗口大小。
1 - Soft:對端應該限制其輸出帶寬到指示的窗口大小,或者已經有限制在其作用的話就取兩者之間的較小值。
2 - Dynamic:如果先前的限制類型為 Hard,處理這個消息就好像它被標記為 Hard,否則的話忽略這個消息。
6. RTMP 消息格式
這一節定義了使用下層傳輸層 (比如 RTMP 塊流協議) 傳輸的 RTMP 消息的格式。
RTMP 協議設計使用 RTMP 塊流,可以使用其他任意傳輸協議對消息進行發送。RTMP 塊流和 RTMP 一起適用於多種音頻 - 視頻應用,從一對一和一對多直播到點播服務,再到互動會議應用。
6.1. RTMP 消息格式
服務器端和客戶端通過網絡發送 RTMP 消息來進行彼此通信。消息可以包含音頻、視頻、數據,或者其他消息。
RTMP 消息有兩部分:頭和它的有效載荷。
6.1.1. 消息頭
消息頭包含以下:
Message Type (消息類型):一個字節的字段來表示消息類型。類型 ID 1 - 6 被保留用於協議控制消息。
Length (長度):三個字節的字段來表示有效負載的字節數。以大端格式保存。
Timestamp:四個字節的字段包含了當前消息的 timestamp。四個字節也以大端格式保存。
Message Stream Id (消息流 ID):三個字節的字段以指示出當前消息的流。這三個字節以大端格式保存。

6.1.2. 消息有效載荷
消息的另一個部分就是有效負載,這是這個消息所包含的實際內容。例如,它可以是一些音頻樣本或者壓縮的視頻數據。有效載荷格式和解釋不在本文檔范圍之內。
6.2. 用戶控制消息 (4=user control message)
RTMP 使用消息類型 ID(message type ID) 4 表示用戶控制消息。這些消息包含 RTMP 流傳輸層所使用的信息。RTMP 塊流協議使用 ID 為 1、2、3、5 和 6 (5.4 節介紹)。
用戶控制消息應該使用消息流 ID(message stream ID) 0 (以被認為是控制流),並且以 RTMP 塊流發送時以塊流 ID(chunk stream id) 為 2。用戶控制消息一旦被接收立馬生效;它們的 timestamp 是被忽略的。
客戶端或者服務器端發送這個消息來通知對端用戶操作事件。這一消息攜帶有事件類型和事件數據。

消息數據的前兩個字節用於指示事件類型。事件類型被事件數據緊隨。事件數據字段的大小是可變的。但是,如果消息必須通過 RTMP 塊流層傳輸時,最大塊大小 (5.4.1 節) 應該足夠大以允許這些消息填充在一個單一塊中。
事件類型和事件數據格式將在 7.1.7 小節列出。
7. RTMP 命令消息
這一節描述了在服務器端和客戶端彼此通信交換的消息和命令的不同的類型。
服務器端和客戶端交換的不同消息類型包括用於發送音頻數據的音頻消息、用於發送視頻數據的視頻消息、用於發送任意用戶數據的數據消息、共享對象消息以及命令消息。共享對象消息提供了一個通用的方法來管理多用戶和一台服務器之間的分布式數據。命令消息在客戶端和服務器端傳輸 AMF 編碼的命令。客戶端或者服務器端可以通過使用命令消息和對端通信的流請求遠程方法調用 (RPC)。
7.1. 消息的類型
服務器端和客戶端通過在網絡中發送消息來進行彼此通信。消息可以是任何類型,包含音頻消息,視頻消息,命令消息,共享對象消息,數據消息,以及用戶控制消息。
7.1.1. 命令消息 (20, 17)
命令消息(類似play、connect、closestream)在客戶端和服務器端傳遞 AMF 編碼的命令。這些消息被分配以消息類型值為 20 以進行 AMF0(AMF0 command 0x14) 編碼,消息類型值為 17(AMF3 command 0x11)以進行 AMF3 編碼。這些消息發送以進行一些操作,比如,連接,創建流,發布,播放,對端暫停。命令消息,像 onstatus、result 等等,用於通知發送者請求的命令的狀態。一個命令消息由命令名、事務 ID 和包含相關參數的命令對象組成。一個客戶端或者一個服務器端可以通過和對端通信的流使用這些命令消息請求遠程調用 (RPC)。
7.1.2. 數據消息 (18, 15)
客戶端或者服務器端通過發送這些消息以發送元數據或者任何用戶數據到對端。元數據包括數據 (音頻,視頻等等) 的詳細信息,比如創建時間,時長,主題等等。這些消息被分配以消息類型為 18 以進行 AMF0 編碼和消息類型 15 以進行 AMF3 編碼。
7.1.3. 共享對象消息 (19, 16)
所謂共享對象其實是一個 Flash 對象 (一個名值對的集合),這個對象在多個不同客戶端、應用實例中保持同步。消息類型 19 用於 AMF0 編碼、16 用於 AMF3 編碼都被為共享對象事件保留。每個消息可以包含有不同事件。

支持以下事件類型:
事件 | 描述 |
---|---|
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) | 服務器端發送給客戶端這一事件表示連接成功。 |
7.1.4. 音頻消息 (8=audio data)
客戶端或者服務器端發送這一消息以發送音頻數據到對端。消息類型 8 為音頻消息保留。
7.1.5. 視頻消息 (9=video data)
客戶端或者服務器發送這一消息以發送視頻數據到對端。消息類型 9 為視頻消息保留。
7.1.6. 統計消息 (22)
統計消息是一個單一的包含一系列的使用 6.1 節描述的 RTMP 子消息的消息。消息類型 22 用於統計消息。其實就是0x16大包消息。


統計消息的消息流 ID 覆蓋了統計中子消息的消息流 ID。
統計消息里的 timestamp 和第一個子消息的 timestamp 的不同點在於子消息的 timestamp 被相對流時間標調整了偏移。每個子消息的 timestamp 被加入偏移以達到一個統一流時間。第一個子消息的 timestamp 應該和統計消息的 timestamp 一樣,所以這個偏移量應該為 0。
反向指針包含有前一個消息的大小 (包含前一個消息的頭)。這樣子匹配了 FLV 文件的格式,用於反向查找。
使用統計消息具有以下性能優勢:
- 塊流可以在一個塊中以至多一個單一完整的消息發送。因此,增加塊大小並使用統計消息減少了發送塊的數量。
- 子消息可以在內存中連續存儲。在網絡中系統調用發送這些數據時更高效。
7.1.7. 用戶控制消息事件
客戶端或者服務器端發送這一消息來通知對端用戶控制事件。關於這個的消息格式參考 6.2 節。
支持以下用戶控制事件類型:
事件 | 描述 |
Stream Begin (=0) | 服務器發送這個事件來通知客戶端一個流已就緒並可以用來通信。默認情況下,這一事件在成功接收到客戶端的應用連接命令之后以 ID 0 發送。這一事件數據為 4 字節,代表了已就緒流的流 ID。 |
Stream EOF (=1) | 服務器端發送這一事件來通知客戶端請求的流的回放數據已經結束。在發送額外的命令之前不再發送任何數據。客戶端將丟棄接收到的這個流的消息。這一事件數據為 4 字節,代表了回放已結束的流的流 ID。 |
StreamDry (=2) | 服務器端發送這一事件來通知客戶端當前流中已沒有數據。當服務器端在一段時間內沒有檢測到任何消息,它可以通知相關客戶端當前流已經沒數據了。這一事件數據為 4 字節,代表了已沒數據的流的流 ID。 |
SetBuffer Length (=3) | 客戶端發送這一事件來通知服務器端用於緩存流中任何數據的緩存大小 (以毫秒為單位)。這一事件在服務器端開始處理流之前就發送。這一事件數據的前 4 個字節代表了流 ID 后 4 個字節代表了以毫秒為單位的緩存的長度。 |
StreamIs Recorded (=4) | 服務器端發送這一事件來通知客戶端當前流是一個錄制流。這一事件數據為 4 字節,代表了錄制流的流 ID。 |
PingRequest (=6) | 服務器端發送這一事件用於測試是否能夠送達客戶端。時間數據是為一個 4 字節的 timestamp,代表了服務器端發送這一命令時的服務器本地時間。客戶端在接收到這一消息后會立即發送 PingResponse 回復。 |
PingResponse (=7) | 客戶端作為對 ping 請求的回復發送這一事件到服務器端。這一事件數據是為一個 4 字節的 timestamp,就是接收自 PingRequest 那個。 |
7.2. 命令類型
客戶端和服務器端交換 AMF 編碼的命令。服務器端發送一個命令消息,這個命令消息由命令名、事務 ID 以及包含有相關參數的命令對象組成。例如,包含有 'app' 參數的連接命令,這個命令說明了客戶端連接到的服務器端的應用名。接收者處理這一命令並回發一個同樣事務 ID 的響應。回復字符串可以是 _result、_error 或者一個方法名的任意一個,比如,verifyClient 或者 contactExternalServer。
命令字符串 _result 或者 _error 是響應信號。事務 ID 指示出響應所指向的命令。這和 AMAP 和其他一些協議的標簽一樣。命令字符串中的方法名表示發送者試圖執行接收者一端的一個方法。
以下類的對象用於發送不同的命令:
NetConnection 代表上層的服務器端和客戶端之間連接的一個對象。
NetStream 一個代表發送音頻流、視頻流和其他相關數據的通道的對象。當然,我們也會發送控制數據流的命令,諸如 play、pause 等等。
7.2.1. NetConnection 命令
NetConnection 管理着一個客戶端應用和服務器端之間的雙相連接。此外,它還提供遠程方法的異步調用。
NetConnection 可以發送以下命令:
- connect
- call
- close
- createStream
7.2.1.1. connect 命令
客戶端發送 connect 命令到服務器端來請求連接到一個服務器應用的實例。
由客戶端發送到服務器端的 connect 命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令的名字。設置給 "connect"。 |
Transaction ID | 數字 | 總是設置為 1。 |
Command Object | 對象 | 具有名值對的命令信息對象。 |
Optional User Arguments | 對象 | 任意可選信息。 |
以下是為 connect 命令中使用的名值對對象的描述。
屬性 | 類型 | 描述 | 范例 |
app | 字符串 | 客戶端連接到的服務器端應用的名字。 | testapp |
flashver | 字符串 | Flash Player 版本號。和ApplicationScript getversion() 方法返回的是同一個字符串。 | FMSc/1.0 |
swfUrl | 字符串 | 進行當前連接的 SWF 文件源地址。例如:app/viewplayer.swf | file://C:/FlvPlayer.swf |
tcUrl | 字符串 | 服務器 URL。具有以下格式:protocol://servername:port/appName/appInstance,例如rtmp:IP:PORT/live/ | 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。可以是undefined | http://somehost/sample.html |
objectEncoding | 數字 | AMF 編碼方法。0、3.一般采用3 | AMF3 |
audioCodecs 屬性的標識值:

videoCodecs 屬性的標識值:

videoFunction 屬性的標識值:

encoding 屬性值:

服務器端到客戶端的命令的結構如下:



命令執行時消息流動如下:
1. 客戶端發送 connect 命令到服務器端以請求對服務器端應用實例的連接。
2. 收到 connect 命令后,服務器端發送協議消息 '窗口確認大小' 到客戶端。服務器端也會連接到 connect 命令中提到的應用。
3. 服務器端發送協議消息 '設置對端帶寬' 到客戶端。
4. 在處理完協議消息 '設置對端帶寬' 之后客戶端發送協議消息 '窗口確認大小' 到服務器端。
5. 服務器端發送另一個用戶控制消息 (StreamBegin) 類型的協議消息到客戶端。
6. 服務器端發送結果命令消息告知客戶端連接狀態 (success/fail)。這一命令定義了事務 ID (常常為 connect 命令設置為 1)。這一消息也定義了一些屬性,比如 FMS 服務器版本 (字符串)。此外,它還定義了其他連接關聯到的信息,比如 level (字符串)、code (字符串)、description (字符串)、objectencoding (數字) 等等。
7.2.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 | 對象 | 調用方法的回復。 |
7.2.1.3. createStream 命令
客戶端發送這一命令到服務器端以為消息連接創建一個邏輯通道。音頻、視頻和元數據使用 createStream 命令創建的流通道傳輸。
NetConnection 是默認的通信通道,流 ID 為 0。協議和一些命令消息,包括 createStream,使用默認的通信通道。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名。設置給 "createStream"。 |
Transaction ID | 數字 | 命令的事務 ID。 |
Command Object | 對象 | 如果存在一些命令信息要設置這個對象,否則置空。 |
服務器端發送給客戶端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | _result 或者 _error;表明回復是一個結果還是錯誤。 |
Transaction ID | 數字 | 響應所屬的命令的 ID。 |
Command Object | 對象 | 如果存在一些命令信息要設置這個對象,否則置空。 |
Stream ID | 數字 | 返回值要么是一個流 ID 要么是一個錯誤信息對象。 |
7.2.2. NetStream 命令
NetStream 定義了傳輸通道,通過這個通道,音頻流、視頻流以及數據消息流可以通過連接客戶端到服務端的 NetConnection 傳輸。
以下命令可以由客戶端使用 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" (字符串):關於這個消息人類可讀描述。 |
7.2.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,表示用戶首先嘗試播放流名字段中定義的直播流。如果那個名字的直播流沒有找到,它將播放同名的錄制流。如果沒有那個名字的錄制流,客戶端將等待一個新的那個名字的直播流,並當其有效時進行播放。如果你在 Start 字段中傳遞 -1,那么就只播放流名中定義的那個名字的直播流。如果你在 Start 字段中傳遞 0 或一個整數,那么將從 Start 字段定義的時間開始播放流名中定義的那個錄制流。如果沒有找到錄制流,那么將播放播放列表中的下一項。 |
Duration | 數字 | 一個可選的參數,以秒為單位定義了回放的持續時間。默認值為 -1。-1 值意味着一個直播流會一直播放直到它不再可用或者一個錄制流一直播放直到結束。如果你傳遞 0 值,它將只播放單一一幀,因為播放時間已經在錄制流的開始的 Start 字段指定了。假定定義在 Start 字段中的值大於或者等於 0。如果你傳遞一個正數,將播放 Duration 字段定義的一段直播流。之后,變為可播放狀態,或者播放 Duration 字段定義的一段錄制流。(如果流在 Duration 字段定義的時間段內結束,那么流結束時回放結束)。如果你在 Duration 字段中傳遞一個 -1 以外的負數的話,它將把你給的值當做 -1 處理。 |
Reset | 布爾 | 一個可選的布爾值或者數字定義了是否對以前的播放列表進行 flush。 |

命令執行時的消息流動是為:
1. 當客戶端從服務器端接收到 createStream 命令的結果是為 success 時,發送 play 命令。
2. 一旦接收到 play 命令,服務器端發送一個協議消息來設置塊大小。
3. 服務器端發送另一個協議消息 (用戶控制),這個消息中定義了 'StreamIsRecorded' 事件和流 ID。消息在前兩個字節中保存事件類型,在后四個字節中保存流 ID。
4. 服務器端發送另一個協議消息 (用戶控制),這一消息包含 'StreamBegin' 事件,來指示發送給客戶端的流的起點。
5. 如果客戶端發送的 play 命令成功,服務器端發送一個 onStatus 命令消息 NetStream.Play.Start & NetStream.Play.Reset。只有當客戶端發送的 play 命令設置了 reset 時服務器端才會發送 NetStream.Play.Reset。如果要播放的流沒有找到,服務器端發送 onStatus 消息 NetStream.Play.StreamNotFound。
之后,服務器端發送視頻和音頻數據,客戶端對其進行播放。
7.2.2.2. play2
不同於 play 命令的是,play2 可以在不改變播放內容時間軸的情況下切換到不同的比特率。服務器端為客戶端可以在 play2 中請求所有支持的碼率維護了不同的字段。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "play2"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息不存在,設置為 null 類型。 |
Parameters | 對象 | 一個 AMF 編碼的對象,該對象的屬性是為公開的 flash.net.NetStreamPlayOptions ActionScript 對象所描述的屬性。 |
NetStreamPlayOptions 對象的公開屬性在 ActionScript 3 語言指南中 [AS3] 有所描述。
命令執行時的消息流動如下圖所示:

7.2.2.3. deleteStream 命令
當 NetStream 對象消亡時 NetStream 發送 deleteStream 命令。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "deleteStream"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設為 null 類型。 |
Stream ID | 數字 | 服務器端消亡的流 ID。 |
服務器端不再發送任何回復。
7.2.2.4. receiveAudio 命令
NetStream 通過發送 receiveAudio 消息來通知服務器端是否發送音頻到客戶端。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "receiveAudio"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設置為 null 類型。 |
Bool Flag | 布爾 | true 或者 false 以表明是否接受音頻。 |
如果發送來的 receiveAudio 命令布爾字段被設為 false 時服務器端不發送任何回復。如果這一標識被設為 true,服務器端以狀態消息 NetStream.Seek.Notify 和 NetStream.Play.Start 進行回復。
7.2.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 進行回復。
7.2.2.6. publish 命令
客戶端發送給服務器端這一命令以發布一個已命名的流。使用這個名字,任意客戶端都可以播放這個流,並接受發布的音頻、視頻以及數據消息。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,設置為 "publish"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設置為 null 類型。 |
Publishing Name | 字符串 | 發布的流的名字。 |
Publishing Type | 字符串 | 發布類型。可以設置為 "live"、"record" 或者 "append"。record:流被發布,數據被錄制到一個新的文件。新文件被存儲在服務器上包含服務應用目錄的子路徑。如果文件已存在,將重寫。append:流被發布,數據被添加到一個文件。如果該文件沒找着,將新建一個。live:直播數據只被發布,並不對其進行錄制。 |
服務器端回復 onStatus 命令以標注發布的起始位置。
7.2.2.7. seek 命令
客戶端發送 seek 命令以查找一個多媒體文件或一個播放列表的偏移量 (以毫秒為單位)。
客戶端發送到服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令的名字,設為 "seek"。 |
Transaction ID | 數字 | 事務 ID 設為 0。 |
Command Object | Null | 沒有命令信息對象,設置為 null 類型。 |
milliSeconds | 數字 | 播放列表查找的毫秒數。 |
seek 命令執行成功時服務器會發送一個狀態消息 NetStream.Seek.Notify。失敗的話,服務器端返回一個 _error 消息。
7.2.2.8. pause 命令
客戶端發送 pause 命令以告知服務器端是暫停還是開始播放。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,設為 "pause"。 |
Transaction ID | 數字 | 沒有這一命令的事務 ID,設為 0。 |
Command Object | Null | 命令信息對象不存在,設為 null 類型。 |
Pause/Unpause Flag | 布爾 | true 或者 false,來指示暫停或者重新播放。 |
milliSeconds | 數字 | 流暫停或者重新開始所在的毫秒數。這個是客戶端暫停的當前流時間。當回放已恢復時,服務器端值發送帶有比這個值大的 timestamp 消息。 |
當流暫停時,服務器端發送一個狀態消息 NetStream.Pause.Notify。NetStream.Unpause.Notify 只有針對沒有暫停的流進行發放。失敗的話,服務器端返回一個 _error 消息。
7.3. 消息交換例子
這里有幾個解釋使用 RTMP 交換消息的例子。
7.3.1. 發布錄制視頻
這個例子說明了一個客戶端是如何能夠發布一個直播流然后傳遞視頻流到服務器的。然后其他客戶端可以對發布的流進行訂閱並播放視頻。

7.3.2. 廣播一個共享對象消息
這個例子說明了在一個共享對象的創建和改變期間交換消息的變化。它也說明了共享對象消息廣播的處理過程。

7.3.3. 發布來自錄制流的元數據
這個例子描述了用於發布元數據的消息交換。

8. 參考文獻
[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, <http://www.adobe.com/devnet/actionscript/documentation.html>.
[AMF0] Adobe Systems, Inc., "Action Message Format -- AMF 0", December 2007, <http://opensource.adobe.com/wiki/download/attachments/1114283/amf0_spec_121207.pdf>.
[AMF3] Adobe Systems, Inc., "Action Message Format -- AMF 3", May 2008, <http://opensource.adobe.com/wiki/download/attachments/1114283/amf3_spec_05_05_08.pdf>.
作者地址
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/
原文鏈接:http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf
譯序:
本文是為截至發稿時止最新 Adobe 官方公布的 RTMP 規范。本文包含 RTMP 規范的全部內容。是第一個比較全面的 RTMP 規范的中譯本。由於成文時間倉促,加上作者知識面所限,翻譯錯誤之處在所難免,懇請各位朋友熱心指出,可以直接在博客后面留言,先行謝過。rtmp_specification_1.0.pdf 官方下載地址http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf,國內下載地址http://download.csdn.net/detail/defonds/6312051。請隨時關注官方文檔更新:http://www.adobe.com/cn/devnet/rtmp.html。以下內容來自 rtmp_specification_1.0.pdf。
1. 簡介
Adobe 公司的實時消息傳輸協議 (RTMP) 通過一個可靠地流傳輸提供了一個雙向多通道消息服務,比如 TCP [RFC0793],意圖在通信端之間傳遞帶有時間信息的視頻、音頻和數據消息流。實現通常對不同類型的消息分配不同的優先級,當運載能力有限時,這會影響等待流傳輸的消息的次序。
本文檔將對實時流傳輸協議 (Real Time Messaging Protocol) 的語法和操作進行描述。
1.1. 術語
本文檔中出現的關鍵字,"MUST"、"MUST NOT"、"REQUIRED"、"SHALL"、"SHALL NOT"、"SHOULD"、"SHOULD NOT"、"RECOMMENDED"、"NOT RECOMMENDED"、"MAY" 、"OPTIONAL",都將在 [RFC2119] 中進行解釋。
2. 貢獻者
Rajesh Mallipeddi,Adobe Systems 原成員,起草了本文檔原始規范,並提供大部分的原始內容。
Mohit Srivastava,Adobe Systems 成員,促成了本規范的開發。
3. 名詞解釋
Payload (有效載荷):包含於一個數據包中的數據,例如音頻采樣或者壓縮的視頻數據。payload 的格式和解釋,超出了本文檔的范圍。
Packet (數據包):一個數據包由一個固定頭和有效載荷數據構成。一些個底層協議可能會要求對數據包定義封裝。
Port (端口):"傳輸協議用以區分開指定一台主機的不同目的地的一個抽象。TCP/IP 使用小的正整數對端口進行標識。" OSI 傳輸層使用的運輸選擇器 (TSEL) 相當於端口。
Transport address (傳輸地址):用以識別傳輸層端點的網絡地址和端口的組合,例如一個 IP 地址和一個 TCP 端口。數據包由一個源傳輸地址傳送到一個目的傳輸地址。
Message stream (消息流):通信中消息流通的一個邏輯通道。
Message stream ID (消息流 ID):每個消息有一個關聯的 ID,使用 ID 可以識別出流通中的消息流。
Chunk (塊):消息的一段。消息在網絡發送之前被拆分成很多小的部分。塊可以確保端到端交付所有消息有序 timestamp,即使有很多不同的流。
Chunk stream (塊流):通信中允許塊流向一個特定方向的邏輯通道。塊流可以從客戶端流向服務器,也可以從服務器流向客戶端。
Chunk stream ID (塊流 ID):每個塊有一個關聯的 ID,使用 ID 可以識別出流通中的塊流。
Multiplexing (合成):將獨立的音頻/視頻數據合成為一個連續的音頻/視頻流的加工,這樣可以同時發送幾個視頻和音頻。
DeMultiplexing (分解):Multiplexing 的逆向處理,將交叉的音頻和視頻數據還原成原始音頻和視頻數據的格式。
Remote Procedure Call (RPC 遠程方法調用):允許客戶端或服務器調用對端的一個子程序或者程序的請求。
Metadata (元數據):關於數據的一個描述。一個電影的 metadata 包括電影標題、持續時間、創建時間等等。
Application Instance (應用實例):服務器上應用的實例,客戶端可以連接這個實例並發送連接請求。
Action Message Format (AMF 動作消息格式協議):一個用於序列化 ActionScript 對象圖的緊湊的二進制格式。AMF 有兩個版本:AMF 0 [AMF0] 和 AMF 3 [AMF3]。
4. 字節序、對齊和時間格式
所有整數型屬性以網絡字節順序傳輸,字節 0 代表第一個字節,零位是一個單詞或字段最常用的有效位。字節序通常是大端排序。關於傳輸順序的更多細節描述參考 IP 協議 [RFC0791]。除非另外注明,本文檔中的數值常量都是十進制的 (以 10 為基礎)。
除非另有規定,RTMP 中的所有數據都是字節對准的;例如,一個十六位的屬性可能會在一個奇字節偏移上。填充后,填充字節應該有零值。
RTMP 中的 Timestamps 以一個整數形式給出,表示一個未指明的時間點。典型地,每個流會以一個為 0 的 timestamp 起始,但這不是必須的,只要雙端能夠就時間點達成一致。注意這意味着任意不同流 (尤其是來自不同主機的) 的同步需要 RTMP 之外的機制。
因為 timestamp 的長度為 32 位,每隔 49 天 17 小時 2 分鍾和 47.296 秒就要重來一次。因為允許流連續傳輸,有可能要多年,RTMP 應用在處理 timestamp 時應該使用序列碼算法 [RFC1982],並且能夠處理無限循環。例如,一個應用假定所有相鄰的 timestamp 都在 2^31 - 1 毫秒之內,因此 10000 在 4000000000 之后,而 3000000000 在 4000000000 之前。
timestamp 也可以使用無符整數定義,相對於前面的 timestamp。timestamp 的長度可能會是 24 位或者 32 位。
5. RTMP 塊流
本節介紹實時消息傳輸協議的塊流 (RTMP 塊流)。 它為上層多媒體流協議提供合並和打包的服務。
當設計 RTMP 塊流使用實時消息傳輸協議時,它可以處理任何發送消息流的協議。每個消息包含 timestamp 和 payload 類型標識。RTMP塊流和RTMP一起適合各種音頻-視頻應用,從一對一和一對多直播到點播服務,到互動會議應用。
當使用可靠傳輸協議時,比如 TCP [RFC0793],RTMP 塊流能夠對於多流提供所有消息可靠的 timestamp 有序端對端傳輸。RTMP 塊流並不提供任何優先權或類似形式的控制,但是可以被上層協議用來提供這種優先級。例如,一個直播視頻服務器可能會基於發送時間或者每個消息的確認時間丟棄一個傳輸緩慢的客戶端的視頻消息以確保及時獲取其音頻消息。
RTMP 塊流包括其自身的帶內協議控制信息,並且提供機制為上層協議植入用戶控制消息。
5.1 消息格式
可以被分割為塊以支持組合的消息的格式取決於上層協議。消息格式必須包含以下創建塊所需的字段。
Timestamp:消息的 timestamp。這個字段可以傳輸四個字節。
Length:消息的有效負載長度。如果不能省略掉消息頭,那它也被包括進這個長度。這個字段占用了塊頭的三個字節。
Type Id:一些類型 ID 保留給協議控制消息使用。這些傳播信息的消息由 RTMP 塊流協議和上層協議共同處理。其他的所有類型 ID 可用於上層協議,它們被 RTMP 塊流處理為不透明值。事實上,RTMP 塊流中沒有任何地方要把這些值當做類型使用;所有消息必須是同一類型,或者應用使用這一字段來區分同步跟蹤,而不是類型。這一字段占用了塊頭的一個字節。
Message Stream ID:message stream (消息流) ID 可以使任意值。合並到同一個塊流的不同的消息流是根據各自的消息流 ID 進行分解。除此之外,對 RTMP 塊流而言,這是一個不透明的值。這個字段以小端格式占用了塊頭的四個字節。
5.2 簡單握手(simple handshake)
這里主要討論的是簡單握手協議。simple handshake是rtmp spec 1.0定義的握手方式。而complex handshake是后來變更的方式,Adobe沒有公開。在此暫不討論。
一個 RTMP 連接以握手開始。RTMP 的握手不同於其他協議;RTMP 握手由三個固定長度的塊組成,而不是像其他協議一樣的帶有報頭的可變長度的塊。
客戶端 (發起連接請求的終端) 和服務器端各自發送相同的三塊。便於演示,當發送自客戶端時這些塊被指定為 C0、C1 和 C2;當發送自服務器端時這些塊分別被指定為 S0、S1 和 S2。
5.2.1. 握手順序
握手以客戶端發送 C0 和 C1 塊開始。
客戶端必須等待接收到 S1 才能發送 C2。
客戶端必須等待接收到 S2 才能發送任何其他數據。
服務器端必須等待接收到 C0 才能發送 S0 和 S1,也可以等待接收到 C1 再發送 S0 和 S1。服務器端必須等待接收到 C1 才能發送 S2。服務器端必須等待接收到 C2 才能發送任何其他數據。
5.2.2. C0 和 S0 的格式
C0 和 S0 包都是一個單一的八位字節,以一個單獨的八位整型域進行處理:

以下是 C0/S0 包中的字段:
版本號 (八位):在 C0 中,這一字段指示出客戶端要求的 RTMP 版本號。在 S0 中,這一字段指示出服務器端選擇的 RTMP 版本號。本文檔中規范的版本號為 3。0、1、2 三個值是由早期其他產品使用的,是廢棄值;4 - 31 被保留為 RTMP 協議的未來實現版本使用;32 - 255 不允許使用(以區分開 RTMP 和其他常以一個可打印字符開始的文本協議)。無法識別客戶端所請求版本號的服務器應該以版本 3 響應,(收到響應的) 客戶端可以選擇降低到版本 3,或者放棄握手。
如上圖,是一個c0+c1一起發送的抓包例子(complex handshake的)。其中選中的字節為c0字段,代表版本號。后面的數據是c1字段。整體包長1537.
5.2.3. C1 和 S1 的格式
C1 和 S1 數據包的長度都是 1536 字節,包含以下字段:

Time (四個字節):這個字段包含一個 timestamp,用於本終端發送的所有后續塊的時間起點。這個值可以是 0,或者一些任意值。要同步多個塊流,終端可以發送其他塊流當前的 timestamp 的值。
Zero (四個字節):這個字段必須都是 0。
Random data (1528 個字節):這個字段可以包含任意值。終端需要區分出響應來自它發起的握手還是對端發起的握手,這個數據應該發送一些足夠隨機的數。這個不需要對隨機數進行加密保護,也不需要動態值。
5.2.4. C2 和 S2 的格式
C2 和 S2 數據包長度都是 1536 字節,基本就是 S1 和 C1 的副本 (分別),包含有以下字段:

Time (四個字節):這個字段必須包含終端在 S1 (給 C2) 或者 C1 (給 S2) 發的 timestamp。
Time2 (四個字節):這個字段必須包含終端先前發出數據包 (s1 或者 c1) timestamp。
Random echo (1528 個字節):這個字段必須包含終端發的 S1 (給 C2) 或者 S2 (給 C1) 的隨機數。兩端都可以一起使用 time 和 time2 字段再加當前 timestamp 以快速估算帶寬和/或者連接延遲,但這不太可能是有多大用處。
5.2.5. 握手示意圖

下面描述了握手示意圖中提到的狀態:
Uninitialized (未初始化):協議的版本號在這個階段被發送。客戶端和服務器都是 uninitialized (未初始化) 狀態。之后客戶端在數據包 C0 中將協議版本號發出。如果服務器支持這個版本,它將在回應中發送 S0 和 S1。如果不支持呢,服務器會才去適當的行為進行響應。在 RTMP 協議中,這個行為就是終止連接。
Version Sent (版本已發送):在未初始化狀態之后,客戶端和服務器都進入 Version Sent (版本已發送) 狀態。客戶端會等待接收數據包 S1 而服務器在等待 C1。一旦拿到期待的包,客戶端會發送數據包 C2 而服務器發送數據包 S2。(客戶端和服務器各自的)狀態隨即變為 Ack Sent (確認已發送)。
Ack Sent (確認已發送):客戶端和服務器分別等待 S2 和 C2。
Handshake Done (握手結束):客戶端和服務器可以開始交換消息了。
5.3 復雜握手(complex handshake)
rtmp 1.0規范中,指定了RTMP的握手協議:
- c0/s0:一個字節,說明是明文還是加密。
- c1/s1: 1536字節,4字節時間,4字節0x00,1528字節隨機數
- c2/s2: 1536字節,4字節時間1,4字節時間2,1528隨機數和s1相同。這個就是srs以及其他開源軟件所謂的simple handshake,簡單握手,標准握手,FMLE也是使用這個握手協議。
Flash播放器連接服務器時,若服務器只支持簡單握手,則無法播放h264和aac的流,可能是adobe的限制。adobe將簡單握手改為了有一系列加密算法的復雜握手(complex handshake)。下表是總結:
由上表可知,當服務器和客戶端的握手是按照rtmp協議進行,是不支持h264/aac的,有數據,就是沒有視頻和聲音。原因是adobe變更了握手的數據結構,標准rtmp協議的握手的包是隨機的1536字節(S1S2C1C2),變更后的是需要進行摘要和加密。rtmp協議定義的為simple handshake,變更后加密握手可稱為complex handshake。
本文詳細分析了rtmpd(ccrtmpserver)中的處理邏輯,以及rtmpdump的處理邏輯,從一個全是魔法數字的世界找到他們的數據結構和算法。
5.3.1. complex handshake C1 S1結構(此處參照了winlin博客的相關文章)
complex handshake將C1S1分為4個部分,它們的順序(schema)一種可能是:
- // c1s1 schema0
- time: 4bytes
- version: 4bytes
- key: 764bytes
- digest: 764bytes
其中,key和digest可能會交換位置,即schema可能是:
- // c1s1 schema1
- time: 4bytes
- version: 4bytes
- digest: 764bytes
- key: 764bytes
客戶端來決定使用哪種schema,服務器端則需要先嘗試按照schema0解析,失敗則用schema1解析。如下圖所示:
無論key和digest位置如何,它們的結構是不變的:
- // 764bytes key結構
- random-data: (offset)bytes
- key-data: 128bytes
- random-data: (764-offset-128-4)bytes
- offset: 4bytes
- // 764bytes digest結構
- offset: 4bytes
- random-data: (offset)bytes
- digest-data: 32bytes
- random-data: (764-4-offset-32)bytes
備注:發現FMS只認識digest-key結構。
如下圖所示:
crtmp中這些全是魔法數字。
5.3.2.complex handshake C2 S2結構:
c2 s2主要是用來提供對C1 S1的驗證,結構如下:
- // 1536bytes C2S2結構
- random-data: 1504bytes
- digest-data: 32bytes
C2S2的結構相對比較簡單。如下圖所示:
下面介紹C1S1C2S2的生成以及驗證算法。
5.3.3.complex handshake C1 S1算法
C1S1中都是包含32字節的digest,而且digest將C1S1分成兩部分:
- // C1S1被digest分成兩部分
- c1s1-part1: n bytes
- digest-data: 32bytes
- c1s1-part2: (1536-n-32)bytes
如下圖所示:
在生成C1時,需要用到c1s1-part1和c1s1-part2這兩個部分的字節拼接起來的字節,定義為:
- c1s1-joined = bytes_join(c1s1-part1, c1s1-part2)
也就是說,把1536字節的c1s1中的32字節的digest拿剪刀剪掉,剩下的頭和尾加在一起就是c1s1-joined。用到的兩個常量FPKey和FMSKey:
- u_int8_t FMSKey[] = {
- 0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
- 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
- 0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
- 0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
- 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
- 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
- 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
- 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
- }; // 68
- u_int8_t FPKey[] = {
- 0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
- 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
- 0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
- 0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
- 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
- 0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
- 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
- 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
- }; // 62
生成C1的算法如下:
- calc_c1_digest(c1, schema) {
- get c1s1-joined from c1 by specified schema
- digest-data = HMACsha256(c1s1-joined, FPKey, 30)
- return digest-data;
- }
- random fill 1536bytes c1 // also fill the c1-128bytes-key
- time = time() // c1[0-3]
- version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
- schema = choose schema0 or schema1
- digest-data = calc_c1_digest(c1, schema)
- copy digest-data to c1
生成S1的算法如下:
- /*decode c1 try schema0 then schema1*/
- c1-digest-data = get-c1-digest-data(schema0)
- if c1-digest-data equals to calc_c1_digest(c1, schema0) {
- c1-key-data = get-c1-key-data(schema0)
- schema = schema0
- } else {
- c1-digest-data = get-c1-digest-data(schema1)
- if c1-digest-data not equals to calc_c1_digest(c1, schema1) {
- switch to simple handshake.
- return
- }
- c1-key-data = get-c1-key-data(schema1)
- schema = schema1
- }
- /*generate s1*/
- random fill 1536bytes s1
- time = time() // c1[0-3]
- version = [0x04, 0x05, 0x00, 0x01] // s1[4-7]
- DH_compute_key(key = c1-key-data, pub_key=s1-key-data)
- get c1s1-joined by specified schema
- s1-digest-data = HMACsha256(c1s1-joined, FMSKey, 36)
- copy s1-digest-data and s1-key-data to s1.
C1S1的算法完畢。
5.3.4.complex handshake C2 S2
C2S2的生成算法如下:
- random fill c2s2 1536 bytes
- // client generate C2, or server valid C2
- temp-key = HMACsha256(s1-digest, FPKey, 62)
- c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
- // server generate S2, or client valid S2
- temp-key = HMACsha256(c1-digest, FMSKey, 68)
- s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
驗證的算法是一樣的。
5.3.5.解析補充
讀rtmpd(ccrtmpserver)代碼發現,handshake確實變更了。BTW:rtmpd的代碼可讀性要強很多,很快就能知道它在做什么;不過,它是部分C++部分C的混合體;譬如,使用了BaseRtmpProtocol和InboundRtmpProtocol這種C++的解決方式;以及在解析complex handshake時對1536字節的包直接操作,offset=buf[772]+buf[773]+buf[774]+buf[775],這個就很難看明白在做什么了,其實1536是由4字節的time+4字節的version+764字節的key+764字節的digest,key的offset在后面,digest的offset在前面,若定義兩個結構再讓它們自己去解析,就很明白在做什么了。
sources\thelib\src\protocols\rtmp\inboundrtmpprotocol.cpp: 51
- InboundRTMPProtocol::PerformHandshake
- // 沒有完成握手。
- case RTMP_STATE_NOT_INITIALIZED:
- // buffer[1537]
- // 第一個字節,即c0,表示握手類型(03是明文,06是加密,其他值非法)
- handshakeType = GETIBPOINTER(buffer)[0];
- // 刪除第一個字節,buffer[1536] 即c1
- buffer.Ignore(1);
- // 第5-8共4個字節表示FPVersion,這個必須非0,0表示不支持complex handshake。
- _currentFPVersion = ENTOHLP(GETIBPOINTER(buffer) + 4);
- // 進行明文握手(false表示明文)
- PerformHandshake(buffer, false);
- InboundRTMPProtocol::PerformHandshake
- // 先驗證client,即驗證c1
- // 先嘗試scheme0
- valid = ValidClientScheme(0);
- // 若失敗,再嘗試scheme1
- valid = ValidClientScheme(1)
- // 若驗證成功
- if(valid)
- // 復雜的handshake:PerformComplexHandshake,主要流程如下:
- S0 = 3或6
- 隨機數初始化S1S2
- 根據C1的public key生成S1的128byets public key
- 生成S1的32bytes digest
- 根據C1和S2生成S2的32bytes digest
- else
- // rtmp spec 1.0定義的握手方式
- PerformSimpleHandshake();
其實到后面看明白了,scheme1和scheme2這兩種方式,是包結構的調換。
- <strong>complex的包結構如下:</strong>C1/S1 1536bytes
- time: 4bytes 開頭是4字節的當前時間。(u_int32_t)time(NULL)
- peer_version: 4bytes 為程序版本。C1一般是0x80000702。S1是0x04050001。
- 764bytes: 可能是KeyBlock或者DigestBlock
- 764bytes: 可能是KeyBlock或者DigestBlock
- 其中scheme1就是KeyBlock在前面DigestBlock在后面,而scheme0是DigestBlock在前面KeyBlock在后面。
- <strong>子結構KeyBlock定義:</strong>
- 760bytes: 包含128bytes的key的數據。
- key_offset: 4bytes 最后4字節定義了key的offset(相對於KeyBlock開頭而言)
- <pre name="code" class="cpp"><strong>子結構DigestBlock定義:</strong>
- digest_offset: 4bytes 開頭4字節定義了digest的offset(相對於第DigestBlock的第5字節而言,offset=3表示digestBlock[7~38]為digest
- 760bytes: 包含32bytes的digest的數據。
其中,key和digest的主要算法是:C1的key為128bytes隨機數。C1_32bytes_digest = HMACsha256(P1+P2, 1504, FPKey, 30) ,其中P1為digest之前的部分,P2為digest之后的部分,P1+P2是將這兩部分拷貝到新的數組,共1536-32長度。S1的key根據C1的key算出來,算法如下:
- DHWrapper dhWrapper(1024);
- dhWrapper.Initialize()
- dhWrapper.CreateSharedKey(c1_key, 128)
- dhWrapper.CopyPublicKey(s1_key, 128)
S1的digest算法同C1。注意,必須先計算S1的key,因為key變化后digest也也重新計算。
S2/C2沒有key,只有一個digest,是根據C1/S1算出來的:
- 先用隨機數填充S2
- s2data=S2[0-1504]; 前1502字節為隨機的數據。
- s2digest=S2[1505-1526] 后32bytes為digest。
- // 計算s2digest方法如下:
- ptemphash[512]: HMACsha256(c1digest, 32, FMSKey, 68, ptemhash)
- ps2hash[512]: HMACsha256(s2data, 1504, ptemphash, 32, ps2hash)
- 將ps2hash[0-31]拷貝到s2的后32bytes。
5.3.6.代碼實現
- /*
- The MIT License (MIT)
- Copyright (c) 2013 winlin
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
- #ifndef SRS_CORE_COMPLEX_HANDSHKAE_HPP
- #define SRS_CORE_COMPLEX_HANDSHKAE_HPP
- /*
- #include <srs_core_complex_handshake.hpp>
- */
- #include <srs_core.hpp>
- class SrsSocket;
- /**
- * rtmp complex handshake,
- * @see also crtmp(crtmpserver) or librtmp,
- * @see also: http://blog.csdn.net/win_lin/article/details/13006803
- * @doc update the README.cmd
- */
- class SrsComplexHandshake
- {
- public:
- SrsComplexHandshake();
- virtual ~SrsComplexHandshake();
- public:
- /**
- * complex hanshake.
- * @_c1, size of c1 must be 1536.
- * @remark, user must free the c1.
- * @return user must:
- * continue connect app if success,
- * try simple handshake if error is ERROR_RTMP_TRY_SIMPLE_HS,
- * otherwise, disconnect
- */
- virtual int handshake(SrsSocket& skt, char* _c1);
- };
- #endif
- /*
- The MIT License (MIT)
- Copyright (c) 2013 winlin
- Permission is hereby granted, free of charge, to any person obtaining a copy of
- this software and associated documentation files (the "Software"), to deal in
- the Software without restriction, including without limitation the rights to
- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- the Software, and to permit persons to whom the Software is furnished to do so,
- subject to the following conditions:
- The above copyright notice and this permission notice shall be included in all
- copies or substantial portions of the Software.
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
- #include <srs_core_complex_handshake.hpp>
- #include <time.h>
- #include <stdlib.h>
- #include <srs_core_error.hpp>
- #include <srs_core_log.hpp>
- #include <srs_core_auto_free.hpp>
- #include <srs_core_socket.hpp>
- // 68bytes FMS key which is used to sign the sever packet.
- u_int8_t SrsGenuineFMSKey[] = {
- 0x47, 0x65, 0x6e, 0x75, 0x69, 0x6e, 0x65, 0x20,
- 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x46, 0x6c,
- 0x61, 0x73, 0x68, 0x20, 0x4d, 0x65, 0x64, 0x69,
- 0x61, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72,
- 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Media Server 001
- 0xf0, 0xee, 0xc2, 0x4a, 0x80, 0x68, 0xbe, 0xe8,
- 0x2e, 0x00, 0xd0, 0xd1, 0x02, 0x9e, 0x7e, 0x57,
- 0x6e, 0xec, 0x5d, 0x2d, 0x29, 0x80, 0x6f, 0xab,
- 0x93, 0xb8, 0xe6, 0x36, 0xcf, 0xeb, 0x31, 0xae
- }; // 68
- // 62bytes FP key which is used to sign the client packet.
- u_int8_t SrsGenuineFPKey[] = {
- 0x47, 0x65, 0x6E, 0x75, 0x69, 0x6E, 0x65, 0x20,
- 0x41, 0x64, 0x6F, 0x62, 0x65, 0x20, 0x46, 0x6C,
- 0x61, 0x73, 0x68, 0x20, 0x50, 0x6C, 0x61, 0x79,
- 0x65, 0x72, 0x20, 0x30, 0x30, 0x31, // Genuine Adobe Flash Player 001
- 0xF0, 0xEE, 0xC2, 0x4A, 0x80, 0x68, 0xBE, 0xE8,
- 0x2E, 0x00, 0xD0, 0xD1, 0x02, 0x9E, 0x7E, 0x57,
- 0x6E, 0xEC, 0x5D, 0x2D, 0x29, 0x80, 0x6F, 0xAB,
- 0x93, 0xB8, 0xE6, 0x36, 0xCF, 0xEB, 0x31, 0xAE
- }; // 62
- #include <openssl/evp.h>
- #include <openssl/hmac.h>
- int openssl_HMACsha256(const void* data, int data_size, const void* key, int key_size, void* digest) {
- HMAC_CTX ctx;
- HMAC_CTX_init(&ctx);
- HMAC_Init_ex(&ctx, (unsigned char*) key, key_size, EVP_sha256(), NULL);
- HMAC_Update(&ctx, (unsigned char *) data, data_size);
- unsigned int digest_size;
- HMAC_Final(&ctx, (unsigned char *) digest, &digest_size);
- HMAC_CTX_cleanup(&ctx);
- if (digest_size != 32) {
- return ERROR_OpenSslSha256DigestSize;
- }
- return ERROR_SUCCESS;
- }
- #include <openssl/dh.h>
- #define RFC2409_PRIME_1024 \
- "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1" \
- "29024E088A67CC74020BBEA63B139B22514A08798E3404DD" \
- "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245" \
- "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED" \
- "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381" \
- "FFFFFFFFFFFFFFFF"
- int __openssl_generate_key(
- u_int8_t*& _private_key, u_int8_t*& _public_key, int32_t& size,
- DH*& pdh, int32_t& bits_count, u_int8_t*& shared_key, int32_t& shared_key_length, BIGNUM*& peer_public_key
- ){
- int ret = ERROR_SUCCESS;
- //1. Create the DH
- if ((pdh = DH_new()) == NULL) {
- ret = ERROR_OpenSslCreateDH;
- return ret;
- }
- //2. Create his internal p and g
- if ((pdh->p = BN_new()) == NULL) {
- ret = ERROR_OpenSslCreateP;
- return ret;
- }
- if ((pdh->g = BN_new()) == NULL) {
- ret = ERROR_OpenSslCreateG;
- return ret;
- }
- //3. initialize p, g and key length
- if (BN_hex2bn(&pdh->p, RFC2409_PRIME_1024) == 0) {
- ret = ERROR_OpenSslParseP1024;
- return ret;
- }
- if (BN_set_word(pdh->g, 2) != 1) {
- ret = ERROR_OpenSslSetG;
- return ret;
- }
- //4. Set the key length
- pdh->length = bits_count;
- //5. Generate private and public key
- if (DH_generate_key(pdh) != 1) {
- ret = ERROR_OpenSslGenerateDHKeys;
- return ret;
- }
- // CreateSharedKey
- if (pdh == NULL) {
- ret = ERROR_OpenSslGenerateDHKeys;
- return ret;
- }
- if (shared_key_length != 0 || shared_key != NULL) {
- ret = ERROR_OpenSslShareKeyComputed;
- return ret;
- }
- shared_key_length = DH_size(pdh);
- if (shared_key_length <= 0 || shared_key_length > 1024) {
- ret = ERROR_OpenSslGetSharedKeySize;
- return ret;
- }
- shared_key = new u_int8_t[shared_key_length];
- memset(shared_key, 0, shared_key_length);
- peer_public_key = BN_bin2bn(_private_key, size, 0);
- if (peer_public_key == NULL) {
- ret = ERROR_OpenSslGetPeerPublicKey;
- return ret;
- }
- if (DH_compute_key(shared_key, peer_public_key, pdh) == -1) {
- ret = ERROR_OpenSslComputeSharedKey;
- return ret;
- }
- // CopyPublicKey
- if (pdh == NULL) {
- ret = ERROR_OpenSslComputeSharedKey;
- return ret;
- }
- int32_t keySize = BN_num_bytes(pdh->pub_key);
- if ((keySize <= 0) || (size <= 0) || (keySize > size)) {
- //("CopyPublicKey failed due to either invalid DH state or invalid call"); return ret;
- ret = ERROR_OpenSslInvalidDHState;
- return ret;
- }
- if (BN_bn2bin(pdh->pub_key, _public_key) != keySize) {
- //("Unable to copy key"); return ret;
- ret = ERROR_OpenSslCopyKey;
- return ret;
- }
- return ret;
- }
- int openssl_generate_key(char* _private_key, char* _public_key, int32_t size)
- {
- int ret = ERROR_SUCCESS;
- // Initialize
- DH* pdh = NULL;
- int32_t bits_count = 1024;
- u_int8_t* shared_key = NULL;
- int32_t shared_key_length = 0;
- BIGNUM* peer_public_key = NULL;
- ret = __openssl_generate_key(
- (u_int8_t*&)_private_key, (u_int8_t*&)_public_key, size,
- pdh, bits_count, shared_key, shared_key_length, peer_public_key
- );
- if (pdh != NULL) {
- if (pdh->p != NULL) {
- BN_free(pdh->p);
- pdh->p = NULL;
- }
- if (pdh->g != NULL) {
- BN_free(pdh->g);
- pdh->g = NULL;
- }
- DH_free(pdh);
- pdh = NULL;
- }
- if (shared_key != NULL) {
- delete[] shared_key;
- shared_key = NULL;
- }
- if (peer_public_key != NULL) {
- BN_free(peer_public_key);
- peer_public_key = NULL;
- }
- return ret;
- }
- // the digest key generate size.
- #define OpensslHashSize 512
- /**
- * 764bytes key結構
- * random-data: (offset)bytes
- * key-data: 128bytes
- * random-data: (764-offset-128-4)bytes
- * offset: 4bytes
- */
- struct key_block
- {
- // (offset)bytes
- char* random0;
- int random0_size;
- // 128bytes
- char key[128];
- // (764-offset-128-4)bytes
- char* random1;
- int random1_size;
- // 4bytes
- int32_t offset;
- };
- // calc the offset of key,
- // the key->offset cannot be used as the offset of key.
- int srs_key_block_get_offset(key_block* key)
- {
- int max_offset_size = 764 - 128 - 4;
- int offset = 0;
- u_int8_t* pp = (u_int8_t*)&key->offset;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- return offset % max_offset_size;
- }
- // create new key block data.
- // if created, user must free it by srs_key_block_free
- void srs_key_block_init(key_block* key)
- {
- key->offset = (int32_t)rand();
- key->random0 = NULL;
- key->random1 = NULL;
- int offset = srs_key_block_get_offset(key);
- srs_assert(offset >= 0);
- key->random0_size = offset;
- if (key->random0_size > 0) {
- key->random0 = new char[key->random0_size];
- for (int i = 0; i < key->random0_size; i++) {
- *(key->random0 + i) = rand() % 256;
- }
- }
- for (int i = 0; i < (int)sizeof(key->key); i++) {
- *(key->key + i) = rand() % 256;
- }
- key->random1_size = 764 - offset - 128 - 4;
- if (key->random1_size > 0) {
- key->random1 = new char[key->random1_size];
- for (int i = 0; i < key->random1_size; i++) {
- *(key->random1 + i) = rand() % 256;
- }
- }
- }
- // parse key block from c1s1.
- // if created, user must free it by srs_key_block_free
- // @c1s1_key_bytes the key start bytes, maybe c1s1 or c1s1+764
- int srs_key_block_parse(key_block* key, char* c1s1_key_bytes)
- {
- int ret = ERROR_SUCCESS;
- char* pp = c1s1_key_bytes + 764;
- pp -= sizeof(int32_t);
- key->offset = *(int32_t*)pp;
- key->random0 = NULL;
- key->random1 = NULL;
- int offset = srs_key_block_get_offset(key);
- srs_assert(offset >= 0);
- pp = c1s1_key_bytes;
- key->random0_size = offset;
- if (key->random0_size > 0) {
- key->random0 = new char[key->random0_size];
- memcpy(key->random0, pp, key->random0_size);
- }
- pp += key->random0_size;
- memcpy(key->key, pp, sizeof(key->key));
- pp += sizeof(key->key);
- key->random1_size = 764 - offset - 128 - 4;
- if (key->random1_size > 0) {
- key->random1 = new char[key->random1_size];
- memcpy(key->random1, pp, key->random1_size);
- }
- return ret;
- }
- // free the block data create by
- // srs_key_block_init or srs_key_block_parse
- void srs_key_block_free(key_block* key)
- {
- if (key->random0) {
- srs_freepa(key->random0);
- }
- if (key->random1) {
- srs_freepa(key->random1);
- }
- }
- /**
- * 764bytes digest結構
- * offset: 4bytes
- * random-data: (offset)bytes
- * digest-data: 32bytes
- * random-data: (764-4-offset-32)bytes
- */
- struct digest_block
- {
- // 4bytes
- int32_t offset;
- // (offset)bytes
- char* random0;
- int random0_size;
- // 32bytes
- char digest[32];
- // (764-4-offset-32)bytes
- char* random1;
- int random1_size;
- };
- // calc the offset of digest,
- // the key->offset cannot be used as the offset of digest.
- int srs_digest_block_get_offset(digest_block* digest)
- {
- int max_offset_size = 764 - 32 - 4;
- int offset = 0;
- u_int8_t* pp = (u_int8_t*)&digest->offset;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- offset += *pp++;
- return offset % max_offset_size;
- }
- // create new digest block data.
- // if created, user must free it by srs_digest_block_free
- void srs_digest_block_init(digest_block* digest)
- {
- digest->offset = (int32_t)rand();
- digest->random0 = NULL;
- digest->random1 = NULL;
- int offset = srs_digest_block_get_offset(digest);
- srs_assert(offset >= 0);
- digest->random0_size = offset;
- if (digest->random0_size > 0) {
- digest->random0 = new char[digest->random0_size];
- for (int i = 0; i < digest->random0_size; i++) {
- *(digest->random0 + i) = rand() % 256;
- }
- }
- for (int i = 0; i < (int)sizeof(digest->digest); i++) {
- *(digest->digest + i) = rand() % 256;
- }
- digest->random1_size = 764 - 4 - offset - 32;
- if (digest->random1_size > 0) {
- digest->random1 = new char[digest->random1_size];
- for (int i = 0; i < digest->random1_size; i++) {
- *(digest->random1 + i) = rand() % 256;
- }
- }
- }
- // parse digest block from c1s1.
- // if created, user must free it by srs_digest_block_free
- // @c1s1_digest_bytes the digest start bytes, maybe c1s1 or c1s1+764
- int srs_digest_block_parse(digest_block* digest, char* c1s1_digest_bytes)
- {
- int ret = ERROR_SUCCESS;
- char* pp = c1s1_digest_bytes;
- digest->offset = *(int32_t*)pp;
- pp += sizeof(int32_t);
- digest->random0 = NULL;
- digest->random1 = NULL;
- int offset = srs_digest_block_get_offset(digest);
- srs_assert(offset >= 0);
- digest->random0_size = offset;
- if (digest->random0_size > 0) {
- digest->random0 = new char[digest->random0_size];
- memcpy(digest->random0, pp, digest->random0_size);
- }
- pp += digest->random0_size;
- memcpy(digest->digest, pp, sizeof(digest->digest));
- pp += sizeof(digest->digest);
- digest->random1_size = 764 - 4 - offset - 32;
- if (digest->random1_size > 0) {
- digest->random1 = new char[digest->random1_size];
- memcpy(digest->random1, pp, digest->random1_size);
- }
- return ret;
- }
- // free the block data create by
- // srs_digest_block_init or srs_digest_block_parse
- void srs_digest_block_free(digest_block* digest)
- {
- if (digest->random0) {
- srs_freepa(digest->random0);
- }
- if (digest->random1) {
- srs_freepa(digest->random1);
- }
- }
- /**
- * the schema type.
- */
- enum srs_schema_type {
- srs_schema0 = 0, // key-digest sequence
- srs_schema1 = 1, // digest-key sequence
- srs_schema_invalid = 2,
- };
- void __srs_time_copy_to(char*& pp, int32_t time)
- {
- // 4bytes time
- *(int32_t*)pp = time;
- pp += 4;
- }
- void __srs_version_copy_to(char*& pp, int32_t version)
- {
- // 4bytes version
- *(int32_t*)pp = version;
- pp += 4;
- }
- void __srs_key_copy_to(char*& pp, key_block* key)
- {
- // 764bytes key block
- if (key->random0_size > 0) {
- memcpy(pp, key->random0, key->random0_size);
- }
- pp += key->random0_size;
- memcpy(pp, key->key, sizeof(key->key));
- pp += sizeof(key->key);
- if (key->random1_size > 0) {
- memcpy(pp, key->random1, key->random1_size);
- }
- pp += key->random1_size;
- *(int32_t*)pp = key->offset;
- pp += 4;
- }
- void __srs_digest_copy_to(char*& pp, digest_block* digest, bool with_digest)
- {
- // 732bytes digest block without the 32bytes digest-data
- // nbytes digest block part1
- *(int32_t*)pp = digest->offset;
- pp += 4;
- if (digest->random0_size > 0) {
- memcpy(pp, digest->random0, digest->random0_size);
- }
- pp += digest->random0_size;
- // digest
- if (with_digest) {
- memcpy(pp, digest->digest, 32);
- pp += 32;
- }
- // nbytes digest block part2
- if (digest->random1_size > 0) {
- memcpy(pp, digest->random1, digest->random1_size);
- }
- pp += digest->random1_size;
- }
- /**
- * copy whole c1s1 to bytes.
- */
- void srs_schema0_copy_to(char* bytes, bool with_digest,
- int32_t time, int32_t version, key_block* key, digest_block* digest)
- {
- char* pp = bytes;
- __srs_time_copy_to(pp, time);
- __srs_version_copy_to(pp, version);
- __srs_key_copy_to(pp, key);
- __srs_digest_copy_to(pp, digest, with_digest);
- if (with_digest) {
- srs_assert(pp - bytes == 1536);
- } else {
- srs_assert(pp - bytes == 1536 - 32);
- }
- }
- void srs_schema1_copy_to(char* bytes, bool with_digest,
- int32_t time, int32_t version, digest_block* digest, key_block* key)
- {
- char* pp = bytes;
- __srs_time_copy_to(pp, time);
- __srs_version_copy_to(pp, version);
- __srs_digest_copy_to(pp, digest, with_digest);
- __srs_key_copy_to(pp, key);
- if (with_digest) {
- srs_assert(pp - bytes == 1536);
- } else {
- srs_assert(pp - bytes == 1536 - 32);
- }
- }
- /**
- * c1s1 is splited by digest:
- * c1s1-part1: n bytes (time, version, key and digest-part1).
- * digest-data: 32bytes
- * c1s1-part2: (1536-n-32)bytes (digest-part2)
- */
- char* srs_bytes_join_schema0(int32_t time, int32_t version, key_block* key, digest_block* digest)
- {
- char* bytes = new char[1536 -32];
- srs_schema0_copy_to(bytes, false, time, version, key, digest);
- return bytes;
- }
- /**
- * c1s1 is splited by digest:
- * c1s1-part1: n bytes (time, version and digest-part1).
- * digest-data: 32bytes
- * c1s1-part2: (1536-n-32)bytes (digest-part2 and key)
- */
- char* srs_bytes_join_schema1(int32_t time, int32_t version, digest_block* digest, key_block* key)
- {
- char* bytes = new char[1536 -32];
- srs_schema1_copy_to(bytes, false, time, version, digest, key);
- return bytes;
- }
- /**
- * compare the memory in bytes.
- */
- bool srs_bytes_equals(void* pa, void* pb, int size){
- u_int8_t* a = (u_int8_t*)pa;
- u_int8_t* b = (u_int8_t*)pb;
- for(int i = 0; i < size; i++){
- if(a[i] != b[i]){
- return false;
- }
- }
- return true;
- }
- /**
- * c1s1 schema0
- * time: 4bytes
- * version: 4bytes
- * key: 764bytes
- * digest: 764bytes
- * c1s1 schema1
- * time: 4bytes
- * version: 4bytes
- * digest: 764bytes
- * key: 764bytes
- */
- struct c1s1
- {
- union block {
- key_block key;
- digest_block digest;
- };
- // 4bytes
- int32_t time;
- // 4bytes
- int32_t version;
- // 764bytes
- // if schema0, use key
- // if schema1, use digest
- block block0;
- // 764bytes
- // if schema0, use digest
- // if schema1, use key
- block block1;
- // the logic schema
- srs_schema_type schema;
- c1s1();
- virtual ~c1s1();
- /**
- * get the digest key.
- */
- virtual char* get_digest();
- /**
- * copy to bytes.
- */
- virtual void dump(char* _c1s1);
- /**
- * client: create and sign c1 by schema.
- * sign the c1, generate the digest.
- * calc_c1_digest(c1, schema) {
- * get c1s1-joined from c1 by specified schema
- * digest-data = HMACsha256(c1s1-joined, FPKey, 30)
- * return digest-data;
- * }
- * random fill 1536bytes c1 // also fill the c1-128bytes-key
- * time = time() // c1[0-3]
- * version = [0x80, 0x00, 0x07, 0x02] // c1[4-7]
- * schema = choose schema0 or schema1
- * digest-data = calc_c1_digest(c1, schema)
- * copy digest-data to c1
- */
- virtual int c1_create(srs_schema_type _schema);
- /**
- * server: parse the c1s1, discovery the key and digest by schema.
- * use the c1_validate_digest() to valid the digest of c1.
- */
- virtual int c1_parse(char* _c1s1, srs_schema_type _schema);
- /**
- * server: validate the parsed schema and c1s1
- */
- virtual int c1_validate_digest(bool& is_valid);
- /**
- * server: create and sign the s1 from c1.
- */
- virtual int s1_create(c1s1* c1);
- private:
- virtual int calc_s1_digest(char*& digest);
- virtual int calc_c1_digest(char*& digest);
- virtual void destroy_blocks();
- };
- /**
- * the c2s2 complex handshake structure.
- * random-data: 1504bytes
- * digest-data: 32bytes
- */
- struct c2s2
- {
- char random[1504];
- char digest[32];
- c2s2();
- virtual ~c2s2();
- /**
- * copy to bytes.
- */
- virtual void dump(char* _c2s2);
- /**
- * create c2.
- * random fill c2s2 1536 bytes
- *
- * // client generate C2, or server valid C2
- * temp-key = HMACsha256(s1-digest, FPKey, 62)
- * c2-digest-data = HMACsha256(c2-random-data, temp-key, 32)
- */
- virtual int c2_create(c1s1* s1);
- /**
- * create s2.
- * random fill c2s2 1536 bytes
- *
- * // server generate S2, or client valid S2
- * temp-key = HMACsha256(c1-digest, FMSKey, 68)
- * s2-digest-data = HMACsha256(s2-random-data, temp-key, 32)
- */
- virtual int s2_create(c1s1* c1);
- };
- c2s2::c2s2()
- {
- for (int i = 0; i < 1504; i++) {
- *(random + i) = rand();
- }
- for (int i = 0; i < 32; i++) {
- *(digest + i) = rand();
- }
- }
- c2s2::~c2s2()
- {
- }
- void c2s2::dump(char* _c2s2)
- {
- memcpy(_c2s2, random, 1504);
- memcpy(_c2s2 + 1504, digest, 32);
- }
- int c2s2::c2_create(c1s1* s1)
- {
- int ret = ERROR_SUCCESS;
- char temp_key[OpensslHashSize];
- if ((ret = openssl_HMACsha256(s1->get_digest(), 32, SrsGenuineFPKey, 62, temp_key)) != ERROR_SUCCESS) {
- srs_error("create c2 temp key failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate c2 temp key success.");
- char _digest[OpensslHashSize];
- if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
- srs_error("create c2 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate c2 digest success.");
- memcpy(digest, _digest, 32);
- return ret;
- }
- int c2s2::s2_create(c1s1* c1)
- {
- int ret = ERROR_SUCCESS;
- char temp_key[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1->get_digest(), 32, SrsGenuineFMSKey, 68, temp_key)) != ERROR_SUCCESS) {
- srs_error("create s2 temp key failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate s2 temp key success.");
- char _digest[OpensslHashSize];
- if ((ret = openssl_HMACsha256(random, 1504, temp_key, 32, _digest)) != ERROR_SUCCESS) {
- srs_error("create s2 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("generate s2 digest success.");
- memcpy(digest, _digest, 32);
- return ret;
- }
- c1s1::c1s1()
- {
- schema = srs_schema_invalid;
- }
- c1s1::~c1s1()
- {
- destroy_blocks();
- }
- char* c1s1::get_digest()
- {
- srs_assert(schema != srs_schema_invalid);
- if (schema == srs_schema0) {
- return block1.digest.digest;
- } else {
- return block0.digest.digest;
- }
- }
- void c1s1::dump(char* _c1s1)
- {
- srs_assert(schema != srs_schema_invalid);
- if (schema == srs_schema0) {
- srs_schema0_copy_to(_c1s1, true, time, version, &block0.key, &block1.digest);
- } else {
- srs_schema1_copy_to(_c1s1, true, time, version, &block0.digest, &block1.key);
- }
- }
- int c1s1::c1_create(srs_schema_type _schema)
- {
- int ret = ERROR_SUCCESS;
- if (_schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("create c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- destroy_blocks();
- time = ::time(NULL);
- version = 0x02070080; // client c1 version
- if (_schema == srs_schema0) {
- srs_key_block_init(&block0.key);
- srs_digest_block_init(&block1.digest);
- } else {
- srs_digest_block_init(&block0.digest);
- srs_key_block_init(&block1.key);
- }
- schema = _schema;
- char* digest = NULL;
- if ((ret = calc_c1_digest(digest)) != ERROR_SUCCESS) {
- srs_error("sign c1 error, failed to calc digest. ret=%d", ret);
- return ret;
- }
- srs_assert(digest != NULL);
- SrsAutoFree(char, digest, true);
- if (schema == srs_schema0) {
- memcpy(block1.digest.digest, digest, 32);
- } else {
- memcpy(block0.digest.digest, digest, 32);
- }
- return ret;
- }
- int c1s1::c1_parse(char* _c1s1, srs_schema_type _schema)
- {
- int ret = ERROR_SUCCESS;
- if (_schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- destroy_blocks();
- time = *(int32_t*)_c1s1;
- version = *(int32_t*)(_c1s1 + 4); // client c1 version
- if (_schema == srs_schema0) {
- if ((ret = srs_key_block_parse(&block0.key, _c1s1 + 8)) != ERROR_SUCCESS) {
- srs_error("parse the c1 key failed. ret=%d", ret);
- return ret;
- }
- if ((ret = srs_digest_block_parse(&block1.digest, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
- srs_error("parse the c1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("parse c1 key-digest success");
- } else if (_schema == srs_schema1) {
- if ((ret = srs_digest_block_parse(&block0.digest, _c1s1 + 8)) != ERROR_SUCCESS) {
- srs_error("parse the c1 key failed. ret=%d", ret);
- return ret;
- }
- if ((ret = srs_key_block_parse(&block1.key, _c1s1 + 8 + 764)) != ERROR_SUCCESS) {
- srs_error("parse the c1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("parse c1 digest-key success");
- } else {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("parse c1 failed. invalid schema=%d, ret=%d", _schema, ret);
- return ret;
- }
- schema = _schema;
- return ret;
- }
- int c1s1::c1_validate_digest(bool& is_valid)
- {
- int ret = ERROR_SUCCESS;
- char* c1_digest = NULL;
- if ((ret = calc_c1_digest(c1_digest)) != ERROR_SUCCESS) {
- srs_error("validate c1 error, failed to calc digest. ret=%d", ret);
- return ret;
- }
- srs_assert(c1_digest != NULL);
- SrsAutoFree(char, c1_digest, true);
- if (schema == srs_schema0) {
- is_valid = srs_bytes_equals(block1.digest.digest, c1_digest, 32);
- } else {
- is_valid = srs_bytes_equals(block0.digest.digest, c1_digest, 32);
- }
- return ret;
- }
- int c1s1::s1_create(c1s1* c1)
- {
- int ret = ERROR_SUCCESS;
- if (c1->schema == srs_schema_invalid) {
- ret = ERROR_RTMP_CH_SCHEMA;
- srs_error("create s1 failed. invalid schema=%d, ret=%d", c1->schema, ret);
- return ret;
- }
- destroy_blocks();
- schema = c1->schema;
- time = ::time(NULL);
- version = 0x01000504; // server s1 version
- if (schema == srs_schema0) {
- srs_key_block_init(&block0.key);
- srs_digest_block_init(&block1.digest);
- } else {
- srs_digest_block_init(&block0.digest);
- srs_key_block_init(&block1.key);
- }
- if (schema == srs_schema0) {
- if ((ret = openssl_generate_key(c1->block0.key.key, block0.key.key, 128)) != ERROR_SUCCESS) {
- srs_error("calc s1 key failed. ret=%d", ret);
- return ret;
- }
- } else {
- if ((ret = openssl_generate_key(c1->block1.key.key, block1.key.key, 128)) != ERROR_SUCCESS) {
- srs_error("calc s1 key failed. ret=%d", ret);
- return ret;
- }
- }
- srs_verbose("calc s1 key success.");
- char* s1_digest = NULL;
- if ((ret = calc_s1_digest(s1_digest)) != ERROR_SUCCESS) {
- srs_error("calc s1 digest failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("calc s1 digest success.");
- srs_assert(s1_digest != NULL);
- SrsAutoFree(char, s1_digest, true);
- if (schema == srs_schema0) {
- memcpy(block1.digest.digest, s1_digest, 32);
- } else {
- memcpy(block0.digest.digest, s1_digest, 32);
- }
- srs_verbose("copy s1 key success.");
- return ret;
- }
- int c1s1::calc_s1_digest(char*& digest)
- {
- int ret = ERROR_SUCCESS;
- srs_assert(schema == srs_schema0 || schema == srs_schema1);
- char* c1s1_joined_bytes = NULL;
- if (schema == srs_schema0) {
- c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
- } else {
- c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
- }
- srs_assert(c1s1_joined_bytes != NULL);
- SrsAutoFree(char, c1s1_joined_bytes, true);
- digest = new char[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFMSKey, 36, digest)) != ERROR_SUCCESS) {
- srs_error("calc digest for s1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("digest calculated for s1");
- return ret;
- }
- int c1s1::calc_c1_digest(char*& digest)
- {
- int ret = ERROR_SUCCESS;
- srs_assert(schema == srs_schema0 || schema == srs_schema1);
- char* c1s1_joined_bytes = NULL;
- if (schema == srs_schema0) {
- c1s1_joined_bytes = srs_bytes_join_schema0(time, version, &block0.key, &block1.digest);
- } else {
- c1s1_joined_bytes = srs_bytes_join_schema1(time, version, &block0.digest, &block1.key);
- }
- srs_assert(c1s1_joined_bytes != NULL);
- SrsAutoFree(char, c1s1_joined_bytes, true);
- digest = new char[OpensslHashSize];
- if ((ret = openssl_HMACsha256(c1s1_joined_bytes, 1536 - 32, SrsGenuineFPKey, 30, digest)) != ERROR_SUCCESS) {
- srs_error("calc digest for c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("digest calculated for c1");
- return ret;
- }
- void c1s1::destroy_blocks()
- {
- if (schema == srs_schema_invalid) {
- return;
- }
- if (schema == srs_schema0) {
- srs_key_block_free(&block0.key);
- srs_digest_block_free(&block1.digest);
- } else {
- srs_digest_block_free(&block0.digest);
- srs_key_block_free(&block1.key);
- }
- }
- SrsComplexHandshake::SrsComplexHandshake()
- {
- }
- SrsComplexHandshake::~SrsComplexHandshake()
- {
- }
- int SrsComplexHandshake::handshake(SrsSocket& skt, char* _c1)
- {
- int ret = ERROR_SUCCESS;
- ssize_t nsize;
- static bool _random_initialized = false;
- if (!_random_initialized) {
- srand(0);
- _random_initialized = true;
- srs_trace("srand initialized the random.");
- }
- // decode c1
- c1s1 c1;
- // try schema0.
- if ((ret = c1.c1_parse(_c1, srs_schema0)) != ERROR_SUCCESS) {
- srs_error("parse c1 schema%d error. ret=%d", srs_schema0, ret);
- return ret;
- }
- // try schema1
- bool is_valid = false;
- if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
- if ((ret = c1.c1_parse(_c1, srs_schema1)) != ERROR_SUCCESS) {
- srs_error("parse c1 schema%d error. ret=%d", srs_schema1, ret);
- return ret;
- }
- if ((ret = c1.c1_validate_digest(is_valid)) != ERROR_SUCCESS || !is_valid) {
- ret = ERROR_RTMP_TRY_SIMPLE_HS;
- srs_info("all schema valid failed, try simple handshake. ret=%d", ret);
- return ret;
- }
- }
- srs_verbose("decode c1 success.");
- // encode s1
- c1s1 s1;
- if ((ret = s1.s1_create(&c1)) != ERROR_SUCCESS) {
- srs_error("create s1 from c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("create s1 from c1 success.");
- c2s2 s2;
- if ((ret = s2.s2_create(&c1)) != ERROR_SUCCESS) {
- srs_error("create s2 from c1 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("create s2 from c1 success.");
- // sendout s0s1s2
- char* s0s1s2 = new char[3073];
- SrsAutoFree(char, s0s1s2, true);
- // plain text required.
- s0s1s2[0] = 0x03;
- s1.dump(s0s1s2 + 1);
- s2.dump(s0s1s2 + 1537);
- if ((ret = skt.write(s0s1s2, 3073, &nsize)) != ERROR_SUCCESS) {
- srs_warn("complex handshake send s0s1s2 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("complex handshake send s0s1s2 success.");
- // recv c2
- char* c2 = new char[1536];
- SrsAutoFree(char, c2, true);
- if ((ret = skt.read_fully(c2, 1536, &nsize)) != ERROR_SUCCESS) {
- srs_warn("complex handshake read c2 failed. ret=%d", ret);
- return ret;
- }
- srs_verbose("complex handshake read c2 success.");
- return ret;
- }
5.4. 分塊
握手之后,連接開始對一個或多個塊流進行合並。創建的每個塊都有一個唯一 ID 對其進行關聯,這個 ID 叫做 chunk stream ID (塊流 ID),即csid。這些塊通過網絡進行傳輸。傳遞時,每個塊必須被完全發送才可以發送下一塊。在接收端,這些塊被根據塊流 ID 被組裝成消息。
分塊允許上層協議將大的消息分解為更小的消息,例如,防止體積大的但優先級小的消息 (比如視頻) 阻礙體積較小但優先級高的消息 (比如音頻或者控制命令)。
分塊也讓我們能夠使用較小開銷發送小消息,因為塊頭包含包含在消息內部的信息壓縮提示。
塊的大小是可以配置的。它可以使用一個設置塊大小的控制消息進行設置 (參考 5.4.1)。更大的塊大小可以降低 CPU 開銷,但在低帶寬連接時因為它的大量的寫入也會延遲其他內容的傳遞。更小的塊不利於高比特率的流化。所以塊的大小設置取決於具體情況。
5.4.1. 塊格式
每個塊包含一個頭和數據體。塊頭包含三個部分:

Basic Header (基本頭,1 到 3 個字節):這個字段對塊流 ID 和塊類型進行編碼。塊類型決定了消息頭的編碼格式。(這一字段的) 長度完全取決於塊流 ID,因為塊流 ID 是一個可變長度的字段。
Message Header (消息頭,0,3,7,或者 11 個字節):這一字段對正在發送的消息 (不管是整個消息,還是只是一小部分) 的信息進行編碼。這一字段的長度可以使用塊頭中定義的塊類型進行決定。
Extended Timestamp (擴展 timestamp,0 或 4 字節):這一字段是否出現取決於塊消息頭中的 timestamp 或者 timestamp delta 字段。更多信息參考 5.4.1.3 節。
Chunk Data (可變的大小):當前塊的有效負載,大小由配置決定。
5.4.1.1. 塊基本頭
塊基本頭對塊流 ID (chunk stream id=csid)和塊類型 (由下圖中的 fmt 字段表示) 進行編碼。塊基本頭字段可能會有 1,2 或者 3 個字節,取決於塊流 ID。
一個 (RTMP) 實現應該使用能夠容納這個 ID 的最小的容量進行表示。
RTMP 協議最多支持 65597 個流,流 ID 范圍 3 - 65599。ID 0、1、2 被保留。0 值表示二字節形式,並且 ID 范圍 64 - 319 (第二個字節 + 64)。1 值表示三字節形式,並且 ID 范圍為 64 - 65599 ((第三個字節) * 256 + 第二個字節 + 64)。3 - 63 范圍內的值表示整個流 ID。帶有 2 值的塊流 ID 被保留,用於下層協議控制消息和命令。
塊基本頭中的 0 - 5 位 (最低有效) 代表塊流 ID。
塊流 ID 2 - 63 可以編進這一字段的一字節版本中。

塊流 ID 64 - 319 可以以二字節的形式編碼在頭中。ID 計算為 (第二個字節 + 64),這個時候第一字節的fmt后面的6個bit為000000:

塊流 ID 64 - 65599 可以編碼在這個字段的三字節版本中。ID 計算為 ((第三個字節) * 256 + (第二個字節) + 64),這個時候第一字節的fmt后面的6個bit為000001。

例如:如果csid=288=224+64,那么應該表示成fmt00000011100000(2-byte)或者fmt0000010000000011100000(3-byte)。
csid (六位):這一字段包含有塊流 ID,值的范圍是 2 - 63。fmt后面緊跟着的6位的值, 0 和 1 用於指示這一字段是 2- 或者 3- 字節版本。
fmt (兩個字節):這一字段指示 'chunk message header' 使用的四種格式之一。每種塊類型的 'chunk message header' 會在下一小節解釋。
csid - 64 (8 或者 16 位):這一字段包含了塊流 ID 減掉 64 后的值塊流 ID,即csid - 64為塊流 ID在字節中的表現形式。
塊流 ID 64 - 319 可以使用 2-byte 或者 3-byte 的形式在頭中表示,塊流 ID320-65599只能使用3-byte的形式表示。
5.4.1.2. 塊消息頭(fmt的值)
塊消息頭又四種不同的格式,由塊基本頭中的 "fmt" 字段進行選擇。
一個 (RTMP) 實現應該為每個塊消息頭使用最緊湊的表示。
5.4.1.2.1. 類型 0
類型 0 塊頭的長度是 11 個字節。即AMF0包頭為11字節。這一類型必須用在塊流的起始位置,和流 timestamp 重來的時候 (比如,重置)。

圖例1:符合AMF0類型的協議控制包(play&set buffer length)
例如圖例1,是一個fmt=0的rtmp的協議控制包(play&set buffer length),csid=8。圖中選中字段為包含消息流ID一字節的AMF0包頭(12字節,去掉第一個字節的0x08,即為AMF0的11字節包頭)。其中timestamp(3個字節,00 01 8d) 為397,message length(body size字段,3個字節,00 00 18)為24.type id(stream id,1個字節)代表body的幀類型為0x11(AMF3 command類型,1字節 0x11),代表是指令幀。msg stream id為1(四個字節,01 00 00 00).

timestamp (三個字節):對於 type-0 塊,當前消息的絕對 timestamp 在這里發送。如果 timestamp 大於或者等於 16777215 (十六進制 0xFFFFFF),這一字段必須是 16777215,表明有擴展 timestamp 字段來補充完整的 32 位 timestamp。否則的話,這一字段必須是整個的 timestamp。
5.4.1.2.2. 類型 1
類型 1 塊頭長為 7 個字節。即AMF1包頭為7字節。不包含消息流 ID;這一塊使用前一塊一樣的流 ID。可變長度消息的流 (例如,一些視頻格式) 應該在第一塊之后使用這一格式表示之后的每個新消息。
圖例2:符合AMF1類型的視頻包
例如圖例2,是一個fmt=1的rtmp的視頻包。csid=4,圖中選中字段為包含消息流ID一字節的AMF1包頭(8字節,去掉第一個字節的0x44,即為AMF1的包頭)。其中timestamp delta為6,message length(body size字段)為628.type id代表body的幀類型為9,是視頻幀。

5.4.1.2.3. 類型 2
類型 2 塊頭長度為 3 個字節。即AMF2包頭為3字節。既不包含流 ID 也不包含消息長度;這一塊具有和前一塊相同的流 ID 和消息長度。具有不變長度的消息 (例如,一些音頻和數據格式) 應該在第一塊之后使用這一格式表示之后的每個新消息。
圖例3:符合AMF2類型的包
例如圖例3,是一個fmt=2的rtmp的固定長度的數據包。csid=37,圖中選中字段為不包含消息流ID 1字節的AMF2包頭(3字節,消息流ID為0xa5)。其中timestamp delta為4357388,無message length字段,因為默認與前以對應csid包的數據大小相同。沒有幀類型,緊跟數據。

5.4.1.2.4. 類型 3
類型 3 的塊沒有消息頭。流 ID、消息長度以及 timestamp delta 等字段都不存在;這種類型的塊使用前面塊一樣的塊流 ID。即AMF3無包頭。當單一一個消息被分割為多塊時,除了第一塊的其他塊都應該使用這種類型。參考例 2 (5.3.2.2 小節)。組成流的消息具有同樣的大小,流 ID 和時間間隔應該在類型 2 之后的所有塊都使用這一類型。參考例 1 (5.3.2.1 小節)。如果第一個消息和第二個消息之間的 delta 和第一個消息的 timestamp 一樣的話,那么在類型 0 的塊之后要緊跟一個類型 3 的塊,因為無需再來一個類型 2 的塊來注冊 delta 了。如果一個類型 3 的塊跟着一個類型 0 的塊,那么這個類型 3 塊的 timestamp delta 和類型 0 塊的 timestamp 是一樣的。
5.4.1.2.5. 通用頭字段
塊消息頭中各字段的描述如下:
timestamp delta (三個字節):對於一個類型 1 或者類型 2 的塊,前一塊的 timestamp 和當前塊的 timestamp 的區別在這里發送。如果 delta 大於或者等於 16777215 (十六進制 0xFFFFFF),那么這一字段必須是為 16777215,表示具有擴展 timestamp 字段來對整個 32 位 delta 進行編碼。否則的話,這一字段應該是為具體 delta。
message length (三個字節):對於一個類型 0 或者類型 1 的塊,消息長度在這里進行發送。注意這通常不同於塊的有效載荷的長度。塊的有效載荷代表所有的除了最后一塊的最大塊大小,以及剩余的 (也可能是小消息的整個長度) 最后一塊。
message type id (消息類型 id,一個字節):對於類型 0 或者類型 1 的塊,消息的類型在這里發送。
message stream id (四個字節):對於一個類型為 0 的塊,保存消息流 ID。消息流 ID 以小端格式保存。所有同一個塊流下的消息都來自同一個消息流。當可以將不同的消息流組合進同一個塊流時,這種方法比頭壓縮的做法要好。但是,當一個消息流被關閉而其他的隨后另一個是打開着的,就沒有理由將現有塊流以發送一個新的類型 0 的塊進行復用了。
5.4.1.3. 擴展 timestamp
擴展 timestamp 字段用於對大於 16777215 (0xFFFFFF) 的 timestamp 或者 timestamp delta 進行編碼;也就是,對於不適合於在 24 位的類型 0、1 和 2 的塊里的 timestamp 和 timestamp delta 編碼。這一字段包含了整個 32 位的 timestamp 或者 timestamp delta 編碼。可以通過設置類型 0 塊的 timestamp 字段、類型 1 或者 2 塊的 timestamp delta 字段 16777215 (0xFFFFFF) 來啟用這一字段。當最近的具有同一塊流的類型 0、1 或 2 塊指示擴展 timestamp 字段出現時,這一字段才會在類型為 3 的塊中出現。
5.4.2. 例子
5.4.2.1. 例子 1
這個例子演示了一個簡單地音頻消息流。這個例子演示了信息的冗余。

下一個表格演示了這個流所產生的塊。從消息 3 起,數據傳輸得到了最佳化利用。每條消息的開銷在這一點之后都只有一個字節。

5.4.2.2. 例子 2
這一例子闡述了一條消息太大,無法裝在一個 128 字節的塊里,被分割為若干塊。

這是傳輸的塊:

塊 1 的數據頭說明了整個消息長度是為 307 個字節。
由以上倆例子可以得知,塊類型 3 可以被用於兩種不同的方式。第一種是用於定義一條消息的配置。第二種是定義一個可以從現有狀態數據中派生出來的新消息的起點。
5.5. 協議控制消息
RTMP 塊流使用消息類型 ID(stream id) 為 1、2、3、5 和 6 用於協議控制消息。這些消息包含有 RTMP 塊流協議所需要的信息。
這些協議控制消息必須使用消息流 ID 0 (作為已知控制流) 並以流 ID 為 2 的塊發送。協議控制消息一旦被接收到就立即生效;協議控制消息的 timestamp 被忽略。
5.5.1. 設置塊類型 (1=set chunk size)
協議控制消息 1,設置塊大小,以通知對端一個新的最大塊大小(set chunk size)。
默認的最大塊大小是為 128 字節,但是客戶端或者服務器可以改變這個大小,並使用這一消息對對端進行更新。例如,假定一個客戶端想要發送一個 131 字節的音頻數據,當前塊大小是默認的 128。在這種情況下,客戶端可以發送這種消息到服務器以通知它塊大小現在是 131 字節了。這樣客戶端就可以在單一塊中發送整個音頻數據了。
最大塊大小設置的話最少為 128 字節,包含內容最少要一個字節。最大塊大小由每個方面 (服務器或者客戶端) 自行維護。

0:這個位必須為 0。
chunk size (塊大小,31 位):這一字段保存新的最大塊大小值,以字節為單位,這將用於之后發送者發送的塊,直到有更多 (關於最大塊大小的) 通知。有效值為 1 到 2147483647 (0x7FFFFFFF,1 和 2147483647 都可取); 但是所有大於 16777215 (0xFFFFFF) 的大小值是等價的,因為沒有一個塊比一整個消息大,並且沒有一個消息大於 16777215 字節。
5.5.2. 終止消息(2,)
協議控制消息 2,終止消息,用於通知對端,如果對端在等待去完成一個消息的塊的話,然后拋棄一個塊流中已接受到的部分消息。對端接收到塊流 ID 作為當前協議消息的有效負載。一些程序可能會在關閉的時候使用這個消息以指示不需要進一步對這個消息的處理了。

chunk stream ID (塊流 ID,32 位):這一字段保存塊流 ID,該流的當前消息會被丟棄。
5.5.3. 確認 (3=acknowledgement)
客戶端或者服務器在接收到等同於窗口大小的字節之后必須要發送給對端一個確認。窗口大小是指發送者在沒有收到接收者確認之前發送的最大數量的字節。這個消息定義了序列號,也就是目前接收到的字節數。

sequence number (序列號,32 位):這一字段保存有目前接收到的字節數。
5.5.4. 窗口確認大小 (5=window acknowledgement size)
客戶端或者服務器端發送這條消息來通知對端發送和應答之間的窗口大小。發送者在發送完窗口大小字節之后期待對端的確認。接收端在上次確認發送后接收到的指示數值后,或者會話建立之后尚未發送確認,必須發送一個確認 (5.4.3 小節)。

5.5.5. 設置對端帶寬 (6=set peer bandwidth)
客戶端或者服務器端發送這一消息來限制其對端的輸出帶寬。對端接收到這一消息后,將通過限制這一消息中窗口大小指出的已發送但未被答復的數據的數量以限制其輸出帶寬。如果這個窗口大小不同於其發送給 (設置對端帶寬) 發送者的最后一條消息,那么接收到這一消息的對端應該回復一個窗口確認大小消息。

限制類型取以下值之一:
0 - Hard:對端應該限制其輸出帶寬到指示的窗口大小。
1 - Soft:對端應該限制其輸出帶寬到指示的窗口大小,或者已經有限制在其作用的話就取兩者之間的較小值。
2 - Dynamic:如果先前的限制類型為 Hard,處理這個消息就好像它被標記為 Hard,否則的話忽略這個消息。
6. RTMP 消息格式
這一節定義了使用下層傳輸層 (比如 RTMP 塊流協議) 傳輸的 RTMP 消息的格式。
RTMP 協議設計使用 RTMP 塊流,可以使用其他任意傳輸協議對消息進行發送。RTMP 塊流和 RTMP 一起適用於多種音頻 - 視頻應用,從一對一和一對多直播到點播服務,再到互動會議應用。
6.1. RTMP 消息格式
服務器端和客戶端通過網絡發送 RTMP 消息來進行彼此通信。消息可以包含音頻、視頻、數據,或者其他消息。
RTMP 消息有兩部分:頭和它的有效載荷。
6.1.1. 消息頭
消息頭包含以下:
Message Type (消息類型):一個字節的字段來表示消息類型。類型 ID 1 - 6 被保留用於協議控制消息。
Length (長度):三個字節的字段來表示有效負載的字節數。以大端格式保存。
Timestamp:四個字節的字段包含了當前消息的 timestamp。四個字節也以大端格式保存。
Message Stream Id (消息流 ID):三個字節的字段以指示出當前消息的流。這三個字節以大端格式保存。

6.1.2. 消息有效載荷
消息的另一個部分就是有效負載,這是這個消息所包含的實際內容。例如,它可以是一些音頻樣本或者壓縮的視頻數據。有效載荷格式和解釋不在本文檔范圍之內。
6.2. 用戶控制消息 (4=user control message)
RTMP 使用消息類型 ID(message type ID) 4 表示用戶控制消息。這些消息包含 RTMP 流傳輸層所使用的信息。RTMP 塊流協議使用 ID 為 1、2、3、5 和 6 (5.4 節介紹)。
用戶控制消息應該使用消息流 ID(message stream ID) 0 (以被認為是控制流),並且以 RTMP 塊流發送時以塊流 ID(chunk stream id) 為 2。用戶控制消息一旦被接收立馬生效;它們的 timestamp 是被忽略的。
客戶端或者服務器端發送這個消息來通知對端用戶操作事件。這一消息攜帶有事件類型和事件數據。

消息數據的前兩個字節用於指示事件類型。事件類型被事件數據緊隨。事件數據字段的大小是可變的。但是,如果消息必須通過 RTMP 塊流層傳輸時,最大塊大小 (5.4.1 節) 應該足夠大以允許這些消息填充在一個單一塊中。
事件類型和事件數據格式將在 7.1.7 小節列出。
7. RTMP 命令消息
這一節描述了在服務器端和客戶端彼此通信交換的消息和命令的不同的類型。
服務器端和客戶端交換的不同消息類型包括用於發送音頻數據的音頻消息、用於發送視頻數據的視頻消息、用於發送任意用戶數據的數據消息、共享對象消息以及命令消息。共享對象消息提供了一個通用的方法來管理多用戶和一台服務器之間的分布式數據。命令消息在客戶端和服務器端傳輸 AMF 編碼的命令。客戶端或者服務器端可以通過使用命令消息和對端通信的流請求遠程方法調用 (RPC)。
7.1. 消息的類型
服務器端和客戶端通過在網絡中發送消息來進行彼此通信。消息可以是任何類型,包含音頻消息,視頻消息,命令消息,共享對象消息,數據消息,以及用戶控制消息。
7.1.1. 命令消息 (20, 17)
命令消息(類似play、connect、closestream)在客戶端和服務器端傳遞 AMF 編碼的命令。這些消息被分配以消息類型值為 20 以進行 AMF0(AMF0 command 0x14) 編碼,消息類型值為 17(AMF3 command 0x11)以進行 AMF3 編碼。這些消息發送以進行一些操作,比如,連接,創建流,發布,播放,對端暫停。命令消息,像 onstatus、result 等等,用於通知發送者請求的命令的狀態。一個命令消息由命令名、事務 ID 和包含相關參數的命令對象組成。一個客戶端或者一個服務器端可以通過和對端通信的流使用這些命令消息請求遠程調用 (RPC)。
7.1.2. 數據消息 (18, 15)
客戶端或者服務器端通過發送這些消息以發送元數據或者任何用戶數據到對端。元數據包括數據 (音頻,視頻等等) 的詳細信息,比如創建時間,時長,主題等等。這些消息被分配以消息類型為 18 以進行 AMF0 編碼和消息類型 15 以進行 AMF3 編碼。
7.1.3. 共享對象消息 (19, 16)
所謂共享對象其實是一個 Flash 對象 (一個名值對的集合),這個對象在多個不同客戶端、應用實例中保持同步。消息類型 19 用於 AMF0 編碼、16 用於 AMF3 編碼都被為共享對象事件保留。每個消息可以包含有不同事件。

支持以下事件類型:
事件 | 描述 |
---|---|
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) | 服務器端發送給客戶端這一事件表示連接成功。 |
7.1.4. 音頻消息 (8=audio data)
客戶端或者服務器端發送這一消息以發送音頻數據到對端。消息類型 8 為音頻消息保留。
7.1.5. 視頻消息 (9=video data)
客戶端或者服務器發送這一消息以發送視頻數據到對端。消息類型 9 為視頻消息保留。
7.1.6. 統計消息 (22)
統計消息是一個單一的包含一系列的使用 6.1 節描述的 RTMP 子消息的消息。消息類型 22 用於統計消息。其實就是0x16大包消息。


統計消息的消息流 ID 覆蓋了統計中子消息的消息流 ID。
統計消息里的 timestamp 和第一個子消息的 timestamp 的不同點在於子消息的 timestamp 被相對流時間標調整了偏移。每個子消息的 timestamp 被加入偏移以達到一個統一流時間。第一個子消息的 timestamp 應該和統計消息的 timestamp 一樣,所以這個偏移量應該為 0。
反向指針包含有前一個消息的大小 (包含前一個消息的頭)。這樣子匹配了 FLV 文件的格式,用於反向查找。
使用統計消息具有以下性能優勢:
- 塊流可以在一個塊中以至多一個單一完整的消息發送。因此,增加塊大小並使用統計消息減少了發送塊的數量。
- 子消息可以在內存中連續存儲。在網絡中系統調用發送這些數據時更高效。
7.1.7. 用戶控制消息事件
客戶端或者服務器端發送這一消息來通知對端用戶控制事件。關於這個的消息格式參考 6.2 節。
支持以下用戶控制事件類型:
事件 | 描述 |
Stream Begin (=0) | 服務器發送這個事件來通知客戶端一個流已就緒並可以用來通信。默認情況下,這一事件在成功接收到客戶端的應用連接命令之后以 ID 0 發送。這一事件數據為 4 字節,代表了已就緒流的流 ID。 |
Stream EOF (=1) | 服務器端發送這一事件來通知客戶端請求的流的回放數據已經結束。在發送額外的命令之前不再發送任何數據。客戶端將丟棄接收到的這個流的消息。這一事件數據為 4 字節,代表了回放已結束的流的流 ID。 |
StreamDry (=2) | 服務器端發送這一事件來通知客戶端當前流中已沒有數據。當服務器端在一段時間內沒有檢測到任何消息,它可以通知相關客戶端當前流已經沒數據了。這一事件數據為 4 字節,代表了已沒數據的流的流 ID。 |
SetBuffer Length (=3) | 客戶端發送這一事件來通知服務器端用於緩存流中任何數據的緩存大小 (以毫秒為單位)。這一事件在服務器端開始處理流之前就發送。這一事件數據的前 4 個字節代表了流 ID 后 4 個字節代表了以毫秒為單位的緩存的長度。 |
StreamIs Recorded (=4) | 服務器端發送這一事件來通知客戶端當前流是一個錄制流。這一事件數據為 4 字節,代表了錄制流的流 ID。 |
PingRequest (=6) | 服務器端發送這一事件用於測試是否能夠送達客戶端。時間數據是為一個 4 字節的 timestamp,代表了服務器端發送這一命令時的服務器本地時間。客戶端在接收到這一消息后會立即發送 PingResponse 回復。 |
PingResponse (=7) | 客戶端作為對 ping 請求的回復發送這一事件到服務器端。這一事件數據是為一個 4 字節的 timestamp,就是接收自 PingRequest 那個。 |
7.2. 命令類型
客戶端和服務器端交換 AMF 編碼的命令。服務器端發送一個命令消息,這個命令消息由命令名、事務 ID 以及包含有相關參數的命令對象組成。例如,包含有 'app' 參數的連接命令,這個命令說明了客戶端連接到的服務器端的應用名。接收者處理這一命令並回發一個同樣事務 ID 的響應。回復字符串可以是 _result、_error 或者一個方法名的任意一個,比如,verifyClient 或者 contactExternalServer。
命令字符串 _result 或者 _error 是響應信號。事務 ID 指示出響應所指向的命令。這和 AMAP 和其他一些協議的標簽一樣。命令字符串中的方法名表示發送者試圖執行接收者一端的一個方法。
以下類的對象用於發送不同的命令:
NetConnection 代表上層的服務器端和客戶端之間連接的一個對象。
NetStream 一個代表發送音頻流、視頻流和其他相關數據的通道的對象。當然,我們也會發送控制數據流的命令,諸如 play、pause 等等。
7.2.1. NetConnection 命令
NetConnection 管理着一個客戶端應用和服務器端之間的雙相連接。此外,它還提供遠程方法的異步調用。
NetConnection 可以發送以下命令:
- connect
- call
- close
- createStream
7.2.1.1. connect 命令
客戶端發送 connect 命令到服務器端來請求連接到一個服務器應用的實例。
由客戶端發送到服務器端的 connect 命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令的名字。設置給 "connect"。 |
Transaction ID | 數字 | 總是設置為 1。 |
Command Object | 對象 | 具有名值對的命令信息對象。 |
Optional User Arguments | 對象 | 任意可選信息。 |
以下是為 connect 命令中使用的名值對對象的描述。
屬性 | 類型 | 描述 | 范例 |
app | 字符串 | 客戶端連接到的服務器端應用的名字。 | testapp |
flashver | 字符串 | Flash Player 版本號。和ApplicationScript getversion() 方法返回的是同一個字符串。 | FMSc/1.0 |
swfUrl | 字符串 | 進行當前連接的 SWF 文件源地址。例如:app/viewplayer.swf | file://C:/FlvPlayer.swf |
tcUrl | 字符串 | 服務器 URL。具有以下格式:protocol://servername:port/appName/appInstance,例如rtmp:IP:PORT/live/ | 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。可以是undefined | http://somehost/sample.html |
objectEncoding | 數字 | AMF 編碼方法。0、3.一般采用3 | AMF3 |
audioCodecs 屬性的標識值:

videoCodecs 屬性的標識值:

videoFunction 屬性的標識值:

encoding 屬性值:

服務器端到客戶端的命令的結構如下:



命令執行時消息流動如下:
1. 客戶端發送 connect 命令到服務器端以請求對服務器端應用實例的連接。
2. 收到 connect 命令后,服務器端發送協議消息 '窗口確認大小' 到客戶端。服務器端也會連接到 connect 命令中提到的應用。
3. 服務器端發送協議消息 '設置對端帶寬' 到客戶端。
4. 在處理完協議消息 '設置對端帶寬' 之后客戶端發送協議消息 '窗口確認大小' 到服務器端。
5. 服務器端發送另一個用戶控制消息 (StreamBegin) 類型的協議消息到客戶端。
6. 服務器端發送結果命令消息告知客戶端連接狀態 (success/fail)。這一命令定義了事務 ID (常常為 connect 命令設置為 1)。這一消息也定義了一些屬性,比如 FMS 服務器版本 (字符串)。此外,它還定義了其他連接關聯到的信息,比如 level (字符串)、code (字符串)、description (字符串)、objectencoding (數字) 等等。
7.2.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 | 對象 | 調用方法的回復。 |
7.2.1.3. createStream 命令
客戶端發送這一命令到服務器端以為消息連接創建一個邏輯通道。音頻、視頻和元數據使用 createStream 命令創建的流通道傳輸。
NetConnection 是默認的通信通道,流 ID 為 0。協議和一些命令消息,包括 createStream,使用默認的通信通道。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名。設置給 "createStream"。 |
Transaction ID | 數字 | 命令的事務 ID。 |
Command Object | 對象 | 如果存在一些命令信息要設置這個對象,否則置空。 |
服務器端發送給客戶端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | _result 或者 _error;表明回復是一個結果還是錯誤。 |
Transaction ID | 數字 | 響應所屬的命令的 ID。 |
Command Object | 對象 | 如果存在一些命令信息要設置這個對象,否則置空。 |
Stream ID | 數字 | 返回值要么是一個流 ID 要么是一個錯誤信息對象。 |
7.2.2. NetStream 命令
NetStream 定義了傳輸通道,通過這個通道,音頻流、視頻流以及數據消息流可以通過連接客戶端到服務端的 NetConnection 傳輸。
以下命令可以由客戶端使用 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" (字符串):關於這個消息人類可讀描述。 |
7.2.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,表示用戶首先嘗試播放流名字段中定義的直播流。如果那個名字的直播流沒有找到,它將播放同名的錄制流。如果沒有那個名字的錄制流,客戶端將等待一個新的那個名字的直播流,並當其有效時進行播放。如果你在 Start 字段中傳遞 -1,那么就只播放流名中定義的那個名字的直播流。如果你在 Start 字段中傳遞 0 或一個整數,那么將從 Start 字段定義的時間開始播放流名中定義的那個錄制流。如果沒有找到錄制流,那么將播放播放列表中的下一項。 |
Duration | 數字 | 一個可選的參數,以秒為單位定義了回放的持續時間。默認值為 -1。-1 值意味着一個直播流會一直播放直到它不再可用或者一個錄制流一直播放直到結束。如果你傳遞 0 值,它將只播放單一一幀,因為播放時間已經在錄制流的開始的 Start 字段指定了。假定定義在 Start 字段中的值大於或者等於 0。如果你傳遞一個正數,將播放 Duration 字段定義的一段直播流。之后,變為可播放狀態,或者播放 Duration 字段定義的一段錄制流。(如果流在 Duration 字段定義的時間段內結束,那么流結束時回放結束)。如果你在 Duration 字段中傳遞一個 -1 以外的負數的話,它將把你給的值當做 -1 處理。 |
Reset | 布爾 | 一個可選的布爾值或者數字定義了是否對以前的播放列表進行 flush。 |

命令執行時的消息流動是為:
1. 當客戶端從服務器端接收到 createStream 命令的結果是為 success 時,發送 play 命令。
2. 一旦接收到 play 命令,服務器端發送一個協議消息來設置塊大小。
3. 服務器端發送另一個協議消息 (用戶控制),這個消息中定義了 'StreamIsRecorded' 事件和流 ID。消息在前兩個字節中保存事件類型,在后四個字節中保存流 ID。
4. 服務器端發送另一個協議消息 (用戶控制),這一消息包含 'StreamBegin' 事件,來指示發送給客戶端的流的起點。
5. 如果客戶端發送的 play 命令成功,服務器端發送一個 onStatus 命令消息 NetStream.Play.Start & NetStream.Play.Reset。只有當客戶端發送的 play 命令設置了 reset 時服務器端才會發送 NetStream.Play.Reset。如果要播放的流沒有找到,服務器端發送 onStatus 消息 NetStream.Play.StreamNotFound。
之后,服務器端發送視頻和音頻數據,客戶端對其進行播放。
7.2.2.2. play2
不同於 play 命令的是,play2 可以在不改變播放內容時間軸的情況下切換到不同的比特率。服務器端為客戶端可以在 play2 中請求所有支持的碼率維護了不同的字段。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "play2"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息不存在,設置為 null 類型。 |
Parameters | 對象 | 一個 AMF 編碼的對象,該對象的屬性是為公開的 flash.net.NetStreamPlayOptions ActionScript 對象所描述的屬性。 |
NetStreamPlayOptions 對象的公開屬性在 ActionScript 3 語言指南中 [AS3] 有所描述。
命令執行時的消息流動如下圖所示:

7.2.2.3. deleteStream 命令
當 NetStream 對象消亡時 NetStream 發送 deleteStream 命令。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "deleteStream"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設為 null 類型。 |
Stream ID | 數字 | 服務器端消亡的流 ID。 |
服務器端不再發送任何回復。
7.2.2.4. receiveAudio 命令
NetStream 通過發送 receiveAudio 消息來通知服務器端是否發送音頻到客戶端。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
Command Name | 字符串 | 命令名,設置為 "receiveAudio"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設置為 null 類型。 |
Bool Flag | 布爾 | true 或者 false 以表明是否接受音頻。 |
如果發送來的 receiveAudio 命令布爾字段被設為 false 時服務器端不發送任何回復。如果這一標識被設為 true,服務器端以狀態消息 NetStream.Seek.Notify 和 NetStream.Play.Start 進行回復。
7.2.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 進行回復。
7.2.2.6. publish 命令
客戶端發送給服務器端這一命令以發布一個已命名的流。使用這個名字,任意客戶端都可以播放這個流,並接受發布的音頻、視頻以及數據消息。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,設置為 "publish"。 |
Transaction ID | 數字 | 事務 ID 設置為 0。 |
Command Object | Null | 命令信息對象不存在,設置為 null 類型。 |
Publishing Name | 字符串 | 發布的流的名字。 |
Publishing Type | 字符串 | 發布類型。可以設置為 "live"、"record" 或者 "append"。record:流被發布,數據被錄制到一個新的文件。新文件被存儲在服務器上包含服務應用目錄的子路徑。如果文件已存在,將重寫。append:流被發布,數據被添加到一個文件。如果該文件沒找着,將新建一個。live:直播數據只被發布,並不對其進行錄制。 |
服務器端回復 onStatus 命令以標注發布的起始位置。
7.2.2.7. seek 命令
客戶端發送 seek 命令以查找一個多媒體文件或一個播放列表的偏移量 (以毫秒為單位)。
客戶端發送到服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令的名字,設為 "seek"。 |
Transaction ID | 數字 | 事務 ID 設為 0。 |
Command Object | Null | 沒有命令信息對象,設置為 null 類型。 |
milliSeconds | 數字 | 播放列表查找的毫秒數。 |
seek 命令執行成功時服務器會發送一個狀態消息 NetStream.Seek.Notify。失敗的話,服務器端返回一個 _error 消息。
7.2.2.8. pause 命令
客戶端發送 pause 命令以告知服務器端是暫停還是開始播放。
客戶端發送給服務器端的命令結構如下:
字段名 | 類型 | 描述 |
---|---|---|
Command Name | 字符串 | 命令名,設為 "pause"。 |
Transaction ID | 數字 | 沒有這一命令的事務 ID,設為 0。 |
Command Object | Null | 命令信息對象不存在,設為 null 類型。 |
Pause/Unpause Flag | 布爾 | true 或者 false,來指示暫停或者重新播放。 |
milliSeconds | 數字 | 流暫停或者重新開始所在的毫秒數。這個是客戶端暫停的當前流時間。當回放已恢復時,服務器端值發送帶有比這個值大的 timestamp 消息。 |
當流暫停時,服務器端發送一個狀態消息 NetStream.Pause.Notify。NetStream.Unpause.Notify 只有針對沒有暫停的流進行發放。失敗的話,服務器端返回一個 _error 消息。
7.3. 消息交換例子
這里有幾個解釋使用 RTMP 交換消息的例子。
7.3.1. 發布錄制視頻
這個例子說明了一個客戶端是如何能夠發布一個直播流然后傳遞視頻流到服務器的。然后其他客戶端可以對發布的流進行訂閱並播放視頻。

7.3.2. 廣播一個共享對象消息
這個例子說明了在一個共享對象的創建和改變期間交換消息的變化。它也說明了共享對象消息廣播的處理過程。

7.3.3. 發布來自錄制流的元數據
這個例子描述了用於發布元數據的消息交換。

8. 參考文獻
[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, <http://www.adobe.com/devnet/actionscript/documentation.html>.
[AMF0] Adobe Systems, Inc., "Action Message Format -- AMF 0", December 2007, <http://opensource.adobe.com/wiki/download/attachments/1114283/amf0_spec_121207.pdf>.
[AMF3] Adobe Systems, Inc., "Action Message Format -- AMF 3", May 2008, <http://opensource.adobe.com/wiki/download/attachments/1114283/amf3_spec_05_05_08.pdf>.
作者地址
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/
原文鏈接:http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf