接下来我们分析RTMP_SendPacket函数。我们先了解一下rtmp的消息格式chunk。
RTMP的head组成
RTMP的head在协议中的表现形式是chunk head,前面已经说到一个Message + head可以分成一个和多个chunk,为了区分这些chunk,肯定是需要一个chunk head的,具体的实现就把Message head的信息和chunk head的信息合并在一起以chunk head的形式表现。
一个完整的chunk的组成如下图所示
Chunk basic header:
该字段包含chunk的stream ID和 type 。chunk的Type决定了消息头的编码方式。该字段的长度完全依赖于stream ID,该字段是一个可变长的字段。
Chunk Msg Header:0, 3 ,7, 11
该字段包含了将要发送的消息的信息(或者是一部分,一个消息拆成多个chunk的情况下是一部分)该字段的长度由chunk basic header中的type决定。
Extend Timestamp: 0 ,4 bytes
该字段发送的时候必须是正常的时间戳设置成0xffffff时,当正常时间戳不为0xffffff时,该字段不发送。当时间戳比0xffffff小该字段不发送,当时间戳比0xffffff大时该字段必须发送,且正常时间戳设置成0xffffff。
Chunk Data
实际数据(Payload),可以是信令,也可以是媒体数据。
总结如下图所示:
6.1.2 块消息头
有四种格式的块消息ID,供块流基本头中的fmt 字段选择。一个实现应该使用最紧致的方式来表示块消息头。
6.1.2.1 类型0
0 类型的块长度为11 字节。在一个块流的开始和时间戳返回的时候必须有这种块。
时间戳:3 字节
对于0 类型的块。消息的绝对时间戳在这里发送。如果时间戳大于或等于16777215(16 进制0x00ffffff),该值必须为16777215,并且扩展时间戳必须出现。否则该值就是整个的时间戳。
6.1.2.2. 类型1
类型1 的块占7 个字节长。消息流ID 不包含在本块中。块的消息流ID 与先前的块相同。具有可变大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式。
6.1.2.3. 类型2
类型2 的块占3 个字节。既不包含流ID 也不包含消息长度。本块使用的流ID 和消息长度与先前的块相同。具有固定大小消息的流,在第一个消息之后的每个消息的第一个块应该使用这个格式。
6.1.2.4 类型3
类型3 的块没有头。流ID,消息长度,时间戳都不出现。这种类型的块使用与先前块相同的数据。当一个消息被分成多个块,除了第一块以外,所有的块都应使用这种类型。示例可参考6.2.2 节中的例2 。由相同大小,流ID,和时间间隔的流在类型2 的块之后应使用这种块。示例可参考6.2.1 节中的例1 。如果第一个消息和第二个消息的时间增量与第一个消息的时间戳相同,那么0类型的块之后必须是3 类型的块而,不需要类型2 的块来注册时间增量。如果类型3 的块在类型0 的块之后,那么类型3 的时间戳增量与0 类型的块的时间戳相同。
时间戳增量:3 字节
对于类型1 的块和类型2 的块,本字段表示先前块的时间戳与当前块的时间戳的差值。如果增量大于等于1677215(16 进制0x00ffffff),这个值必须是16777215 ,并且扩展时间戳必须出现。否则这个值就是整个的增量。
消息长度:3 字节
对于类型0 或类型1 的块本字段表示消息的长度。注意,这个值通常与负载长度是不相同的。The chunk payload length is the maximum chunk size for all but the last chunk, and the remainder (which may be the entire length, for small messages) for the last chunk.
消息类型ID:1 字节
对于0 类型和1 类型的块,本字段发送消息类型。
消息流ID:4 字节
对于0 类型的块,本字段存储消息流ID。通常,在一个块流中的消息来自于同一个消息流。虽然,由于不同的消息可能复用到一个块流中而使头压缩无法有效实施。但是,如果一个消息流关闭而另一个消息流才打开,那么通过发送一个新的0 类型的块重复使用一个存在的块流也不是不可以。
6.1.3. 扩展时间戳
只有当块消息头中的普通时间戳设置为0x00ffffff 时,本字段才被传送。如果普通时间戳的值小于0x00ffffff,那么本字段一定不能出现。如果时间戳字段不出现本字段也一定不能出现。类型3 的块一定不能含有本字段。本字段在块消息头之后,块时间之前。
代码分析如下:
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue)
{
const RTMPPacket *prevPacket = r->m_vecChannelsOut[packet->m_nChannel];
uint32_t last = 0;
int nSize;
int hSize, cSize;
char *header, *hptr, *hend, hbuf[RTMP_MAX_HEADER_SIZE], c;
uint32_t t;
char *buffer, *tbuf = NULL, *toff = NULL;
int nChunkSize;
int tlen;
// 前一个packet存在且不是完整的ChunkMsgHeader,因此有可能需要调整块消息头的类型
//fmt字节
/*case 0:chunk msg header 长度为11
* case 1:chunk msg header 长度为7
* case 2:chunk msg header 长度为3
* case 3:chunk msg header 长度为0
*/
if (prevPacket && packet->m_headerType != RTMP_PACKET_SIZE_LARGE)
{
/* compress a bit by using the prev packet's attributes */
// 获取ChunkMsgHeader类型,前一个Chunk与当前Chunk比较
// 如果前后两个块的大小、包类型及块头类型都相同,则将块头类型fmt设为2,
// 即可省略消息长度、消息类型id、消息流id
// 可以参考官方协议:流的分块 --- 6.1.2.3节
if (prevPacket->m_nBodySize == packet->m_nBodySize&& prevPacket->m_packetType == packet->m_packetType
&& packet->m_headerType == RTMP_PACKET_SIZE_MEDIUM)
packet->m_headerType = RTMP_PACKET_SIZE_SMALL;
// 前后两个块的时间戳相同,且块头类型fmt为2,则相应的时间戳也可省略,因此将块头类型置为3
// 可以参考官方协议:流的分块 --- 6.1.2.4节
if (prevPacket->m_nTimeStamp == packet->m_nTimeStamp && packet->m_headerType == RTMP_PACKET_SIZE_SMALL)
packet->m_headerType = RTMP_PACKET_SIZE_MINIMUM;
last = prevPacket->m_nTimeStamp;// 前一个包的时间戳
}
// 块头类型fmt取值0、1、2、3,超过3就表示出错(fmt占二个字节)
if (packet->m_headerType > 3) /* sanity */
{
RTMP_Log(RTMP_LOGERROR, "sanity failed!! trying to send header of type: 0x%02x.", (unsigned char)packet->m_headerType);
return FALSE;
}
// 块头初始大小 = 基本头(1字节) + 块消息头大小(11/7/3/0) = [12, 8, 4, 1]
// 块基本头是1-3字节,因此用变量cSize来表示剩下的0-2字节
// nSize 表示块头初始大小, hSize表示块头大小
nSize = packetSize[packet->m_headerType];
hSize = nSize;
cSize = 0;
// 时间戳增量
t = packet->m_nTimeStamp - last;
if (packet->m_body)
{
// m_body是指向负载数据首地址的指针;“-”号用于指针前移
header = packet->m_body - nSize;
// 块头的首指针
hend = packet->m_body;
// 块头的尾指针
}
else
{
header = hbuf + 6;
hend = hbuf + sizeof(hbuf);
}
if (packet->m_nChannel > 319)// 块流id(cs id)大于319,则块基本头占3个字节
cSize = 2;
else if (packet->m_nChannel > 63)// 块流id(cs id)在64与319之间,则块基本头占2个字节
cSize = 1;
// ChunkBasicHeader的长度比初始长度还要长
if (cSize)
{
header -= cSize;// header指向块头
hSize += cSize;// hSize加上ChunkBasicHeader的长度(比初始长度多出来的长度)
}
// nSize > 1表示块消息头至少有3个字节,即存在timestamp字段
// 相对TimeStamp大于0xffffff,此时需要使用ExtendTimeStamp
if (nSize > 1 && t >= 0xffffff)
{
header -= 4;
hSize += 4;
}
hptr = header;
c = packet->m_headerType << 6;// 把ChunkBasicHeader的Fmt类型左移6位
// 设置basic header的第一个字节值,前两位为fmt. 可以参考官方协议:流的分块 --- 6.1.1节
switch (cSize)
{
case 0:// 把ChunkBasicHeader的低6位设置成ChunkStreamID( cs id )
c |= packet->m_nChannel;
break;
case 1:// 同理,但低6位设置成000000
break;
case 2:// 同理,但低6位设置成000001
c |= 1;
break;
}
*hptr++ = c;// 可以拆分成两句*hptr=c; hptr++,此时hptr指向第2个字节
// 设置basic header的第二(三)个字节值
if (cSize)
{
int tmp = packet->m_nChannel - 64;// 将要放到第2字节的内容tmp
*hptr++ = tmp & 0xff;// 获取低位存储与第2字节
if (cSize == 2)// ChunkBasicHeader是最大的3字节时,获取高位存储于最后1个字节(注意:排序使用大端序列,和主机相反)
*hptr++ = tmp >> 8;
}
if (nSize > 1)// ChunkMsgHeader长度为11、7、3, 都含有timestamp(3字节)
{
// 将时间戳(相对或绝对)转化为3个字节存入hptr,如果时间戳超过0xffffff,则后面还要填入Extend Timestamp
hptr = AMF_EncodeInt24(hptr, hend, t > 0xffffff ? 0xffffff : t);
}
if (nSize > 4)// ChunkMsgHeader长度为11、7,都含有 msg length + msg type id
{
// 将消息长度(msg length)转化为3个字节存入hptr
hptr = AMF_EncodeInt24(hptr, hend, packet->m_nBodySize);
*hptr++ = packet->m_packetType;
}
// ChunkMsgHeader长度为11, 含有msg stream id( 小端)
if (nSize > 8)
hptr += EncodeInt32LE(hptr, packet->m_nInfoField2);
if (nSize > 1 && t >= 0xffffff)// 如果时间戳大于0xffffff,则需要写入Extend Timestamp
hptr = AMF_EncodeInt32(hptr, hend, t);
// 到此为止,已经将块头填写好了
// 此时nSize表示负载数据的长度, buffer是指向负载数据区的指针
nSize = packet->m_nBodySize;
buffer = packet->m_body;
nChunkSize = r->m_outChunkSize;//Chunk大小,默认是128字节
RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d, size=%d", __FUNCTION__, r->m_sb.sb_socket, nSize);
/* send all chunks in one HTTP request ,使用HTTP协议 */
if (r->Link.protocol & RTMP_FEATURE_HTTP)
{
// nSize:Message负载长度;nChunkSize:Chunk长度;
// 例nSize:307,nChunkSize:128;
// 可分为(307 + 128 - 1)/128 = 3个
// 为什么加 nChunkSize - 1?因为除法会只取整数部分!
int chunks = (nSize + nChunkSize - 1) / nChunkSize;
if (chunks > 1)// Chunk个数超过一个
{
// 注意:ChunkBasicHeader的长度 = cSize + 1
// 消息分n块后总的开销:
// n个ChunkBasicHeader,1个ChunkMsgHeader,1个Message负载
// 实际上只有第一个Chunk是完整的,剩下的只有ChunkBasicHeader
tlen = chunks * (cSize + 1) + nSize + hSize;
tbuf = malloc(tlen);
if (!tbuf)
return FALSE;
toff = tbuf;
}
}
// 消息的负载 + 头
while (nSize + hSize)
{
int wrote;
if (nSize < nChunkSize)// 消息负载大小 < Chunk大小(不用分块)
nChunkSize = nSize;// Chunk可能小于设定值
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)header, hSize);
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)buffer, nChunkSize);
// 如果r->Link.protocol采用Http协议,则将RTMP包数据封装成多个Chunk,然后一次性发送。
// 否则每封装成一个块,就立即发送出去
if (tbuf)
{
// 将从Chunk头开始的nChunkSize + hSize个字节拷贝至toff中,
// 这些拷贝的数据包括块头数据(hSize字节)和nChunkSize个负载数据
memcpy(toff, header, nChunkSize + hSize);
toff += nChunkSize + hSize;
}
else// 负载数据长度不超过设定的块大小,不需要分块,因此tbuf为NULL;或者r->Link.protocol不采用Http
{
// 直接将负载数据和块头数据发送出去
wrote = WriteN(r, header, nChunkSize + hSize);
if (!wrote)
return FALSE;
}
nSize -= nChunkSize;// 消息负载长度 - Chunk负载长度
buffer += nChunkSize;// buffer指针前移1个Chunk负载长度
hSize = 0;// 重置块头大小为0,后续的块只需要有基本头(或加上扩展时间戳)即可
// 如果消息负载数据还没有发完,准备填充下一个块的块头数据
if (nSize > 0)
{
header = buffer - 1;
hSize = 1;
if (cSize)
{
header -= cSize;
hSize += cSize;
}
*header = (0xc0 | c);
if (cSize)
{
int tmp = packet->m_nChannel - 64;
header[1] = tmp & 0xff;
if (cSize == 2)
header[2] = tmp >> 8;
}
}
}
if (tbuf)
{
int wrote = WriteN(r, tbuf, toff - tbuf);
free(tbuf);
tbuf = NULL;
if (!wrote)
return FALSE;
}
/* we invoked a remote method */
if (packet->m_packetType == 0x14)
{
AVal method;
char *ptr;
ptr = packet->m_body + 1;
AMF_DecodeString(ptr, &method);
RTMP_Log(RTMP_LOGDEBUG, "Invoking %s", method.av_val);
/* keep it in call queue till result arrives */
if (queue)
{
int txn;
ptr += 3 + method.av_len;
txn = (int)AMF_DecodeNumber(ptr);
AV_queue(&r->m_methodCalls, &r->m_numCalls, &method, txn);
}
}
if (!r->m_vecChannelsOut[packet->m_nChannel])
r->m_vecChannelsOut[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
memcpy(r->m_vecChannelsOut[packet->m_nChannel], packet, sizeof(RTMPPacket));
return TRUE;
}