直播推流實現(xiàn)RTMP協(xié)議的一些注意事項

2018年8月4日第三次更新,詳細介紹了RTMP協(xié)議與遇到的坑


1. 簡介

RTMP協(xié)議是Real Time Message Protocol(實時信息傳輸協(xié)議)的縮寫,它是由Adobe公司提出的一種應用層的協(xié)議,用來解決多媒體數(shù)據(jù)傳輸流的多路復用(Multiplexing)和分包(packetizing)的問題。隨著VR技術的發(fā)展,視頻直播等領域逐漸活躍起來,RTMP作為業(yè)內(nèi)廣泛使用的協(xié)議也重新被相關開發(fā)者重視起來。本文主要分享對RTMP的一些簡介和實際開發(fā)中遇到的一些狀況。

RTMP協(xié)議基本特點:

? 基于TCP協(xié)議的應用層協(xié)議

? 默認通信端口1935

RTMP URL格式:
rtmp://ip:[port]/appName/streamName
例如: rtmp://192.168.178.218:1935/live/devzhaoyou

參考:https://blog.csdn.net/ai2000ai/article/details/72771461

2. RTMP 握手

RTMP 握手分為簡單握手和復雜握手,現(xiàn)在Adobe公司使用RTMP協(xié)議的產(chǎn)品用復雜握手的較多,不做介紹。

握手包格式:

 0 1 2 3 4 5 6 7
+-+-+-+-+-+-+-+-+
|     version   |
+-+-+-+-+-+-+-+-+
 C0 and S0 bits

C0和S0:1個字節(jié),包含了RTMP版本, 當前RTMP協(xié)議的版本為 3

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           time (4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           zero (4 bytes)                      |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random bytes                        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           random bytes                        |
|                               (cont)                          |
|                               ....                            |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                        C1 and S1 bits

C1和S1:4字節(jié)時間戳,4字節(jié)的0,1528字節(jié)的隨機數(shù)


  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          time (4 bytes)                       |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          time2 (4 bytes)                      |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          random echo                          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          random echo                          |
 |                             (cont)                            |
 |                              ....                             |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                            C2 and S2 bits

C2和S2:4字節(jié)時間戳,4字節(jié)從對端讀到的時間戳,1528字節(jié)隨機數(shù)

RTMP握手基本過程:

+-------------+                            +-------------+
|   Client    |      TCP/IP Network        |     Server  |
+-------------+             |              +-------------+
       |                    |                     |
Uninitialized               |                Uninitialized
       |        C0          |                     |
       |------------------->|           C0        |
       |                    |-------------------->|
       |        C1          |                     |
       |------------------->|           S0        |
       |                    |<--------------------|
       |                    |           S1        |
  Version sent              |<--------------------|
       |        S0          |                     |
       |<-------------------|                     |
       |        S1          |                     |
       |<-------------------|               Version sent
       |                    |           C1        |
       |                    |-------------------->|
       |        C2          |                     |
       |------------------->|           S2        |
       |                    |<--------------------|
    Ack sent                |                   Ack Sent
       |        S2          |                     |
       |<-------------------|                     |
       |                    |           C2        |
       |                    |-------------------->|
Handshake Done              |               Handshake Done
      |                     |                     |
          Pictorial Representation of Handshake

握手開始于客戶端發(fā)送C0C1塊。服務器收到C0C1后發(fā)送S0S1。

當客戶端收齊S0S1后,開始發(fā)送C2。當服務器收齊C0C1后,開始發(fā)送S2。

當客戶端和服務器分別收到S2C2后,握手完成。

注意事項: 在實際工程應用中,一般是客戶端先將C0, C1塊同時發(fā)出,服務器在收到C1 之后同時將S0, S1, S2發(fā)給客戶端。S2的內(nèi)容就是收到的C1塊的內(nèi)容。之后客戶端收到S1塊,并原樣返回給服務器,簡單握手完成。按照RTMP協(xié)議個要求,客戶端需要校驗C1塊的內(nèi)容和S2塊的內(nèi)容是否相同,相同的話才徹底完成握手過程,實際編寫程序用一般都不去做校驗。

RTMP握手的這個過程就是完成了兩件事:

校驗客戶端和服務器端RTMP協(xié)議版本號

是發(fā)了一堆隨機數(shù)據(jù),校驗網(wǎng)絡狀況。

3. RTMP 消息

RTMP消息格式:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 | Message Type |               Payload length                   |
 |   (1 byte)   |                   (3 bytes)                    |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          Timestamp                            |
 |                          (4 bytes)                            |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          Stream ID            |
 |                          (3 bytes)            |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                         Message Header

? 1字節(jié)消息類型

? 3字節(jié)負載消息長度

? 4字節(jié)時間戳

? 3字節(jié) Stream ID,區(qū)分消息流

注意事項: 實際RTMP通信中并未按照上述格式去發(fā)送RTMP消息,而是將RTMP 消息分塊發(fā)送,之后將介紹RTMP消息分塊。

3.1. RTMP 消息分塊(chunking)

而對于基于TCPRTMP協(xié)議而言,協(xié)議顯得特別繁瑣,但是有沒有更好的替代方案。同時創(chuàng)建RTMP消息分塊是比較復雜的地方,涉及到了AFM(也是Adobe家的東西)格式數(shù)據(jù)的數(shù)據(jù)。

RTMP消息塊格式:

 +--------------+----------------+--------------------+--------------+
 | Basic Header | Message Header | Extended Timestamp |  Chunk Data  |
 +--------------+----------------+--------------------+--------------+
 |                                                    |
 |<------------------- Chunk Header ----------------->|
                            Chunk Format

RTMP消息塊構(gòu)成:

? Basic Header

? Message Header

? Extended Timestamp

? Chunk Data

Chunk Basic header格式有3種:

格式1:

   0 1 2 3 4 5 6 7
  +-+-+-+-+-+-+-+-+
  |fmt|   cs id   |
  +-+-+-+-+-+-+-+-+
 Chunk basic header 1

格式2:

  0                      1
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt|      0    |  cs id - 64   |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      Chunk basic header 2

格式3:

  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt|         1 |          cs id - 64           |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
             Chunk basic header 3

注意事項:

fmt: 用于指定Chunk Header 里面 Message Header的類型,后面會介紹到

cs id: 是chunk stream id的縮寫,同一個RTMP消息拆成的 chunk 塊擁有相同的 cs id, 用于區(qū)分chunk所屬的RTMP消息, chunk basic header 的類型cs id占用的字節(jié)數(shù)來確定

Message Header格式:

Message Header的類型通過上文chunk basic header中的fmt指定,共4種:

格式0:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          timestamp            | message length|
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |      message length (cont)    |message type id| msg stream id |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |          message stream id (cont)             |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
                Chunk Message Header - Type 0

Message Header占用11個字節(jié), 在chunk stream的開始的第一個chunk的時候必須采用這種格式。

? timestamp3個字節(jié),因此它最多能表示到16777215=0xFFFFFF=2^24-1, 當它的值超過這個最大值時,這三個字節(jié)都置為1,實際的timestamp會轉(zhuǎn)存到Extended Timestamp字段中,接受端在判斷timestamp字段24個位都為1時就會去Extended timestamp中解析實際的時間戳。

? message length3個字節(jié),表示實際發(fā)送的消息的數(shù)據(jù)如音頻幀、視頻幀等數(shù)據(jù)的長度,單位是字節(jié)。注意這里是Message的長度,也就是chunk屬于的Message的總數(shù)據(jù)長度,而不是chunk本身Data的數(shù)據(jù)的長度。

? message type id1個字節(jié),表示實際發(fā)送的數(shù)據(jù)的類型,如8代表音頻數(shù)據(jù)、9代表視頻數(shù)據(jù)。

? msg stream id:4個字節(jié),表示該chunk所在的流的ID,和Basic HeaderCSID一樣,它采用小端存儲的方式

格式1:

  0                   1                   2                   3
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |                          timestamp            | message length|
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |      message length (cont)    |message type id|  
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
                Chunk Message Header - Type 1 

Message Header占用7個字節(jié),省去了表示msg stream id的4個字節(jié),表示此chunk和上一次發(fā)的chunk所在的流相同。

? timestamp delta:3個字節(jié),注意這里和格式0時不同,存儲的是和上一個chunk的時間差。類似上面提到的timestamp,當它的值超過3個字節(jié)所能表示的最大值時,三個字節(jié)都置為1,實際的時間戳差值就會轉(zhuǎn)存到Extended Timestamp字段中,接受端在判斷timestamp delta字段24個位都為1時就會去Extended timestamp中解析時機的與上次時間戳的差值。

格式2:

  0                   1                   2     
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
 |                          timestamp            |  
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 
          Chunk Message Header - Type 2 

Message Header占用3個字節(jié),相對于格式1,又省去了表示消息長度的3個字節(jié)和表示消息類型的1個字節(jié),表示此chunk和上一次發(fā)送的chunk所在的流、消息的長度和消息的類型都相同。余下的這三個字節(jié)表示timestamp delta,使用同格式1。

格式3:

0字節(jié),它表示這個chunk的Message Header和上一個是完全相同的,無需再次傳送

Extended Timestamp(擴展時間戳):

chunk中會有時間戳timestamp和時間戳差timestamp delta, 只有這兩者之一大于3個字節(jié)能表示的最大數(shù)值0xFFFFFF=16777215時,才會用這個字段來表示真正的時間戳,否則這個字段不傳(感謝評論區(qū) @hijiang指出錯誤)。

擴展時間戳占4個字節(jié),能表示的最大數(shù)值就是0xFFFFFFFF=4294967295。當擴展時間戳啟用時,timestamp字段或者timestamp delta要全置為1,表示應該去擴展時間戳字段來提取真正的時間戳或者時間戳差。注意擴展時間戳存儲的是完整值,而不是減去時間戳或者時間戳差的值。

Chunk Data(塊數(shù)據(jù)): 用戶層面上真正想要發(fā)送的與協(xié)議無關的數(shù)據(jù),長度在(0,chunkSize]之間, chunk size默認為128字節(jié)。

RTMP 消息分塊注意事項

? Chunk Size:

RTMP是按照chunk size進行分塊,chunk size 指的是 chunkpayload部分的大小,不包括chunk basic headerchunk message header長度。客戶端和服務器端各自維護了兩個chunk size, 分別是自身分塊的chunk size 和 對端 的chunk size, 默認的這兩個chunk size都是128字節(jié)。通過向?qū)Χ税l(fā)送set chunk size 消息可以告知對方更改了 chunk size的大小。

? Chunk Type:

RTMP消息分成的Chunk4種類型,可以通過 chunk basic header的高兩位(fmt)指定,一般在拆包的時候會把一個RTMP消息拆成以格式0開始的chunk,之后的包拆成格式3 類型的chunk,我查看了有不少代碼也是這樣實現(xiàn)的,這樣也是最簡單的實現(xiàn)。

如果第二個message和第一個messagemessage stream ID 相同,并且第二個message的長度也大于了chunk size,那么該如何拆包?當時查了很多資料,都沒有介紹。后來看了一些源碼,如 SRS,FFMPEG中的實現(xiàn),發(fā)現(xiàn)第二個message可以拆成Type_1類型一個chunk, message剩余的部分拆成Type_3類型的chunk。FFMPEG中就是這么做的。

3.2 RTMP 交互消息

推流RTMP消息交互流程:

  • pic_2.png

關于推流的過程,RTMP的協(xié)議文檔上給了上圖示例,說一下推流注意事項:

3.2.1 Connect 消息

RTMP 命令消息格式:

 +----------------+---------+---------------------------------------+
 |  Field Name    |   Type  |               Description             |
 +--------------- +---------+---------------------------------------+
 |   Command Name | String  | Name of the command. Set to "connect".|
 +----------------+---------+---------------------------------------+
 | Transaction ID | Number  |            Always set to 1.           |
 +----------------+---------+---------------------------------------+
 | Command Object | Object  |  Command information object which has |
 |                |         |           the name-value pairs.       |
 +----------------+---------+---------------------------------------+
 | Optional User  | Object  |       Any optional information        |
 |   Arguments    |         |                                       |
 +----------------+---------+---------------------------------------+

RTMP握手之后先發(fā)送一個connect命令消息,命令里面包含什么東西,協(xié)議中沒有具體規(guī)定,實際通信中要攜帶 rtmp url 中的 appName 字段,并且指定一些編解碼的信息,并以AMF格式發(fā)送, 下面是用wireshake抓取connect命令需要包含的參數(shù)信息:

  • pic_3.png

這些信息協(xié)議中并沒有特別詳細說明, 在librtmp,srs-librtmp這些源碼中,以及用wireshark 抓包的時候可以看到。

服務器返回的是一個_result命令類型消息,這個消息的payload length一般不會大于128字節(jié),但是在最新的nginx-rtmp中返回的消息長度會大于128字節(jié)。

消息的transactionID是用來標識command類型的消息的,服務器返回的_result消息可以通過transactionID來區(qū)分是對哪個命令的回應,connect 命令發(fā)完之后還要發(fā)送其他命令消息,要保證他們的transactionID不相同。

發(fā)送完connect命令之后一般會發(fā)一個 set chunk size消息來設置chunk size的大小,也可以不發(fā)。

Window Acknowledgement Size 是設置接收端消息窗口大小,一般是2500000字節(jié),即告訴對端在收到設置的窗口大小長度的數(shù)據(jù)之后要返回一個ACK消息。在實際做推流的時候推流端要接收很少的服務器數(shù)據(jù),遠遠到達不了窗口大小,所以這個消息可以不發(fā)。而對于服務器返回的ACK消息一般也不做處理,默認服務器都已經(jīng)收到了所有消息了。

之后要等待服務器對于connect消息的回應的,一般是把服務器返回的chunk都讀完,組包成完整的RTMP消息,沒有錯誤就可以進行下一步了。

3.2.2 Create Stream 消息

創(chuàng)建完RTMP連接之后就可以創(chuàng)建RTMP流,客戶端要想服務器發(fā)送一個releaseStream命令消息,之后是FCPublish命令消息,在之后是createStream命令消息。

當發(fā)送完createStream消息之后,解析服務器返回的消息會得到一個stream ID。

  • pic_4.png

這個ID也就是以后和服務器通信的 message stream ID, 一般返回的是1,不固定。

3.2.3 Publish Stream

推流準備工作的最后一步是Publish Stream,即向服務器發(fā)一個publish命令消息,消息中會帶有流名稱字段,即rtmp url中的 streamName,這個命令的message stream ID 就是上面 create stream 之后服務器返回的stream ID,發(fā)完這個命令一般不用等待服務器返回的回應,直接發(fā)送音視頻類型的RTMP數(shù)據(jù)包即可。有些rtmp庫還會發(fā)setMetaData消息,這個消息可以發(fā)也可以不發(fā),里面包含了一些音視頻meta data的信息,如視頻的分辨率等等。

整個推流過程rtmp 消息抓包

  • rtmp_pulish_message.png

4. 推送音視頻

當以上工作都完成的時候,就可以發(fā)送音視頻了。音視頻RTMP消息的Payload(消息體)中都放的是按照FLV-TAG格式封的音視頻包,具體可以參照FLV封裝的協(xié)議文檔。格式必須封裝正確,否則會造成播放端不能正常拿到音視頻數(shù)據(jù),無法播放音視頻。

5. 關于RTMP的時間戳

RTMP的時間戳單位是毫秒ms,在發(fā)送音視頻之前一直為零,發(fā)送音視頻消息包后時候必須保證時間戳是單調(diào)遞增的,時間戳必須打準確,否則播放端可能出現(xiàn)音視頻不同步的情況。Srs-librtmp的源碼中,如果推的是視頻文件的話,發(fā)現(xiàn)他們是用H264的dts作為時間戳的。實時音視頻傳輸?shù)臅r候是先獲取了下某一時刻系統(tǒng)時間作為基準,然后每次相機采集到的視頻包,與起始的基準時間相減,得到時間戳,這樣可以保證時間戳的正確性。

6. 關于Chunk Stream ID

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

總結(jié):

RTMP協(xié)議是個比較啰嗦的協(xié)議,實現(xiàn)起來也比較復雜,但通信過程過程相對簡單。在直播的實際工程應用中,協(xié)議上很多地方都沒有詳細說明,注意了以上提到幾點,基本能夠保證RTMP音視頻的通信正常。以上就是對RTMP協(xié)議的簡介和一些注意事項,希望能幫到有需要的朋友,另外本文難免有錯誤或說的不夠詳細的地方,歡迎指正,一起交流探討。


推薦項目

Android RTMP推流項目:

Java重寫了RTMP協(xié)議,方便調(diào)試學習, | 相機采集 -> MediaCodec -> RTMP推流 |
項目地址:https://github.com/pixpark/rtmp-publish-kit

RTMP 協(xié)議腦圖
比較清晰,包括rtmp 消息類型,rtmp 分塊chunking,rtmp分塊例子。

鏈接: https://github.com/gezhaoyou/rtmp-publish-kit/tree/main/xmind
腦圖工具 xmind

  1. rtmp 消息分塊


    Xnip2022-10-11_16-49-35.png
  2. rtmp 消息類型


    Xnip2022-10-11_16-49-48.png

參考文章:

  1. Android RTMP直播推流Demo: http://www.itdecent.cn/p/0318ff29ac32
  2. 帶你吃透RTMP:http://mingyangshang.github.io/2016/03/06/RTMP%E5%8D%8F%E8%AE%AE/

本篇文章2017年版本

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

1. RTMP 握手

RTMP 握手分為簡單握手和復雜握手,現(xiàn)在Adobe公司使用RTMP協(xié)議的產(chǎn)品應該用的都是復雜握手,這里不介紹,只說簡單握手。 按照網(wǎng)上的說法RTMP握手的過程如下

  1. 握手開始于客戶端發(fā)送C0、C1塊。服務器收到C0或C1后發(fā)送S0和S1。
  1. 當客戶端收齊S0和S1后,開始發(fā)送C2。當服務器收齊C0和C1后,開始發(fā)送S2。
  2. 當客戶端和服務器分別收到S2和C2后,握手完成。

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

2. RTMP 分塊

創(chuàng)建RTMP連接算是比較難的地方,開始涉及消息分塊(chunking)和 AFM(也是Adobe家的東西)格式數(shù)據(jù)的一些東西,在上面提到的文章中也有介紹為什要進行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字節(jié)。通過向?qū)Χ税l(fā)送set chunk size 消息告知對方更改了 chunk size的大小,即告訴對端:我接下來要以xxx個字節(jié)拆分RTMP消息,你在接收到消息的時候就按照新的chunk size 來組包。
在實際寫代碼的時候一般會把chunk size設置的很大,有的會設置為4096,F(xiàn)FMPEG推流的時候設置的是 60*1000,這樣設置的好處是避免了頻繁的拆包組包,占用過多的CPU。設置太大的話也不好,一個很大的包如果發(fā)錯了,或者丟失了,播放端就會出現(xiàn)長時間的花屏或者黑屏等現(xiàn)象。

Chunk Type

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

3. RTMP 消息

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

Connect消息

握手之后先發(fā)送一個connect 命令消息,命令里面包含什么東西,協(xié)議中沒有說,真實通信中要指定一些編解碼的信息,這些信息是以AMF格式發(fā)送的, 下面是用swift 寫的connect命令包含的參數(shù)信息:

       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)

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

Create Stream 消息

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

Publish Stream

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

4. 發(fā)布音視頻

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

5. 關于RTMP的時間戳

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

6. 關于Chunk Stream ID

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


另外這篇文章有些地方還是說的模糊,以后有時間慢慢豐富吧。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容