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传输数据粘包问题解决方案(附源码)的主要内容,如果未能解决你的问题,请参考以下文章