視訊技術詳解:RTMP H5 直播流技術解析

網易雲信發表於2019-02-16
本文聚焦 RTMP 協議的最精華的內容,接進行實際操作 Buffer 的練習和協議的學習。

RTMP 是什麼

RTMP 全稱即是 Real-Time Messaging Protocol。顧名思義就是用來作為實時通訊的一種協議。該協議是 Adobe 搞出來的。主要是用來傳遞音視訊流的。它通過一種自定義的協議,來完成對指定直播流的播放和相關的操作。和現行的直播流相比,RTMP 主要的特點就是高效,這裡,我就不多費口舌了。我們先來了解一下 RTMP 是如何進行握手的。

RTMP 握手

RTMP 是基於 TCP 三次握手之後的,所以,RTMP 不是和 TCP 一個 level 的。它本身是基於 TCP 的可靠性連線。RTMP 握手的方式如圖:

視訊技術詳解:RTMP H5 直播流技術解析

(C 代表 Client,S 代表 Server)
它主要是通過兩端的欄位內容協商,來完成可信度認證的。基本過程如下:
  • client: 客戶端需要發 3 個包。C0,C1,C2
  • server: 服務端也需要發同樣 3 個包。 S0,S1,S2。
整個過程如上圖所述,但實際上有些細節需要注意。
握手開始:
【1】 客戶端傳送 C0,C1 包
此時,客戶端處於等待狀態。客戶端有兩個限制:
  • 客戶端在未接受到 S1 之前不能傳送 C2 包
  • 客戶端在未接收到 S2 之前不能傳送任何實際資料包
【2】 服務端在接受到 C0,傳送 S0,S1 包。也可以等到接受到 C1 之後再一起傳送,C1 包的等待不是必須的。
此時,服務端處於等待狀態。服務端有兩個限制:
  • 服務端在未接受到 C1 之前不能傳送 S2.
  • 服務端在未接收到 C2 之前不能傳送任何實際資料包
【3】客戶端接受到 S1/S0 包後,傳送 C2 包。
【4】服務端接受到 C2 包後,返回 S2 包,並且此時握手已經完成。
不過,在實際應用中,並不是嚴格按照上面的來。因為 RTMP 並不是強安全性的協議,所以,S2/C2 包只需要 C1/S1 中的內容,就可以完成內容的拼接。

視訊技術詳解:RTMP H5 直播流技術解析

這麼多限制,說白了,其實就是一種通用模式:
  • C0+C1
  • S0+S1+S2
  • C2
接下來,我們來具體看看 C/S 012 包分別代表什麼。

C0 && S0

C0 和 S0 其實區別不大,我這裡主要講解一下 C0,就差不多了。首先,C0 的長度為 1B。它的主要工作是確定 RTMP 的版本號。
  • C0:客戶端傳送其所支援的 RTMP 版本號:3~31。一般都是寫 3。
  • S1:服務端返回其所支援的版本號。如果沒有客戶端的版本號,預設返回 3。

C1 && S1

C1/S1 長度為 1536B。主要目的是確保握手的唯一性。格式為:

視訊技術詳解:RTMP H5 直播流技術解析

  • time: 傳送時間戳,這個其實不是很重要,不過需要記住,不要超出 4B 的範圍即可。
  • zero: 保留值 0.
  • random: 該欄位長尾 1528B。主要內容就是隨機值,不管你用什麼產生都可以。它主要是為了保證此次握手的唯一性,和確定握手的物件。

C2 && S2

C2/S2 的長度也是 1536B。相當於就是 S1/C1 的響應值。上圖也簡單說明了就是,對應 C1/S1 的 Copy 值,不過第二個欄位有區別。基本格式為:

視訊技術詳解:RTMP H5 直播流技術解析

  • time: 時間戳,同上,也不是很重要
  • time2: C1/S1 傳送的時間戳。
  • random: S1/C1 傳送的隨機數。長度為 1528B。
這裡需要提及的是,RTMP 預設都是使用 Big-Endian 進行寫入和讀取,除非強調對某個欄位使用 Little-Endian 位元組序。
上面握手協議的順序也是根據其中相關的欄位來進行制定的。這樣,看起來很容易啊哈,但是,我們並不僅僅停留在瞭解,而是要真正的瞭解,接下來,我們來實現一下,如果通過 Buffer 來進行 3 次握手。這裡,我們作為 Client 端來進行請求的發起,假設 Server 端是按照標準進行傳送即可。

Buffer 實操握手

我們使用 Buffer 實操主要涉及兩塊,一個塊是 request server 的搭建,還有一塊是 Buffer 的拼接。

Request Server 搭建

這裡的 Server 是直接使用底層的 TCP 連線。
如下,一個簡易的模板:
const client = new net.Socket();

client.connect({
    port: 1935,
    host: "6721.myqcloud.com"},
    ()=>{
        console.log("connected");
    });
    
client.on('data',(data)=>{
    client.write('hello');
});複製程式碼
不過,為了更好的進行實際演練,我們通過 EventEmitter 的方式,來做一個篩選器。這裡,我們使用 mitt 模組來做代理。
const Emitter = require('mitt')();複製程式碼
然後,我們只要分析的就是將要接受到的 S0/1/2 包。根據上面的位元組包圖,可以清楚的知道包裡面的詳細內容。這裡,為了簡單起見,我們排除其他協議的包頭,只是針對 RTMP 裡面的包。而且,我們針對的只有 3 種包,S0/1/2。為了達到這種目的,我們需要在 data 時間中,加上相應的鉤子才行。
這裡,我們借用 Now 直播的 RTMP 流來進行相關的 RTMP 直播講解。

Buffer 操作

Server 的搭建其實上網搜一搜,應該都可以搜尋出來。關鍵點在於,如何針對 RTMP 的實操握手進行 encode/decode。所以,這裡,我們針對上述操作,來主要講解一下。
我們主要的工作量在於如何構造出 C0/1/2。根據上面格式的描述,大家應該可以清楚的知道 C0/1/2 裡面的格式分別有啥。
比如,C1 中的 time 和 random,其實並不是必須欄位,所以,為了簡單起見,我們可以預設設為 0。具體程式碼如下:
class C {
    constructor() {
        this.time;
        this.random;
    }
    C0() {
        let buf = Buffer.alloc(1);
        buf[0] = 3;
        return buf;
    }
    C1() {
    	let buf = Buffer.alloc(1536);
    	return buf;
    }
    /**
     * write C2 package
     * @param {Number} time the 4B Number of time
     * @param {Buffer} random 1528 byte
     */
    produceC2(){
    	let buf = Buffer.alloc(1536);
    	// leave empty value as origin time
    	buf.writeUInt32BE(this.time, 4);
    	this.random.copy(buf,8,0,1528);

    	return buf;
    }
    get getC01(){
    	return Buffer.concat([this.C0(),this.C1()]);
    }
    get C2(){
        return this.produceC2();
    }
}複製程式碼
接下來,我們來看一下,結合 server 完成的 RTMP 客戶端服務。
const Client = new net.Socket();
const RTMP_C = new C();


Client.connect({
    port: 1935,
    host: "6721.liveplay.myqcloud.com"
}, () => {
	console.log('connected')
	Client.write(RTMP_C.getC01);

});

Client.on('data',res=>{
    if(!res){
        console.warn('received empty Buffer ' + res);
        return;
    }
	// start to decode res package
    if(!RTMP_C.S0 && res.length>0){
        RTMP_C.S0 = res.readUInt8(0);
        res = res.slice(1);
    }

    if(!RTMP_C.S1 && res.length>=1536){
        RTMP_C.time = res.readUInt32BE(0);
        RTMP_C.random = res.slice(8,1536);
        RTMP_C.S1 = true;
        res = res.slice(1536);
        console.log('send C2');
        Client.write(RTMP_C.C2);
    }

    if(!RTMP_C.S2 && res.length >= 1536){
        RTMP_C.S2 = true;
        res = res.slice(1536);
    }
})複製程式碼
詳細程式碼可以參考 gist

RTMP 基本架構

RTMP 整個內容,除了握手,其實剩下的就是一些列圍繞 type id 的 message。為了讓大家更清楚的看到整個架構,這裡簡單陳列了一份框架:

視訊技術詳解:RTMP H5 直播流技術解析

在 Message 下的 3 個一級子 Item 就是我們現在將要大致講解的內容。
可以看到上面所有的 item 都有一個共同的父 Item–Message。它的基本結構為:
  • Header: header 部分用來標識不同的 typeID,告訴客戶端相應的 Message 型別。另外,還有個功效就是多路分發。
  • Body: Body 內容就是相應傳送的資料。這個根據不同的 typeID 來說,格式就完全不一樣了。
下面,我們先了解一下 Header 和不同 typeID 的內容:

Header

RTMP 中的 Header 分為 Basic Header 和 Message Header。需要注意,他們兩者並不是獨立的,而是相互聯絡。Message Header 的結構由 Basic Header 的內容來決定。

視訊技術詳解:RTMP H5 直播流技術解析

接下來,先分開來講解:

Basic Header

BH(基礎頭部)主要是定義了該 chunk stream ID 和 chunk type。需要注意的是,BH 是變長度的,即,它的長度範圍是 1-3B。怎麼講呢?就是根據不同的 chunk stream ID 來決定具體的長度。CS ID(Chunk Stream ID)本身的支援的範圍為 <= 65597 ,差不多為 22bit。當然,為了節省這 3B 的內容。 Adobe 搞了一個比較繞的理論,即,通過如下格式中的 CS ID 來確定:
0 1 2 3 4 5 6 7
 +-+-+-+-+-+-+-+-+
 |fmt|   cs id   |
 +-+-+-+-+-+-+-+-+複製程式碼
即,通過 2-7 bit 位來確定整個 BH 的長度。怎麼確定呢?
RTMP 規定,CS ID 的 0,1,2 為保留字,你在設定 CS ID 的時候只能從 3 開始。
  • CS ID: 0 ==> 整個 BH 長為 2B,其中可以表示的 Stream ID 數量為 64-319。例如:
0 1
  0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
 |fmt|    0    |    cs id - 64   |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+複製程式碼
注意上面的 cs id - 64。這個代表的就是,你通過切割第二個 byte 時,是將得到的值加上 64。即:2th byte + 64 = CS ID
  • CS ID: 1 ==> 整個 BH 長為 3B。可以儲存的 Stream ID 數量為 64-65599。例如:
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          |
 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+複製程式碼
當然,後面 CS ID 的計算方法也是最後的結果加上 64。
  • CS ID >2 ==> 整個 BH 長為 1B。可以儲存的 Stream ID 數量為 3-63。例如:
0 1 2 3 4 5 6 7
 +-+-+-+-+-+-+-+-+
 |fmt|  cs id    |
 +-+-+-+-+-+-+-+-+複製程式碼
最後強調一下,因為 RTMP 規定,CS ID 的 0,1,2 為保留字,所以,0,1,2 不作為 CS ID。綜上所述,CS ID 的起始位為 3(並不代表它是 3 個 Stream)。
上面我並沒有提到 fmt 欄位,這其實是用來定義 Message Header 的。

Message Header

根據前面 BH 中 fmt 欄位的定義,可以分為 4 種 MH(Message Header)。或者說,就是一種 MH 格式會存在從繁到簡 4 種:
fmt: 0
當 fmt 為 0 時,MH 的長度為 11B。該型別的 MH 必須要流的開頭部分,這包括當進行快退或者點播時重新獲取的流。該結構的整體格式如下:

視訊技術詳解:RTMP H5 直播流技術解析

也就是說,當 fmt 為 0 時,其格式是一個完整的 MH。
  • timestamp 是絕對時間戳。用來代表當前流編碼。
  • message length: 3B, 傳送 message 的長度。
  • type id: 1B
  • stream id: 4B, 傳送 message stream id 的值。是 little-endian 寫入格式!
fmt: 1
當 fmt 為 1 時,MH 的長度為 7B。該型別的 MH 不帶 msg stream id。msg stream id 由前面一個 package 決定。該數值主要由前一個 fmt 為 0 的 MH 決定。該型別的 MH 通常放在 fmt 為 0 之後。

視訊技術詳解:RTMP H5 直播流技術解析

fmt: 2
當 fmt 為 2 時,MH 的長度為 3B。該型別的 MH 只包括一個 timestamp delta 欄位。其它的資訊都是依照前面一個其他型別 MH 決定的。
fmt: 3
當 fmt 為 3時,這其實 RTMP 裡面就沒有了 MH。官方定義,該型別主要全部都是 payload 的 chunk,其 Header 資訊和第一個非 type:3 的頭一致。因為這主要用於 chunk 中,這些 chunk 都是從一個包裡面切割出來的,所以除了第一個 chunk 外,其它的 chunk 都可以採用這種格式。當 fmt 為 3時,計算它的 timestamp 需要注意幾點,如果前面一個 chunk 裡面存在 timestrameDelta,那麼計算 fmt 為 3 的 chunk 時,就直接相加,如果沒有,則是使用前一個 chunk 的 timestamp 來進行相加,用程式碼表示為:
prevChunk.timeStamp += prevChunk.timeStampDelta || prevChunk.timeStamp;複製程式碼
不過,當 fmt: 3 的情況一般很難遇到。因為,他要求前面幾個包必須存在 fmt 為 0/1/2 的情況。
接下來的就是 Message Body 部分。

Message Body

上面說的主要是 Message Header 的公用部分,但是,對於具體的 RTMP Message 來說,裡面的 type 會針對不同的業務場景有不同的格式。Message 全部內容如上圖所示:

視訊技術詳解:RTMP H5 直播流技術解析

這裡,我們根據流程圖的一級子 item 來展開講解。

PCM

PCM 全稱為:Protocol Control Messages(協議控制訊息)。主要使用來溝通 RTMP 初始狀態的相關連線資訊,比如,windows size,chunk size 等。
PCM 中一共有 5 種不同的 Message 型別,是根據 Header 中的 type ID 決定的,範圍是 1~6 (不包括 4)。另外,PCM 在構造的時候需要注意,它 Heaer 中的 message stream id 和 chunk stream id 需要設定為固定值:
  • message stream ID 為 0
  • chunk stream ID 為 2
如圖所示:

視訊技術詳解:RTMP H5 直播流技術解析

OK,我們接下來一個一個來介紹一下:

Set Chunk Size(1)

看名字大家應該都能猜到這類資訊是用來幹啥的。該型別的 PCM 就是用來設定 server 和 client 之間正式傳輸資訊的 chunk 的大小,type ID 為 1。那這有啥用呢?
SCS(Set Chunk Size) 是針對正式傳送資料而進行資料大小的傳送限制。一般預設為 128B。不過,如果 server 覺得太小了,想傳送更大的包給你,比如 132B,那麼 server 就需要給你傳送一個 SCS,告知你,接下來“我傳送給你的資料大小是 132B”。

視訊技術詳解:RTMP H5 直播流技術解析

  • 0: 只能設為 0 ,用來表示當前的 PCM 的型別。
  • chunk size: 用來表示後面傳送正式資料時的大小。範圍為 1-16777215。
如下,提供過 wireshark 抓包的結果:

視訊技術詳解:RTMP H5 直播流技術解析

Abort Message(2)

該類 PCM 是用來告訴 client,丟棄指定的 stream 中,已經載入到一半或者還未載入完成的 Chunk Message。它需要指定一個 chunk stream ID。
基本格式為:

視訊技術詳解:RTMP H5 直播流技術解析

  • chunk stream id: 指定丟棄 chunk message 的 stream

Acknowledgement(3)

該協議資訊其實就是一個 ACK 包,在實際使用是並沒有用到,它主要是用來作為一個 ACK 包,來表示兩次 ACK 間,接收端所能接收的最大位元組數。
它基本格式為:

視訊技術詳解:RTMP H5 直播流技術解析

  • sequence number[4B]: 大小為 4B
不過,該包在實際應用中,沒有多高的出現頻率。

Window Acknowledgement Size(5)

這是用來協商傳送包的大小的。這個和上面的 chunk size 不同,這裡主要針對的是客戶端可接受的最大資料包的值,而 chunk size 是指每次傳送的包的大小。也可以叫做 window size。一般電腦設定的大小都是 500000B。
詳細格式為:

視訊技術詳解:RTMP H5 直播流技術解析

通過,wireshark 抓包的結果為:

視訊技術詳解:RTMP H5 直播流技術解析

Set Peer Bandwidth(6)

這是 PCM 中,最後一個包。他做的工作主要是根據網速來改變傳送包的大小。它的格式和 WAS 類似,不過後面帶上了一個 Type 用來標明當前頻寬限制演算法。當一方接收到該資訊後,如果設定的 window size 和前面的 WAS 不一致,需要返回一個 WAS 來進行顯示改變。
基本格式為:

視訊技術詳解:RTMP H5 直播流技術解析

其中 Limit Type 有 3 個取值:
  • 0: Hard,表示當前頻寬需要和當前設定的 window size 匹配
  • 1: Soft,將當前寬頻設定為該資訊定義的 window size,或者已經生效的 window size。主要取決於誰的 window size 更小
  • 2: Dynamic,如果前一個 Limit Type 為 Hard 那麼,繼續使用 Hard 為基準,否則忽略該次協議資訊。
實際抓包情況可以參考:

視訊技術詳解:RTMP H5 直播流技術解析

UCM

全稱為:User Control Message(使用者控制資訊)。它的 Type ID 只能為 4。它主要是傳送一些對視訊的控制資訊。其傳送的條件也有一定的限制:
  • msg stream ID 為 0
  • chunk stream ID 為 2
它的 Body 部分的基本格式為:

視訊技術詳解:RTMP H5 直播流技術解析

UCM 根據 Event Type 的不同,對流進行不同的設定。它的 Event Type 一共有 6 種格式 Stream Begin(0),Stream EOF(1),StreamDry(2),SetBuffer Length(3),StreamIs Recorded(4),PingRequest(6),PingResponse(7)。
這裡,根據重要性劃分,只介紹 Begin,EOF,SetBuffer Length 這 3 種。
  • Stream Begin: Event Type 為 0。它常常出現在,當客戶端和服務端成功 connect 後傳送。Event Data 為 4B,內容是已經可以正式用來傳輸資料的 Stream ID(實際沒啥用)。

視訊技術詳解:RTMP H5 直播流技術解析

  • Stream EOF: Event Type 為 1。它常常出現在,當音視訊流已經全部傳輸完時。 Event Data 為 4B,用來表示已經傳送完音視訊流的 Stream ID(實際沒啥用)。
  • Set Buffer Length: Event Type 為 3。它主要是為了通知服務端,每毫秒用來接收流中 Buffer 的大小。Event Data 的前 4B 表示 stream ID,後面 4B 表示每毫秒 Buffer 的大小。通常為 3000ms
OK 剩下就是 Command Msg 裡面的內容了。

Command Msg

Command Msg 裡面的內容,其 type id 涵蓋了 8~22 之間的值。具體內容,可以參考下表:

視訊技術詳解:RTMP H5 直播流技術解析

需要注意,為什麼有些選項裡面有兩個 id,這主要和 AMF 版本選擇有關。第一個 ID 表示 AMF0 的編解碼方式,第二個 ID 表示 AMF3 的編解碼方式。 其中比較重要的是 command Msg,video,audio 這 3 個 Msg。為了讓大家更好的理解 RTMP 流的解析,這裡,先講解一下 video 和 audio 兩個 Msg。

Video Msg

因為 RTMP 是 Adobe 開發的。理所當然,內部的使用格式肯定是 FLV 格式。不過,這和沒說一樣。因為,FLV 格式內部有很多的 tag 和相關的描述資訊。那麼,RTMP 是怎麼解決的呢?是直接傳一整個 FLV 檔案,還自定義協議來分段傳輸 FLV Tag 呢?
這個其實很好回答,因為 RTMP 協議是一個長連線,如果是傳整個 FLV 檔案,根本沒必要用到這個,而且,RTMP 最常用在直播當中。直播中的視訊都是分段播放的。綜上所述,RTMP 是根據自己的自定義協議來分段傳輸 FLV Tag 的。那具體的協議是啥呢?
這個在 RTMP 官方文件中其實也沒有給出。它只是告訴我們 Video Msg 的 type ID 是 9 而已。
因為,RTMP 只是一個傳輸工具,裡面傳什麼還是由具體的流生成框架來決定的。所以,這裡,我選擇了一個非常具有代表性的 RTMP 直播流來進行講解。
通過 wireshark 抓包,可以捕獲到以下的 RTMP 包資料:

視訊技術詳解:RTMP H5 直播流技術解析

這裡需要提及一點,因為 RTMP 是主動將 Video 和 Audio 分開傳輸,所以,它需要交叉釋出 Video 和 Audio,以保證音視訊的同步。那麼具體每個 Video Data 裡面的資料都是一樣的嗎?
如果看 Tag 的話,他們傳輸的都是 VideoData Tag。先看一下 FLV VideoData Tag 的內容:

視訊技術詳解:RTMP H5 直播流技術解析

這是 FLV Video 的協議格式。但,遇到第一個欄位 FrameType 的時候,我們就可能懵逼了,這 TM 有 5 種情況,難道 RTMP 會給你 5 種不同的包嗎?
答案是,有可能,但是,很大情況下,我們只需要支援 1/2 即可。因為,視訊中最重要的是 I 幀,它對應的 FrameType 就是 1。而 B/P 則是剩下的 2。我們只要針對 1/2 進行軟解,即可實現視訊所有資訊的獲取。
所以,在 RTMP 中,也主要(或者大部分)都是傳輸上面兩種 FrameType。我們通過實際抓包來講解一下。
這是 KeyFrame 的包,注意 Buffer 開頭的 17 數字。大家可以找到上面的 FrameType 對應找一找,看結果是不是一致的:

視訊技術詳解:RTMP H5 直播流技術解析

這是 Inter-frame 的包。同上,大家也可以對比一下:

視訊技術詳解:RTMP H5 直播流技術解析

Audio Tag

Aduio Tag 也是和 Video Tag 一樣的蜜汁資料。通過觀察 FLV Audio Tag 的內容:

視訊技術詳解:RTMP H5 直播流技術解析

上面這些欄位全是相關的配置值,換句話說,你必須實現知道這些值才行。這裡,RTMP 傳送 Audio Tag 和 Video Tag 有點不同。因為 Audio Tag 已經不可能再細分為 Config Tag,所以,RTMP 會直接傳遞 上面的 audio Tag 內容。詳細可以參考抓包內容:

視訊技術詳解:RTMP H5 直播流技術解析

這也是所有的 Audio Msg 的內容。
因為 Audio 和 Video 是分開傳送的。所以,在後期進行拼接的時候,需要注意兩者的同步。說道這裡,順便補充一下,音視訊同步的相關知識點。

音視訊同步

音視訊同步簡單來說有三種:
  • 以 Audio 為準,Video 同步 Audio
  • 以 Video 為準,Audio 同步 Video
  • 以外部時間戳為準,AV 同時同步
主要過程變數參考就是 timeStamp 和 duration。因為,這裡主要是做直播的,推薦大家採用第二種方法,以 Video 為準。因為,在實際開發中,會遇到 MP4 檔案生成時,必須要求第一幀為 keyframe,這就造成了,以 Audio 為參考的,會遇到兩個變數的問題。一個是 timeStamp 一個是 keyframe。當然,解決辦法也是有的,就是檢查最後一個拼接的 Buffer 是不是 Keyframe,然後判斷是否移到下一次同步處理。
這裡,我簡單的說一下,以 Video 為準的同步方法。以 Video 同步,不需要管第一幀是不是 keyframe,也不需要關心 Audio 裡面的資料,因為,Audio 資料是非常簡單的 AAC 資料。下面我們通過虛擬碼來說明一下:
// known condition
video.timeStamp && video.perDuration && video.wholeDuration
audio.timeStamp && audio.perDuration

// start
refDuration = video.timeStamp + video.wholeDuration
delta = refDuration - audio.timeStamp
audioCount = Math.round(delta/audio.perDuration);
audDemuxArr = this._tmpArr.splice(0,audioCount);

// begin to demux
this._remuxVideo(vidDemuxArr);
this._remuxAudio(audDemuxArr);複製程式碼
上面演算法可以避免判斷 Aduio 和 Video timeStamp 的比較,保證 Video 一直在 Audio 前面並相差不遠。下面,我們回到 RTMP 內容。來看看 Command Msg 裡面的內容。

Command Msg

Command Msg 是 RTMP 裡面的一個主要資訊傳遞工具。常常用在 RTMP 前期和後期處理。Command Msg 是通過 AMF 的格式進行傳輸的(其實就是類似 JSON 的二進位制編碼規則)。Command Msg 主要分為 net connect 和 net stream 兩大塊。它的交流方式是雙向的,即,你傳送一次 net connect 或者 stream 之後,另外一端都必須返回一個 _result 或者 _error 以表示收到資訊。詳細結構可以參考下圖:

視訊技術詳解:RTMP H5 直播流技術解析

後續,我們分為兩塊進行講解:
  • netConnection
  • netStream
裡面的 _result 和 _error 會穿插在每個包中進行講解。

NetConnection

netConnection 可以分為 4 種 Msg,connect,call,createStream,close。
connect
connect 是客戶端向 Server 端傳送播放請求的。裡面的欄位內容有:
  • Command Name[String]: 預設為 connect。表示資訊名稱
  • Transaction ID[Number]: 預設為 1。
  • Command Object: 鍵值對的形式存放相關資訊。
  • Optional: 可選值。一般沒有
那,Command Object 裡面又可以存放些什麼內容呢?
  • app[String]: 服務端連線應用的名字。這個主要根據你的 RTMP 伺服器設定來設定。比如:live。
  • flashver[String]: Flash Player 的版本號。一般根據自己裝置上的型號來確定即可。也可以設定為預設值:LNX 9,0,124,2。
  • tcUrl[String]: 服務端的 URL 地址。簡單來說,就是 protocol://host/path。比如:rtmp://6521.liveplay.myqcloud.com/live。
  • fpad[Boolean]: 表示是否使用代理。一般為 false。
  • audioCodecs[Number]: 客戶端支援的音訊解碼。後續會介紹。預設可以設定為 4071
  • videoCodecs[Number]: 客戶端支援的視訊解碼。有自定義的標準。預設可以設定為 252
  • videoFunction[Number]: 表明在服務端上呼叫那種特別的視訊函式。預設可以設定為 1
簡單來說,Command Object 就是起到 RTMP Route 的作用。用來請求特定的資源路徑。實際資料,可以參考抓包結果:

視訊技術詳解:RTMP H5 直播流技術解析

上面具體的取值主要是根據 rtmp 官方文件來決定。如果懶得查,可以直接使用上面的取值。上面的內容是相容性比較高的值。當該包成功傳送時,另外一端需要得到一個返回包來響應,具體格式為:
  • Command Name[String]: 為 _result 或者 _error。
  • Transaction ID[Number]: 預設為 1。
  • Command Object: 鍵值對的形式存放相關資訊。
  • Information[Object]: 鍵值對的形式,來描述相關的 response 資訊。裡面存在的欄位有:level,code,description
可以參考:

視訊技術詳解:RTMP H5 直播流技術解析

connect 包傳送的位置,主要是在 RTMP 握手結束之後。如下:

視訊技術詳解:RTMP H5 直播流技術解析

call
call 包主要作用是用來遠端執行接收端的程式(RPC, remote procedure calls)。不過,在我解 RTMP 的過程中,並沒有實際用到過。這裡簡單介紹一下格式。它的內容和 connect 類似:
  • Procedure Name[String]: 呼叫處理程式的名字。
  • Transaction ID[Number]: 如果想要有返回,則我們需要制定一個 id。否則為 0。
  • Command Object: 鍵值對的形式存放相關資訊。AMF0/3
  • Optional: 可選值。一般沒有
Command Object 裡面的內容主要是針對程式,設定相關的呼叫引數。因為內容不固定,這裡就不介紹了。
call 一般是需要有 response 來表明,遠端程式是否執行,以及是否執行成功等。返回的格式為:
  • Command Name[String]: 根據 call 中 Command Object 引數來決定的。
  • Transaction ID[Number]: 如果想要有返回,則我們需要制定一個 id。否則為 0。
  • Command Object: 鍵值對的形式存放相關資訊。AMF0/3
  • Response[Object]: 響應的結果值
createStream
createStream 包只是用來告訴服務端,我們現在要建立一個 channel 開始進行流的交流了。格式和內容都不復雜:
  • Procedure Name[String]: 呼叫處理程式的名字。
  • Transaction ID[Number]: 自己制定一個。一般可以設為 2
  • Command Object: 鍵值對的形式存放相關資訊。AMF0/3
當成功後,服務端會返回一個 _result 或者 _error 包來說明接收成功,詳細內容為:
  • Command Name[String]: 根據 call 中 Command Object 引數來決定的。
  • Transaction ID[Number]: 如果想要有返回,則我們需要制定一個 id。否則為 0。
  • Command Object: 鍵值對的形式存放相關資訊。AMF0/3。一般為 Null
  • Stream ID: 返回的 stream ID 值。
它的返回值很隨意,參考抓包內容:

視訊技術詳解:RTMP H5 直播流技術解析

下面,我們來看一下 RTMP 中第二個比較重要的 command msg – netStream msg。

NetStream Msg

NetStream 裡面的 Msg 有很多,但在直播流中,比較重要的只有 play 包。所以,這裡我們著重介紹一下 play 包。
play
play 包主要是用來告訴 Server 正式播放音視訊流。而且,由於 RTMP 天然是做多流分發的。如果遇到網路出現相應的波動,客戶端可以根據網路條件多次呼叫 play 命令,來切換不同模式的流。
其基本格式為:
  • Command Name[String]: 根據 call 中 Command Object 引數來決定的。
  • Transaction ID[Number]: 預設為 0。也可以設定為其他值
  • Command Object: 不需要該欄位,在該命令中,預設設為 Null
  • Stream Name[String]: 用來指定播放的視訊流檔案。因為,RTMP 天生是支援 FLV 的,所以針對 FLV 檔案來說,並不需要加額外的標識,只需要寫明檔名即可。比如:
StreamName: '6721_75994f92ce868a0cd3cc84600a97f75c'複製程式碼
  • 不過,如果想要支援其它的檔案,那麼則需要額外的表示。當然,音訊和視訊需要不同的支援:
  • 如果是播放音訊檔案,比如 mp3,那麼則需要額外的字首識別符號-mp3。例如:mp3:6721_75994f9。
  • 如果涉及到視訊檔案的話,不僅需要字首,還需要字尾。比如播放的是 MP4 檔案,則標識為:mp4:6721_75994f9.mp4。

  • start[Number][seconds]: 這個欄位其實有點意思。它可以分為 3 類來講解:-2,-1,>=0。
  • -2: 如果是該識別符號,服務端會首先尋找是否有對應的 liveStream。沒有的話,就找 record_stream。如果還沒有的,這次請求會暫時掛起,直到獲取到下一次 live_stream。
  • -1: 只有 live_stream 才會播放。
  • =0: 相當於就是 seek video。它會直接找到 record_stream,並且根據該欄位的值來確定播放開始時間。如果沒有的話,則播放 list 中的下一個 video。

  • duration[Number][seconds]: 用來設定播放時長的。它裡面也有幾個引數需要講解一下,-1,0,>0。
  • -1: 會一直播放到 live_stream 或者 record_stream 結束。
  • 0: 會播放一段一段的 frame。一般不用。
  • 0: 會直接播放指定 duration 之內的流。如果超出,則會播放指定時間段內容的 record_stream。

  • reset[Boolean]: 該欄位沒啥用,一般可以忽略。用來表示否是拋棄掉前面的 playlist。
整個 play 包內容就已經介紹完了。我們可以看看實際的 play 抓包結果:

視訊技術詳解:RTMP H5 直播流技術解析

那 play 包是在那個環節傳送,傳送完之後需不需要對應的 _result 包呢?
play 包比較特殊,它是不需要 _result 回包的。因為,一旦 play 包成功接收後。server 端會直接開始進行 streamBegin 的操作。
整個流程為:

視訊技術詳解:RTMP H5 直播流技術解析

到這裡,後續就可以開始正式接收 video 和 audio 的 stream。


想要閱讀更多技術乾貨文章,歡迎關注網易雲信部落格
瞭解網易雲信,來自網易核心架構的通訊與視訊雲服務。


網易雲信(NeteaseYunXin)是集網易18年IM以及音視訊技術打造的PaaS服務產品,來自網易核心技術架構的通訊與視訊雲服務,穩定易用且功能全面,致力於提供全球領先的技術能力和場景化解決方案。開發者通過整合客戶端SDK和雲端OPEN API,即可快速實現包含IM、音視訊通話、直播、點播、互動白板、簡訊等功能。


相關文章