目前比较主流的直播技术有RTMP、HLS,其中RTMP主要基于TCP协议,HLS主要基于HTTP协议,二者在实施成本、延迟性等方面有较大差异。本文主要讲解RTMP的推流与拉流技术的应用。
理论知识:
原则上说,开发RTMP的推拉流应用,除具备基本的语言及工具链外,还需要掌握以下知识:
a) RTMP协议原理
b) 基本的音视频编解码知识
c) FLV等流媒体封装格式技巧
RTMP协议原理:
与大多的协议格式一样,RTMP也遵循TLV范式,以及其远程过程调用的指令构造格式AMF。
RTMP基于TCP,在TCP三次握手完成后,RTMP也定义了自己的六次握手,主要用于版本适配、RTT计算,解析这块需要较大篇幅,感兴趣的朋友请百度搜索“RTMP协议详解”。
完成握手之后,传输的报文都基于RTMP分块格式,如下:
+---------------------+-----------------------------------+----------------------+----------------------+
| 1~3字节基本块头 | 0~11字节的消息块头 | 可选4字节扩展时间戳 | 变长消息负载 |
+---------------------+-----------------------------------+----------------------+----------------------+
采用分块技术原于分时复用的思想,不同的上层消息交错地在一条TCP链路中传输,通常一个块不会太长,避免因为某一个应用消息过多的占用网络资源而造成其它应用饿死。
基本块头介绍:
基本块头定义了两个关键要素:消息块头格式、块流ID。
先看基本块头的第一字节表示:
+-位7-+-位6-+-位5-+-位4-+-位3-+-位2-+-位1-+-位0-+
| 块头格式 | 块流ID |
+-------------+------------------------------------------+
块头格式占2个位,有4种表示:
00 —— 11字节完整的消息块头。
01 —— 7字节中等消息块头。
10 —— 3字节小型消息块头。
11 —— 0字节无消息块头。
首字节块流ID占6个位,有0~63种表示,每种表示一个块流通道。为了扩展更多的字节表示块流通道,首字节的块流ID被划出0和1两个值用于表示扩展1或2个字节。
扩展1字节的表示法:
+-------------+--------------------+----------------------+
| 块头格式 | 0 | 扩展1字节 |
+-------------+--------------------+----------------------+
扩展2字节的表示法:
+-------------+--------------------+----------------------+----------------------+
| 块头格式 | 1 | 扩展2字节 |
+-------------+--------------------+----------------------+----------------------+
扩展1字节可以表示64+255=319个块流通道。
扩展1字节可以表示64+65525=65599个块流通道。
消息块头介绍:
11字节完整的消息块头格式:
+------------------+------------------+------------------+------------------+
| 3字节绝对时间戳 | 负载消息长度 |
+------------------+------------------+------------------+------------------+
| 3字节负载消息长度 | 1字节消息类型 | 块流ID |
+------------------+------------------+------------------+------------------+
| 4字节块流ID |
+------------------+------------------+------------------+
7字节中等消息块头:
+------------------+------------------+------------------+------------------+
| 3字节相对时间戳 | 负载消息长度 |
+------------------+------------------+------------------+------------------+
| 3字节负载消息长度 | 1字节消息类型 |
+------------------+------------------+------------------+
3字节小型消息块头:
+------------------+------------------+------------------+
| 3字节相对时间戳 |
+------------------+------------------+------------------+
对于不完整的消息块头,省略的字段表示与上次的相同,这可以起到很好的压缩传输效果。
可选扩展时间戳:
只有当块消息头中的普通时间戳设置为0x00ffffff时,本字段才被传送。
变长消息负载:
在RTMP协议规范中在块协议基础上又进一步定义了消息协议,格式如下:
+------------------+------------------+------------------+------------------+
| 1字节消息类型 | 3字节负载消息长度 |
+------------------+------------------+------------------+------------------+
| 4字节时间戳 |
+------------------+------------------+------------------+------------------+
| 3字节消息流ID |
+------------------+------------------+------------------+
消息协议仍然遵循TLV范式,这种套娃式的结构在协议设计模式中也是主流,然而令疑惑的是,消息头的字段设计,与块头过于重合,并且还定义了一个消息流ID。官方对此的解释是,消息头旨在规范开发者应用层消息的设计,与块头属于两个不同的维度,各字段既使同名也互不影响。事实上,如果我们深入分析librtmp的实现,或者抓包分析RTMP的拉流过程,这个消息头规范是没有被采纳的。
基本的音视频编解码知识:
流媒体传输的音视频格式一般采用主流的压缩协议,如音频采用AAC、MP3,视频采用H264、MPEG4。对直播推流来说,我们需要从摄像头抓取YUV或者Bayer格式的视频,从声卡抓取PCM格式的音频,这些数据都是没有经过压缩的,数据量非常大,不便于网络传输。使用压缩协议执行压缩与解压缩的过程称为编解码。编解码可以使用当下流行的FFMpeg工具,开发者遵循其接口调用规范,将原始音视频数据打包,交给指定的编码模块,生成压缩后的编码帧。压缩后的数据大小通常不到原始数据的1/10,对视频来说,由于采用了P帧和B帧前后向预测技术,压缩效率更高。编码帧通过网络传输到目的端如播放器,交给解码器解码还原,当然,通常采用的编码算法都是有损的,解码后的音视频数据既使不能还原到100%的程度,由于人脑视觉和听觉系统的特殊性,大多数情况下仍然不会影响我们对消息的解读。
FLV等流媒体封装格式技巧:
编码器输出的裸音视频数据是不能直接用于推拉流的,RTMP直播通常采用FLV格式,FLV格式要求:
对于音频数据,在裸数据之前,必须增加1个字节的元格式,这个格式定义了采样率、采样精度、通道布局,对于AAC格式,还需要增加ADTS头。
对于视频数据,在裸数据之前,也必须增加1个字节的元格式,这个格式定义了编码器、帧格式,对于H264,还需要增加SPS和PPS。
librtmp库接口介绍:
官网下载:http://rtmpdump.mplayerhq.hu/download
结构定义:
与开发者最直接相关结构包括:
RTMP报文格式:
typedef struct RTMPPacket
{
uint8_t m_headerType; // 块头类型
uint8_t m_packetType; // 负载格式
uint8_t m_hasAbsTimestamp; // 是否绝对时间戳
int m_nChannel; // 块流ID
uint32_t m_nTimeStamp; // 时间戳
int32_t m_nInfoField2; // 块流ID
uint32_t m_nBodySize; // 负载大小
uint32_t m_nBytesRead; // 读入负载大小
RTMPChunk *m_chunk; // 在RTMP_ReadPacket()调用时,若该字段非NULL,表示关心原始块的信息,通常设为NULL
char *m_body; // 负载指针
} RTMPPacket;
RTMP上下文格式:
typedef struct RTMP
{
int m_inChunkSize; // 最大接收块大小
int m_outChunkSize; // 最大发送块大小
int m_nBWCheckCounter; // 带宽检测计数器
int m_nBytesIn; // 接收数据计数器
int m_nBytesInSent; // 当前数据已回应计数器
int m_nBufferMS; // 当前缓冲的时间长度,以MS为单位
int m_stream_id; // 当前连接的流ID
int m_mediaChannel; // 当前连接媒体使用的块流ID
uint32_t m_mediaStamp; // 当前连接媒体最新的时间戳
uint32_t m_pauseStamp; // 当前连接媒体暂停时的时间戳
int m_pausing; // 是否暂停状态
int m_nServerBW; // 服务器带宽
int m_nClientBW; // 客户端带宽
uint8_t m_nClientBW2; // 客户端带宽调节方式
uint8_t m_bPlaying; // 当前是否推流或连接中
uint8_t m_bSendEncoding; // 连接服务器时发送编码
uint8_t m_bSendCounter; // 设置是否向服务器发送接收字节应答
int m_numInvokes; // 0x14命令远程过程调用计数
int m_numCalls; // 0x14命令远程过程请求队列数量
RTMP_METHOD *m_methodCalls; // 远程过程调用请求队列
RTMPPacket *m_vecChannelsIn[RTMP_CHANNELS]; // 对应块流ID上一次接收的报文
RTMPPacket *m_vecChannelsOut[RTMP_CHANNELS]; // 对应块流ID上一次发送的报文
int m_channelTimestamp[RTMP_CHANNELS]; // 对应块流ID媒体的最新时间戳
double m_fAudioCodecs; // 音频编码器代码
double m_fVideoCodecs; // 视频编码器代码
double m_fEncoding; /* AMF0 or AMF3 */
double m_fDuration; // 当前媒体的时长
int m_msgCounter; // 使用HTTP协议发送请求的计数器
int m_polling; // 使用HTTP协议接收消息主体时的位置
int m_resplen; // 使用HTTP协议接收消息主体时的未读消息计数
int m_unackd; // 使用HTTP协议处理时无响应的计数
AVal m_clientID; // 使用HTTP协议处理时的身份ID
RTMP_READ m_read; // RTMP_Read()操作的上下文
RTMPPacket m_write; // RTMP_Write()操作使用的可复用报文对象
RTMPSockBuf m_sb; // RTMP_ReadPacket()读包操作的上下文
RTMP_LNK Link; // RTMP连接上下文
} RTMP;
函数定义:
关于返回值为int类型的函数,大多数其实是bool语义,即1表示成功,0表示失败,但是仍然也有少部分函数可以返回负值。
RTMP报文操作:
// 重置报文
void RTMPPacket_Reset(RTMPPacket *p);
// 为报文分配负载空间
int RTMPPacket_Alloc(RTMPPacket *p, int nSize);
// 释放负载空间
void RTMPPacket_Free(RTMPPacket *p);
// 检查报文是否可读,当报文被分块,且接收未完成时不可读
#define RTMPPacket_IsReady(a) ((a)->m_nBytesRead == (a)->m_nBodySize)
地址解析操作:
// 解析流地址
int RTMP_ParseURL(const char *url, int *protocol, AVal *host, unsigned int *port, AVal *playpath, AVal *app);
媒体缓存时长设置操作:
// 连接前,设置服务器发送给客户端的媒体缓存时长
void RTMP_SetBufferMS(RTMP *r, int size);
// 连接后,更新服务器发送给客户端的媒体缓存时长
void RTMP_UpdateBufferMS(RTMP *r);
RTMP播放地址及上下文选项操作:
// 更新RTMP上下文中的相应选项
int RTMP_SetOpt(RTMP *r, const AVal *opt, AVal *arg);
// 设置流地址
int RTMP_SetupURL(RTMP *r, char *url);
// 设置RTMP上下文播放地址和相应选项,不关心的可以设为NULL
void RTMP_SetupStream(RTMP *r, int protocol,
AVal *hostname,
unsigned int port,
AVal *sockshost,
AVal *playpath,
AVal *tcUrl,
AVal *swfUrl,
AVal *pageUrl,
AVal *app,
AVal *auth,
AVal *swfSHA256Hash,
uint32_t swfSize,
AVal *flashVer,
AVal *subscribepath,
int dStart,
int dStop, int bLiveStream, long int timeout);
RTMP连接及握手操作:
// 客户端连接及握手
int RTMP_Connect(RTMP *r, RTMPPacket *cp);
// 服务端握手
int RTMP_Serve(RTMP *r);
收发报文操作:
// 接收一个报文
int RTMP_ReadPacket(RTMP *r, RTMPPacket *packet);
// 发送一个报文,queue为1表示当包类型为0x14时,将加入队列等待响应
int RTMP_SendPacket(RTMP *r, RTMPPacket *packet, int queue);
// 直接发送块
int RTMP_SendChunk(RTMP *r, RTMPChunk *chunk);
其他操作,不分类了,解释如下:
// 检查网络是否连接
int RTMP_IsConnected(RTMP *r);
// 返回套接字
int RTMP_Socket(RTMP *r);
// 检查连接是否超时
int RTMP_IsTimedout(RTMP *r);
// 获取当前媒体的时长
double RTMP_GetDuration(RTMP *r);
// 暂停与播放切换控制
int RTMP_ToggleStream(RTMP *r);
// 连接流,并指定开始播放的位置
int RTMP_ConnectStream(RTMP *r, int seekTime);
// 重新创建流
int RTMP_ReconnectStream(RTMP *r, int seekTime);
// 删除当前流
void RTMP_DeleteStream(RTMP *r);
// 获取第一个媒体包
int RTMP_GetNextMediaPacket(RTMP *r, RTMPPacket *packet);
// 处理客户端的报文交互,即处理报文分派逻辑
int RTMP_ClientPacket(RTMP *r, RTMPPacket *packet);
// 分配RTMP上下文
RTMP *RTMP_Alloc(void);
// 初使化RTMP上下文,设默认值
void RTMP_Init(RTMP *r);
// 关闭RTMP上下文
void RTMP_Close(RTMP *r);
// 释放RTMP上下文
void RTMP_Free(RTMP *r)
// 开启客户端的RTMP写开关,用于推流
void RTMP_EnableWrite(RTMP *r);
// 返回RTMP的版本
int RTMP_LibVersion(void);
// 开启RTMP工作中断
void RTMP_UserInterrupt(void);
// 发送0x04号命令的控制消息
int RTMP_SendCtrl(RTMP *r, short nType, unsigned int nObject, unsigned int nTime);
// 发送0x14号远程调用控制暂停
int RTMP_SendPause(RTMP *r, int DoPause, int dTime);
int RTMP_Pause(RTMP *r, int DoPause);
// 递归在一个对象中搜索指定的属性
int RTMP_FindFirstMatchingProperty(AMFObject *obj, const AVal *name, AMFObjectProperty * p);
// 底层套接口的网络读取、发送、关闭连接操作
int RTMPSockBuf_Fill(RTMPSockBuf *sb);
int RTMPSockBuf_Send(RTMPSockBuf *sb, const char *buf, int len);
int RTMPSockBuf_Close(RTMPSockBuf *sb);
// 发送建流操作
int RTMP_SendCreateStream(RTMP *r);
// 发送媒体时间定位操作
int RTMP_SendSeek(RTMP *r, int dTime);
// 发送设置服务器应答窗口大小操作
int RTMP_SendServerBW(RTMP *r);
// 发送设置服务器输出带宽操作
int RTMP_SendClientBW(RTMP *r);
// 删除0x14命令远程调用队列中的请求
void RTMP_DropRequest(RTMP *r, int i, int freeit);
// 读取FLV格式数据
int RTMP_Read(RTMP *r, char *buf, int size);
// 发送FLV格式数据
int RTMP_Write(RTMP *r, const char *buf, int size);
推流用法:
推流流程:
步骤:
- 初使化RTMP上下文
- 设置推流地址
- 开启推流标志
- 连接服务器
- 连接流地址
- 若从文件推流,循环读TAG,组织报文发送
- 若发送太快,适当做延迟
- 推流完毕,释放资源
简单例子:
下面这个例子演示了使用librtmp库将本地flv文件推流到服务器,代码如下:
#include
#include
#include
#include
#include
// 大小端字节序转换
#define HTON16(x) ( (x >> 8 & 0x00FF) | (x << 8 & 0xFF00) )
#define HTON24(x) ( (x >> 16 & 0x0000FF) | (x & 0x00FF00) | (x << 16 & 0xFF0000) )
#define HTON32(x) ( (x >> 24 & 0x000000FF) | (x >> 8 & 0x0000FF00) | (x << 8 & 0x00FF0000) | (x << 24 & 0xFF000000) )
#define HTONTIME(x) ( (x >> 16 & 0x000000FF) | (x & 0x0000FF00) | (x << 16 & 0x00FF0000) | (x & 0xFF000000) )
// 从文件读取指定字节
bool ReadFP(char* pBuf, int nSize, FILE* pFile)
{
return (fread(pBuf, 1, nSize, pFile) == nSize);
}
// 从文件读取1个字节整数
bool ReadU8(uint8_t* u8, FILE* fp)
{
return ReadFP((char*)u8, 1, fp);
}
// 从文件读取2个字节整数
bool ReadU16(uint16_t* u16, FILE* fp)
{
if (!ReadFP((char*)u16, 2, fp))
return false;
*u16 = HTON16(*u16);
return true;
}
// 从文件读取3个字节整数
bool ReadU24(uint32_t* u24, FILE* fp)
{
if (!ReadFP((char*)u24, 3, fp))
return false;
*u24 = HTON24(*u24);
return true;
}
// 从文件读取4个字节整数
bool ReadU32(uint32_t* u32, FILE* fp)
{
if (!ReadFP((char*)u32, 4, fp))
return false;
*u32 = HTON32(*u32);
return true;
}
// 从文件读取4个字节时间戳
bool ReadTime(uint32_t* utime, FILE* fp)
{
if (!ReadFP((char*)utime, 4, fp))
return false;
*utime = HTONTIME(*utime);
return true;
}
// 从文件预读1个字节整数
bool PeekU8(uint8_t* u8, FILE* fp)
{
if (!ReadFP((char*)u8, 1, fp))
return false;
fseek(fp, -1, SEEK_CUR);
return true;
}
int main(int argc, char* argv[])
{
FILE* pFile = fopen("1.flv", "rb");
// 初使化RTMP上下文
RTMP* pRTMP = RTMP_Alloc();
RTMP_Init(pRTMP);
// 设置推流地址
pRTMP->Link.timeout = 10;
RTMP_SetupURL(pRTMP, (char*)"rtmp://127.0.0.1:1935/live/a");
// 开启推流标志
RTMP_EnableWrite(pRTMP);
// 连接服务器
bool b = RTMP_Connect(pRTMP, NULL);
if (!b)
{
printf("connect failed! \n");
return -1;
}
// 连接流地址
b = RTMP_ConnectStream(pRTMP, 0);
if (!b)
{
printf("connect stream failed! \n");
return -1;
}
// 跳过FLV文件头的13个字节
fseek(pFile, 9, SEEK_SET);
fseek(pFile, 4, SEEK_CUR);
// 初使化RTMP报文
RTMPPacket packet;
RTMPPacket_Reset(&packet);
packet.m_body = NULL;
packet.m_chunk = NULL;
packet.m_nInfoField2 = pRTMP->m_stream_id;
uint32_t starttime = RTMP_GetTime();
while (true)
{
// 读取TAG头
uint8_t type = 0;
if (!ReadU8(&type, pFile))
break;
uint32_t datalen = 0;
if (!ReadU24(&datalen, pFile))
break;
uint32_t timestamp = 0;
if (!ReadTime(×tamp, pFile))
break;
uint32_t streamid = 0;
if (!ReadU24(&streamid, pFile))
break;
/*
// 跳过0x12 Script
if (type != 0x08 && type != 0x09)
{
fseek(pFile, datalen + 4, SEEK_CUR);
continue;
}
*/
RTMPPacket_Alloc(&packet, datalen);
if (fread(packet.m_body, 1, datalen, pFile) != datalen)
break;
// 组织报文并发送
packet.m_headerType = RTMP_PACKET_SIZE_LARGE;
packet.m_packetType = type;
packet.m_hasAbsTimestamp = 0;
packet.m_nChannel = 6;
packet.m_nTimeStamp = timestamp;
packet.m_nBodySize = datalen;
if (!RTMP_SendPacket(pRTMP, &packet, 0))
{
printf("Send Error! \n");
break;
}
printf("send type:[%d] timestamp:[%d] datasize:[%d] \n", type, timestamp, datalen);
// 跳过PreTag
uint32_t pretagsize = 0;
if (!ReadU32(&pretagsize, pFile))
break;
// 延时,避免发送太快
uint32_t timeago = (RTMP_GetTime() - starttime);
if (timestamp > 1000 && timeago < timestamp - 1000)
{
printf("sleep...\n");
usleep(100000);
}
RTMPPacket_Free(&packet);
}
// 关闭连接,释放RTMP上下文
RTMP_Close(pRTMP);
RTMP_Free(pRTMP);
fclose(pFile);
return 0;
}
运行输出:
g++ -o testrtmp2 testrtmp2.cpp -lrtmp
./testrtmp2
send type:[18] timestamp:[0] datasize:[371]
send type:[8] timestamp:[0] datasize:[209]
send type:[9] timestamp:[25] datasize:[9838]
send type:[8] timestamp:[26] datasize:[210]
send type:[8] timestamp:[52] datasize:[210]
send type:[8] timestamp:[78] datasize:[210]
send type:[9] timestamp:[88] datasize:[11181]
send type:[8] timestamp:[104] datasize:[210]
send type:[8] timestamp:[131] datasize:[210]
send type:[9] timestamp:[150] datasize:[11820]
send type:[8] timestamp:[157] datasize:[210]
send type:[8] timestamp:[183] datasize:[210]
send type:[8] timestamp:[209] datasize:[210]
send type:[9] timestamp:[213] datasize:[11995]
send type:[8] timestamp:[235] datasize:[210]
send type:[8] timestamp:[261] datasize:[210]
send type:[9] timestamp:[275] datasize:[11809]
send type:[8] timestamp:[287] datasize:[210]
send type:[8] timestamp:[313] datasize:[210]
send type:[9] timestamp:[338] datasize:[6124]
send type:[8] timestamp:[340] datasize:[210]
send type:[8] timestamp:[366] datasize:[210]
send type:[8] timestamp:[392] datasize:[210]
send type:[9] timestamp:[400] datasize:[4500]
send type:[8] timestamp:[418] datasize:[210]
send type:[8] timestamp:[444] datasize:[210]
send type:[9] timestamp:[463] datasize:[2850]
send type:[8] timestamp:[470] datasize:[210]
拉流用法:
拉流流程:
步骤:
- 初使化RTMP上下文
- 设置拉流地址
- 连接服务器
- 连接流地址
- 循环拉流,提取媒体数据,保存为文件或者交给解码模块
- 拉流完毕,释放资源
简单例子:
下面这个例子演示了使用librtmp库从服务器拉流到本地保存为flv/mp3文件,代码如下:
#include
#include
#include
#include
int main(int argc, char* argv[])
{
// 初使化RTMP上下文
RTMP* pRTMP = RTMP_Alloc();
RTMP_Init(pRTMP);
// 设置拉流地址
RTMP_SetupURL(pRTMP, (char*)"rtmp://127.0.0.1:1935/live/a");
// 连接服务器
pRTMP->Link.timeout = 10;
pRTMP->Link.lFlags |= RTMP_LF_LIVE;
bool b = RTMP_Connect(pRTMP, NULL);
if (!b)
{
printf("connect failed! \n");
return -1;
}
// 连接流地址
b = RTMP_ConnectStream(pRTMP, 0);
if (!b)
{
printf("connect stream failed! \n");
return -1;
}
bool bSaveMP3 = true;
FILE* pFile = fopen(bSaveMP3 ? "testrtmp.mp3" : "testrtmp.flv", "wb");
while (RTMP_IsConnected(pRTMP))
{
if (bSaveMP3)
{
RTMPPacket packet;
RTMPPacket_Reset(&packet);
packet.m_body = NULL;
packet.m_chunk = NULL;
b = RTMP_ReadPacket(pRTMP, &packet);
if (!b)
break;
if (!RTMPPacket_IsReady(&packet))
continue;
printf("\t headerType:[%d] \n", packet.m_headerType);
printf("\t packetType:[%d] \n", packet.m_packetType);
printf("\t hasAbsTimestamp:[%d] \n", packet.m_hasAbsTimestamp);
printf("\t nChannel:[%d] \n", packet.m_nChannel);
printf("\t nTimeStamp:[%d] \n", packet.m_nTimeStamp);
printf("\t nInfoField2:[%d] \n", packet.m_nInfoField2);
printf("\t nBodySize:[%d] \n", packet.m_nBodySize);
printf("\t nBytesRead:[%d] \n", packet.m_nBytesRead);
//fwrite(packet.m_body, 1, packet.m_nBodySize, pFile);
//fwrite("AAAAAAAAAAAAAAAA", 1, 16, pFile);
if (packet.m_packetType == 0x08)
{
fwrite(packet.m_body + 1, 1, packet.m_nBodySize - 1, pFile);
}
RTMPPacket_Free(&packet);
}
else
{
char sBuf[4096] = {0};
int bytes = RTMP_Read(pRTMP, sBuf, sizeof(sBuf));
printf("RTMP_Read() ret:[%d] \n", bytes);
if (bytes <= 0)
break;
fwrite(sBuf, 1, bytes, pFile);
}
}
fclose(pFile);
RTMP_Close(pRTMP);
RTMP_Free(pRTMP);
return 0;
}
运行输出:
g++ -o testrtmp testrtmp.cpp -lrtmp
./testrtmp
RTMP_Read() ret:[640]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[4096]
RTMP_Read() ret:[4096]
RTMP_Read() ret:[1304]
RTMP_Read() ret:[225]
RTMP_Read() ret:[952]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[1705]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[1142]
RTMP_Read() ret:[225]
RTMP_Read() ret:[225]
RTMP_Read() ret:[1957]
RTMP_Read() ret:[225]