不久之前我們對EasyRTMPClient庫擴展支持了HEVC(H.265),在後續的長期性能測試中,我們發現拉多路流時,會出現拉流播放一直都播不出來的問題,甚至有一定概率出現崩潰,經過長期的測試和排查,我們發現這是由於音視頻數據發送比較頻繁的時候出現的tcp粘包的問題,下面將詳細講述粘包問題的解決過程。
1. EasyRTMPClient流接收流程
EasyRTMPClient底層採用rtmp協議官方提供的librtmp庫來實現rtmp流協議流程的連接建立,讀取流數據,接收FLV數據組包分包等,然後將接收到的FLV數據包進行解析,從中解析出H264、H265、AAC等音視頻編碼幀數據,回調給上層庫調用接口做解碼播放以及進一步數據處理;流接收函數如下代碼所示:
int RecvPacket(char * buf,uint32_t & buflen)
{
if(rtmp_object_ == NULL && Reconnect())
{
return RTMP_UNCONNECTED;
}
if(!RTMP_IsConnected(rtmp_object_) && (Reconnect() !=0))
{
return RTMP_RECONNECTED_FAILED;
}
if(NULL == buf|| buflen < 100)
{
return RTMP_RECV_PARAMERROR;
}
if(!rtmp_object_)
{
return RTMP_UNCONNECTED;
}
int ret = RTMP_Read(rtmp_object_,buf,buflen);
if(ret > 0)
{
EasyRTMPClient_AV_Frame av_frame = {0};
if(ParserRecvPacket(av_frame,buf,ret,process_buf_) != 0)
{
return RTMP_PARSE_FAILED;
}
if(av_frame.u32FrameType == EASY_SDK_VIDEO_FRAME_FLAG && av_frame.u32VFrameType == EASY_SDK_VIDEO_FRAME_I && !bGetFirstKeyFrame_)
{
if(audio_channels_ == -1)
{
audio_channels_ = 0;
}
else
{
bGetFirstKeyFrame_ = true;
EASY_MEDIA_INFO_T mediaInfo;
memset(&mediaInfo, 0, sizeof(EASY_MEDIA_INFO_T));
mediaInfo.u32VideoCodec = av_frame.u32AVFrameFlag;
mediaInfo.u32VideoFps = 25;
mediaInfo.u32AudioChannel = audio_channels_;
mediaInfo.u32AudioBitsPerSample = audio_bit_len_;
mediaInfo.u32AudioCodec = EASY_SDK_AUDIO_CODEC_AAC;
if(audio_sample_rate_ < 13)
mediaInfo.u32AudioSamplerate = samplingFrequencyTable[audio_sample_rate_];
if(av_frame.u32AVFrameFlag == EASY_SDK_VIDEO_CODEC_H265)
{
mediaInfo.u32VpsLength = vps_len_;
memcpy(mediaInfo.u8Vps, vps_buf_, vps_len_);
}
mediaInfo.u32PpsLength = pps_len_;
mediaInfo.u32SpsLength = sps_len_;
memcpy(mediaInfo.u8Sps, sps_buf_, sps_len_);
memcpy(mediaInfo.u8Pps, pps_buf_, pps_len_);
if (easy_rtmp_call_back_)
{
easy_rtmp_call_back_(channelId_, channelPtr_, EASY_SDK_MEDIA_INFO_FLAG, (char*)&mediaInfo, NULL);
}
}
}
else if(av_frame.u32FrameType == EASY_SDK_AUDIO_FRAME_FLAG && !bGetFirstKeyFrame_ && !bHaveSendAudioMediaInfo_)
{
bHaveSendAudioMediaInfo_ = true;
EASY_MEDIA_INFO_T mediaInfo;
memset(&mediaInfo, 0, sizeof(EASY_MEDIA_INFO_T));
mediaInfo.u32AudioChannel = audio_channels_;
mediaInfo.u32AudioBitsPerSample = audio_bit_len_;
mediaInfo.u32AudioCodec = EASY_SDK_AUDIO_CODEC_AAC;
if(audio_sample_rate_ < 13)
mediaInfo.u32AudioSamplerate = samplingFrequencyTable[audio_sample_rate_];
if (easy_rtmp_call_back_)
{
easy_rtmp_call_back_(channelId_, channelPtr_, EASY_SDK_MEDIA_INFO_FLAG, (char*)&mediaInfo, NULL);
}
return 0;
}
if(av_frame.u32FrameType == EASY_SDK_VIDEO_FRAME_FLAG && av_frame.u32VFrameType != EASY_SDK_VIDEO_FRAME_I && !bGetFirstKeyFrame_)
{
//wait key frame
}
else if(av_frame.u32FrameType == EASY_SDK_VIDEO_FRAME_FLAG || av_frame.u32FrameType == EASY_SDK_AUDIO_FRAME_FLAG)
{
if(av_frame.u32VFrameType == 1)
{
av_frame.u32VFrameType = 1;
}
EASY_FRAME_INFO frameinfo;
memset(&frameinfo, 0, sizeof(EASY_FRAME_INFO));
frameinfo.timestamp_sec = av_frame.u32TimestampMsec/1000;
frameinfo.timestamp_usec = (av_frame.u32TimestampMsec%1000)*1000;
frameinfo.length = av_frame.u32AVFrameLen;
frameinfo.type = av_frame.u32VFrameType;
frameinfo.codec = av_frame.u32AVFrameFlag;
frameinfo.width = width_;
frameinfo.height = height_;
frameinfo.channels = audio_channels_;
frameinfo.bits_per_sample = audio_bit_len_;
if(audio_sample_rate_ < 13)
frameinfo.sample_rate = samplingFrequencyTable[audio_sample_rate_];
if (easy_rtmp_call_back_)
{
easy_rtmp_call_back_(channelId_, channelPtr_, av_frame.u32FrameType, process_buf_, &frameinfo);
}
}
}
else if(ret <= 0)
{
DeleteRtmpObj();
}
return ret;
}
2. 接收FLV數據封包解析
從第1節代碼段中我們可以看出,通過RTMP_Read函數讀取到FLV數據包以後,我們通過函數ParserRecvPacket對接收到的數據進行解析,解析函數如下:
int ParserRecvPacket(EasyRTMPClient_AV_Frame& av_frame,char *buf,int len,char* processbuf)
{
if(strncmp(buf,FLV_HEAD,3) == 0)//metadata head
{
return ParserRtmpFirstTag(av_frame,buf,len,processbuf);
}
else if(buf[0] == e_FlvTagType_Audio)
{
av_frame.u32AVFrameFlag = EASY_SDK_AUDIO_CODEC_AAC;// e_MediaType_Aac;
av_frame.u32FrameType = EASY_SDK_AUDIO_FRAME_FLAG;
return ParserAudioPacket(av_frame,buf,len,processbuf);
}
else if(buf[0] == e_FlvTagType_Video)
{
av_frame.u32FrameType = EASY_SDK_VIDEO_FRAME_FLAG;
return ParserVideoPacket(av_frame,buf,len,processbuf);
}
return 0;
}
首先,通過頭三個字節是否是"FLV"來判斷是否是FLV數據包頭,如果是數據包頭,則從封包內解析出音視頻編解碼相關的參數和頭數據,具體解析過程詳解大家可以參考我的另一篇文章EasyRTMPClient擴展支持HEVC(H.265)解決方案之HEVCDecoderConfigurationRecord結構詳解;
3. 數據粘包處理
在第二節中,我們知道開始拉流後,服務器發送FLV封包頭過來,要進行音視頻編碼參數以及頭數據相關解析,而由於數據包比較小,這個時候TCP發送數據的時候就容易發生FLVMetadata頭包和音視頻數據包一起發過來的情況,從而我們需要根據各個數據包的tag類型進行判斷,解析,粘包處理代碼如下所示:
int ParserRtmpFirstTag(EasyRTMPClient_AV_Frame &av_frame,char *buf,int len,char *processbuf)
{
if(buf == NULL || len == 0 || processbuf == NULL)
{
return -1005;
}
int parser_offset = 0;
int metadatalen = 0;
parser_FLVHead *flvhead = (parser_FLVHead*)buf;
parser_offset += sizeof(parser_FLVHead);
parser_offset += 4;
ASSERT_PARSER(parser_offset,len);
unsigned char tag_type = buf[parser_offset];
if(tag_type == e_FlvTagType_Meta)
{
metadatalen = ParserMetaData(buf + parser_offset,len - parser_offset);
parser_offset += metadatalen;
parser_offset += 4;
ASSERT_PARSER(parser_offset,len);
}
while(parser_offset < len - 4)
{
tag_type = buf[parser_offset];
int parse_ret = 0;
int tag_len = (buf[parser_offset + 1]&0xff << 16) | (buf[parser_offset + 2]&0xff << 8 | (buf[parser_offset + 3]&0xff)) + 11;
if(tag_type == e_FlvTagType_Audio)
{
if((parse_ret = ParserAudioPacket(av_frame,buf + parser_offset, /*len - parser_offset*/tag_len,processbuf)) != 0)
{
return __LINE__;
}
}
else if(tag_type == e_FlvTagType_Video)
{
if((parse_ret = ParserVideoPacket(av_frame,buf + parser_offset,/*len - parser_offset*/tag_len,processbuf)) != 0)
{
return __LINE__;
}
}
parser_offset += tag_len;
parser_offset += 4;
if(parser_offset >= len - 4)
{
return 0;
}
}
return -1006;
}
首先,解析Meta tag數據包,然後根據tag類型分別處理視頻包或者音頻數據包。
歡迎大家下載EasyPlayer測試播放支持H265的RTMP流:
https://github.com/EasyDSS/EasyPlayer-RTMP-Win
https://github.com/EasyDSS/EasyPlayerPro-Win
有任何技術問題,歡迎大家和我進行技術交流:
[email protected]
大家也可以加入EasyPlayer流媒體播放器 QQ羣進行討論:
544917793