EasyRTMPClient關於RTMP協議TCP傳輸數據粘包問題解決方案(附源碼)

不久之前我們對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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章