librtmp协议分析---RTMP_ConnectStream函数
Posted alen_xie
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了librtmp协议分析---RTMP_ConnectStream函数相关的知识,希望对你有一定的参考价值。
接下来我们分析RTMP_ConnectStream函数。
代码如下:
//创建流/循环读取服务端发送过来的各种消息,比如window ack, set peer bandwidth, set chunk size, _result等
//直到接收到了play
int RTMP_ConnectStream(RTMP *r, int seekTime)
RTMPPacket packet = 0;
/* seekTime was already set by SetupStream / SetupURL.
* This is only needed by ReconnectStream.
*/
if (seekTime > 0)
r->Link.seekTime = seekTime;
r->m_mediaChannel = 0;
// 没有正在播放,RTMP连接上,从socket读取到消息(message)包
// 接收到的实际上是块(Chunk),而不是消息(Message),因为消息在网上传输的时候要分割成块.
while (!r->m_bPlaying && RTMP_IsConnected(r) && RTMP_ReadPacket(r, &packet))
// 一个消息可能被封装成多个块(Chunk),只有当所有块读取完才处理这个消息包
if (RTMPPacket_IsReady(&packet))//是否读取完毕
if (!packet.m_nBodySize)
continue;
if ((packet.m_packetType == RTMP_PACKET_TYPE_AUDIO) || (packet.m_packetType == RTMP_PACKET_TYPE_VIDEO)
|| (packet.m_packetType == RTMP_PACKET_TYPE_INFO))// 读取到flv数据包,则继续读取下一个包
RTMP_Log(RTMP_LOGWARNING, "Received FLV packet before play()! Ignoring.");
RTMPPacket_Free(&packet);
continue;
//处理Packet!
RTMP_ClientPacket(r, &packet);//建立网络流:处理收到的数据。开始处理收到的数据.则是处理消息(Message),并做出响应
RTMPPacket_Free(&packet);//建立网络流:处理收到的数据。处理完毕,清除数据
return r->m_bPlaying;//返回当前是否接收到了play/publish 或者stopd等
初看起来这个函数的代码量好像挺少的,实际上不然,其复杂度还是挺高的。我觉得比RTMP_Connect()要复杂不少。其关键就在于这个While()循环。首先,循环的三个条件都满足,就能进行循环。只有出错或者建立网络流(NetStream)的步骤完成后,才能跳出循环。
在这个函数中有两个函数尤为重要:RTMP_ReadPacket( ) 和 RTMP_ClientPacket( )。 第一个函数的作用是读取通过Socket接收下来的消息(Message)包,但是不做任何处理。我们会在下面详细分析该函数代码。第二个函数则是处理消息(Message),并做出响应。该函数代码比较长,而且涉及很多子函数的调用,所以该函数代码在后续一篇单独的文章中加以详细分析。 这两个函数结合,就可以完成接收消息然后响应消息的步骤。
我们先了解一下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 块格式
块由头和数据组成。块头由三部分组成:
块基本头:1 到3 字节
本字段包含块流ID 和块类型。块类型决定编码的消息头的格式。长度取决于块流ID。块流ID 是可变长字段。
块消息头:0,3,7 或11 字节。
本字段编码要发送的消息的信息。本字段的长度,取决于块头中指定的块类型。
扩展时间戳:0 个或4 字节
本字段必须在发送普通时间戳(普通时间戳是指块消息头中的时间戳)设置为0xffffff 时发送,正常时间戳为其他值时都不应发送本值。当普通时间戳的值小于0xffffff时,本字段不用出现,而应当使用正常时间戳字段。
6.1.1 块基本头
块基本头编码块流ID 和块类型(在下图中用fmt 表示)。块类型决定编码消息头的格式。块基本头字段可能是1,2 或3 个字节。这取决于块流ID。
一个实现应该用最少的数据来表示ID?。
本协议支持65597 种流,ID 从3-65599。ID 0、1、2 作为保留。0,表示ID 的范围是64-319(第二个字节+64);1,表示ID 范围是64-65599(第三个字节*256+第二个字节+64);2 表示低层协议消息。没有其他的字节来表示流ID。3-63 表示完整的流ID。
3-63 之间的值表示完整的流ID。没有其他的字节表示流ID。
0-5(不显著的)位表示块流ID。
块流ID 2-63 可以用1 字节的字段表示
块流ID 64-319 可以用2-字节表示。ID 计算是(第二个字节+64)
块流ID64-65599 可以表示为3 个字节。ID 计算为第三个字节*255+第二个字节+64
Cs id:6 位
本字段表示范围在2-63 的块流ID。值0 和1 表示本字段的2 或3 字节版本
Fmt:2 位
本字段标识块消息头的4 种格式。每种流类型的块消息头在下一节中表示。
Cs id-64:8-16 位
本字段包含块流ID 减去64 的值。例如365,应在cs id 中表示1,而用这里的1 6
位表示301。
块流ID 在64-319 范围之内,可以用2 个字节版本表示,也可以用3 字节版本表示。
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 的块一定不能含有本字段。本字段在块消息头之后,块时间之前。
代码分析如下:
//函数的作用是读取通过Socket接收下来的消息(Message)包,但是不做任何处理。
/**
* @brief 读取接收到的消息块(Chunk),存放在packet中. 对接收到的消息不做任何处理。 块的格式为:
*
* | basic header(1-3字节)| chunk msg header(0/3/7/11字节) | Extended Timestamp(0/4字节) | chunk data |
*
* 其中 basic header还可以分解为:| fmt(2位) | cs id (3 <= id <= 65599) |
* RTMP协议支持65597种流,ID从3-65599。ID 0、1、2作为保留。
* id = 0,表示ID的范围是64-319(第二个字节 + 64);
* id = 1,表示ID范围是64-65599(第三个字节*256 + 第二个字节 + 64);
* id = 2,表示低层协议消息。
* 没有其他的字节来表示流ID。3 -- 63表示完整的流ID。
*
* 一个完整的chunk msg header 还可以分解为 :
* | timestamp(3字节) | msg length(3字节) | msg type id(1字节,小端) | msg stream id(4字节) |
* **********************-----------参考 6.1 块格式
*/
int RTMP_ReadPacket(RTMP *r, RTMPPacket *packet)
//packet 存读取完后的的数据
//Chunk Header最大值18// Chunk Header长度最大值为3 + 11 + 4 = 18
uint8_t hbuf[RTMP_MAX_HEADER_SIZE] = 0;
char *header = (char *)hbuf; //header 指向的是从Socket中收下来的数据
int nSize, hSize, nToRead, nChunk;// nSize是块消息头长度,hSize是块头长度
int didAlloc = FALSE;
RTMP_Log(RTMP_LOGDEBUG2, "%s: fd=%d", __FUNCTION__, r->m_sb.sb_socket);
//收下来的数据存入hbuf
if (ReadN(r, (char *)hbuf, 1) == 0)// 读取1个字节存入 hbuf[0]
RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header", __FUNCTION__);
return FALSE;
packet->m_headerType = (hbuf[0] & 0xc0) >> 6;//块类型fmt(2bit)
packet->m_nChannel = (hbuf[0] & 0x3f);//块流ID(2-63)
header++;
if (packet->m_nChannel == 0) //块流ID第1字节为0时,块流ID占2个字节// 块流ID第一个字节为0,表示块流ID占2个字节,表示ID的范围是64-319(第二个字节 + 64)
if (ReadN(r, (char *)&hbuf[1], 1) != 1)// 读取接下来的1个字节存放在hbuf[1]中
RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header 2nd byte", __FUNCTION__);
return FALSE;
//计算块流ID(64-319)
packet->m_nChannel = hbuf[1];// 块流ID = 第二个字节 + 64 = hbuf[1] + 64
packet->m_nChannel += 64;
header++;
else if (packet->m_nChannel == 1)// 块流ID第一个字节为1,表示块流ID占3个字节,表示ID范围是64 -- 65599(第三个字节*256 + 第二个字节 + 64)
int tmp;
if (ReadN(r, (char *)&hbuf[1], 2) != 2)// 读取2个字节存放在hbuf[1]和hbuf[2]中
RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header 3nd byte", __FUNCTION__);
return FALSE;
tmp = (hbuf[2] << 8) + hbuf[1];// 块流ID = 第三个字节*256 + 第二个字节 + 64
packet->m_nChannel = tmp + 64;//计算块流ID(64-65599)
RTMP_Log(RTMP_LOGDEBUG, "%s, m_nChannel: %0x", __FUNCTION__, packet->m_nChannel);
header += 2;
// 块消息头(ChunkMsgHeader)有四种类型,大小分别为11、7、3、0,每个值加1 就得到该数组的值
// 块头 = BasicHeader(1-3字节) + ChunkMsgHeader + ExtendTimestamp(0或4字节)
nSize = packetSize[packet->m_headerType];//ChunkHeader的大小(4种),这个值取决于Basic Header中的fmt的2bit的值,可以参考一个文档
// 块类型fmt为0的块,在一个块流的开始和时间戳返回的时候必须有这种块
// 块类型fmt为1、2、3的块使用与先前块相同的数据
// 关于块类型的定义,可参考官方协议:流的分块 --- 6.1.2节
if (nSize == RTMP_LARGE_HEADER_SIZE) /* if we get a full header the timestamp is absolute */
packet->m_hasAbsTimestamp = TRUE;//11字节的完整ChunkMsgHeader的TimeStamp是绝对值// 11个字节的完整ChunkMsgHeader的TimeStamp是绝对时间戳
else if (nSize < RTMP_LARGE_HEADER_SIZE)
/* using values from the last message of this channel */
if (r->m_vecChannelsIn[packet->m_nChannel])
memcpy(packet, r->m_vecChannelsIn[packet->m_nChannel], sizeof(RTMPPacket));
nSize--;// 真实的ChunkMsgHeader的大小,此处减1是因为前面获取包类型的时候多加了1
if (nSize > 0 && ReadN(r, header, nSize) != nSize)// 读取nSize个字节存入header
RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet header. type: %x", __FUNCTION__, (unsigned int)hbuf[0]);
return FALSE;
hSize = nSize + (header - (char *)hbuf);// 目前已经读取的字节数 = chunk msg header + basic header
if (nSize >= 3)//chunk msg header为11、7、3字节,fmt类型值为0、1、2
packet->m_nTimeStamp = AMF_DecodeInt24(header);// 首部前3个字节为timestamp
/*RTMP_Log(RTMP_LOGDEBUG, "%s, reading RTMP packet chunk on channel %x, headersz %i, timestamp %i, abs timestamp %i", __FUNCTION__, packet.m_nChannel, nSize, packet.m_nTimeStamp, packet.m_hasAbsTimestamp); */
if (nSize >= 6)//chunk msg header为11或7字节,fmt类型值为0或1
packet->m_nBodySize = AMF_DecodeInt24(header + 3);
packet->m_nBytesRead = 0;
RTMPPacket_Free(packet);
if (nSize > 6)//(11,7字节首部都有)
packet->m_packetType = header[6];
if (nSize == 11)
packet->m_nInfoField2 = DecodeInt32LE(header + 7);// msg stream id,小端字节序
if (packet->m_nTimeStamp == 0xffffff)// Extend Tiemstamp,占4个字节
if (ReadN(r, header + nSize, 4) != 4)
RTMP_Log(RTMP_LOGERROR, "%s, failed to read extended timestamp", __FUNCTION__);
return FALSE;
packet->m_nTimeStamp = AMF_DecodeInt32(header + nSize);
hSize += 4;
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)hbuf, hSize);
// 如果消息长度非0,且消息数据缓冲区为空,则为之申请空间
if (packet->m_nBodySize > 0 && packet->m_body == NULL)
if (!RTMPPacket_Alloc(packet, packet->m_nBodySize))
RTMP_Log(RTMP_LOGDEBUG, "%s, failed to allocate packet", __FUNCTION__);
return FALSE;
didAlloc = TRUE;
packet->m_headerType = (hbuf[0] & 0xc0) >> 6;
// 剩下的消息数据长度如果比块尺寸大,则需要分块,否则块尺寸就等于剩下的消息数据长度
nToRead = packet->m_nBodySize - packet->m_nBytesRead;
nChunk = r->m_inChunkSize;
if (nToRead < nChunk)
nChunk = nToRead;
/* Does the caller want the raw chunk? */
if (packet->m_chunk)
packet->m_chunk->c_headerSize = hSize;// 块头大小
memcpy(packet->m_chunk->c_header, hbuf, hSize);// 填充块头数据
packet->m_chunk->c_chunk = packet->m_body + packet->m_nBytesRead;// 块消息数据缓冲区指针
packet->m_chunk->c_chunkSize = nChunk;// 块大小
// 读取一个块大小的数据存入块消息数据缓冲区
if (ReadN(r, packet->m_body + packet->m_nBytesRead, nChunk) != nChunk)
RTMP_Log(RTMP_LOGERROR, "%s, failed to read RTMP packet body. len: %lu", __FUNCTION__, packet->m_nBodySize);
return FALSE;
RTMP_LogHexString(RTMP_LOGDEBUG2, (uint8_t *)packet->m_body + packet->m_nBytesRead, nChunk);
packet->m_nBytesRead += nChunk;// 更新已读数据字节个数
/* keep the packet as ref for other packets on this channel */// 将这个包作为通道中其他包的参考
if (!r->m_vecChannelsIn[packet->m_nChannel])
r->m_vecChannelsIn[packet->m_nChannel] = malloc(sizeof(RTMPPacket));
memcpy(r->m_vecChannelsIn[packet->m_nChannel], packet, sizeof(RTMPPacket));
if (RTMPPacket_IsReady(packet)) //读取完毕
/* make packet's timestamp absolute *//*绝对时间戳 = 上一次绝对时间戳 + 时间戳增量 */
if (!packet->m_hasAbsTimestamp)
packet->m_nTimeStamp += r->m_channelTimestamp[packet->m_nChannel]; /* timestamps seem to be always relative!! */
// 当前绝对时间戳保存起来,供下一个包转换时间戳使用
r->m_channelTimestamp[packet->m_nChannel] = packet->m_nTimeStamp;
/* reset the data from the stored packet. we keep the header since we may use it later if a new packet for this channel */
/* arrives and requests to re-use some info (small packet header) */
// 重置保存的包。保留块头数据,因为通道中新到来的包(更短的块头)可能需要使用前面块头的信息.
r->m_vecChannelsIn[packet->m_nChannel]->m_body = NULL;
r->m_vecChannelsIn[packet->m_nChannel]->m_nBytesRead = 0;
r->m_vecChannelsIn[packet->m_nChannel]->m_hasAbsTimestamp = FALSE; /* can only be false if we reuse header */
else
packet->m_body = NULL; /* so it won't be erased on free */
return TRUE;
//处理消息(Message),并做出响应
int RTMP_ClientPacket(RTMP *r, RTMPPacket *packet)
int bHasMediaPacket = 0;
switch (packet->m_packetType)
case 0x01://RTMP消息类型ID=1,设置块大小
/* chunk size */
HandleChangeChunkSize(r, packet);
break;
case 0x03: //RTMP消息类型ID=3,致谢
/* bytes read report */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: bytes read report", __FUNCTION__);
break;
case 0x04://RTMP消息类型ID=4,用户控制
/* ctrl */
HandleCtrl(r, packet);
break;
case 0x05:
/* server bw */
HandleServerBW(r, packet);
break;
case 0x06:
/* client bw */
HandleClientBW(r, packet);
break;
case 0x08://RTMP消息类型ID=8,音频数据
/* audio data */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: audio %lu bytes", __FUNCTION__, packet.m_nBodySize); */
HandleAudio(r, packet);
bHasMediaPacket = 1;
if (!r->m_mediaChannel)
r->m_mediaChannel = packet->m_nChannel;
if (!r->m_pausing)
r->m_mediaStamp = packet->m_nTimeStamp;
break;
case 0x09: //RTMP消息类型ID=9,视频数据
/* video data */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: video %lu bytes", __FUNCTION__, packet.m_nBodySize); */
HandleVideo(r, packet);
bHasMediaPacket = 1;
if (!r->m_mediaChannel)
r->m_mediaChannel = packet->m_nChannel;
if (!r->m_pausing)
r->m_mediaStamp = packet->m_nTimeStamp;
break;
case 0x0F: /* flex stream send *///RTMP消息类型ID=15,AMF3编码,忽略
RTMP_Log(RTMP_LOGDEBUG, "%s, flex stream send, size %lu bytes, not supported, ignoring", __FUNCTION__, packet->m_nBodySize);
break;
case 0x10: /* flex shared object */ //RTMP消息类型ID=16,AMF3编码,忽略
RTMP_Log(RTMP_LOGDEBUG, "%s, flex shared object, size %lu bytes, not supported, ignoring", __FUNCTION__, packet->m_nBodySize);
break;
case 0x11: /* flex message */ //RTMP消息类型ID=17,AMF3编码,忽略
RTMP_Log(RTMP_LOGDEBUG, "%s, flex message, size %lu bytes, not fully supported", __FUNCTION__, packet->m_nBodySize);
/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */
/* some DEBUG code */
#if 0
RTMP_LIB_AMFObject obj;
int nRes = obj.Decode(packet.m_body+1, packet.m_nBodySize-1);
if(nRes < 0)
RTMP_Log(RTMP_LOGERROR, "%s, error decoding AMF3 packet", __FUNCTION__);
/*return; */
obj.Dump();
#endif
//HandleInvoke 远程过程调用。其里面实际是个状态机
if (HandleInvoke(r, packet->m_body + 1, packet->m_nBodySize - 1) == 1)
bHasMediaPacket = 2;
break;
case 0x12: //RTMP消息类型ID=18,AMF0编码,数据消息
/* metadata (notify) */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: notify %lu bytes", __FUNCTION__, packet->m_nBodySize);
if (HandleMetadata(r, packet->m_body, packet->m_nBodySize))
bHasMediaPacket = 1;
break;
case 0x13: //RTMP消息类型ID=19,AMF0编码,忽略
RTMP_Log(RTMP_LOGDEBUG, "%s, shared object, not supported, ignoring", __FUNCTION__);
break;
//RTMP消息类型ID=20,AMF0编码,命令消息
//处理命令消息!
case 0x14:
/* invoke */
RTMP_Log(RTMP_LOGDEBUG, "%s, received: invoke %lu bytes", __FUNCTION__, packet->m_nBodySize);
/*RTMP_LogHex(packet.m_body, packet.m_nBodySize); */
//HandleInvoke 远程过程调用。其里面实际是个状态机
if (HandleInvoke(r, packet->m_body, packet->m_nBodySize) == 1)
bHasMediaPacket = 2;
break;
case 0x16:
/* go through FLV packets and handle metadata packets */
unsigned int pos = 0;
uint32_t nTimeStamp = packet->m_nTimeStamp;
while (pos + 11 < packet->m_nBodySize)
uint32_t dataSize = AMF_DecodeInt24(packet->m_body + pos + 1); /* size without header (11) and prevTagSize (4) */
if (pos + 11 + dataSize + 4 > packet->m_nBodySize)
RTMP_Log(RTMP_LOGWARNING, "Stream corrupt?!");
break;
if (packet->m_body[pos] == 0x12)
HandleMetadata(r, packet->m_body + pos + 11, dataSize);
else if (packet->m_body[pos] == 8 || packet->m_body[pos] == 9)
nTimeStamp = AMF_DecodeInt24(packet->m_body + pos + 4);
nTimeStamp |= (packet->m_body[pos + 7] << 24);
pos += (11 + dataSize + 4);
if (!r->m_pausing)
r->m_mediaStamp = nTimeStamp;
/* FLV tag(s) */
/*RTMP_Log(RTMP_LOGDEBUG, "%s, received: FLV tag(s) %lu bytes", __FUNCTION__, packet.m_nBodySize); */
bHasMediaPacket = 1;
break;
default:
RTMP_Log(RTMP_LOGDEBUG, "%s, unknown packet type received: 0x%02x", __FUNCTION__, packet->m_packetType);
#ifdef _DEBUG
RTMP_LogHex(RTMP_LOGDEBUG, packet->m_body, packet->m_nBodySize);
#endif
return bHasMediaPacket;
以上是关于librtmp协议分析---RTMP_ConnectStream函数的主要内容,如果未能解决你的问题,请参考以下文章
librtmp协议分析---RTMP_ConnectStream函数
librtmp协议分析---RTMP_ConnectStream函数
librtmp协议分析---RTMP_SendPacket函数