(这篇博文的完整代码在我的另一篇博文《使用Librtmp接收H264 + AAC》)
librtmp是一个RTMP的开源库,很多地方用它来做推流、拉流。它是RTMPDump开源软件里的一部分,librtmp的下载地址:http://rtmpdump.mplayerhq.hu/,目前最新版是V2.3。librtmp如何使用在很多博客已经有介绍,雷神的博客也有几个相关的例子介绍其使用,这里就不多说,这里只讲一下我用librtmp遇到过的几个问题和给出其解决办法。
1. 我发现librtmp V2.3代码里有个Bug,不知道之前的版本有没有,反正提出来,对大家做个提醒。
原来的librtmp\amf.c文件里有个函数(大概是1124行):
AMFObjectProperty *
AMF_GetProp(AMFObject *obj, const AVal *name, int nIndex)
{
if (nIndex >= 0)
{
if (nIndex <= obj->o_num) //这里有问题
return &obj->o_props[nIndex];
}
else
{
int n;
for (n = 0; n < obj->o_num; n++)
{
if (AVMATCH(&obj->o_props[n].p_name, name))
return &obj->o_props[n];
}
}
return (AMFObjectProperty *)&AMFProp_Invalid;
}
大家注意到没有,nIndex变量不能等于obj->o_num。如果等于就发生越界访问了。我测试时发现这个Bug,发生时机是在播放完一个flv节目之后,接收函数里就出错了。正确的代码应该这样:
AMFObjectProperty *
AMF_GetProp(AMFObject *obj, const AVal *name, int nIndex)
{
if (nIndex >= 0)
{
if (nIndex < obj->o_num) // 解决访问数组越界的问题
return &obj->o_props[nIndex];
}
else
{
int n;
for (n = 0; n < obj->o_num; n++)
{
if (AVMATCH(&obj->o_props[n].p_name, name))
return &obj->o_props[n];
}
}
return (AMFObjectProperty *)&AMFProp_Invalid;
}
2. 怎么创建和初始化对象?写上这个有点罗嗦,但为了能使后面解说代码上下文更连贯,我就把这个初始化的代码也贴出来。这里要注意的一个地方是:设置超时,默认的超时时间太大了(30秒)。
m_rtmp = RTMP_Alloc();
RTMP_Init(m_rtmp);
//set connection timeout,default 30s
m_rtmp->Link.timeout = 10;
if (!RTMP_SetupURL(m_rtmp, (char*)m_InputUrl.c_str()))
{
TRACE("RTMP_SetupURL Err\n");
RTMP_Free(m_rtmp);
return FALSE;
}
if (bLiveStream)
{
m_rtmp->Link.lFlags |= RTMP_LF_LIVE;
}
RTMP_SetBufferMS(m_rtmp, 5 * 1000);
3. 接收数据的循环应该怎么写?我看到网上的一些例子对接收处理不是太完善,下面给出我的一个实现:
int readRTMPDataLoop()
{
int nVideoFramesNum = 0;
int64_t first_pts_time = 0;
long countbufsize = 0;
int nRead = 0;
int total_bytes = 0, n = 0;
int64_t pts_time;
FLVHead flvHead;
FLVTag flvTag;
memset(&flvHead, 0, sizeof(flvHead));
if (!RTMP_Connect(m_rtmp, NULL))
{
TRACE("RTMP_Connect Err\n");
RTMP_Free(m_rtmp);
m_rtmp = NULL;
return -1;
}
if (!RTMP_ConnectStream(m_rtmp, 0))
{
TRACE("ConnectStream Err\n");
RTMP_Free(m_rtmp);
m_rtmp = NULL;
return -1;
}
nRead = RTMP_Read(m_rtmp, (char*)&flvHead, sizeof(FLVHead));
if (nRead <= 0)
{
TRACE("RTMP_Read failed \n");
RTMP_Close(m_rtmp);
RTMP_Free(m_rtmp);
m_rtmp = NULL;
return -2;
}
flvHead.offset = HTON32(flvHead.offset);
TRACE("文件类型: %c %c %c\n", flvHead.type[0], flvHead.type[1], flvHead.type[2]);
TRACE("版本: %d\n", flvHead.version);
TRACE("流信息: %d\n", flvHead.stream_info);
TRACE("head长度: %d\n", flvHead.offset);
AVCodecID audio_codec_id = AV_CODEC_ID_AAC;
memset(&flvTag, 0, sizeof(FLVTag));
int bufsize = 1024 * 1024 * 2;
char *data = (char*)malloc(bufsize);
memset(data, 0, bufsize);
while(1)
{
if(m_stop_status == true)
{
break;
}
//音视频
if ((nRead = RTMP_Read(m_rtmp, (char*)&flvTag, sizeof(FLVTag))) == sizeof(FLVTag))
{
flvTag.tag_size = HTON32(flvTag.tag_size);
int length = 0;
memcpy(&length, flvTag.length, 3);
length = HTON24(length);
unsigned int time = 0;
memcpy(&time, flvTag.timecamp, 3);
time = HTONTIME(time);
//TRACE("\nTag大小: %d\n",flvTag.tag_size);
//TRACE("Tag类型: %d, 长度:%d, 时间戳: %d\n",flvTag.type, length, time);
pts_time = time; //libRTMP的时间戳以毫秒为单位
//TRACE("RTMP Packet Type: %d, TimeStamp: %u \n", flvTag.type, time);
nRead = RTMP_Read(m_rtmp, data, length);
if (nRead > 0)
{
if (flvTag.type == 8) //音频
{
}
else if (flvTag.type == 9) //视频
{
m_nVideoFramesNum++;
}
}
}
if (nRead < 0)
{
TRACE("RTMP_Read return: %d \n", nRead);
break;
}
else if (nRead == 0)
{
TRACE("RTMP_Read return: %d \n", nRead);
Sleep(10);
continue;
}
n++;
}//while
free(data);
if (m_rtmp != NULL)
{
RTMP_Close(m_rtmp);
RTMP_Free(m_rtmp);
m_rtmp = NULL;
}
return 0;
}
4. 如果RTMP流的组成是:H264 + AAC,则接收到的H264和AAC流不能直接保存或播放,还需要经过处理。收到的H264是没有前面四个字节开始码的,需要自己插入开始码(0x01000000);AAC也需要经过处理,转换成ADTS-AAC。
5. 有一个问题:某些RTMP服务器(ngnix-rtmp)在转发H264视频的时候会去掉除第一个I帧前的SPS+PPS段,也就是说当客户端接收刚开始会收到SPS+PPS(一般是第一帧),但后面的I帧前面都去掉了SPS和PPS的。RTMP服务器这样做是为了减少数据发送量和节省带宽,这个问题对你的应用可能有影响也可能没影响。如果你只是播放视频或录制视频,只要第一个I帧包含SPS+PPS,你的视频是可以播放的。但是,如果你需要将收到的流再转发(国内很多人喜欢搞拉流再推流的服务),那就要考虑处理措施。假如你是做转发的(相当于一个服务器),当收到RTMP服务器发来的第一个的I帧(携带SPS+PPS)之后就开始转发,这时候记为第1秒,当第3秒的时候才有一个客户端连上来,那么这个客户端就收不到SPS和SPS了,也就不能正常解码和播放。解决办法是自己强行插入SPS+SPS,代码如下:
nRead = RTMP_Read(m_rtmp, data, length);
if (nRead > 0)
{
if (flvTag.type == 8) //音频
{
if (audio_codec_id == AV_CODEC_ID_AAC)
{
int nOutAACLen = 0;
AAC_TO_ADTS((unsigned char*)data, nRead, 44100, m_aacADTSBuf, AAC_MAX_FRAME_SIZE, &nOutAACLen);
}
}
else if (flvTag.type == 9) //视频
{
m_nVideoFramesNum++;
if (!(data[0] == 0x0 && data[1] == 0x0 && data[2] == 0x0 && data[3] == 0x01))
{
//TRACE("Not H264 StartCode!\n");
int outLen = 0;
UnPackH264(data, nRead, m_h264data, outLen);
if (outLen < 4)
{
TRACE("Video frame was too short! \n");
continue;
}
int nalu_type = (m_h264data[4] & 0x1F);
TRACE("FrameNo: %d, nalu_type: %d, size: %d \n", m_nVideoFramesNum, nalu_type, outLen);
if (!m_bSpsPpsGot && nalu_type == 7)
{
ASSERT(outLen < sizeof(m_SpsPpsBuffer));
memcpy(m_SpsPpsBuffer, m_h264data, outLen);
m_nSpsPpsSize = outLen;
m_bSpsPpsGot = true;
}
if (nalu_type == 5) //I frame
{
if (m_bSpsPpsGot) //在I帧前插入SPS和PPS
{
memmove(m_h264data + m_nSpsPpsSize, m_h264data, outLen);
memcpy(m_h264data, m_SpsPpsBuffer, m_nSpsPpsSize);
outLen += m_nSpsPpsSize;
}
}
if (outLen > 0)
{
}
}
else
{
}
}
}