RTMP協議分析及H.264打包原理

RTMP是Real Time Messaging Protocol(實時消息傳輸協議)的首字母縮寫。該協議基於TCP,是一個協議族,包括RTMP基本協議及RTMPT/RTMPS/RTMPE等多種變種。RTMP是一種設計用來進行實時數據通信的網絡協議,主要用來在Flash/AIR平臺和支持RTMP協議的流媒體/交互服務器之間進行音視頻和數據通信。

RTMP協議是一個互聯網五層體系結構中應用層的協議。RTMP協議中基本的數據單元稱爲消息(Message)。當RTMP協議在互聯網中傳輸數據的時候,消息會被拆分成更小的單元,稱爲塊(Chunk)。

一.定義

Payload(載荷):包含於一個數據包中的數據,例如音頻採樣或者視頻壓縮數據。
Packet(數據包):一個數據包由固定頭和載荷數據構成。一些底層協議可能會要求對數據包進行封裝。
Port(端口):TCP/IP使用小的正整數對端口進行標識。OSI傳輸層使用的運輸選擇器 (TSEL) 相當於端口。
Transport address(傳輸地址):用以識別傳輸層端點的網絡地址和端口的組合,例如一個IP地址和一個TCP端口。
Message stream(消息流):通信中消息流通的一個邏輯通道。
Message stream ID(消息流ID):每個消息有一個關聯的ID,使用ID可以識別出該消息屬於哪個消息流。
Chunk(塊):消息的一段。消息在網絡發送之前被拆分成很多小的部分。塊按照時間戳的順序進行端到端的傳輸。
Chunk stream(塊流):通信中允許塊流向一個特定方向的邏輯通道。塊流可以從客戶端流向服務器,也可以從服務器流向客戶端。
Chunk stream ID(塊流 ID):每個塊有一個關聯的ID,使用ID可以識別出該塊屬於哪個塊流。
Multiplexing(合成):將獨立的音頻/視頻數據合成爲一個連續的音頻/視頻流,這樣就可以同時發送視頻和音頻了。
DeMultiplexing(分解):Multiplexing 的逆向處理,將交叉的音頻和視頻數據還原成原始音頻和視頻數據的格式。
Remote Procedure Call(RPC 遠程方法調用):允許客戶端或服務器調用對端的一個子程序或者程序的請求。
Metadata(元數據):關於數據的描述。比如電影的 metadata 包括電影標題、持續時間、創建時間等等。
Application Instance (應用實例):應用實例運行於服務器上,客戶端可連接這個實例併發送連接請求,連接服務器。
Action Message Format (AMF,操作消息格式):AMF是Adobe獨家開發出來的通信協議,它採用二進制壓縮,序列化、反序列化、傳輸數據,從而爲Flash 播放器與Flash Remoting網關通信提供了一種輕量級的、高效能的通信方式。如下圖所示。


AMF的初衷只是爲了支持Flash ActionScript的數據類型,目前有兩個版本:AMF0和AMF3。AMF從Flash MX時代的AMF0發展到現在的AMF3。AMF3用作Flash Playe 9的ActionScript 3.0的默認序列化格式,而AMF0則用作舊版的ActionScript 1.0和2.0的序列化格式。在網絡傳輸數據方面,AMF3比AMF0更有效率。AMF3能將int和uint對象作爲整數(integer)傳輸,並且能序列化 ActionScript 3.0才支持的數據類型, 比如ByteArray,XML和Iexternalizable。


二.握手

握手以客戶端發送C0和C1塊開始。
客戶端必須等待接收到S1才能發送C2。
客戶端必須等待接收到S2才能發送任何其他數據。
服務器端必須等待接收到C0才能發送S0和S1,也可以等待接收到C1再發送S0和S1。服務器端必須等待接收到C1才能發送S2。服務器端必須等待接收到C2才能發送任何其他數據。

1.C0和S0格式

C0和S0都是八位,即一個字節,如下所示:


Version(8bits):在C0中,它表示客戶端的RTMP版本;在S0中,它表示服務器端的RTMP版本。RTMP規範目前將它定義爲3。0—2用於早期的產品,已經被廢棄。4—31保留,用於RTMP未來版本。32—255禁止使用。

2.C1和S1格式

C1和S1都是1536個字節,如下所示:


time(4字節):包含時間戳,該時間戳應該被用做本終端發送的塊的起點。該字段可以爲0或者其他任意值。

zero(4字節):該字段必須爲0。

random data(1528字節):該字段可以包含任意值。終端需要區分出是它發起的握手還是對端發起的握手,所以這該字段應該發送一些足夠隨機的數。

3.C2和S2格式

C2和S2也都是1536個字節,幾乎是C1和S1的副本,如下所示:


time(4字節):包含時間戳,必須與C1或S1中的時間戳相同。

time2(4字節):包含時間戳,必須與前一個C1或S1中的時間戳相同。

random echo(1528字節):該字段必須與S1或者S2中的隨機數相同。

4.握手示意圖



三.塊

握手之後,連接開始對一個或多個塊流進行合併。每個塊都有一個唯一ID對其進行關聯,這個ID叫做chunk stream ID(塊流ID)。這些塊通過網絡進行傳輸,在發送端,每個塊必須被完全發送纔可以發送下一塊。在接收端,這些塊根據塊流ID被組裝成消息。

每個塊都是由塊頭和塊數據體組成,而塊頭自身也是由三部分組成,塊格式如下所示:


Basic Header(基本頭,1—3字節):該字段編碼了塊流ID和塊類型。塊類型決定了Message Header(消息頭)的編碼格式。該字段長度完全取決於塊流ID,因爲塊流ID是一個可變長度的字段。

Message Header(消息頭,0、3、7或11字節):該字段編碼了消息的相關信息,標識了塊數據所屬的消息。該字段的長度由塊類型決定。

Extended Timestamp(擴展時間戳,0或4字節):該字段只在特定情況下出現。

Chunk Data(塊數據,可變大小):塊的載荷部分,取決於配置的最大塊尺寸,一般爲128字節。

1.Basic Header

塊基本頭對塊類型(用fmt 字段表示,參見下圖) 塊流ID(chunk stream ID)進行編碼。fmt字段佔2bits,取值範圍時0—3。RTMP協議最多支持65597個流,流的ID範圍是3—65599。ID值0、1和2被保留,0表示兩字節形式,1表示三字節形式,2的塊流ID被保留,用於下層協議控制消息和命令。

☆一字節形式


ID取值範圍3—63,0、1和2用於標識多字節形式。

☆兩字節形式


ID取值範圍64—319,即第二個字節+64。

☆三字節形式


ID取值範圍64—68899,即第三個字節*256+第二個字節+64。

2.Message Header

有四種類型的塊消息頭,由塊基本頭中的“fmt”字段決定。

☆類型0

由11個字節組成,必須用於塊流的起始塊或者流時間戳重置的時候。


timestamp(3字節):消息的絕對時間戳,如果大於等於16777215(0xFFFFFF),該字段仍爲16777215,此時Extend Timestamp(擴展時間戳)字段存在,用於對溢出值進行擴展。否則,該字段標識整個時間戳,不需要擴展。

message length(3字節):通常與塊載荷的長度不同,塊載荷長度通常表示塊的最大長度128字節(除了最後一個塊)和最後一個塊的剩餘空間。

message type id(1字節):消息類型。

message stream id(4字節):該字段用小端模式保存。

☆類型1

由7個字節組成,不包括message stream ID(消息流ID),此時塊與之前的塊取相同的消息流ID。可變長度消息的流(例如,一些視頻格式)應該在第一塊之後使用這一格式表示之後的每個新塊。


timestamp delta(3字節):前一個塊時間戳與當前塊時間戳的差值,即相對時間戳,如果大於等於16777215(0xFFFFFF),該字段仍爲16777215,此時Extend Timestamp(擴展時間戳)字段存在,用於對溢出值進行擴展。否則,該字段標識整個差值,不需要擴展。

message length(3字節):通常與塊載荷的長度不同,塊載荷長度通常表示塊的最大長度(除了最後一個塊)和最後一個塊的剩餘空間。該長度是指爲塊載荷AMF編碼後的長度。

message type id(1字節):消息類型。

☆類型2

由3個字節組成,既不包括message stream ID(消息流ID),也不包括message length(消息長度),此時塊與之前的塊取相同的消息流ID和消息長度。固定長度消息的流(例如,一些音頻格式)應該在第一塊之後使用這一格式表示之後的每個新塊。


timestamp delta(3字節):前一個塊時間戳與當前塊時間戳的差值,如果大於等於16777215(0xFFFFFF),該字段仍爲16777215,此時Extend Timestamp(擴展時間戳)字段存在,用於對溢出值進行擴展。否則,該字段標識整個差值,不需要擴展。

☆類型3

沒有消息頭,從之前具有相同塊流ID的塊中取相應的值。當一條消息被分割成多個塊時,所有的塊(除了第一個塊)應該使用這種類型。

3.Extended timestamp(3字節)

只有當塊消息頭中的普通時間戳設置爲0x00ffffff時,本字段才被傳送。如果普通時間戳的值小於0x00ffffff,那麼本字段一定不能出現。如果普通時間戳字段不出現本字段也一定不能出現。

4.例子

☆不分割消息



從上面兩個表中可以看出,從消息3開始,數據處理得到優化,此時Chunk除了載荷以外,只多了一個塊基本頭。

☆分割消息



當消息的載荷長度超過128字節時,需要將消息分割爲若干個塊進行傳輸。

從上面兩個例子可以看出,塊類型3有兩種用法。一個是指定一個新消息的消息頭可以派生自已經存在的狀態數據(例一),另一個是指定消息的連續性(例二)。


四.消息

消息是RTMP協議中基本的數據單元。不同種類的消息包含不同的Message Type ID,代表不同的功能。RTMP協議中一共規定了十多種消息類型,分別發揮着不同的作用。例如,Message Type ID在1-7的消息用於協議控制,這些消息一般是RTMP協議自身管理要使用的消息,用戶一般情況下無需操作其中的數據。Message Type ID爲8,9的消息分別用於傳輸音頻和視頻數據。Message Type ID爲15-20的消息用於發送AMF編碼的命令,負責用戶與服務器之間的交互,比如播放,暫停等等。

1.消息頭

消息頭(Message Header)有四部分組成:標誌消息類型的Message Type ID,標誌載荷長度的Payload Length,標識時間戳的Timestamp,標識消息所屬媒體流的Stream ID。消息的格式如下所示。


2.載荷

載荷是消息包含的實際數據,它們可能是音頻採樣數據或者是視頻壓縮數據。

由於端與端之間實際傳輸的是塊,所以只需要將載荷加上塊頭封裝成塊。實際應用中,無擴展時間戳,一字節形式的塊基本頭就能滿足要求,整個塊頭滿足以下四種長度:

fmt=0:Basic Head+Message Head=1+11=12

fmt=1:Basic Head+Message Head=1+7=8

fmt=2:Basic Head+Message Head=1+3=4

fmt=3:Basic Head+Message Head=1+0=1

需要注意的是,當載荷爲H.264數據時,要使用AMF3進行編碼(即序列化),關於AMF3可以參考:AMF3中文版


五.打包H.264

如果整個打包過程都自己弄,是非常繁瑣的,還好網上有大神開源了RTMP庫,這裏使用librtmp進行H.264數據的打包推送。

librtmp的編譯可以參考:Win7(Windows 7)下用VS2012(Visual Studio 2012)編譯librtmp

使用librtmp時,解析RTMP地址、握手、建立流媒體鏈接和AMF編碼這塊我們都不需要關心,但是數據是如何打包並通過int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue) 函數推送的還是得學習一下。

RTMPPacket類型的結構體定義如下,一個RTMPPacket對應RTMP協議規範裏面的一個塊(Chunk)。

  1. typedef struct RTMPPacket    
  2.   {    
  3.     uint8_t m_headerType;//塊消息頭的類型(4種)    
  4.     uint8_t m_packetType;//消息類型ID(1-7協議控制;8,9音視頻;10以後爲AMF編碼消息)    
  5.     uint8_t m_hasAbsTimestamp;  //時間戳是絕對值還是相對值    
  6.     int m_nChannel;         //塊流ID    
  7.     uint32_t m_nTimeStamp;  //時間戳  
  8.     int32_t m_nInfoField2;  //last 4 bytes in a long header,消息流ID     
  9.     uint32_t m_nBodySize;   //消息載荷大小    
  10.     uint32_t m_nBytesRead;  //暫時沒用到  
  11.     RTMPChunk *m_chunk;     //<span style="font-family: Arial, Helvetica, sans-serif;">暫時沒用到</span>  
  12.     char *m_body;           //消息載荷,可分割爲多個塊載荷  
  13.   } RTMPPacket;   
一些定義
  1. #define RTMP_DEFAULT_CHUNKSIZE  128//默認塊大小  
  2.   
  3. #define RTMP_BUFFER_CACHE_SIZE (16*1024)//開闢16K字節空間  
  4.   
  5. #define RTMP_PACKET_TYPE_AUDIO 0x08//音頻的消息類型  
  6. #define RTMP_PACKET_TYPE_VIDEO 0x09//視頻的消息類型  
  7.   
  8. #define RTMP_MAX_HEADER_SIZE 18//塊基本頭+塊消息頭+擴展時間戳=3+11+4=18  
  9.   
  10. #define RTMP_PACKET_SIZE_LARGE    0//塊消息頭類型0  
  11. #define RTMP_PACKET_SIZE_MEDIUM   1//塊消息頭類型1  
  12. #define RTMP_PACKET_SIZE_SMALL    2//塊消息頭類型2  
  13. #define RTMP_PACKET_SIZE_MINIMUM  3//塊消息頭類型3  

RTMP_SendPacket函數

  1. //queue:TRUE爲放進發送隊列,FALSE是不放進發送隊列,直接發送  
  2. int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)    
  3. {    
  4.   const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];    
  5.   uint32_t last = 0;//上一個塊的時間戳  
  6.   int nSize;//消息載荷大小,可分割爲多個塊載荷大小  
  7.   int hSize;//塊頭大小  
  8.   int cSize;//塊基本頭大小增量  
  9.   char *header;//指向塊頭起始位置    
  10.   char *hptr;  
  11.   char *hend;//指向塊頭結束位置   
  12.   char hbuf[RTMP_MAX_HEADER_SIZE];   
  13.   char c;    
  14.   uint32_t t;//相對時間戳    
  15.   char *buffer;//指向消息載荷  
  16.   char *tbuf = NULL;  
  17.   char *toff = NULL;    
  18.   int nChunkSize;//塊載荷大小   
  19.   int tlen;    
  20.   //不是完整塊消息頭(即不是11字節的塊消息頭)    
  21.   if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)    
  22.   {      
  23.       //前一個塊和這個塊對比  
  24.       //原理參考 例子—不分割消息  
  25.       if (prevPacket->m_nBodySize == packet->m_nBodySize    
  26.       && prevPacket->m_packetType == packet->m_packetType    
  27.       && packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)    
  28.     packet->m_headerType = RTMP_PACKET_SIZE_SMALL;    
  29.       //原理參考 例子—分割消息  
  30.       if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp    
  31.       && packet->m_headerType == RTMP_PACKET_SIZE_SMALL)    
  32.     packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;    
  33.       //上一個塊的時間戳  
  34.       last = prevPacket->m_nTimeStamp;    
  35.   }    
  36.   //非法  
  37.   if (packet->m_headerType > 3)  
  38.   {    
  39.       RTMP_Log(RTMP_LOGERROR, "sanity failed!! trying to send header of type: 0x%02x.",    
  40.       (unsigned char)packet->m_headerType);    
  41.       return FALSE;    
  42.   }    
  43.   //nSize暫時設置爲塊頭大小;packetSize[] = { 12, 8, 4, 1 }    
  44.   nSize = packetSize[packet->m_headerType];   
  45.   //塊頭大小初始化  
  46.   hSize = nSize;  
  47.   cSize = 0;    
  48.   //相對時間戳,當塊時間戳與上一個塊時間戳的差值  
  49.   t = packet->m_nTimeStamp - last;    
  50.     
  51.   if (packet->m_body)    
  52.   {    
  53.    
  54.       //m_body是指向載荷數據首地址的指針,“-”號用於指針前移   
  55.       //header:塊頭起始位置   
  56.       header = packet->m_body - nSize;    
  57.       //hend:塊頭結束位置  
  58.       hend = packet->m_body;    
  59.   }    
  60.   else    
  61.   {    
  62.       header = hbuf + 6;    
  63.       hend = hbuf + sizeof(hbuf);    
  64.   }    
  65.   //當塊流ID大於319時    
  66.   if (packet->m_nChannel > 319)    
  67.     //塊基本頭是3個字節    
  68.     cSize = 2;    
  69.   //當塊流ID大於63時    
  70.   else if (packet->m_nChannel > 63)    
  71.     //塊基本頭是2個字節    
  72.     cSize = 1;    
  73.   if (cSize)    
  74.   {    
  75.       //header指針指塊頭起始位置,“-”號用於指針前移   
  76.       header -= cSize;    
  77.       //當cSize不爲0時,塊頭需要進行擴展,默認的塊基本頭爲1字節,但是也可能是2字節或3字節  
  78.       hSize += cSize;    
  79.   }    
  80.   //如果塊消息頭存在,且相對時間戳大於0xffffff,此時需要使用ExtendTimeStamp    
  81.   if (nSize > 1 && t >= 0xffffff)    
  82.   {    
  83.       header -= 4;    
  84.       hSize += 4;    
  85.   }    
  86.     
  87.   hptr = header;    
  88.   //把塊基本頭的fmt類型左移6位。   
  89.   c = packet->m_headerType << 6;    
  90.   switch (cSize)    
  91.   {    
  92.     //把塊基本頭的低6位設置成塊流ID  
  93.     case 0:    
  94.       c |= packet->m_nChannel;    
  95.       break;    
  96.     //同理,但低6位設置成000000    
  97.     case 1:    
  98.       break;    
  99.     //同理,但低6位設置成000001    
  100.     case 2:    
  101.       c |= 1;    
  102.       break;    
  103.   }    
  104.   //可以拆分成兩句*hptr=c;hptr++,此時hptr指向第2個字節    
  105.   *hptr++ = c;    
  106.   //cSize>0,即塊基本頭大於1字節    
  107.   if (cSize)    
  108.   {    
  109.     //將要放到第2字節的內容tmp    
  110.       int tmp = packet->m_nChannel - 64;    
  111.     //獲取低位存儲於第2字節    
  112.       *hptr++ = tmp & 0xff;    
  113.     //塊基本頭是最大的3字節時    
  114.       if (cSize == 2)    
  115.     //獲取高位存儲於第三個字節(注意:排序使用大端序列,和主機相反)    
  116.     *hptr++ = tmp >> 8;    
  117.   }    
  118.   //塊消息頭一共有4種,包含的字段數不同,nSize>1,塊消息頭存在。    
  119.   if (nSize > 1)    
  120.   {    
  121.       //塊消息頭的最開始三個字節爲時間戳,返回值hptr=hptr+3  
  122.       hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);    
  123.   }    
  124.   //如果塊消息頭包括MessageLength+MessageTypeID(4字節)    
  125.   if (nSize > 4)    
  126.   {    
  127.       //消息長度,爲消息載荷AMF編碼後的長度   
  128.       hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);    
  129.       //消息類型ID  
  130.       *hptr++ = packet->m_packetType;    
  131.   }    
  132.   //消息流ID(4字節)    
  133.   if (nSize > 8)    
  134.     hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);    
  135.       
  136.   //如果塊消息頭存在,且相對時間戳大於0xffffff,此時需要使用ExtendTimeStamp     
  137.   if (nSize > 1 && t >= 0xffffff)    
  138.     hptr = AMF_EncodeInt32(hptr, hend, t);    
  139.   //消息載荷大小   
  140.   nSize = packet->m_nBodySize;    
  141.   //消息載荷指針  
  142.   buffer = packet->m_body;    
  143.   //塊大小,默認128字節    
  144.   nChunkSize = r->m_outChunkSize;    
  145.     
  146.   RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket,    
  147.       nSize);     
  148.   //使用HTTP    
  149.   if (r->Link.protocol & RTMP_FEATURE_HTTP)    
  150.   {    
  151.     //nSize:消息載荷大小;nChunkSize:塊載荷大小   
  152.     //例nSize:307,nChunkSize:128;    
  153.     //可分爲(307+128-1)/128=3個    
  154.     //爲什麼減1?因爲除法會只取整數部分!    
  155.     int chunks = (nSize+nChunkSize-1) / nChunkSize;    
  156.     //如果塊的個數超過一個    
  157.     if (chunks > 1)    
  158.     {    
  159.     //消息分n塊後總的開銷:    
  160.     //n個塊基本頭,1個塊消息頭,1個消息載荷,這裏是沒有擴展時間戳的情況    
  161.     //實際中只有第一個塊是完整的,剩下的只有塊基本頭   
  162.     tlen = chunks * (cSize + 1) + nSize + hSize;//這裏其實多算了一個塊基本頭    
  163.     //分配內存    
  164.     tbuf = (char *) malloc(tlen);    
  165.     if (!tbuf)    
  166.        return FALSE;    
  167.     toff = tbuf;    
  168.     }    
  169.   }    
  170.   while (nSize + hSize)    
  171.   {    
  172.       int wrote;    
  173.       //消息載荷小於塊載荷(不用分塊)    
  174.       if (nSize < nChunkSize)    
  175.       nChunkSize = nSize;    
  176.     
  177.       RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)header, hSize);    
  178.       RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)buffer, nChunkSize);    
  179.       if (tbuf)    
  180.       {     
  181.         memcpy(toff, header, nChunkSize + hSize);    
  182.         toff += nChunkSize + hSize;    
  183.       }    
  184.       else    
  185.       {    
  186.         wrote = WriteN(r, header, nChunkSize + hSize);    
  187.         if (!wrote)    
  188.           return FALSE;    
  189.       }    
  190.       //消息載荷長度塊載荷長度    
  191.       nSize -= nChunkSize;    
  192.       //Buffer指針前移1個塊載荷長度    
  193.       buffer += nChunkSize;    
  194.       hSize = 0;    
  195.           
  196.       //如果消息沒有發完    
  197.       if (nSize > 0)    
  198.       {    
  199.         header = buffer - 1;    
  200.         hSize = 1;    
  201.         if (cSize)    
  202.         {    
  203.           header -= cSize;    
  204.           hSize += cSize;    
  205.         }    
  206.         //塊基本頭第1個字節    
  207.         *header = (0xc0 | c);    
  208.         //如果塊基本頭大於1字節    
  209.         if (cSize)    
  210.         {    
  211.           int tmp = packet->m_nChannel - 64;    
  212.           header[1] = tmp & 0xff;    
  213.           if (cSize == 2)    
  214.           header[2] = tmp >> 8;    
  215.         }    
  216.        }    
  217.   }    
  218.   if (tbuf)    
  219.   {    
  220.       int wrote = WriteN(r, tbuf, toff-tbuf);    
  221.       free(tbuf);    
  222.       tbuf = NULL;    
  223.       if (!wrote)    
  224.         return FALSE;    
  225.   }    
  226.     
  227.   /* we invoked a remote method */    
  228.   if (packet->m_packetType == 0x14)    
  229.   {    
  230.       AVal method;    
  231.       char *ptr;    
  232.       ptr = packet->m_body + 1;    
  233.       AMF_DecodeString(ptr, &method);    
  234.       RTMP_Log(RTMP_LOGDEBUG, "Invoking %s", method.av_val);    
  235.       /* keep it in call queue till result arrives */    
  236.       if (queue)   
  237.       {    
  238.         int txn;    
  239.         ptr += 3 + method.av_len;    
  240.         txn = (int)AMF_DecodeNumber(ptr);    
  241.         AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);    
  242.       }    
  243.   }    
  244.     
  245.   if (!r->m_vecChannelsOut[packet->m_nChannel])    
  246.     r->m_vecChannelsOut[packet->m_nChannel] = (RTMPPacket *) malloc(sizeof(RTMPPacket));    
  247.   memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));    
  248.   return TRUE;    
  249. }    

現在要解決的是如何給結構體RTMPPacket中的消息載荷m_body賦值,即如何將H.264的NALU打包進消息載荷。

1.sps和pps的打包

sps和pps是需要在其他NALU之前打包推送給服務器。由於RTMP推送的音視頻流的封裝形式和FLV格式相似,向FMS等流媒體服務器推送H264和AAC直播流時,需要首先發送"AVC sequence header"和"AAC sequence header"(這兩項數據包含的是重要的編碼信息,沒有它們,解碼器將無法解碼),因此這裏的"AVC sequence header"就是用來打包sps和pps的。

AVC sequence header其實就是AVCDecoderConfigurationRecord結構,該結構在標準文檔“ISO/IEC-14496-15:2004”的5.2.4.1章節中有詳細說明,如下所示:


用表格表示如下:

FLV 是一個二進制文件,簡單來說,其是由一個文件頭(FLV header)和很多 tag 組成(FLV body)。tag 又可以分成三類: audio, video, script,分別代表音頻流,視頻流,腳本流,而每個 tag 又由 tag header 和 tag data 組成。

然後參照“Video File Format Specification Version 10”中The FLV File Format的Video tags章節,如下所示:


上表中tag header爲兩個4bits,即一個字節,其他的是tag data。inter frame即P frame。

AVC時,3字節CompositionTime無意義,通常設置爲0。

AVCDecoderConfigurationRecord結構的表格中可以看出,由於NALUnitLength-1=3,因此每個NALU包都有NALUnitLength=4個字節來描述它的長度。這4個字節需要添加到每個NALU的前面,因此上表中Data的結構實際上如下所示:


一個典型的打包示例如下所示:

  1. body = (unsigned char *)packet->m_body;  
  2. i = 0;  
  3. body[i++] = 0x17;// 1:Iframe  7:AVC ,元數據當做keyframe發送</span>  
  4. body[i++] = 0x00;  
  5.   
  6. body[i++] = 0x00;  
  7. body[i++] = 0x00;  
  8. body[i++] = 0x00;  
  9.   
  10. //AVCDecoderConfigurationRecord  
  11. body[i++] = 0x01;  
  12. body[i++] = sps[1];  
  13. body[i++] = sps[2];  
  14. body[i++] = sps[3];  
  15. body[i++] = 0xff;  
  16.   
  17. /*sps*/  
  18. body[i++]   = 0xe1;  
  19. body[i++] = (sps_len >> 8) & 0xff;  
  20. body[i++] = sps_len & 0xff;  
  21. memcpy(&body[i],sps,sps_len);  
  22. i +=  sps_len;  
  23.   
  24. /*pps*/  
  25. body[i++]   = 0x01;  
  26. body[i++] = (pps_len >> 8) & 0xff;  
  27. body[i++] = (pps_len) & 0xff;  
  28. memcpy(&body[i],pps,pps_len);  
  29. i +=  pps_len;  
2.其它NALU的打包

一個典型的打包示例如下所示:

  1. int i = 0;   
  2. if(bIsKeyFrame)  
  3. {    
  4.     body[i++] = 0x17;// 1:Iframe  7:AVC     
  5.     body[i++] = 0x01;// AVC NALU     
  6.     body[i++] = 0x00;    
  7.     body[i++] = 0x00;    
  8.     body[i++] = 0x00;    
  9.   
  10.   
  11.     // NALU size     
  12.     body[i++] = size>>24 &0xff;    
  13.     body[i++] = size>>16 &0xff;    
  14.     body[i++] = size>>8 &0xff;    
  15.     body[i++] = size&0xff;  
  16.     // NALU data     
  17.     memcpy(&body[i],data,size);    
  18. }  
  19. else  
  20. {    
  21.     body[i++] = 0x27;// 2:Pframe  7:AVC     
  22.     body[i++] = 0x01;// AVC NALU     
  23.     body[i++] = 0x00;    
  24.     body[i++] = 0x00;    
  25.     body[i++] = 0x00;    
  26.   
  27.   
  28.     // NALU size     
  29.     body[i++] = size>>24 &0xff;    
  30.     body[i++] = size>>16 &0xff;    
  31.     body[i++] = size>>8 &0xff;    
  32.     body[i++] = size&0xff;  
  33.     // NALU data     
  34.     memcpy(&body[i],data,size);    
  35. }  

一個具體的例子:Qt基於librtmp推送H.264

參考鏈接:

http://wwwimages.adobe.com/www.adobe.com/content/dam/Adobe/en/devnet/rtmp/pdf/rtmp_specification_1.0.pdf

http://www.adobe.com/content/dam/Adobe/en/devnet/flv/pdfs/video_file_format_spec_v10.pdf

發佈了126 篇原創文章 · 獲贊 61 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章