调试libRTMP代码来分析RTMP协议
2017-07-25 22:13 jiayayao 阅读(3921) 评论(0) 编辑 收藏 举报RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP,是一个协议族,常用在视频直播领域。RTMP协议的默认端口是1935。
学习一个协议最好的方法就是调试其通信过程,期间还可以使用wireshark抓包分析。本人在libRTMP的基础上添加了推流部分,并且使得整个过程变得可调试,学习其协议就变得简单多了。配置好的VS2010可调试的libRTMP工程:https://github.com/jiayayao/librtmp。该工程可以使用VS调试RTMP协议内部的代码,并且对RTMP协议部分做了详细的注释。推流部分参考leixiaohua的blog,RTMP Server可以采用Nginx-RTMP module的方式,搭建RTMP Server过程可以参考:使用nginx+nginx-rtmp-module+ffmpeg搭建流媒体服务器笔记(一)。
testRTMP工程是推流客户端,推送一个FLV文件需要经过以下几个步骤:握手,建立连接,建立流,推流。RTMP连接都是以握手作为开始的。建立连接阶段用于建立客户端与服务器之间的“网络连接”;建立流阶段用于建立客户端与服务器之间的“网络流”;推流即按照FLV格式将数据传送至RTMP Server。
一、握手
RTMP握手过程如下:
1. 客户端向服务器发送C0、C1块,服务器收到后发送S0和S1块;
2. 客户端收到S0和S1后,向服务器发送C2块;服务器收到C2块后发送S2块;
3. 客户端和服务器分别收到S2和C2后,握手建立完成。
与HandShake相对应,还有SHandShake函数是服务器部分的握手部分,有兴趣的可以看一下。
二、建立网络连接
三、建立网络流
RTMP_ConnectStream()时接收到的packet的type依次是:
0x05: Set server bindwidth(BW = 5000000) 0x06: Set client bindwidth(BW = 5000000) 0x01: Set in chunk size(4096) 0x20:Invoke <_result> (object begin) (object begin) Property: <Name: fmsVer, STRING: FMS/3,0,1,123> Property: <Name: capabilities, NUMBER: 31.00> (object end) (object begin) Property: <Name: level, STRING: status> Property: <Name: code, STRING: NetConnection.Connect.Success> Property: <Name: description, STRING: Connection succeeded.> Property: <Name: objectEncoding, NUMBER: 0.00> (object end) (object end) 0x20:Invoke <_result> (object begin) Property: NULL (object end) 0x20:Invoke <onStatus> (object begin) Property: NULL (object begin) Property: <Name: level, STRING: status> Property: <Name: code, STRING: NetStream.Publish.Start> Property: <Name: description, STRING: Start publishing> (object end) (object end)
服务器接收到“connect”消息后,会返回_result给客户端,客户端接收到是connect的response后,会发送“createStream”命令到服务器。
服务器接收到“createStream”消息后,会返回_result给客户端,客户端接收到是“createStream”命令返回的response后,会发送“publish”命令到服务器。网络流建立完成,开始传送数据。
四、推流
推流部分的关键代码如下:
int publish_using_packet(){ RTMP *rtmp=NULL; RTMPPacket *packet=NULL; uint32_t start_time=0; uint32_t now_time=0; //the timestamp of the previous frame long pre_frame_time=0; long lasttime=0; int bNextIsKey=1; uint32_t preTagsize=0; //packet attributes uint32_t type=0; uint32_t datalength=0; uint32_t timestamp=0; uint32_t streamid=0; FILE*fp=NULL; fp=fopen("cuc_ieschool.flv","rb"); if (!fp){ RTMP_LogPrintf("Open File Error.\n"); CleanupSockets(); return -1; } if (!InitSockets()){ RTMP_LogPrintf("Init Socket Err\n"); return -1; } // 创建一个RTMP会话的句柄 rtmp=RTMP_Alloc(); // 初始化RTMP句柄 RTMP_Init(rtmp); //set connection timeout,default 30s rtmp->Link.timeout=5; // 设置URL if(!RTMP_SetupURL(rtmp,"rtmp://192.168.37.130:1935/myapp/test1")) { RTMP_Log(RTMP_LOGERROR,"SetupURL Err\n"); RTMP_Free(rtmp); CleanupSockets(); return -1; } //if unable,the AMF command would be 'play' instead of 'publish' RTMP_EnableWrite(rtmp); // RTMP_Connect分为2步:RTMP_Connect0和RTMP_Connect1 // 0负责建立TCP底层连接 // 1负责RTMP握手操作 if (!RTMP_Connect(rtmp,NULL)){ RTMP_Log(RTMP_LOGERROR,"Connect Err\n"); RTMP_Free(rtmp); CleanupSockets(); return -1; } if (!RTMP_ConnectStream(rtmp,0)){ RTMP_Log(RTMP_LOGERROR,"ConnectStream Err\n"); RTMP_Close(rtmp); RTMP_Free(rtmp); CleanupSockets(); return -1; } packet=(RTMPPacket*)malloc(sizeof(RTMPPacket)); RTMPPacket_Alloc(packet,1024*64); RTMPPacket_Reset(packet); packet->m_hasAbsTimestamp = 0; packet->m_nChannel = 0x04; packet->m_nInfoField2 = rtmp->m_stream_id; RTMP_LogPrintf("Start to send data ...\n"); //jump over FLV Header // FLV格式的header为9个字节 fseek(fp,9,SEEK_SET); //jump over previousTagSizen // 跳过表征前一段Tag大小的4个字节 fseek(fp,4,SEEK_CUR); start_time=RTMP_GetTime(); while(1) { if((((now_time=RTMP_GetTime())-start_time) <(pre_frame_time)) && bNextIsKey){ //wait for 1 sec if the send process is too fast //this mechanism is not very good,need some improvement if(pre_frame_time>lasttime){ RTMP_LogPrintf("TimeStamp:%8lu ms\n",pre_frame_time); lasttime=pre_frame_time; } Sleep(1000); continue; } //not quite the same as FLV spec // 读取当前Tag的类型(1个字节) if(!ReadU8(&type,fp)) break; // 读取当前Tag data部分的大小(3个字节) if(!ReadU24(&datalength,fp)) break; // 读取时间戳(4个字节) if(!ReadTime(×tamp,fp)) break; // 读取stream id(3个字节),一般为0 if(!ReadU24(&streamid,fp)) break; // 跳过既非视频也非音频的Tag if (type!=0x08&&type!=0x09){ //jump over non_audio and non_video frame, //jump over next previousTagSizen at the same time fseek(fp,datalength+4,SEEK_CUR); continue; } // 读取当前音视频Tag的数据到packet if(fread(packet->m_body,1,datalength,fp)!=datalength) break; packet->m_headerType = RTMP_PACKET_SIZE_LARGE; packet->m_nTimeStamp = timestamp; packet->m_packetType = type; packet->m_nBodySize = datalength; pre_frame_time=timestamp; if (!RTMP_IsConnected(rtmp)){ RTMP_Log(RTMP_LOGERROR,"rtmp is not connect\n"); break; } // 这样看下来是一个FLV的Tag发送一个RTMPPacket if (!RTMP_SendPacket(rtmp,packet,0)){ RTMP_Log(RTMP_LOGERROR,"Send Error\n"); break; } // 读取前一个Tag的size if(!ReadU32(&preTagsize,fp)) break; // 读取当前Tag的type if(!PeekU8(&type,fp)) break; if(type==0x09){ if(fseek(fp,11,SEEK_CUR)!=0) break; if(!PeekU8(&type,fp)){ break; } if(type==0x17) bNextIsKey=1; else bNextIsKey=0; fseek(fp,-11,SEEK_CUR); } } RTMP_LogPrintf("\nSend Data Over\n"); if(fp) fclose(fp); if (rtmp!=NULL){ RTMP_Close(rtmp); RTMP_Free(rtmp); rtmp=NULL; } if (packet!=NULL){ RTMPPacket_Free(packet); free(packet); packet=NULL; } CleanupSockets(); return 0; }
在推送过程中,打开VLC播放器,输入网络流地址为:"rtmp://192.168.37.130:1935/myapp/test1",即可看到推流客户端推送的视频。
参考资料:
1. RTMP流媒体播放过程
2. RTMP规范简单分析
本文来自博客园,作者:jiayayao,邮箱:jiayayao@126.com,转载请注明原文链接:https://www.cnblogs.com/jiayayao/p/6863345.html