EasyRTMP實現的rtmp推流的基本協議流程
EasyRTMP介紹
EasyRTMP是結合了多種音視訊快取及網路技術的一個rtmp直播推流端,包括:圓形緩衝區(circular buffer)、智慧丟幀、自動重連、rtmp協議等等多種技術,能夠非常有效地適應各種平臺(Windows、Linux、ARM、Android、iOS),各種網路環境(有線、wifi、4G),以及各種情況下的直播恢復(伺服器重啟、網路重啟、硬體裝置重啟),今天我們講解的是EasyRTMP中rtmp推送連線的建立、推送H264+AAC音視訊、以及rtmp推送重連過程的詳細講解;
librtmp實現的RTMP基本協議
EasyRTMP中RTMP推送流程依然採用的是業界良心的librtmp,其穩定性和可靠性毋庸置疑,已經得到了廣大的開發者的驗證,所以EasyRTMP也直接採用了librtmp,librtmp的下載可以到:
整個程式包含3個介面函式:
RTMP264_Connect():建立RTMP連線。
RTMP264_Send():傳送資料。
RTMP264_Close():關閉RTMP連線。
按照順序呼叫上述3個介面函式就可以完成H.264碼流的傳送。
結構圖中關鍵函式的作用如下所列。
RTMP264_Connect()中包含以下函式:
InitSockets():初始化Socket
RTMP_Alloc():為結構體“RTMP”分配記憶體。
RTMP_Init():初始化結構體“RTMP”中的成員變數。
RTMP_SetupURL():設定輸入的RTMP連線的URL。
RTMP_EnableWrite():釋出流的時候必須要使用。如果不使用則代表接收流。
RTMP_Connect():建立RTMP連線,建立一個RTMP協議規範中的NetConnection。
RTMP_ConnectStream():建立一個RTMP協議規範中的NetStream。
RTMP264_Send()中包含以下函式:
ReadFirstNaluFromBuf():從記憶體中讀取出第一個NAL單元
ReadOneNaluFromBuf():從記憶體中讀取出一個NAL單元
h264_decode_sps():解碼SPS,獲取視訊的寬,高,幀率資訊
SendH264Packet():傳送一個NAL單元
SendH264Packet()中包含以下函式:
SendVideoSpsPps():如果是關鍵幀,則在傳送該幀之前先發送SPS和PPS
SendPacket():組裝一個RTMPPacket,呼叫RTMP_SendPacket()傳送出去
RTMP_SendPacket():傳送一個RTMP資料RTMPPacket
RTMP264_Close()中包含以下函式:
RTMP_Close():關閉RTMP連線
RTMP_Free():釋放結構體“RTMP”
CleanupSockets():關閉Socket
RTMP直播推流中需要注意的點
1. RTMP 握手
RTMP 握手分為簡單握手和複雜握手,現在Adobe公司使用RTMP協議的產品應該用的都是複雜握手,這裡不介紹,只說簡單握手。 按照網上的說法RTMP握手的過程如下
- 握手開始於客戶端傳送C0、C1塊。伺服器收到C0或C1後傳送S0和S1。
- 當客戶端收齊S0和S1後,開始傳送C2。當伺服器收齊C0和C1後,開始傳送S2。
- 當客戶端和伺服器分別收到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 訊息
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)
伺服器返回的是一個_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 型別的,表明我是一個新的訊息的起始。
EasyRTMP實現的重連過程
EasyRTMP對librtmp實現了一層封裝,不斷會檢測librtmp直播推送當前的RTMP連線狀態,當檢測到librtmp連線與RTMP流媒體伺服器斷開的時候,我們會重新進行整個RTMP的握手、Connect、metadata的流程,這樣就能夠保證在沒有人為干預的情況下,EasyRTMP對外能提供穩定的推送服務,外部只需要考慮將生產者生產的音視訊資料來源源不斷地往EasyRTMP介面進行推送就可以了,而且EasyRTMP的音視訊推送介面也是採用的非同步模式,內部執行緒進行傳送處理,這樣就保證了外圍呼叫者的工作效率,不會像直接呼叫librtmp那樣,在網路較差的情況下,會出現傳送阻塞等問題!
Github
獲取更多資訊
Copyright © EasyDarwin.org 2012-2016