• 调试libRTMP代码来分析RTMP协议


        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 = 50000000x06: Set client bindwidth(BW = 50000000x01: Set in chunk size(40960x20: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.
    ");
            CleanupSockets();
            return -1;
        }
    
        if (!InitSockets()){
            RTMP_LogPrintf("Init Socket Err
    ");
            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
    ");
            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
    ");
            RTMP_Free(rtmp);
            CleanupSockets();
            return -1;
        }
    
        if (!RTMP_ConnectStream(rtmp,0)){
            RTMP_Log(RTMP_LOGERROR,"ConnectStream Err
    ");
            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 ...
    ");
    
        //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
    ",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(&timestamp,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
    ");
                break;
            }
            // 这样看下来是一个FLV的Tag发送一个RTMPPacket
            if (!RTMP_SendPacket(rtmp,packet,0)){
                RTMP_Log(RTMP_LOGERROR,"Send Error
    ");
                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("
    Send Data Over
    ");
    
        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规范简单分析

  • 相关阅读:
    关于JsonObject的笔记
    addHeader() 与 setHeader() 区别
    BeanUtils.copyProperties(A,B)字段复制用法
    servletcontext的小结
    枚举笔记
    关于spring mvc接受前台参数的笔记
    关于session和cookie
    servlet学习
    tomcat到底是干嘛的
    .json文件报错 ,点进去是Expected value at 1:0
  • 原文地址:https://www.cnblogs.com/jiayayao/p/6863345.html
Copyright © 2020-2023  润新知