1. 程式人生 > >直播推流實現RTMP協議的一些注意事項

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

018年8月4日第三次更新,詳細介紹了RTMP協議與遇到的坑,另外純Java重寫了RTMP協議,做了個Android 推流專案,包含安卓相機採集,編碼和RTMP推流,上傳到github了。
專案地址:https://github.com/gezhaoyou/SimpleLivePublisherLite
參考文章:

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

:RTMP 協議整理成腦圖,比較清晰,包括rtmp 訊息型別,rtmp 分塊chunking,rtmp分塊例子。免費腦圖工具

Xmind 格式,Here Get It

  1. rtmp 訊息型別

     

    Paste_Image.png

  2. rtmp 訊息分塊

     

    Paste_Image.png

1. 簡介

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

RTMP協議基本特點:

• 基於TCP協議的應用層協議

• 預設通訊埠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 握手分為簡單握手和複雜握手,現在Adobe公司使用RTMP協議的產品用複雜握手的較多,不做介紹。

握手包格式:

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

C0和S0:1個位元組,包含了RTMP版本, 當前RTMP協議的版本為 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位元組時間戳,4位元組的01528位元組的隨機數


  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位元組時間戳,4位元組從對端讀到的時間戳,1528位元組隨機數

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

握手開始於客戶端傳送C0C1塊。伺服器收到C0C1後傳送S0S1

當客戶端收齊S0S1後,開始傳送C2。當伺服器收齊C0C1後,開始傳送S2

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

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

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

校驗客戶端和伺服器端RTMP協議版本號

是發了一堆隨機資料,校驗網路狀況。

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位元組訊息型別

• 3位元組負載訊息長度

• 4位元組時間戳

• 3位元組 Stream ID,區分訊息流

注意事項: 實際RTMP通訊中並未按照上述格式去傳送RTMP訊息,而是將RTMP 訊息分塊傳送,之後將介紹RTMP訊息分塊。

3.1. RTMP 訊息分塊(chunking)

而對於基於TCPRTMP協議而言,協議顯得特別繁瑣,但是有沒有更好的替代方案。同時建立RTMP訊息分塊是比較複雜的地方,涉及到了AFM(也是Adobe家的東西)格式資料的資料。

RTMP訊息塊格式:

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

RTMP訊息塊構成:

• 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, 用於區分chunk所屬的RTMP訊息, chunk basic header 的型別cs id佔用的位元組數來確定

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個位元組, 在chunk stream的開始的第一個chunk的時候必須採用這種格式。

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

message length3個位元組,表示實際傳送的訊息的資料如音訊幀、視訊幀等資料的長度,單位是位元組。注意這裡是Message的長度,也就是chunk屬於的Message的總資料長度,而不是chunk本身Data的資料的長度。

message type id1個位元組,表示實際傳送的資料的型別,如8代表音訊資料、9代表視訊資料。

msg stream id:4個位元組,表示該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個位元組,省去了表示msg stream id的4個位元組,表示此chunk和上一次發的chunk所在的流相同。

timestamp delta:3個位元組,注意這裡和格式0時不同,儲存的是和上一個chunk的時間差。類似上面提到的timestamp,當它的值超過3個位元組所能表示的最大值時,三個位元組都置為1,實際的時間戳差值就會轉存到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個位元組,相對於格式1,又省去了表示訊息長度的3個位元組和表示訊息型別的1個位元組,表示此chunk和上一次傳送的chunk所在的流、訊息的長度和訊息的型別都相同。餘下的這三個位元組表示timestamp delta,使用同格式1。

格式3:

0位元組,它表示這個chunk的Message Header和上一個是完全相同的,無需再次傳送

Extended Timestamp(擴充套件時間戳):

chunk中會有時間戳timestamp和時間戳差timestamp delta,並且它們不會同時存在,只有這兩者之一大於3個位元組能表示的最大數值0xFFFFFF=16777215時,才會用這個欄位來表示真正的時間戳,否則這個欄位為0。

擴充套件時間戳佔4個位元組,能表示的最大數值就是0xFFFFFFFF=4294967295。當擴充套件時間戳啟用時,timestamp欄位或者timestamp delta要全置為1,表示應該去擴充套件時間戳欄位來提取真正的時間戳或者時間戳差。注意擴充套件時間戳儲存的是完整值,而不是減去時間戳或者時間戳差的值。

Chunk Data(塊資料): 使用者層面上真正想要傳送的與協議無關的資料,長度在(0,chunkSize]之間, chunk size預設為128位元組。

RTMP 訊息分塊注意事項

Chunk Size:

RTMP是按照chunk size進行分塊,chunk size 指的是 chunkpayload部分的大小,不包括chunk basic headerchunk message header長度。客戶端和伺服器端各自維護了兩個chunk size, 分別是自身分塊的chunk size 和 對端 的chunk size, 預設的這兩個chunk size都是128位元組。通過向對端傳送set chunk size 訊息可以告知對方更改了 chunk size的大小。

Chunk Type:

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

如果第二個message和第一個messagemessage stream ID 相同,並且第二個message的長度也大於了chunk size,那麼該如何拆包?當時查了很多資料,都沒有介紹。後來看了一些原始碼,如 SRSFFMPEG中的實現,發現第二個message可以拆成Type_1型別一個chunkmessage剩餘的部分拆成Type_3型別的chunkFFMPEG中就是這麼做的。

3.2 RTMP 互動訊息

推流RTMP訊息互動流程:

  • pic_2.png

關於推流的過程,RTMP的協議文件上給了上圖示例,說一下推流注意事項:

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握手之後先發送一個connect命令訊息,命令裡面包含什麼東西,協議中沒有具體規定,實際通訊中要攜帶 rtmp url 中的 appName 欄位,並且指定一些編解碼的資訊,並以AMF格式傳送, 下面是用wireshake抓取connect命令需要包含的引數資訊:

  • pic_3.png

這些資訊協議中並沒有特別詳細說明, 在librtmpsrs-librtmp這些原始碼中,以及用wireshark 抓包的時候可以看到。

伺服器返回的是一個_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訊息,沒有錯誤就可以進行下一步了。

3.2.2 Create Stream 訊息

建立完RTMP連線之後就可以建立RTMP流,客戶端要想伺服器傳送一個releaseStream命令訊息,之後是FCPublish命令訊息,在之後是createStream命令訊息。

當傳送完createStream訊息之後,解析伺服器返回的訊息會得到一個stream ID

  • pic_4.png

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

3.2.3 Publish Stream

推流準備工作的最後一步是Publish Stream,即向伺服器發一個publish命令訊息,訊息中會帶有流名稱欄位,即rtmp url中的 streamName,這個命令的message stream ID 就是上面 create stream 之後伺服器返回的stream ID,發完這個命令一般不用等待伺服器返回的迴應,直接傳送音視訊型別的RTMP資料包即可。有些rtmp庫還會發setMetaData訊息,這個訊息可以發也可以不發,裡面包含了一些音視訊meta data的資訊,如視訊的解析度等等。

整個推流過程rtmp 訊息抓包

  • rtmp_pulish_message.png

4. 推送音視訊

當以上工作都完成的時候,就可以傳送音視訊了。音視訊RTMP訊息的Payload(訊息體)中都放的是按照FLV-TAG格式封的音視訊包,具體可以參照FLV封裝的協議文件。格式必須封裝正確,否則會造成播放端不能正常拿到音視訊資料,無法播放音視訊。

5. 關於RTMP的時間戳

RTMP的時間戳單位是毫秒ms,在傳送音視訊之前一直為零,傳送音視訊訊息包後時候必須保證時間戳是單調遞增的,時間戳必須打準確,否則播放端可能出現音視訊不同步的情況。Srs-librtmp的原始碼中,如果推的是視訊檔案的話,發現他們是用H264的dts作為時間戳的。實時音視訊傳輸的時候是先獲取了下某一時刻系統時間作為基準,然後每次相機採集到的視訊包,與起始的基準時間相減,得到時間戳,這樣可以保證時間戳的正確性。

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 型別的,表明新的訊息的起始。

總結:

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


本篇文章2017年版本

前一段時間寫過一篇文章: 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。
  1. 當客戶端收齊S0和S1後,開始傳送C2。當伺服器收齊C0和C1後,開始傳送S2。
  2. 當客戶端和伺服器分別收到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 型別的,表明我是一個新的訊息的起始。



作者:devzhaoyou
連結:https://www.jianshu.com/p/00aceabce944
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯絡作者獲得授權並註明出處。