1. 程式人生 > >調試libRTMP代碼來分析RTMP協議

調試libRTMP代碼來分析RTMP協議

bind 部分 字節 attribute err nco last esc command

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.\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(&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\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規範簡單分析

調試libRTMP代碼來分析RTMP協議