概述
本文篇幅較長,主要介紹RTMP協(xié)議格式、信令過程、FLV tag三個部分,文檔結(jié)構(gòu)參考rtmp_specification_1.0,最后通過wireshark抓取項目推流過程介紹其中細(xì)節(jié)。
筆者項目背景是iOS推流端,所以研究的側(cè)重點也在RTMP推流過程,后續(xù)涉及到拉流端再繼續(xù)完善,如有錯誤感謝指出。
手打不易,感謝支持,轉(zhuǎn)載請注明出處。
1. 簡介
Adobe的實時消息傳遞協(xié)議(RTMP)在可靠的流傳輸協(xié)議(如TCP)之上提供雙向消息多路服務(wù),使得通信的兩端傳遞并行的音視頻流、數(shù)據(jù)流以及相關(guān)的時間信息。通過抽象出的chunk流,發(fā)送細(xì)粒度的數(shù)據(jù)包,并且可以為不同類型的信息分配不同的流通道,以便在傳輸能力受限時(如網(wǎng)絡(luò)不佳)優(yōu)先傳輸高優(yōu)先級數(shù)據(jù)以減少網(wǎng)絡(luò)延時。
2. 專有名詞
- Message stream:數(shù)據(jù)流中的邏輯通道。
-
Message stream ID:數(shù)據(jù)流中每一條message都有對應(yīng)的msid來標(biāo)識邏輯通道。
默認(rèn)0,推流端在createStream的_result回調(diào)時更新此id,之后發(fā)送消息使用新的msid。 - Chunk:消息分塊,消息真正在網(wǎng)絡(luò)傳輸前會根據(jù)chunk size拆分成更小的數(shù)據(jù)包,chunk是RTMP協(xié)議層最基本的數(shù)據(jù)包,也是發(fā)送給下層協(xié)議的最小單位,音視頻數(shù)據(jù)的chunks需保證時間戳遞增(回退播放除外)。
- Chunk stream:chunk流的邏輯通道。
-
Chunk stream ID:csid用來標(biāo)識對應(yīng)的chunk流。
通??刂屏鱟sid為2,命令流為3,開發(fā)中發(fā)現(xiàn)音視頻流csid可自定義,如音頻流4,視頻流6。 - Action Message Format(AMF):用來序列化ActionScript對象圖的二進(jìn)制格式(功能類似json,相比json占用更少的字節(jié)),兩個版本AMF0和AMF3。
3. 字節(jié)序,對齊及時間格式
除非特殊說明,字節(jié)序采用大端格式。
默認(rèn)字節(jié)對齊,如一個16-bit的字段填充數(shù)據(jù)時不足16bit需要用0補齊。
時間戳采用毫秒級無符號整型數(shù)。
4. Handshake
建立RTMP連接首先需要握手,由三個定長的chunk組成。
4.1 順序要求
- client必須收到s1才能發(fā)送c2;
- client必須收到s2才能發(fā)送其他數(shù)據(jù),如connect等連接操作;
- server必須收到c0才能發(fā)送s0和s1;
- server必須收到c1才能發(fā)送s2;
- server必須收到c2才能發(fā)送其他數(shù)據(jù);
如下圖所示

實際開發(fā)中,為了盡量減少通信的次數(shù),發(fā)送順序可以優(yōu)化成三步:

4.2 c0 and s0 Format
c0和s0都是8-bit的整型字段,如下圖

Version(8 bit):該字段分別表示client/server支持的version,通常為3。
4.3 c1 and s1 Format
c1和s1都是1536字節(jié),如下圖

Time(4 bytes):時間戳,通常為0,是之后發(fā)送chunk流的參考時間;
Zero(4 bytes):恒為0;
Random data(1528 bytes):隨機(jī)數(shù);
4.4 c2 and s2 Format
c2和s2同樣都是1536字節(jié),如下圖

Time(4 bytes):對于c2,填入對端發(fā)來的s1包里帶的時間戳,s2同理;
Time2(4 bytes):對于c2,填入讀取到s1包時刻的時間戳,s2同理,開發(fā)中發(fā)現(xiàn)可填0;
Random echo(1528 bytes):對于c2,填入對端發(fā)來的s1包里帶的隨機(jī)數(shù),s2同理;
至此,握手階段完成,可以發(fā)送message了。
5. RTMP Chunk Stream
Chunk Stream是對傳輸RTMP Chunk的流的抽象,可以對Control、Audio、Video等多路流同時傳輸,并在邏輯上區(qū)別開來。
5.1 Message Format
一個可以拆分成chunk發(fā)送的message,應(yīng)該包含以下必要的字段:
Timestamp(3 bytes):message的時間戳;
Length(3 bytes):message的payload負(fù)載的長度,而非message的長度,對于control類型的消息是命令的長度,對于audio和video類型的消息是FLV audio/video tag的長度;
Type Id(1 byte):消息類型,預(yù)留的包含control類消息的各個細(xì)分命令,audio,video等,也可自定義id供RTMP協(xié)議之上的業(yè)務(wù)協(xié)議使用;
Message Stream ID(4 bytes):消息的唯一標(biāo)識,簡稱msid,多路消息流復(fù)用相同的chunk流時根據(jù)這個id解復(fù)用,以便區(qū)分是否是同一個消息的chunk,小端格式存儲;
5.2 Chunking
RTMP發(fā)送的message需要根據(jù)chunk size拆分成chunk發(fā)送,而且必須在上一個chunk發(fā)送完成后才能發(fā)送下一個chunk,所以chunk是數(shù)據(jù)封包及發(fā)送的最小單位。每個chunk中帶有(或間接找到)msid,接收端會按照這個id將chunk組裝成message。
這么做的好處是避免一些數(shù)據(jù)量大但優(yōu)先級較低的消息(如video)阻塞數(shù)據(jù)量小但優(yōu)先級較高的消息(如audio和control),這樣可以一定程度避免直播場景中因網(wǎng)絡(luò)波動造成的卡頓現(xiàn)象。
并且chunk header的長度是變長的,根據(jù)場景可以選擇type0-type3類型壓縮header,從而減少分包造成的數(shù)據(jù)浪費。
chunk size默認(rèn)128字節(jié),在傳輸過程中,可通過Set Chunk Size 消息通知對端chunk拆分的最大值。更大的chunk size可減少cpu占用,但同時寫入時間較長容易在低帶寬網(wǎng)絡(luò)時阻塞后面更重要的message。更小的chunk size可以減少阻塞問題,但是會引入更多額外的信息(chunk中的header),小量多次的傳輸也會造成發(fā)送的間斷導(dǎo)致不能充分利用高帶寬網(wǎng)絡(luò)的優(yōu)勢。所以實際傳輸中要根據(jù)不同的網(wǎng)絡(luò)帶寬動態(tài)調(diào)整chunk size的大小,以在cpu利用率和網(wǎng)絡(luò)帶寬之間做出最佳權(quán)衡。
5.2.1 Chunk Format
每一個chunk由header和data組成,如下圖

Basic Header(1-3 bytes):該字段包含chunk type和chunk stream id(簡稱csid),其中fmt決定了chunk的類型及message header的長度,占2 bit,而Basic header的長度取決于csid的數(shù)值大小,最少占1 byte;
Message Header(0,3,7 or 11 bytes):長度取決于Basic Header中的chunk type,有Type 0,1,2,3類型的header,注意message length字段指的是message body的長度(不包含header),而且如果被拆分成chunk,此字段填充拆分前message的body長度,而不是chunk的長度;
Extended Timestamp(0 or 4 bytes):當(dāng)message header中的timestamp或timestamp delta大于0xFFFFFF時啟用這個字段,存儲直接或者間接時間戳,注意存儲直接時間戳?xí)r要寫入真實的時間戳;
Chunk Data(variable size):chunk的負(fù)載數(shù)據(jù),最大為chunk size;
5.2.1.1 Chunk Basic Header
fmt(2 bits):該字段即是上文提到的chunk type,取值[0, 3],表示四種類型的Chunk Message Header;
csid:RTMP支持用戶自定義[3, 65599]區(qū)間的csid,因為0、1、2為協(xié)議預(yù)留,預(yù)留的id可推算出Basic header長度,小端存儲。
0表示Basic Header總共占2個字節(jié),csid在[64, 319]之間;
1表示占3個字節(jié),csid在[64, 65599]之間;
2表示該chunk是控制信息,后面會講到Protocol Control Message和User Control Message的csid必須為2。
而在[2, 63]區(qū)間的csid可以使用1字節(jié)的Basic Header,csid直接讀出即可。

在[64, 319]區(qū)間的csid可使用2字節(jié)的header,實際的csid通過 the second byte + 64 計算得出。

在[64, 65599]區(qū)間的csid可使用3字節(jié)的header,其中csid占用2個字節(jié),這就涉及到字節(jié)序,注意此時是小端存儲,實際的csid通過 (the third byte)*256 + the second byte + 64 計算得出。

盡管可以隨意自定義csid,但盡量使用較小的id以減小Header的長度。
5.2.1.2 Chunk Message Header
5.2.1.2.1 Type 0
Type 0的Chunk Message Header共11字節(jié),在chunk stream開始的第一個chunk和時間戳有回退(如回退播放)的情況下必須使用Type 0。

timestamp(3 bytes):直接時間戳(區(qū)別于后續(xù)提到的間接時間戳 ts delta),如果時間戳大于等于16777215(二進(jìn)制0xFFFFFF),該字段必須等于16777215,然后轉(zhuǎn)存到4字節(jié)的Extended Timstamp字段中;
message length(3 bytes):注意這里是Message的長度,即拆分成chunk之前Message的payload負(fù)載的長度,而不是chunk里data的長度;
message type id(1 byte):消息類型,如8代表audio數(shù)據(jù),9代表video,其他值代表細(xì)分的control消息,后面會詳細(xì)講到;
message stream id(4 bytes):通常相同的chunk stream中的所有消息都來自相同的message stream,雖然可以將不同的message streams傳輸?shù)较嗤腸hunk stream中,但這就抵消了Chunk Message Header頭壓縮的好處。所以開發(fā)直播推流時,audio和video的csid不一樣,但msid一般都一樣;
5.2.1.2.2 Type 1
共7字節(jié),相比Type 0,省略msid字段,和前一個chunk共用msid。

timestamp delta(3 bytes):間接時間戳,表示和上一個chunk的時間戳差值,可用上一個的timestamp+delta計算得出。
5.2.1.2.3 Type 2
共3字節(jié),省略msid和message length,和前一個chunk共用這倆字段,在發(fā)送定長的數(shù)據(jù)時,在第一個chunk之后使用這個類型。

5.2.1.2.4 Type 3
Type 3類型的chunk沒有Message header。
當(dāng)它跟在Type 0的chunk后面時,表示和前一個chunk的時間戳是相同的,也就是一個Message拆分成多個chunk時,后一個chunk和前一個chunk同屬一個Message自然也就可以不用傳Message header。
當(dāng)它跟在Type 1或者Type2的chunk后面時,表示和前一個時間戳的差相同。如第一個chunk是Type 0,timestamp = 0,第二個chunk是Type 2,timestamp delta = 20,表示時間戳為0+20=20,第三個chunk是Type 3,則timestamp delta = 20,表示時間戳為20+20=40;
5.2.2 Examples
5.2.2.1 Example 1
下圖展示了常見的audio消息流

分析:
- 第一個Message的chunk的chunk type為0,因為前面沒有可參考的chunk,此時chunk的長度為Basic Header(1 byte)+ Message Header(11 bytes)+ Payload(32 bytes)= 44 bytes;
- 第二個Message的chunk可與前一個共用length、type id和msid,但是不可省略timestamp delta字段,所以chunk type為2,同理可得chunk長度為36 bytes;
- 第三個Message的chunk可以共用前一個的timestamp delta字段,所以chunk type為0,chunk長度為33 bytes;
詳見下圖

5.2.2.2 Example 2
下圖展示了如何將過長的message以128字節(jié)的chunk size拆分成多個chunks

分析:
- Payload length為307 > 128字節(jié),所以需要將Message拆分成多個chunks,首個chunk使用Type 0;
- 之后的chunk由于是同一個Message拆分而來,以上字段都可共享,所以直接使用Type 3;
- 需要注意的是,第一個chunk的length需要傳入整個message的負(fù)載長度即307;

以上兩個例子可以看出,Type 3類型的chunk可以用在兩個場景:
- 單個較大Message拆分的非首個chunk;
- 某個Message可以與之前的Message共享以上字段,其第一個chunk也可為Type 3;
開發(fā)中場景1比較常見,比如視頻幀大于chunk size(默認(rèn)128 bytes)時,使用Type 3可有效減少chunk header長度。
5.3 Protocol Control Messages(協(xié)議控制消息)
在RTMP的chunk流中使用特殊的type id如1,2,3,5,6,代表協(xié)議控制消息,這類控制消息msid必須為0,csid必須為2,并且時間戳為0,接收端收到立即生效。
5.3.1 Set Chunk Size(Message Type ID = 1)
上文提到,rtmp消息需要以chunk size為單位封裝成chunk包發(fā)送,因此接收端需要根據(jù)chunk size才能正確解包,所以雙端都要記錄對端的封包單位chunk size,默認(rèn)128 bytes。
通信過程中可發(fā)送此消息通知對端更新其記錄的本端的chunk size。比如client想發(fā)送131 bytes的音頻數(shù)據(jù)(此時chunk size為128 bytes,不更新chunk size的話需要拆成兩個chunk),此時client可通知對端,這邊client的chunk size更新為131 bytes,之后發(fā)送一個data為131 bytes的chunk即可,server端收到Set Chunk Size之后更新chunk size即可正確解析之后到來的chunk。
雙端的chunk size各自獨立維護(hù),可以不同。

如上圖,payload的第一個bit恒為0。
chunk size(31 bits):可表示[1, 0x7FFFFFFF]區(qū)間,但是由于chunk size要小于Message的length,而Message length字段用3個字節(jié)存儲,最大值為0xFFFFFF,所以實際可取值區(qū)間為[1, 0xFFFFFF]。
5.3.2 Abort Message(2)
發(fā)送數(shù)據(jù)過程中,發(fā)送端可發(fā)送Abort消息通知接收端丟棄當(dāng)前未接收完的Message及忽略之后的消息,接收端根據(jù)Abort消息中的csid可丟棄對應(yīng)chunk流中之后的所有數(shù)據(jù)。比如在發(fā)送端需要關(guān)閉時,發(fā)送此消息通知對端之后的數(shù)據(jù)可以不用處理了。

chunk stream ID(32 bits):payload中就一個字段csid,表示可忽略的chunk流。
5.3.3 Acknowledge(3)
當(dāng)收到對端消息字節(jié)數(shù)等于window size時,接收端要回復(fù)一個ack告知對端可以繼續(xù)發(fā)送數(shù)據(jù)。當(dāng)然這個窗口大小也可以理解為接收端read buffer的大小,意為接收端在發(fā)送ack之前最多可以處理的數(shù)據(jù)大小。

sequence number(32 bits):接收端自handshake起接收到的總的數(shù)據(jù)量,單位字節(jié)。
5.3.4 Window Acknowledge Size(5)
對應(yīng)于上文提到的ack,發(fā)送端可以發(fā)送此消息通知對端更新窗口大小,一般在音視頻數(shù)據(jù)之前發(fā)送。
不同于chunk size,window size通常比較大,以容納更多的數(shù)據(jù)在緩沖區(qū)中,并且雙端的window size共同維護(hù),保持相同。

5.3.5 Set Peer Bandwidth(6)
限制對端的輸出帶寬,接收端收到該消息后,通過限制已發(fā)送但尚未收到ack的數(shù)據(jù)大小,來限制輸出帶寬。如果帶寬與上次發(fā)送給發(fā)送端的window size不同的話,接收端需要重新發(fā)送Window Acknowledge Size消息。

limit type有以下取值:
0 - Hard:接收端立即將window size更新為window ack size。
1 - Soft:接收端可以更新window size,也可以保留原值,但是原值要小于window ack size。
2 - Dynamic:如果上次收到的Set Peer Bandwidth消息的limit type是Hard,這次也按Hard處理,否則可忽略此消息。
6. RTMP Command Messages
上文說到的Protocol Control Message(協(xié)議控制消息)是在RTMP Chunk Stream Protocol(RTMP Chunk流協(xié)議層),總的來說是對chunk流的管理。本章介紹在RTMP Stream layer(流協(xié)議層)的命令消息。
6.1 Types of Messages
6.1.1 Command Message(Message Type ID = 20,17)
命令消息,攜帶由AMF編碼格式的命令,AMF0的type id是20,AMF3是17。例如connect,createStream,publish,play,pause等命令,對端會回復(fù)如_result,_error等狀態(tài),具體什么格式以及如何將命令和回調(diào)一一對應(yīng)后面會詳細(xì)介紹。命令消息中包含command name、transaction ID和params。
6.1.2 Data Message(18,15)
數(shù)據(jù)消息,傳遞音視頻元數(shù)據(jù)和其他用戶數(shù)據(jù),AMF0的type id是18,AMF3是15。
這個是很有必要的,比如直播推流發(fā)送實際音視頻數(shù)據(jù)之前,必須要先發(fā)送@setDataFrame命令,發(fā)送視頻分辨率、幀率碼率、音頻采樣率、音視頻編碼方式等元數(shù)據(jù),以便接收端收到后續(xù)數(shù)據(jù)可以正確解析。
6.1.3 Share Object Message(19,16)
共享消息,多個客戶端需要共享一些鍵值對時使用,AMF0是19,AMF3是16。
6.1.4 Audio Message(8)音頻消息
6.1.5 Video Message(9)視頻消息
6.1.6 Aggregate Message(22)
聚合消息,可以將多個子消息聚合到一個message中從而減少發(fā)送的chunk。而且子消息在內(nèi)存中是連續(xù)存儲的,在進(jìn)行系統(tǒng)調(diào)用時效率更高。
6.1.7 User Control Message Events(4)
用戶控制消息,client和server端可以發(fā)送該消息通知對端執(zhí)行用戶控制事件。此類消息msid必須為0,csid必須為2。負(fù)載格式(即RTMP Chunk Data)如下圖

Event Type(16 bits):指定事件類型,共7種,如下圖;
Event Data(變長):因為底層還要包裝成chunk,所以為了不被拆分成多個chunk,需要保證Event Type+Event Data <= chunk size;

其中常見的有
Set Buffer Length:在server端處理數(shù)據(jù)流之前,client通知server每毫秒接收的流的大?。?br>
Ping Request & Response:推流過程中server如果長時間收不到數(shù)據(jù),會發(fā)送ping詢問client是否可達(dá),client狀態(tài)正常時需要回復(fù)該消息表明網(wǎng)絡(luò)可達(dá);
6.2 Type of Commands
命令類型的消息有很多種,如上文所說都需要AMF編碼,包含命令名稱、事務(wù)ID和相關(guān)參數(shù)。如client端發(fā)送connect命令時需要包含要連接的應(yīng)用名稱作為參數(shù),然后server端回復(fù)消息時帶上收到的transaction ID對應(yīng)此條消息。回復(fù)命令有_result,_error,或者其他如verifyClient,contactExternalServer的方法名。命令類型的csid通常為3。
命令分兩大類,NetConnection和NetStream。
6.2.1 NetConnection Commands
連接命令,維護(hù)兩端的連接狀態(tài),也可以實現(xiàn)RPC遠(yuǎn)程進(jìn)程調(diào)用,接收端的回調(diào)為_result和_error,分別表成功和失敗,有四個命令:
- connect
- call
- close
- createStream
6.2.1.1 connect
client發(fā)送connect命令請求連接對應(yīng)的應(yīng)用,格式如下

Command Object用到的鍵值對如下

audioCodecs取值如下

videoCodecs如下

videoFunction如下

編碼類型如下

server端回復(fù)的消息結(jié)構(gòu)如下

connect大致流程如下

- client發(fā)送連接命令,要求連接server的應(yīng)用實例;
- server啟動對應(yīng)的應(yīng)用,并發(fā)送Window Acknowledge Size告知client設(shè)置window size;
- server發(fā)送Set Peer Bandwidth限制client默認(rèn)輸出帶寬,這個值一般等于上一步的window size;
- client收到Set Peer Bandwidth后,同樣發(fā)送Window Acknowledge Size消息,一般等于之前收到的window size;
- server發(fā)送User Control Message(StreamBegin),表示準(zhǔn)備接收message stream;
- server回復(fù)步驟1 _result或者_(dá)error告知client連接結(jié)果,并且包含相同的transcation ID(即1),同時參數(shù)攜帶如fmsVer表示flash server版本,capabilities表示可承載的連接數(shù)量,以及l(fā)evel,code,description等狀態(tài)描述;
6.2.1.2 Call
Remote Procedure calls遠(yuǎn)程進(jìn)程調(diào)用,也可理解為遠(yuǎn)程方法調(diào)用,格式如下

響應(yīng)格式如下

6.2.1.3 createStream
client通過發(fā)送此命令創(chuàng)建發(fā)布音視頻的邏輯通道,即message stream channel。之前提到msid默認(rèn)為0,client收到createStream的回調(diào)之后,解析其中的msid并更新本地,自此之后發(fā)送數(shù)據(jù)的msid都為此id。client發(fā)送的格式如下

server回復(fù)的格式如下

6.2.2 NetStream Commands
流命令,對音視頻流的狀態(tài)管理,如FCPublish/publish發(fā)布流,play播放流等,接收端的回調(diào)為onStatus,有如下命令:
- play
- play2
- deleteStream
- closeStream
- receiveAudio
- receiveVideo
- publish
- seek
- pause
onStatus回調(diào)的格式大致一致,如下圖

6.2.2.1 play
拉流端向server端請求播放音視頻流,可以多次調(diào)用以請求多個視頻流,其中resset字段設(shè)置true用來播放和切換多路流,設(shè)置false會清除其他流僅播放當(dāng)前請求的流。結(jié)構(gòu)如下

play流程大致如下

- client接收到createStream _result之后,發(fā)送play命令請求播放視頻流;
- server接收到play命令后,發(fā)送Set Chunk Size,后續(xù)發(fā)送的chunk包以此大小拆分;
- server緊接著發(fā)送User Control Message,如StreamIsRecorded和StreamBegin,通知client當(dāng)前流的狀態(tài)信息;
- server發(fā)送onStatus消息回復(fù)對應(yīng)的play命令,如果流存在則回復(fù)NetStream.Play.Start,不存在則回復(fù)NetStream.Play.StreamNotFound,如果play命令設(shè)置了reset,server還會回復(fù)NetStream.Play.Reset。
- 之后server開始發(fā)送要播放的音視頻數(shù)據(jù)。
6.2.2.2 play 2
和上文play命令都是請求播放音視頻流,區(qū)別在于server會維護(hù)多中比特率的視頻流,client可以調(diào)用play2命令切換。
結(jié)構(gòu)如下,其中NetStreamPlayOptions使用ActionScript 3格式。

play2流程類似play,如下

6.2.2.3 deleteStream
對應(yīng)于createStream,客戶端在結(jié)束推流時發(fā)送deleteStream命令,server不需要回復(fù)此消息,結(jié)構(gòu)如下圖,其中Stream ID是要關(guān)閉的msid。

6.2.2.4 receiveAudio
client通知server是否要接收音頻數(shù)據(jù),結(jié)構(gòu)如下

其中,如果flag為false,則服務(wù)端不需要回復(fù)。如果為true,server會回復(fù)NetStream.Seek.Notify和NetStream.Play.Start消息。
6.2.2.5 receiveVideo
client通知server是否要接收視頻數(shù)據(jù),結(jié)構(gòu)如下

其中,如果flag為false,則服務(wù)端不需要回復(fù)。如果為true,server會回復(fù)NetStream.Seek.Notify和NetStream.Play.Start消息。
6.2.2.6 publish
上文說到,推流時client發(fā)送connect連接到server對應(yīng)的應(yīng)用,收到回調(diào)后發(fā)送createStream創(chuàng)建發(fā)布的msid,收到回調(diào)后就需要發(fā)送publish指定接下來推送的流名稱,收到回調(diào)后就可以推音視頻流了,拉流端就可以連接server拉取到對應(yīng)名稱的音視頻流。結(jié)構(gòu)如下

publish過程如下

6.2.2.7 seek
定位到視頻的某個時間點,以毫秒為單位,server返回NetStream.Seek.Notify表成功,_error表失敗,結(jié)構(gòu)如下

6.2.2.8 pause
client發(fā)送pause通知server暫?;蚧謴?fù)播放,server回復(fù)NetStream.Pause.Notify表暫停成功,NetStream.Unpause.Notify表恢復(fù)成功,_error表暫停失敗,結(jié)構(gòu)如下

7. RTMP Video Payload
經(jīng)過上文提到的handshake,connect,publish/play等一系列流程,接下來需要收發(fā)真正的音視頻數(shù)據(jù),這些數(shù)據(jù)放在message的payload里,拆分成chunk后即放在data字段。
以flv格式的視頻為例,將編碼后的音視頻幀以FLV tag的AUDIODATA和VIDEODATA字段的格式存儲到chunk的data中。
7.1.1 AUDIODATA

SoundFormat(4 bits):音頻幀格式,一般為AAC;
SoundRate(2 bits):采樣率,對于AAC取值恒為3;
SoundSize(1 bit):采樣精度,對于壓縮格式恒為1;
SoundType(1 bit):聲道數(shù),對于AAC恒為1;
SoundData(變長):音頻幀數(shù)據(jù),對于AAC取值為AACAUDIODATA;
對于AAC格式,SoundRate和SoundType取值恒定并不意味著采樣率和聲道數(shù)恒定,而是因為播放端可以通過解析AAC碼流中的AudioSpecificConfig獲得,所以忽略了這兩個字段。
7.1.2 AACAUDIODATA

AAC碼流需要傳輸兩種類型的幀
AudioSpecificConfig:其中包含編碼細(xì)分類別、采樣率、聲道數(shù)三個解碼必要的參數(shù);
raw frame data:AAC編碼器輸出的序列幀;
7.1.3 AudioSpecificConfig

object type(5 bits):編碼細(xì)分類別,如1表AAC_Main,2表AAC_LC(低復(fù)雜度);
frequency index(4 bits):采樣率,如3表48000Hz,4表44100Hz;
channel configuration(4 bits):聲道配置,如1表單聲道,2表雙聲道;
以上三字段5+4+4=13 bits,不足2字節(jié)的bit需要補位0以便字節(jié)對齊,如下

7.2.1 VIDEODATA

FrameType(4 bits):幀類型,如1表關(guān)鍵幀,2表非關(guān)鍵幀即PB幀;
CodecID(4 bits):編碼格式,如7表avcC格式包裝的H264序列幀;
這里說明下H264是編碼格式,其輸出的是可以在網(wǎng)絡(luò)傳輸?shù)腘AL Unit,由于NALU長度不一,如果不加以封裝則無法正確拆分多個NALU,主流包裝格式有avcC和AnnexB兩種;
VideoData(變長):視頻幀數(shù)據(jù),對于H264取值為AVCVIDEOPACKET;
7.2.2 AVCVIDEOPACKET

其中
AVCPacketType(8 bits):常用如0表包含sps/pps等解碼必要數(shù)據(jù);1表編碼器輸出的序列幀;
CompositionTime(3 bytes):單位毫秒,對于序列幀表示延遲解碼間隔時間,因為B幀有解碼延遲,若不存在B幀此值一般為0;
Data(變長):AVCDecoderConfigurationRecord或者avcC格式封裝的H264序列幀;
對于傳輸NALU的情況,不同編碼器的輸出格式不同,如iOS的VideoToolBox輸出avcC格式,Android的MediaCodec輸出AnnexB格式,在RTMP場景下必要時需要做格式轉(zhuǎn)換;
7.2.3 AVCDecoderConfigurationRecord

下面通過wireshark抓取的RTMP包分析

圖中高亮部分即是AVCVIDEOPACKET,00表示Data字段存儲的是AVCDecoderConfigurationRecord,其中
configurationVersion=01,版本號恒為1;
AVCProfileIndication=42,等于sps[1],表baseline,H264編碼檔次有baseline/main/high,其中baseline效率最高適用于實時通信;
profile_compatibility=e0,等于sps[2];
AVCLevelIndication=1f,等于sps[3],表auto;
lengthSizeMinusOne=ff,NALU單元頭長度-1,即(lengthSizeMinusOne&0x03)+1=4字節(jié);
numOfSequenceParameterSets=e1,sps個數(shù),一般為1,(numOfSequenceParameterSets&0x1f)=1;
sequenceParameterSetLength=00 13,sps長度;
sequenceParameterSetNALUnit=27 42 e0 1f a9 18 3c 11 d8 0b 50 20 20 23 0a d7 bd f0 10,sps unit,可以看出AVCProfileIndication,profile_compatibility,AVCLevelIndication分別對應(yīng)sps的1,2,3字節(jié);
numOfPictureParameterSets=01,pps個數(shù),一般為1;
pictureParameterSetLength=00 04,pps長度;
pictureParameterSetNALUnit=28 de 09 88,pps unit;
sps和pps一般都可通過API直接獲得,其內(nèi)部字段的意義屬于編碼器范疇,讀者有興趣可以自行深究:)
8. 附錄
8.1 RTMP Messages
RTMP消息種類較多且繁瑣,理解起來較為困難,筆者總結(jié)下圖結(jié)構(gòu)幫助理解。

8.2 wireshark分析推流過程

上圖是推流項目中實際的交互流程,雖然與文檔相比少了部分命令,基本也涵蓋了主體流程,并且適用于多個平臺的rtmp server,下面講下流程細(xì)節(jié):
8.2.1 Handshake
簡化版的握手流程,相對簡單不再贅述;
8.2.2 connect:c to s
下圖高亮部分是rtmp header,由chunk basic header+chunk message header組成。
format:0表type 0 chunk,因為這是第一個chunk;
csid:Protocol Control Message和User Control Message為2,Command Messages通常為3,所以這里是3;
msid:上文說到createStream會創(chuàng)建消息流通道對應(yīng)msid,所以一般在createStream之后msid才會有值;
transactionId:恒為1,之后會有_result與之對應(yīng);

8.2.3 Window Acknowledge Size:s2c
server通知client其緩沖窗口大小5000000字節(jié)。
csid:該消息屬Protocol Control Message,所以為2;

8.2.4 Set Peer Bandwidth/Set Chunk Size/_result('NetConnection.Connect.Success'):s2c
可以看到三條消息的csid與上文說的規(guī)則一致,其中_result的transactionId是1,與connect對應(yīng)。

8.2.5 Window Acknowledgement Size:c2s
client每收到window size大小的數(shù)據(jù)之后,都要返回ack,message body包含當(dāng)前收到的字節(jié)數(shù)。

8.2.6 Set Chunk Size:c2s
client通知server其發(fā)送的chunk分包大小,自此client發(fā)的chunk以8192拆分,server發(fā)的chunk以4096拆分,雙端各自維護(hù)chunk size且可以不同。

8.2.7 createStream:c2s
client發(fā)送的第二個command message,transactionId為2。

8.2.8 _result() of createStream:s2c
自此雙端都明確創(chuàng)建了一個msid為1的邏輯通道,此后發(fā)送的消息msid都要為1。
這里由于底層TCP是全雙工通信,可能導(dǎo)致之后緊接著發(fā)送的msid來不及更新為1,如FCPublish,這種情況也是允許的。

8.2.9 FCPublish:c2s
FCPublish沒在官方文檔中,可以看到與publish消息類似,猜測是為了版本兼容,讀者了解的話麻煩告知:)。

8.2.10 onFCPublish:s2c

8.2.11 publish:c2s
此消息更新到了msid,自此之后的消息msid都為1。

8.2.12 onStatus('NetStream.Publish.Start'):s2c
publish屬于NetStream Command Message,上文提到此類消息回調(diào)為onStatus且transactionId為0,client收到此消息后不能通過transactionId對應(yīng)上,可通過其中的code對應(yīng)。
自此發(fā)布流成功,接下來開始發(fā)送音視頻相關(guān)數(shù)據(jù)。

8.2.13 @setDataFrame:c2s
發(fā)送音視頻原數(shù)據(jù),這些信息可在拉流端共享。

8.2.14 Audio Data:c2s
csid:音頻和視頻數(shù)據(jù)采用不同的chunk通道,即使用不同的csid,并且此csid可自定義;
format:當(dāng)前是此chunk通道的首個chunk,所以使用type 0 chunk;
注意Audio data第一個字節(jié)是00,說明此chunk里包含的是AAC sequence header。

format:此chunk通道的非首個chunk,可使用type 1 chunk;
注意Audio data第一個字節(jié)是01,說明此chunk里包含的是音頻序列幀。

8.2.15 Video Data:c2s
format:當(dāng)前是此chunk通道的首個chunk,所以使用type 0 chunk;
注意Video data第一個字節(jié)是00,說明此chunk里包含的是AVC sequence header。

format:此chunk通道的非首個chunk,可使用type 1 chunk;
注意Video data第一個字節(jié)是01,說明此chunk里包含的是NAL Unit。
視頻幀大于chunk size時會拆分成多個chunks,為減少數(shù)據(jù)量chunks中非首個chunk要使用Type 3類型,但是實測wireshark并不會顯示Type 3的chunk,猜測內(nèi)部實現(xiàn)把Type 3又整合成了Type 1,讀者抓包時不必疑惑。

8.2.16 FCUnpublish:c2s

8.2.17 onFCUnpublish:s2c

8.2.18 closeStream:c2s

8.2.19 onStatus('NetStream.Unpublish.Success'):s2c

參考資料
1. RTMP
rtmp_specification_1.0.pdf
2. Action Message Format
AMF 0
AMF 3
3. FLV
video_file_format_spec_v10.pdf
4. AAC
Understanding_AAC
AudioSpecificConfig
5. H.264
H.264
AVC