RTMP協議是Real Time Message Protocol(實時信息傳輸協議)的縮寫,它是由Adobe公司提出的一種應用層的協議,用來解決多媒體數據傳輸流的多路複用(Multiplexing)和分包(packetizing)的問題。隨着VR技術的發展,視頻直播等領域逐漸活躍起來,RTMP做爲業內普遍使用的協議也從新被相關開發者重視起來。正好最近在從事這方面的工做,在此記錄下本身對RTMP的理解,文章內容多翻譯自英文版RTMP文檔,按照本人的理解從新整理,但願能夠幫助想要了解RTMP協議的朋友,也方面本身往後查閱。 git
1. 整體介紹: RTMP協議是應用層協議,是要靠底層可靠的傳輸層協議(一般是TCP)來保證信息傳輸的可靠性的。在基於傳輸層協議的連接創建完成後,RTMP協議也要客戶端和服務器經過「握手」來創建基於傳輸層連接之上的RTMP Connection連接,在Connection連接上會傳輸一些控制信息,如SetChunkSize,SetACKWindowSize。其中CreateStream命令會建立一個Stream連接,用於傳輸具體的音視頻數據和控制這些信息傳輸的命令信息。RTMP協議傳輸時會對數據作本身的格式化,這種格式的消息咱們稱之爲RTMP Message,而實際傳輸的時候爲了更好地實現多路複用、分包和信息的公平性,發送端會把Message劃分爲帶有Message ID的Chunk,每一個Chunk多是一個單獨的Message,也多是Message的一部分,在接受端會根據chunk中包含的data的長度,message id和message的長度把chunk還原成完整的Message,從而實現信息的收發。 2. 握手 要創建一個有效的RTMP Connection連接,首先要「握手」:客戶端要向服務器發送C0,C1,C2(按序)三個chunk,服務器向客戶端發送S0,S1,S2(按序)三個chunk,而後才能進行有效的信息傳輸。RTMP協議自己並無規定這6個Message的具體傳輸順序,但RTMP協議的實現者須要保證這幾點: - 客戶端要等收到S1以後才能發送C2 - 客戶端要等收到S2以後才能發送其餘信息(控制信息和真實音視頻等數據) - 服務端要等到收到C0以後發送S1 - 服務端必須等到收到C1以後才能發送S2 - 服務端必須等到收到C2以後才能發送其餘信息(控制信息和真實音視頻等數據) 若是每次發送一個握手chunk的話握手順序會是這樣:
理論上來說只要知足以上條件,如何安排6個Message的順序都是能夠的,但實際實現中爲了在保證握手的身份驗證功能的基礎上儘可能減小通訊的次數,通常的發送順序是這樣的,這一點能夠經過wireshark抓ffmpeg推流包進行驗證: |client|Server | |---C0+C1—->| |<--S0+S1+S2– | |---C2----> |
3. RTMP Chunk Streamgithub
Chunk Stream是對傳輸RTMP Chunk的流的邏輯上的抽象,客戶端和服務器之間有關RTMP的信息都在這個流上通訊。這個流上的操做也是咱們關注RTMP協議的重點。windows
3.1 Message(消息)服務器
這裏的Message是指知足該協議格式的、能夠切分紅Chunk發送的消息,消息包含的字段以下:網絡
Message Stream ID(消息的流ID):每一個消息的惟一標識,劃分紅Chunk和還原Chunk爲Message的時候都是根據這個ID來辨識是不是同一個消息的Chunk的,4個字節,而且以小端格式存儲app
3.2 Chunking(Message分塊)RTMP在收發數據的時候並非以Message爲單位的,而是把Message拆分紅Chunk發送,並且必須在一個Chunk發送完成以後才能開始發送下一個Chunk。每一個Chunk中帶有MessageID表明屬於哪一個Message,接受端也會按照這個id來將chunk組裝成Message。 爲何RTMP要將Message拆分紅不一樣的Chunk呢?經過拆分,數據量較大的Message能夠被拆分紅較小的「Message」,這樣就能夠避免優先級低的消息持續發送阻塞優先級高的數據,好比在視頻的傳輸過程當中,會包括視頻幀,音頻幀和RTMP控制信息,若是持續發送音頻數據或者控制數據的話可能就會形成視頻幀的阻塞,而後就會形成看視頻時最煩人的卡頓現象。同時對於數據量較小的Message,能夠經過對Chunk Header的字段來壓縮信息,從而減小信息的傳輸量。(具體的壓縮方式會在後面介紹) Chunk的默認大小是128字節,在傳輸過程當中,經過一個叫作Set Chunk Size的控制信息能夠設置Chunk數據量的最大值,在發送端和接受端會各自維護一個Chunk Size,能夠分別設置這個值來改變本身這一方發送的Chunk的最大大小。大一點的Chunk減小了計算每一個chunk的時間從而減小了CPU的佔用率,可是它會佔用更多的時間在發送上,尤爲是在低帶寬的網絡狀況下,極可能會阻塞後面更重要信息的傳輸。小一點的Chunk能夠減小這種阻塞問題,但小的Chunk會引入過多額外的信息(Chunk中的Header),少許屢次的傳輸也可能會形成發送的間斷致使不能充分利用高帶寬的優點,所以並不適合在高比特率的流中傳輸。在實際發送時應對要發送的數據用不一樣的Chunk Size去嘗試,經過抓包分析等手段得出合適的Chunk大小,而且在傳輸過程當中能夠根據當前的帶寬信息和實際信息的大小動態調整Chunk的大小,從而儘可能提升CPU的利用率並減小信息的阻塞機率。 3.3 Chunk Format(塊格式) 3.3.1 Basic Header(基本的頭信息): 包含了chunk stream ID(流通道Id)和chunk type(chunk的類型),chunk stream id通常被簡寫爲CSID,用來惟一標識一個特定的流通道,chunk type決定了後面Message Header的格式。Basic Header的長度多是1,2,或3個字節,其中chunk type的長度是固定的(佔2位,注意單位是位,bit),Basic Header的長度取決於CSID的大小,在足夠存儲這兩個字段的前提下最好用盡可能少的字節從而減小因爲引入Header增長的數據量。 RTMP協議支持用戶自定義[3,65599]之間的CSID,0,1,2由協議保留表示特殊信息。0表明Basic Header總共要佔用2個字節,CSID在[64,319]之間,1表明佔用3個字節,CSID在[64,65599]之間,2表明該chunk是控制信息和一些命令信息,後面會有詳細的介紹。 chunk type的長度固定爲2位,所以CSID的長度是(6=8-2)、(14=16-2)、(22=24-2)中的一個。 當Basic Header爲1個字節時,CSID佔6位,6位最多能夠表示64個數,所以這種狀況下CSID在[0,63]之間,其中用戶可自定義的範圍爲[3,63]。
當Basic Header爲2個字節時,CSID佔14位,此時協議將與chunk type所在字節的其餘位都置爲0,剩下的一個字節來表示CSID-64,這樣共有8個二進制位來存儲CSID,8位能夠表示[0,255]共256個數,所以這種狀況下CSID在[64,319],其中319=255+64。
當Basic Header爲3個字節時,CSID佔22位,此時協議將[2,8]字節置爲1,餘下的16個字節表示CSID-64,這樣共有16個位來存儲CSID,16位能夠表示[0,65535]共65536個數,所以這種狀況下CSID在[64,65599],其中65599=65535+64,須要注意的是,Basic Header是採用小端存儲的方式,越日後的字節數量級越高,所以經過這3個字節每一位的值來計算CSID時,應該是:<第三個字節的值>x256+<第二個字節的值>+64
能夠看到2個字節和3個字節的Basic Header所能表示的CSID是有交集的[64,319],但實際實現時仍是應該秉着最少字節的原則使用2個字節的表示方式來表示[64,319]的CSID。異步
3.3.2 Message Header(消息的頭信息): 包含了要發送的實際信息(多是完整的,也多是一部分)的描述信息。Message Header的格式和長度取決於Basic Header的chunk type,共有4種不一樣的格式,由上面所提到的Basic Header中的fmt字段控制。其中第一種格式能夠表示其餘三種表示的全部數據,但因爲其餘三種格式是基於對以前chunk的差量化的表示,所以能夠更簡潔地表示相同的數據,實際使用的時候仍是應該採用儘可能少的字節表示相贊成義的數據。如下按照字節數從多到少的順序分別介紹這4種格式的Message Header。 Type=0: type=0時Message Header佔用11個字節,其餘三種能表示的數據它都能表示,但在chunk stream的開始的第一個chunk和頭信息中的時間戳後退(即值與上一個chunk相比減少,一般在回退播放的時候會出現這種狀況)的時候必須採用這種格式。ide
msg stream id(消息的流id):佔用4個字節,表示該chunk所在的流的ID,和Basic Header的CSID同樣,它採用小端存儲的方式, Type = 1: type=1時Message Header佔用7個字節,省去了表示msg stream id的4個字節,表示此chunk和上一次發的chunk所在的流相同,若是在發送端只和對端有一個流連接的時候能夠儘可能去採起這種格式。 timestamp delta:佔用3個字節,注意這裏和type=0時不一樣,存儲的是和上一個chunk的時間差。相似上面提到的timestamp,當它的值超過3個字節所能表示的最大值時,三個字節都置爲1,實際的時間戳差值就會轉存到Extended Timestamp字段中,接受端在判斷timestamp delta字段24個位都爲1時就會去Extended timestamp中解析時機的與上次時間戳的差值。 Type = 2:
type=2時Message Header佔用3個字節,相對於type=1格式又省去了表示消息長度的3個字節和表示消息類型的1個字節,表示此chunk和上一次發送的chunk所在的流、消息的長度和消息的類型都相同。餘下的這三個字節表示timestamp delta,使用同type=1 Type = 3 0字節!!!好吧,它表示這個chunk的Message Header和上一個是徹底相同的,天然就不用再傳輸一遍了。當它跟在Type=0的chunk後面時,表示和前一個chunk的時間戳都是相同的。何時連時間戳都相同呢?就是一個Message拆分紅了多個chunk,這個chunk和上一個chunk同屬於一個Message。而當它跟在Type=1或者Type=2的chunk後面時,表示和前一個chunk的時間戳的差是相同的。好比第一個chunk的Type=0,timestamp=100,第二個chunk的Type=2,timestamp delta=20,表示時間戳爲100+20=120,第三個chunk的Type=3,表示timestamp delta=20,時間戳爲120+20=140 3.3.3 Extended Timestamp(擴展時間戳): 上面咱們提到在chunk中會有時間戳timestamp和時間戳差timestamp delta,而且它們不會同時存在,只有這二者之一大於3個字節能表示的最大數值0xFFFFFF=16777215時,纔會用這個字段來表示真正的時間戳,不然這個字段爲0。擴展時間戳佔4個字節,能表示的最大數值就是0xFFFFFFFF=4294967295。當擴展時間戳啓用時,timestamp字段或者timestamp delta要全置爲1,表示應該去擴展時間戳字段來提取真正的時間戳或者時間戳差。注意擴展時間戳存儲的是完整值,而不是減去時間戳或者時間戳差的值。 3.3.4 Chunk Data(塊數據): 用戶層面上真正想要發送的與協議無關的數據,長度在(0,chunkSize]之間。 3.3.5 chunk表示例1
首先包含第一個Message的chunk的Chunk Type爲0,由於它沒有前面可參考的chunk,timestamp爲1000,表示時間戳。type爲0的header佔用11個字節,假定chunkstreamId爲3<127,所以Basic Header佔用1個字節,再加上Data的32個字節,所以第一個chunk共44=11+1+32個字節。 第二個chunk和第一個chunk的CSID,TypeId,Data的長度都相同,所以採用Chunk Type=2,timestamp delta=1020-1000=20,所以第二個chunk佔用36=3+1+32個字節。 第三個chunk和第二個chunk的CSID,TypeId,Data的長度和時間戳差都相同,所以採用Chunk Type=3省去所有Message Header的信息,佔用33=1+32個字節。 第四個chunk和第三個chunk狀況相同,也佔用33=1+32個字節。 最後實際發送的chunk以下:
函數
3.3.6 chunk表示例2 注意到Data的Length=307>128,所以這個Message要切分紅幾個chunk發送,第一個chunk的Type=0,Timestamp=1000,承擔128個字節的Data,所以共佔用140=11+1+128個字節。 第二個chunk也要發送128個字節,其餘字段也同第一個chunk,所以採用Chunk Type=3,此時時間戳也爲1000,共佔用129=1+128個字節。 第三個chunk要發送的Data的長度爲307-128-128=51個字節,仍是採用Type=3,共佔用1+51=52個字節。 最後實際發送的chunk以下:
3.4 協議控制消息(Protocol Control Message) 在RTMP的chunk流會用一些特殊的值來表明協議的控制消息,它們的Message Stream ID必須爲0(表明控制流信息),CSID必須爲2,Message Type ID能夠爲1,2,3,5,6,具體表明的消息會在下面依次說明。控制消息的接受端會忽略掉chunk中的時間戳,收到後當即生效。學習
4. 不一樣類型的RTMP Message
字段 | 類型 | 說明 |
---|---|---|
Command Name(命令名字) | String | 命令的名字,如」connect」 |
Transaction ID(事務ID) | Number | 恆爲1 |
Command Object(命令包含的參數對象) | Object | 鍵值對集合表示的命令參數 |
Optional User Arguments(額外的用戶參數) | Object | 用戶自定義的額外信息 |
第三個字段中的Command Object中會涉及到不少鍵值對,這裏再也不一一列出,使用時能夠參考協議的官方文檔。 消息的迴應有兩種,_result表示接受鏈接,_error表示鏈接失敗
4.1.1.2 Call:用於在對端執行某函數,即常說的RPC:遠程進程調用,消息的結構以下:
字段 | 類型 | 說明 |
---|---|---|
Procedure Name(進程名) | String | 要調用的進程名稱 |
Transaction ID | Number|若是想要對端響應的話置爲非0值,不然置爲0 | |
Command Object | Object | 命令參數 |
Optional Arguents | Object | 用戶自定義參數 |
若是消息中的TransactionID不爲0的話,對端須要對該命令作出響應,響應的消息結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name(命令名) | String | 命令的名稱 |
TransactionID | Number | 上面接收到的命令消息中的TransactionID |
Command Object | Object | 命令參數 |
Optional Arguments | Object | 用戶自定義參數 |
4.1.1.3 Create Stream:建立傳遞具體信息的通道,從而能夠在這個流中傳遞具體信息,傳輸信息單元爲Chunk。
字段 | 類型 | 說明 |
---|---|---|
Command Name(命令名) | String | 「createStream」 |
TransactionID | Number | 上面接收到的命令消息中的TransactionID |
Command Object | Object | 命令參數 |
Optional Arguments | Object | 用戶自定義參數 |
4.1.2 NetStream Commands(流鏈接上的命令)
Netstream創建在NetConnection之上,經過NetConnection的createStream命令建立,用於傳輸具體的音頻、視頻等信息。在傳輸層協議之上只能鏈接一個NetConnection,但一個NetConnection能夠創建多個NetStream來創建不一樣的流通道傳輸數據。 如下會列出一些經常使用的NetStream Commands,服務端收到命令後會經過onStatus的命令來響應客戶端,表示當前NetStream的狀態。 onStatus命令的消息結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「onStatus」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL | 對onSatus命令來講不須要這個字段 |
Info Object | Object | AMF類型的Object,至少包含如下三個屬性:1,「level」,String類型,能夠爲「warning」、」status」、」error」中的一種;2,」code」,String類型,表明具體狀態的關鍵字,好比」NetStream.Play.Start」表示開始播流;3,」description」,String類型,表明對當前狀態的描述,提供對當前狀態可讀性更好的解釋,除了這三種必要信息,用戶還能夠本身增長自定義的鍵值對 |
4.1.2.1 play(播放):由客戶端向服務器發起請求從服務器端接受數據(若是傳輸的信息是視頻的話就是請求開始播流),能夠屢次調用,這樣本地就會造成一組數據流的接收者。注意其中有一個reset字段,表示是覆蓋以前的播流(設爲true)仍是從新開始一路播放(設爲false)。 play命令的結構以下:
字段 | 類型 | 說明 |
---|---|---|
命令名 | String | 「play」 |
事務ID | Number | 恆爲0 |
命令參數對象 | Null | 不須要此字段,設爲空 |
流名稱 | String | 要播放的流的名稱 |
開始位置 | Number | 可選參數,表示從什麼時候開始播流,以秒爲單位。默認爲-2,表明選取對應該流名稱的直播流,即當前正在推送的流開始播放,若是對應該名稱的直播流不存在,就選取該名稱的流的錄播版本,若是這也沒有,當前播流端要等待直到對端開始該名稱的流的直播。若是傳值-1,那麼只會選取直播流進行播放,即便有錄播流也不會播放;若是傳值或者正數,就表明從該流的該時間點開始播放,若是流不存在的話就會自動播放播放列表中的下一個流 |
週期 | Number | 可選參數,表示回退的最小間隔單位,以秒爲單位計數。默認值爲-1,表明直到直播流再也不可用或者錄播流中止後才能回退播放;若是傳值爲0,表明從當前幀開始播放 |
重置 | Boolean | 可選參數,true表明清除以前的流,從新開始一路播放,false表明保留原來的流,向本地的播放列表中再添加一條播放流 |
4.1.2.2 play2(播放):和上面的play命令不一樣的是,play2命令能夠將當前正在播放的流切換到一樣數據但不一樣比特率的流上,服務器端會維護多種比特率的文件來供客戶端使用play2命令來切換。
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「play2」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL,對onSatus命令來講不須要這個字段 | |
parameters | Object | AMF編碼的Flash對象,包括了一些用於描述flash.net.NetstreamPlayOptions ActionScript obejct的參數 |
4.1.2.3 deleteStream(刪除流):用於客戶端告知服務器端本地的某個流對象已被刪除,不須要再傳輸此路流。
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「deleteStream」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL,對onSatus命令來講不須要這個字段 | |
Stream ID(流ID) | Number | 本地已刪除,再也不須要服務器傳輸的流的ID |
4.1.2.4 receiveAudio(接收音頻):通知服務器端該客戶端是否要發送音頻 receiveAudio命令結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「receiveAudio」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL | 對onSatus命令來講不須要這個字段 |
Bool Flag | Boolean | true表示發送音頻,若是該值爲false,服務器端不作響應,若是爲true的話,服務器端就會準備接受音頻數據,會向客戶端回覆NetStream.Seek.Notify和NetStream.Play.Start的Onstatus命令告知客戶端當前流的狀態 |
4.1.2.5 receiveVideo(接收視頻):通知服務器端該客戶端是否要發送視頻 receiveVideo命令結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「receiveVideo」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL | 對onSatus命令來講不須要這個字段 |
Bool Flag | Boolean | true表示發送視頻,若是該值爲false,服務器端不作響應,若是爲true的話,服務器端就會準備接受視頻數據,會向客戶端回覆NetStream.Seek.Notify和NetStream.Play.Start的Onstatus命令告知客戶端當前流的狀態 |
4.1.2.6 publish(推送數據):由客戶端向服務器發起請求推流到服務器。 publish命令結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「publish」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL,對onSatus命令來講不須要這個字段 | |
Publishing Name(推流的名稱) | String | 流名稱| |
Publishing Type(推流類型) | String | 「live」、」record」、」append」中的一種。live表示該推流文件不會在服務器端存儲;record表示該推流的文件會在服務器應用程序下的子目錄下保存以便後續播放,若是文件已經存在的話刪除原來全部的內容從新寫入;append也會將推流數據保存在服務器端,若是文件不存在的話就會創建一個新文件寫入,若是對應該流的文件已經存在的話保存原來的數據,在文件末尾接着寫入 |
4.1.2.7 seek(定位流的位置):定位到視頻或音頻的某個位置,以毫秒爲單位。 seek命令的結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「seek」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL,對onSatus命令來講不須要這個字段 | |
milliSeconds | Number | 定位到該文件的xx毫秒處| |
4.1.2.8 pause(暫停):客戶端告知服務端中止或恢復播放。 pause命令的結構以下:
字段 | 類型 | 說明 |
---|---|---|
Command Name | String | 「pause」 |
TransactionID | Number | 恆爲0 |
Command Object | NULL,對onSatus命令來講不須要這個字段 | |
Pause/Unpause Flag | Boolean | true表示暫停,false表示恢復 |
milliSeconds | Number | 暫停或者恢復的時間,以毫秒爲單位| |
若是Pause爲true即表示客戶端請求暫停的話,服務端暫停對應的流會返回NetStream.Pause.Notify的onStatus命令來告知客戶端當前流處於暫停的狀態,當Pause爲false時,服務端會返回NetStream.Unpause.Notify的命令來告知客戶端當前流恢復。若是服務端對該命令響應失敗,返回_error信息。
5. 表明流程 5.1 推流流程
5.2 播流流程
6. 新手建議
若是讀者仔細讀完了上面講的RTMP協議,想必會以爲RTMP協議很是繁瑣,事實也確實是這樣,RTMP協議中充斥着不少冗餘的字段,好比三次握手中的時間戳的校對,還有一些特殊的命令,如FCPublish、UnFCPublish等,但在實際實現中爲了保證更大兼容性一般仍是要處理這些看似多餘的命令。加上Adobe對RTMP協議的實現細節有些並無按照協議來或者協議中沒有寫清楚本身搞了一套實現,其餘應用開發時還要兼容Adobe錯誤的實現,從而使的RTMP也一直爲開發者所詬病。但無論怎樣,RTMP確實提供了一種可以全面而且實現簡單的協議來保證流信息的傳輸,這方面暫時尚未一種更完善更簡潔的協議可以取代它在視頻流開發中的地位。 新人一開始接觸RTMP的時候確定會以爲頭大,這也是RTMP協議不簡潔的後果。建議讀者在學習時先過一遍協議理解大概的概念和流程,而後對照wireshark抓的包,和協議進行比對,這樣將理論和實踐結合,應該會理解的更快一點。
from:http://mingyangshang.github.io/2016/03/06/RTMP協議/