Adobe公司太坑人了,官方文檔公布的信息根本就不全,如果只按照他上面的寫的話,是沒法用的。按照文檔上面的流程,server和client連接之后首先要進行握手,握手成功之后進行一些交互,其實就是交互一些信息以確認大家都是用的同一個協議,交互成功之后就開始傳數據了。
首先說下rtmp協議包的格式。握手之后,rtmp傳輸一個數據默認的長度是128bytes,這128bytes不包括包頭的長度,只是數據的長度,文檔上面沒有說明,很憋了我一段時間,數據超過這個長度之后就要分塊,超過128bytes的數據放到下一個塊中,以此類推。塊大小是可配置的,最大塊是65535字節,最小塊是128字節。塊越大CPU使用率越低,但是也導致大的寫入,在低帶寬下產生其他內容的延遲。
Rtmp協議是包頭加包體(數據)組成,包頭可以是4種長度的任意一種:12, 8, 4, 1 byte(s)。包頭包含了head type、時間戳、amf size、amf type、streamID。
完整的RTMP包頭有12字節,由下面5個部分組成:
| 用途 |
大小(Byte) |
含義 |
| Head_Type |
1 |
包頭 |
| TIMER |
3 |
時間戳 |
| AMFSize |
3 |
數據大小 |
| AMFType |
1 |
數據類型 |
| StreamID |
4 |
流ID |
Head_Type占用RTMP包的第一個字節,這個字節里面記錄了包的類型和包的ChannelID。Head_Type字節的前兩個Bits決定了包頭的長度。
Head_Type的前兩個Bit和長度對應關系,header length是包頭的長度:
| Bits |
Header Length |
| 00 |
12 bytes |
| 01 |
8 bytes |
| 10 |
4 bytes |
| 11 |
1 byte |
一個視頻流的第一個包必須是12bytes,也就是00,要告訴對方這個流的信息。
Head_Type的后面6個Bits記錄着ChannelID,其為一下內容:
| 02 |
Ping 和ByteRead通道 |
| 03 |
Invoke通道 我們的connect() publish()和自字寫的NetConnection.Call() 數據都是在這個通道的 |
| 04 |
Audio和Vidio通道 |
| 05 06 07 |
服務器保留,經觀察FMS2用這些Channel也用來發送音頻或視頻數據 |
比如傳一個視頻流,第一個塊的head type就是00 00 00 04.(0x04)。實測fms用4或者5在傳影視頻。官方文檔上面說head_type可能會不止一個byte,但實際情況用一個byte也夠了。
TiMMER - 時間戳
音視頻的播放同步是由時間戳來控制的,單位是毫秒。如果時間戳大於或等於16777215(16進制0x00ffffff),該值必須為16777215,並且擴展時間戳必須出現,4bytes的擴展時間戳出現在包頭之后包體之前。這個時間戳一般去flv文件里面每個tag里面的時間戳。
AMFSize - 數據大小
AMFSize占三個字節,這個長度是AMF長度,其實就是本次數據的長度。
AMFType - 數據類型
AMFType是RTMP包里面的數據的類型,占用1個字節。例如音頻包的類型為8,視頻包的類型為9。下面列出的是常用的數據類型:
| 0×01 |
Chunk Size |
changes the chunk size for packets |
| 0×02 |
Unknown |
|
| 0×03 |
Bytes Read |
send every x bytes read by both sides |
| 0×04 |
Ping |
ping is a stream control message, has subtypes |
| 0×05 |
Server BW |
the servers downstream bw |
| 0×06 |
Client BW |
the clients upstream bw |
| 0×07 |
Unknown |
|
| 0×08 |
Audio Data |
packet containing audio |
| 0×09 |
Video Data |
packet containing video data |
| 0x0A-0x0E |
Unknown |
|
| 0x0F |
FLEX_STREAM_SEND |
TYPE_FLEX_STREAM_SEND |
| 0x10 |
FLEX_SHARED_OBJECT |
TYPE_FLEX_SHARED_OBJECT |
| 0x11 |
FLEX_MESSAGE |
TYPE_FLEX_MESSAGE |
| 0×12 |
Notify |
an invoke which does not expect a reply |
| 0×13 |
Shared Object |
has subtypes |
| 0×14 |
Invoke |
like remoting call, used for stream actions too. |
| 0×16 |
StreamData |
這是FMS3出來后新增的數據類型,這種類型數據中包含AudioData和VideoData |
StreamID - 流ID
占用RTMP包頭的最后4個字節,是一個big-endian的int型數據,每個消息所關聯的ID,用於區分其所在的消息流。
前面說包頭有幾種長度,第一個長度是12bytes,包含了全部的頭信息,第一個數據流也就是流的開始必須是這個長度。
第二種8bytes的包,沒有了streamID,發這種 包,對方就默認此streamID和上次相同,一個視頻數據在第一個流之后都可以使這種格式,比如一個1M的視頻,第一次發128bytes用 12bytes的包,之后每次發的數據都應該用8bytes的包。當然如果每次都用12bytes也沒有問題。
第三種為4bytes,只有head_type和時間戳,缺少的對方認為與之前的一樣,實際應用中很難出現。
第四中就只有head_type一個byte。如果一次數據超過了長度(默認是128bytes),就要分塊,第一個塊是12bytes或者8bytes的,之后的塊就是1byte。因為分的N塊,每一塊的信息都是一樣的,所以只需要告訴對方此次包的長度就行了。
例子:
例如有一個RTMP封包的數據0300 00 0000 01 021400 00 00 000200 0763 6F 6E 6E 65 63 74003F F0 00 00 00 00 00 0008 ,,,
數據依次解析的含義
03表示12字節頭,channelid=3
000000表示時間戳 Timer=0
000102表示AMFSize=18
14表示AMFType=Invoke 方法調用
00 00 00 00 表示StreamID = 0
//到此,12字節RTMP頭結束下面的是AMF數據分析
02表示String
0007表示String長度7
63 6F 6E 6E 65 63 74 是String的Ascall值"connect"
00表示Double
3F F0 00 00 00 00 00 00 表示double的0.0
08表示Map數據開始
官方文檔上面的例子:
例1展示一個簡單的音頻消息流。這個例子顯示了信息的冗余。
|
|
Message Stream ID |
Message Type ID |
Time |
Length |
| Msg # 1 |
12345 |
8 |
1000 |
32 |
| Msg # 2 |
12345 |
8 |
1020 |
32 |
| Msg # 3 |
12345 |
8 |
1040 |
32 |
| Msg # 4 |
12345 |
8 |
1060 |
32 |
下表顯示了這個流產生的塊。從消息3開始,數據傳輸開始優化。在消息3之后,每個消息只有一個字節的開銷。
|
|
Chunk Stream ID |
Chunk Type |
Header Data |
No.of Bytes After Header |
Total No.of Bytes in the Chunk |
| Chunk#1 |
3 |
0 |
delta: 1000 length: 32 type: 8 stream ID:1234 (11bytes) |
32 |
44 |
| Chunk#2 |
3 |
2 |
20 (3 bytes) |
32 |
36 |
| Chunk#3 |
3 |
3 |
none(0 bytes) |
32 |
33 |
| Chunk#4 |
3 |
3 |
none(0 bytes) |
32 |
33 |
演示一個消息由於太長,而被分割成128字節的塊。
|
|
Message Stream ID |
Message TYpe ID |
Time |
Length |
| Msg # 1 |
12346 |
9 (video) |
1000 |
307 |
下面是產生的塊。
|
|
Chunk Stream ID |
Chunk Type |
Header Data |
No. of Bytes after Header |
Total No. of bytes in the chunk |
| Chunk#1 |
4 |
0 |
delta: 1000 length: 307 type: 9 streamID: 12346 (11 bytes) |
128 |
140 |
| Chunk#2 |
4 |
3 |
none (0 bytes) |
128 |
129 |
| Chunk#3 |
4 |
3 |
none (0 bytes) |
51 |
52 |
數據傳輸的限定大小默認是128bytes,可以通過控制消息來改變,RTMP的約定是當Chunk流ID為2,消息流ID為0的時候,被認為是控制消息。
具體傳輸的過程是這樣的,首先雙方先進行握手,握手過程官 方文檔上有說明,但是在flash10.1之后,adobe公司改了握手,文檔上那個握手不能用了,至少播放AVC和ACC不能用,這東西太坑人了,改了 又不說一聲,而且一個本來簡單的握手改的很是復雜,居然要依賴openssl加密,有必要嗎。網上找不到有關文章,我只有看rtmpserver開源項目 源碼來弄。
握手步驟沒有變,但內容完全不一樣,以前叫sample handshake,現在叫復雜握手(complex handshake)。
它的步驟是由三個固定大小的塊組成,而不是可變大小的塊加上頭。握手開始於客戶端發送C0,C1塊。在發送C2之前客戶端必須等待接收S1。在發送任何數據之前客戶端必須等待接收S2。服務端在發送S0和S1之前必須等待接收C0,也可以等待接收C1。服務端在發送S2之前必須等待接收C1。服務端在發送任何數據之前必須等待接收C2。
網上有一個人說出了complex handshake的方式,http://blog.csdn.net/winlinvip/article/details/7714493,以下是他的說明:

scheme1和scheme2這兩種方式,是包結構的調換。

key和digest的主要算法是:C1的key為128bytes隨機數。C1_32bytes_digest = HMACsha256(P1+P2, 1504, FPKey, 30) ,其中P1為digest之前的部分,P2為digest之后的部分,P1+P2是將這兩部分拷貝到新的數組,共1536-32長度。S1的key根據C1的key算出來,算法如下:

在Rtmpserver的sources\thelib\src\protocols\rtmp\inboundrtmpprotocol.cpp中有握手的代碼,看他的說明也馬馬虎虎,我也看源碼后才清楚具體過程。具體過程和他說的差不多,只是有些計算要用到openssl加密,所以要安裝openssl,我也是從rtmpserver上面扣出來的代碼然后自己改一下。
握手之后就開始一些交互信令,這個如果按照文檔上的來,根本不行,文檔上省略的一些東西,坑人。
我的交互過程是按照抓的FMS3.5的包來做的。握手之后首先client會發送一個connect消息過來。另外,RTMP交互的消息格式官方說明上有,還是比較清楚的。
| 字段名 |
類型 |
描述 |
| 命令名 |
字符串 |
命令名。設置為”connect” |
| 傳輸ID |
數字 |
總是設為1 |
| 命令對象 |
對象 |
含有名值對的命令信息對象 |
| 可選的用戶變量 |
對象 |
任何可選信息 |
下面是在連接命令的命令對象中使用的名值對的描述:
| 屬性 |
類型 |
描述 |
示例值 |
| App |
字符串 |
客戶端要連接到的服務應用名 |
Testapp |
| Flashver |
字符串 |
Flash播放器版本。和應用文檔中getversion()函數返回的字符串相同。 |
FMSc/1.0 |
| SwfUrl |
字符串 |
發起連接的swf文件的url |
file://C:/ FlvPlayer.swf |
| TcUrl |
字符串 |
服務url。有下列的格式。protocol://servername:port/appName/appInstance |
rtmp://localhost::1935/testapp/instance1 |
| fpad |
布爾值 |
是否使用代理 |
true or false |
| audioCodecs |
數字 |
指示客戶端支持的音頻編解碼器 |
SUPPORT_SND_MP3 |
| videoCodecs |
數字 |
指示支持的視頻編解碼器 |
SUPPORT_VID_SORENSON |
| pageUrl |
字符串 |
SWF文件被加載的頁面的Url |
http:// somehost/sample.html |
| objectEncoding |
數字 |
AMF編碼方法 |
kAMF3 |

上圖中app的vod就是請求文件的路徑。
表示使用amf0編碼格式傳輸命令,如果是3就是表示用amf3。
之后server回復windows acknowledgement size- set peer bandwidth- user control message(begin 0)- result(connect response)

User control message (用戶控制信息)的格式官方文檔上有,消息數據的頭兩個字節用於標識事件類型,0表示Stream Begin。事件類型之后是事件數據。事件數據字段是可變長的,此時的數據時00。

在發送了windows acknowledgement size之后client會回一個windows adknowledgement size,此時需要接收

然后server接收client發的一個create stream過來

接收到之后server返回一個_result

Server接收client發來的recv set buffer length - play commcand from

| 字段名 |
類型 |
描述 |
| 命令名 |
字符串 |
命令名,設為”play” |
| 傳輸ID |
數字 |
設置為0 |
| 命令對象 |
NULL |
命令信息不存在,設為NULL 類型 |
| 流名 |
字符串 |
要播放的流名。對於播放一個FLV文件,則流名不帶文件后綴(例如,"sample”)。對於回放MP3或ID3標簽,必須在流名前加MP3(例如:”MP3:Sample”)。對於播放H264/AAC文件,必須在流名前加MP4前綴,並且指定擴展名,例如播放sample.m4v,則指定”mp4:sample.m4v”。 |
| 開始 |
數字 |
一個指定開始時間的可選參數。默認值是-2,意味着用戶首先嘗試播放流名中指定的直播流。如果流名字段中指定的直播流不存在,則播放同名的錄制流。如果本字段設置為-1,則只播放流名字段中指定的直播流。如果,本字段為0,或正值,則在本字段指定的時間,播放流名字段中指定的錄制流。如果指定的錄制流不存在,則播放播放列表中的下一項。 |
| 時長 |
數字 |
指定播放時長的可選字段。默認值是-1。-1意味着播放一個直播流,直到沒有數據可以活到,或者播放一個錄制流知道結束。如果本字段位0,則播放一個錄制流中從start字段中指定的時間開始的單個幀。假設,start字段中指定的是大於或等於0的值。如果本字段為正值,則以本字段中的值為時間周期播放直播流。之后,則以時長字段中指定的時間播放錄制流?。(如果一個流在時長字段中指定的時間之前結束,則回放結束。)。如果傳遞一個非-1的負值,則把值當作-1。 |
| Reset |
布爾值 |
指定是否刷新先前的播放列表的BOOL值或數值。 |

Client發的play命了中包含的請求的文件名,上圖是1(string ‘1’ 這個數據)。
然后server發送set chunk size - user control message(stream is recorded) - onstatus-play reset (AMF0 Command) - user control message(begin1) - onstatus-play start (AMF0 Command) - RtmpSampleAccess (AMF0 Data) –空audio數據 - onstatus-data start (AMF0 Data)



Set chunk size這里就是改默認128字節的消息,這里改成了4096,當然也可以不發次消息。注意,在改之前發送的消息如果超過了128bytes都要分塊發送的之后就可以每塊大小事4096了。
Begin這里,這個前2bytes是0,表示stream begin,和上面的begin是個類型,只是數據段是1,這個抓包有點問題,沒有顯示數據段而已,此begin 1之后,所有的包頭里面的streamID都是1了,之前的都是0
| 字段 |
類型 |
描述 |
| 命令名 |
字符串 |
命令名。如果播放成功,則命令名設為響應狀態。 |
| 描述 |
字符串 |
如果播放命令成功,則客戶端從服務端接收NetStream.Play.Start狀態響應消息。如果指定的流沒有找到,則接收到NetStream.Play.StreamNotFound 響應狀態消息。 |

這里的加載amf數據和解析amf數據(也就是交互的消息)可以用我寫的一個amf0的解析模塊來做。
然后就發送flv的元數據(metadata)了,如果沒有也可以不發這個。之后再發音視頻的配置信息項。最后就可以發送音視頻數據了。
發送音視頻數據的時候可以用以前的方式發:包頭的AMFType是8或者9,數據是flv的 tag data(沒有tag header)

也可以按照新的類型amf type為0x16,數據就是一個tag (header + data)來發。
其中的amf0數據請看這里。
要注意下就是rtmp都是大端模式,所以發送數據,包頭、交互消息都要填成大端模式的,但是只有streamID是小端模式,這個也是文檔沒有說明,rtmp坑人的地方。
