SkeyeRTMPClient关于RTMP协议TCP传输数据粘包问题解决方案(附源码)

Posted OpenSKEYE

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SkeyeRTMPClient关于RTMP协议TCP传输数据粘包问题解决方案(附源码)相关的知识,希望对你有一定的参考价值。

不久之前我们对SkeyeRTMPClient库扩展支持了HEVC(H.265),在后续的长期性能测试中,我们发现拉多路流时,会出现拉流播放一直都播不出来的问题,甚至有一定概率出现崩溃,经过长期的测试和排查,我们发现这是由于音视频数据发送比较频繁的时候出现的tcp粘包的问题,下面将详细讲述粘包问题的解决过程。

1. SkeyeRTMPClient流接收流程

SkeyeRTMPClient底层采用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)
    
        SkeyeRTMPClient_AV_Frame av_frame = 0;
        
        if(ParserRecvPacket(av_frame,buf,ret,process_buf_) != 0)
        
            return RTMP_PARSE_FAILED;
        

		if(av_frame.u32FrameType == SKEYE_SDK_VIDEO_FRAME_FLAG && av_frame.u32VFrameType == SKEYE_SDK_VIDEO_FRAME_I && !bGetFirstKeyFrame_)
		
            if(audio_channels_ == -1)
            
                audio_channels_ = 0;
            
            else
            
			    bGetFirstKeyFrame_ = true;
			    SKEYE_MEDIA_INFO_T mediaInfo;
			    memset(&mediaInfo, 0, sizeof(SKEYE_MEDIA_INFO_T));
			    mediaInfo.u32VideoCodec = av_frame.u32AVFrameFlag;
			    mediaInfo.u32VideoFps = 25;
			    mediaInfo.u32AudioChannel = audio_channels_;
			    mediaInfo.u32AudioBitsPerSample = audio_bit_len_;
			    mediaInfo.u32AudioCodec = SKEYE_SDK_AUDIO_CODEC_AAC;
			    if(audio_sample_rate_ < 13)
				    mediaInfo.u32Audiosamplerate = samplingFrequencyTable[audio_sample_rate_];
				if(av_frame.u32AVFrameFlag == SKEYE_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 (skeye_rtmp_call_back_)
				
					skeye_rtmp_call_back_(channelId_, channelPtr_, SKEYE_SDK_MEDIA_INFO_FLAG, (char*)&mediaInfo, NULL);
				
				
            
		
        else if(av_frame.u32FrameType == SKEYE_SDK_AUDIO_FRAME_FLAG && !bGetFirstKeyFrame_ && !bHaveSendAudioMediaInfo_)
		
			bHaveSendAudioMediaInfo_ = true;
			SKEYE_MEDIA_INFO_T mediaInfo;
			memset(&mediaInfo, 0, sizeof(SKEYE_MEDIA_INFO_T));
			mediaInfo.u32AudioChannel = audio_channels_;
			mediaInfo.u32AudioBitsPerSample = audio_bit_len_;
			mediaInfo.u32AudioCodec = SKEYE_SDK_AUDIO_CODEC_AAC;
			if(audio_sample_rate_ < 13)
				mediaInfo.u32AudioSamplerate = samplingFrequencyTable[audio_sample_rate_];
			if (skeye_rtmp_call_back_)
			
				skeye_rtmp_call_back_(channelId_, channelPtr_, SKEYE_SDK_MEDIA_INFO_FLAG, (char*)&mediaInfo, NULL);
			
			
            return 0;
		


		if(av_frame.u32FrameType == SKEYE_SDK_VIDEO_FRAME_FLAG && av_frame.u32VFrameType != SKEYE_SDK_VIDEO_FRAME_I && !bGetFirstKeyFrame_)
		
			//wait key frame
		
		else if(av_frame.u32FrameType ==SKEYE_SDK_VIDEO_FRAME_FLAG || av_frame.u32FrameType == SKEYE_SDK_AUDIO_FRAME_FLAG)
		
			if(av_frame.u32VFrameType == 1)
			
				av_frame.u32VFrameType = 1;
			
			SKEYE_FRAME_INFO frameinfo;
			memset(&frameinfo, 0, sizeof(SKEYE_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 (skeye_rtmp_call_back_)
			
				skeye_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(SkeyeRTMPClient_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 = SKEYE_SDK_AUDIO_CODEC_AAC;// e_MediaType_Aac;
		av_frame.u32FrameType = SKEYE_SDK_AUDIO_FRAME_FLAG;
        return ParserAudioPacket(av_frame,buf,len,processbuf);
    
    else if(buf[0] == e_FlvTagType_Video)
    
		av_frame.u32FrameType = SKEYE_SDK_VIDEO_FRAME_FLAG;
        return ParserVideoPacket(av_frame,buf,len,processbuf);
    
    return 0;

首先,通过头三个字节是否是"FLV"来判断是否是FLV数据包头,如果是数据包头,则从封包内解析出音视频编解码相关的参数和头数据,具体解析过程详解大家可以参考我的另一篇文章SkeyeRTMPClient扩展支持HEVC(H.265)解决方案之HEVCDecoderConfigurationRecord结构详解

3. 数据粘包处理

在第二节中,我们知道开始拉流后,服务器发送FLV封包头过来,要进行音视频编码参数以及头数据相关解析,而由于数据包比较小,这个时候TCP发送数据的时候就容易发生FLVMetadata头包和音视频数据包一起发过来的情况,从而我们需要根据各个数据包的tag类型进行判断,解析,粘包处理代码如下所示:

int ParserRtmpFirstTag(SkeyeRTMPClient_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类型分别处理视频包或者音频数据包。

欢迎大家下载SkeyePlayer测试播放支持H265的RTMP流:
https://gitee.com/visual-opening/skeyplayer

有任何技术问题,欢迎大家和我进行技术交流:
295222688@qq.com

大家也可以加入SkeyePlayer流媒体播放器 QQ群进行讨论:
102644504

RTMP协议

RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议族,包括RTMP基本协议及RTMPT/RTMPS/RTMPE等多种变种。RTMP是一种设计用来进行实时数据通信的网络协议,主要用来在Flash/AIR平台和支持RTMP协议的流媒体/交互服务器之间进行音视频和数据通信。支持该协议的软件包括Adobe Media Server/Ultrant Media Server/red5等。RTMP与HTTP一样,都属于TCP/IP四层模型的应用层。(百度百科)

协议握手的过程

Client 发送 C0 和 C1 消息来启动握手过程。客户端必须接收到 S1 消息,然后发送 C2 消息。客户端必须接收到S2 消息,然后发送其他数据。服务端必须接收到 C0 或者 C1 消息,然后发送 S0 和 S1 消息。服务端必须接收到 C2 消息,然后发送其他数据。

	+-------------+                           +-------------+
    |    Client   |       TCP/IP Network      |    Server   |
    +-------------+            |              +-------------+
          |                    |                     |
    Uninitialized              |               Uninitialized
          |          C0        |                     |
          |------------------->|         C0          |
          |                    |-------------------->|
          |          C1        |                     |
          |------------------->|         S0          |
          |                    |<--------------------|
          |                    |         S1          |
     Version sent              |<--------------------|
          |          S0        |                     |
          |<-------------------|                     |
          |          S1        |                     |
          |<-------------------|                Version sent
          |                    |         C1          |
          |                    |-------------------->|
          |          C2        |                     |
          |------------------->|         S2          |
          |                    |<--------------------|
       Ack sent                |                  Ack Sent
          |          S2        |                     |
          |<-------------------|                     |
          |                    |         C2          |
          |                    |-------------------->|
     Handshake Done            |               Handshake Done
          |                    |                     |
              Pictorial Representation of Handshake


抓包分析一下,RTMP是在TCP协议基础之上的,首先要进行TCP握手,在实际中先会发送C0+C1,然后服务端响应S0+S1+S2,然后客户端再发送C2。
通过wireshark了解到,RTMP应用层协议1537字节,版本号一个字节,数据占用1536字节。

RTMP连接的建立

论文中是这样描述的:

实际上通过协议分析,有出入。
省略了客户端向服务端发送Peer bandwidth

协议中RTMP流的创建

论文中定义创建如下,但是在Abobe实现中,通过抓包工具看出,并非如此。


推送RTMP流

进行四步操作:
1.publish 告诉服务端我要进行推流了。
2.onStatus 服务端说你开始吧。
3.客户端发送MetaData:告诉服务端我推送的流媒体的信息。

4.发送Video/Audio Data

播放RTMP流

以上是关于SkeyeRTMPClient关于RTMP协议TCP传输数据粘包问题解决方案(附源码)的主要内容,如果未能解决你的问题,请参考以下文章

RTMP代理的协议规范(RtmpProxy)

RTMP代理的协议规范(RtmpProxy)

实现手机直播推送屏幕推送及录像功能RTMP推流组件之EasyRTMP-Android设置授权Key介绍

rtmp协议详解

RTMP协议

Wowza 的 RTMP 身份验证