直播推流實現RTMP協議的一些注意事項

—— 2017-2-12 更新
RTMP 協議整理了一下,包括rtmp 消息類型,rtmp 如何分塊,rtmp分塊例子。 用腦圖整理了一下,使用Xmind 打開,URL: https://github.com/gezhaoyou/RtmpMindmap

  1. rtmp 消息類型

Paste_Image.png
  1. rtmp 消息分塊

Paste_Image.png

總體介紹

前一段時間寫過一篇文章: iOS直播視頻數據採集、硬編碼保存h264文件,比較詳細的記錄了在做iOS端進行視頻數據採集和編碼的過程,下一步要做的就是RTMP協議推流。因爲在公司將RTMP協議用Java 和 Swift 分別實現了一遍,所以對這塊比較瞭解,中間遇到了不少坑,記錄下來也怕自己忘掉。
RTMP協議是 Adobe 公司開發的一個基於TCP的應用層協議,Adobe 公司也公佈了關於RTMP的規範,但是這個協議規範介紹的有些地方非常模糊,很多東西和實際應用是有差別的。網上也有不少關於這個協議的介紹,但都不是太詳細。我遇到的比較好的參考資料就是這篇:帶你吃透RTMP, 這篇文章只是在理論上對RTMP進行了比較詳細的解釋,很多東西還是和實際應用有出入。我這篇文章只是把遇到的一些坑記錄下來,並不是詳解RTMP消息的。
另外懂RTMP消息拆包分包,而不真正的寫寫的話是很難把RTMP協議弄得的很清楚,關於RTMP協議的實現也是比較麻煩的事,懂和做事兩回事。
另外用wireshark 抓一下包的話可以非常直觀的看到RTMP通信的過程,對理解RTMP非常有幫助,在調試代碼的時候也大量藉助wireshark排錯,是一個非常有用的工具。

1. RTMP 握手

RTMP 握手分爲簡單握手和複雜握手,現在Adobe公司使用RTMP協議的產品應該用的都是複雜握手,這裏不介紹,只說簡單握手。 按照網上的說法RTMP握手的過程如下

  1. 握手開始於客戶端發送C0、C1塊。服務器收到C0或C1後發送S0和S1。
  2. 當客戶端收齊S0和S1後,開始發送C2。當服務器收齊C0和C1後,開始發送S2。
  3. 當客戶端和服務器分別收到S2和C2後,握手完成。

在實際工程應用中,一般是客戶端先將C0, C1塊同時發出,服務器在收到C1 之後同時將S0, S1, S2發給客戶端。S2的內容就是收到的C1塊的內容。之後客戶端收到S1塊,並原樣返回給服務器,簡單握手完成。按照RTMP協議個要求,客戶端需要校驗C1塊的內容和S2塊的內容是否相同,相同的話才徹底完成握手過程,實際編寫程序用一般都不去做校驗。
RTMP握手的這個過程就是完成了兩件事:1. 校驗客戶端和服務器端RTMP協議版本號,2. 是發了一堆數據,猜想應該是測試一下網絡狀況,看看有沒有傳錯或者不能傳的情況。RTMP握手是整個RTMP協議中最容易實現的一步,接下來纔是大頭。

2. RTMP 分塊

創建RTMP連接算是比較難的地方,開始涉及消息分塊(chunking)和 AFM(也是Adobe家的東西)格式數據的一些東西,在上面提到的文章中也有介紹爲什要進行RTMP分塊。

Chunk Size

RTMP是按照chunk size進行分塊,chunk size指的是 chunk的payload部分的大小,不包括chunk basic header 和 chunk message header,即chunk的body的大小。客戶端和服務器端各自維護了兩個chunk size, 分別是自身分塊的chunk size 和 對端 的chunk size, 默認的這兩個chunk size都是128字節。通過向對端發送set chunk size 消息告知對方更改了 chunk size的大小,即告訴對端:我接下來要以xxx個字節拆分RTMP消息,你在接收到消息的時候就按照新的chunk size 來組包。
在實際寫代碼的時候一般會把chunk size設置的很大,有的會設置爲4096,FFMPEG推流的時候設置的是 60*1000,這樣設置的好處是避免了頻繁的拆包組包,佔用過多的CPU。設置太大的話也不好,一個很大的包如果發錯了,或者丟失了,播放端就會出現長時間的花屏或者黑屏等現象。

Chunk Type

RTMP 分成的Chunk有4中類型,可以通過 chunk basic header的 高兩位指定,一般在拆包的時候會把一個RTMP消息拆成以 Type_0 類型開始的chunk,之後的包拆成 Type_3 類型的chunk,我查看了有不少代碼也是這樣實現的,這樣也是最簡單的實現。
RTMP 中關於Message 分chunk只舉了兩個例子,這兩個例子不是很具有代表性。假如第二個message和第一個message的message stream ID 相同,並且第二個message的長度也大於了chunk size,那麼該如何拆包?當時查了很多資料,都沒有介紹。後來看了一些源碼,發現第二個message可以拆成Type_1類型一個chunk, message剩餘的部分拆成Type_3類型的chunk。FFMPEG中好像就是這麼做的。

3. RTMP 消息

關於推流的過程,RTMP的協議文檔上給了一個示例,而真實的RTMP通信過程和它有較大的差異,只說推流,RTMP播放端我沒有做過。

Connect消息

握手之後先發送一個connect 命令消息,命令裏面包含什麼東西,協議中沒有說,真實通信中要指定一些編解碼的信息,這些信息是以AMF格式發送的, 下面是用swift 寫的connect命令包含的參數信息:

       transactionID += 1 // 0x01
        let command:RTMPCommandMessage = RTMPCommandMessage(commandName: "connect", transactionId: transactionID, messageStreamId: 0x00)
        let objects:Amf0Object = Amf0Object()
        objects.setProperties("app", value: rtmpSocket.appName)
        objects.setProperties("flashVer",value: "FMLE/3.0 (compatible; FMSc/1.0)")
        objects.setProperties("swfUrl", value:"")
        objects.setProperties("tcUrl", value: "rtmp://" + rtmpSocket.hostname + "/" + rtmpSocket.appName)
        objects.setProperties("fpad", value: false)
        objects.setProperties("capabilities", value:239)
        objects.setProperties("audioCodecs", value:3575)
        objects.setProperties("videoCodecs", value:252)
        objects.setProperties("videoFunction",value: 1)
        objects.setProperties("pageUrl",value: "")
        objects.setProperties("objectEncoding",value: 0)

這些信息具體什麼意思我也不太明白,協議中也沒有,都是我在看librtmp,srs-librtmp這些源碼,以及用wireshark 抓包的時候看到的。其中參數少一兩個貌似也沒問題,但是audioCodecsvideoCodecs這兩個指定音視頻編碼信息的不能少。
服務器返回的是一個_result命令類型消息,這個消息的payload length一般不會大於128字節,但是在最新的nginx-rtmp中返回的消息長度會大於128字節,所以一定要做好收包,組包的工作。
關於消息的transactionID是用來標識command類型的消息的,服務器返回的_result消息可以通過 transactionID來區分是對哪個命令的迴應,connect 命令發完之後還要發送其他命令消息,要保證他們的transactionID不相同。
發送完connect命令之後一般會發一個 set chunk size消息來設置chunk size 的大小,也可以不發。
Window Acknowledgement Size 是設置接收端消息窗口大小,一般是2500000字節,即告訴客戶端你在收到我設置的窗口大小的這麼多數據之後給我返回一個ACK消息,告訴我你收到了這麼多消息。在實際做推流的時候推流端要接收很少的服務器數據,遠遠到達不了窗口大小,所以基本不用考慮這點。而對於服務器返回的ACK消息一般也不做處理,我們默認服務器都已經收到了這麼多消息。
之後要等待服務器對於connect的迴應的,一般是把服務器返回的chunk都讀完組成完整的RTMP消息,沒有錯誤就可以進行下一步了。

Create Stream 消息

創建完RTMP連接之後就可以創建RTMP流,客戶端要想服務器發送一個releaseStream命令消息,之後是FCPublish命令消息,在之後是createStream命令消息。當發送完createStream消息之後,解析服務器返回的消息會得到一個stream ID, 這個ID也就是以後和服務器通信的 message stream ID, 一般返回的是1,不固定。

Publish Stream

推流準備工作的最後一步是 Publish Stream,即向服務器發一個publish命令,這個命令的message stream ID 就是上面 create stream 之後服務器返回的stream ID,發完這個命令一般不用等待服務器返回的迴應,直接下一步發送音視頻數據。有些rtmp庫 還會發setMetaData消息,這個消息可以發也可以不發,裏面包含了一些音視頻編碼的信息。

4. 發佈音視頻

當以上工作都完成的時候,就可以發送音視頻了。音視頻RTMP消息的Payload中都放的是按照FLV-TAG格式封的音視頻包,具體可以參照FLV協議文檔。

5. 關於RTMP的時間戳

RTMP的時間戳在發送音視頻之前都爲零,開始發送音視頻消息的時候只要保證時間戳是單增的基本就可以正常播放音視頻。我讀Srs-librtmp的源碼,發現他們是用h264的dts作爲時間戳的。我在用java寫的時候是先獲取了下當前系統時間,然後每次發送消息的時候都與這個起始時間相減,得到時間戳。

6. 關於Chunk Stream ID

RTMP 的Chunk Steam ID是用來區分某一個chunk是屬於哪一個message的 ,0和1是保留的。每次在發送一個不同類型的RTMP消息時都要有不用的chunk stream ID, 如上一個Message 是command類型的,之後要發送視頻類型的消息,視頻消息的chunk stream ID 要保證和上面 command類型的消息不同。每一種消息類型的起始chunk 的類型必須是 Type_0 類型的,表明我是一個新的消息的起始。

發佈了126 篇原創文章 · 獲贊 61 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章