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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值