1. 程式人生 > >ONVIF、RTSP/RTP、FFMPEG的開發實錄

ONVIF、RTSP/RTP、FFMPEG的開發實錄

ONVIF、RTSP/RTP、FFMPEG的開發實錄

 

前言

    本文從零基礎一步步實現ONVIF協議、RTSP/RTP協議獲取IPC實時視訊流、FFMPEG解碼。開發環境為WIN7 32位 + VS2010。
    最終成功獲取浩雲、海康、大華的IPC實時視訊流。
    如果要了解本文更多細節,或者用本文作設計指導,那最好把文中提到的連線都開啟,與本文對照著看。

前期準備

1.準備一個ONVIF伺服器
    既然開發的是客戶端,那必需要有服務端了。我這裡大把的IPC,好幾個品牌的,就隨便拿了一個。
    如果沒有IPC,倒是可以用 VLC media player 搭建一下。或者其他播放器也可以。這個網上很多資料。

2.準備一個ONVIF 測試工具
    這個工具在ONVIF的官網上可以找到:ONVIF Device Test Tool 。

3.準備解碼器相關資料及資源
    收到視訊流後,需要解碼。可以用ffmpeg,也可以用其他解碼庫。這個是後話了,等ONVIF搞定之後再搞解碼也不遲。推薦連結:
    http://wenku.baidu.com/view/f8c94355c281e53a5802ffe4.html?re=view 
    (Windows下使用MinGW編譯ffmpeg與x265)

4.準備資料
    ONVIF協議書必看,ONVIF官網自然是不能少的。其他資料推薦幾個連結:
http://www.cuplayer.com/player/PlayerCode/camera/2015/1119/2156.html 
http://blog.csdn.net/gubenpeiyuan/article/details/25618177#
http://blog.csdn.net/saloon_yuan/article/details/24901597
http://www.onvif.org/onvif/ver20/util/operationIndex.html

5.準備抓包工具IPAnalyse
    關係到網路通訊的有個IP抓包工具能讓你省去很多麻煩,也能讓你清楚的看到協議的細節。IPAnalyse很容易在網上可以下載。

測試ONVIF

    看《ONVIF協議書》估計很多人都會雲裡霧裡,實在搞不懂的話,那就接上IPC,開啟 Test Tool,測試一下各項功能。Test Tool裡可以看到整個協議的工作細節,每一步做什麼,發了什麼報文,收了什麼報文,都可以看到。

Test Tool

IPA

    如果你沒有IPC,那用VLC理論上也可以,不過我沒測試過。兩個VLC(一個作為伺服器一個作為客戶端)加上IP抓包工具,這個我用過也可以看到協議細節。不過從抓包工具裡看到的只是一段段的報文,沒有步驟說明,不如Test tool來得明瞭。
    當然,如果你看懂了ONVIF協議,那就不必搞這些。

Soap及開發框架生成

    本人一開始並沒有聽過soap,只好自已查資料去了,這裡也不班門弄斧。這個開發框架生成網上大把大把的資料,但系都不好使啊。每個人的開發環境都有一點點差別,以至於很多文章都不能從頭至尾的跟著走一次。唯有看比較多的文章再總結一下,才能自己生成一個框架。所以我這裡也不多說了,推薦連結:

E.http://blog.csdn.net/saloon_yuan/article/details/24901597

當然也可以不用框架。如你所見,ONVIF的實現最終不過是傳送報文和接收報文,用到的功能不多的話完全可以自己手動發報文過去,再解析接收到的報文。

後面也會說到不用框架來發現裝置。

ONVIF裝置發現、裝置搜尋

    裝置發現的過程:客戶端傳送報文到239.255.255.250的3702埠(ONVIF協議規定),伺服器收到報文後再向客戶端返回一個報文,客戶端收到這個報文然後解析出Xaddrs,這就完成了一次裝置發現。
    理論上只要報文能正確收發就可以發現裝置。不過,用soap框架搜尋裝置的時候,多網絡卡、跨網段等複雜網路會出現搜尋不到的問題。這時候就需要路由支援才行(網上說可以加入相關的路由協議)。
    為了解決這個問題,自己又寫了一個非SOAP框架的裝置發現函式。兩個發現函式請看下面程式碼。
/*
    SOAP初始化
*/
int UserInitSoap(struct soap *soap,struct SOAP_ENV__Header  *header)
{   
    if(soap == NULL){
        return NULL;
    }
    //名稱空間
    soap_set_namespaces( soap, namespaces);

    //引數設定
    int timeout = 5 ;
    soap->recv_timeout      =  timeout ;
    soap->send_timeout      =  timeout ;
    soap->connect_timeout   =  timeout ;

    // 生成GUID , Linux下叫UUID
    char buf[128];
    GUID guid; 
    if(CoCreateGuid(&guid)==S_OK){
        _snprintf_s(buf, sizeof(buf) 
            ,"urn:uuid:%08X-%04X-%04x-%02X%02X-%02X%02X%02X%02X%02X%02X"
            , guid.Data1   , guid.Data2   , guid.Data3
            , guid.Data4[0], guid.Data4[1]
            , guid.Data4[2], guid.Data4[3], guid.Data4[4],guid.Data4[5]
            , guid.Data4[6], guid.Data4[7] );
    }
    else{
        _snprintf_s(buf, sizeof(buf),"a5e4fffc-ebb3-4e9e-913e-7eecdf0b05e8");
    }

    //初始化
    soap_default_SOAP_ENV__Header(soap, header);

    //相關引數寫入控制代碼
    header->wsa__MessageID =(char *)soap_malloc(soap,128);
    memset(header->wsa__MessageID, 0, 128);

    memcpy(header->wsa__MessageID, buf, strlen(buf));
    header->wsa__Action = "http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe"; 
    header->wsa__To     = "urn:schemas-xmlsoap-org:ws:2005:04:discovery";

    //寫入變數
    soap->header = header;

    return 0 ;
}
    SOAP裡所有要申請的空間請用帶有soap_XXX()的,否則不能通過soap_del(soap);來釋放所有空間。這會造成記憶體洩漏。
/*
    SOAP釋放
*/
int UserReleaseSoap(struct soap *soap)
{

    //soap_free(soap);
    soap_destroy(soap); 
    soap_end(soap); 
    soap_done(soap);
    soap_del(soap);

    /*
    The gSOAP engine uses a memory management method to allocate and deallocate memory. 
    The deallocation is performed with soap_destroy() followed by soap_end(). 
    However, when you compile with -DDEBUG or -DSOAP_MEM_DEBUG then no memory 
    is released until soap_done() is invoked. This ensures that the gSOAP engine 
    can track all malloced data to verify leaks and double frees in debug mode. 
    Use -DSOAP_DEBUG to use the normal debugging facilities without memory debugging. 
    Note that some compilers have DEBUG enabled in the debug configuration, so this behavior 
    should be expected unless you compile in release config
    */

    return 0 ;
}
    以下這裡有用到類,其中IP為要發現裝置IP,XAddrs為裝置的端地址。要複製這段程式碼的,請自行修改。
int GetVideo::FindONVIFservers()
{
    //清除錯誤
    this->error = 0 ;
    memset(this->error_info , 0, 1024);
    memset(this->error_info_, 0, 1024);

    int FoundDevNo = 0;     //發現的裝置數
    int retval = SOAP_OK;   //返回值

    struct __wsdd__ProbeMatches  resp;

    struct SOAP_ENV__Header  header;
    struct soap*  soap;
    soap = soap_new();
    UserInitSoap(soap,&header);

    ////////////////////////////////////////////
    //send the message broadcast and wait
    //IP Adress and PortNo, broadCast   
    const char *soap_endpoint = "soap.udp://239.255.255.250:3702/"; //從ONVIF協議得來

    //範圍相關引數
    wsdd__ProbeType  req;    
    wsdd__ScopesType  sScope;
    soap_default_wsdd__ScopesType(soap, &sScope);
    sScope.__item = "";//"http://www.onvif.org/??"
    soap_default_wsdd__ProbeType(soap, &req);
    req.Scopes = &sScope;
    req.Types = ""; //"dn:NetworkVideoTransmitter";

    ///////////////////////////////////////////////////////////////
    //傳送10次,直到成功為止
    int time=10;
    do{
        retval = soap_send___wsdd__Probe(soap, soap_endpoint, NULL, &req);
        Sleep(100);
    }while(retval != SOAP_OK && time--);

    ////////////////////////////////////////////////////////////////
    //一直接收,直到收到當前IP的資訊後退出,或收不到當前IP但所有已接收完退出
    while (retval == SOAP_OK)
    {
        retval = soap_recv___wsdd__ProbeMatches(soap, &resp);
        //printf("\nrecv retval = %d \n",retval);

        if (retval == SOAP_OK) {
            if (!soap->error){ 
                FoundDevNo ++;  //找到一個裝置
                if (resp.wsdd__ProbeMatches->ProbeMatch != NULL && 
                    resp.wsdd__ProbeMatches->ProbeMatch->XAddrs != NULL
                    ){
                    //判斷IP是否是你想要找的IP
                    char *http = strstr(resp.wsdd__ProbeMatches->ProbeMatch->XAddrs, this->IP );
                    if(http!=NULL){
                        //得到XAddrs(這裡認為不超過256)
                        //因為有些裝置有多個XAddrs,這裡要分離出一個
                        memcpy(this->XAddrs,"http://",7);   
                        for(int t=0;t<255-7;t++){   
                            if(http[t]==' ' || http[t]=='\n' || 
                                http[t]=='\r'|| http[t]=='\0' ){
                                    retval = 1234 ; //退出while
                                    break;
                            }
                            this->XAddrs[t+7] = http[t] ;
                        }
                    }//end if(http!=NULL)
                }
            }
            else{
                retval = soap->error;   //退出while
                this->error = soap->error ; //錯誤程式碼
                const char *tmp  = *soap_faultcode(soap)   ;    //錯誤資訊
                if(tmp)
                    memcpy(error_info , tmp , strlen(tmp ));    //複製到類
                const char *tmp_ = *soap_faultstring(soap) ;
                if(tmp_)
                    memcpy(error_info_, tmp_, strlen(tmp_));
            }
        }
        else if (soap->error){  //搜尋完所有裝置
            if (FoundDevNo){
                soap->error = 0 ;
                retval = 0;
            }
            else {
                retval = soap->error; 
            }
            break; //退出while
        }  

        #ifdef DEBUG_INFOPRINT
        if (retval == SOAP_OK) {
            if (!soap->error){ //找到一個裝置
                //列印相關資訊
                if (resp.wsdd__ProbeMatches->ProbeMatch != NULL && resp.wsdd__ProbeMatches->ProbeMatch->XAddrs != NULL){
                    printf("****** No %d Devices Information ******\n", FoundDevNo );
                    printf("Device Service Address  : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->XAddrs);    
                    printf("Device EP Address       : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->wsa__EndpointReference.Address);  
                    printf("Device Type             : %s\r\n", resp.wsdd__ProbeMatches->ProbeMatch->Types);  
                    printf("Device Metadata Version : %d\r\n", resp.wsdd__ProbeMatches->ProbeMatch->MetadataVersion);  
                    //sleep(1);
                }
            }
            else{
                printf("[%d]: recv soap error :%d, %s, %s\n", __LINE__, soap->error, *soap_faultcode(soap), *soap_faultstring(soap)); 
            }
        }
        else if (soap->error){  
            if (FoundDevNo){
                printf("Search end! Find %d Device! \n", FoundDevNo);
            }
            else {
                printf("No Device found!\n"); 
            }
            break;
        }  
        #endif
    }

    //錯誤處理
    if(retval!=1234){
        this->error = NO_DEVICE ;
        memcpy(this->error_info_, "ONVIF:  this device NOT found\n", 128);
        char tmp[128];
        sprintf_s(tmp,128,"ONVIF:  found %d devices \n",FoundDevNo);
        memcpy(this->error_info, tmp, 128);
    }


    ////////////////////////////////////////////////
    // 退出
    Sleep(10);
    UserReleaseSoap(soap);

    return retval;
}
    以上程式碼實現了整個裝置發現的過程,最終得到裝置的XAddrs。流程為:設定名稱空間 >> 超時設定 >> 生成GUID >> 填充header >> 設定搜尋範圍 >> 傳送報文 >> 接收並解析報文 。 

非SOAP框架 裝置發現

    以下程式碼也實現了事個發現過程,用到幾個類變數:this->IP是要發現的裝置IP,this->Port是其埠號,this->LocalIP是用來儲存與裝置IP同一網段的本地IP地址,this->FIND_BASE_UDP_PORT是用於連線裝置的本地埠號(如果此埠號被佔用,會自動加2查詢下一個)。
    關於char *cxml 報文,最直接的方法是從Test tool裡複製過來,或者從抓包工具裡複製出來,當然也可以複製本文的。
    流程為:輸入要發現的裝置IP - 判斷本地是否添加了此IP網段 - 測試裝置是否可以連線 - 繫結本地IP - 準備報文 - 傳送報文 - 接收報文 - 解析報文 。
/*
    獲取本機IP
*/ 
bool GetLocalIPs(char ips[][32], int maxCnt, int *cnt)  
{  
    //初始化wsa  
    WSADATA wsaData;  
    int ret=WSAStartup(0x0202,&wsaData);  
    if (ret!=0){  return false; }  

    //獲取主機名  
    char hostname[256];  
    ret=gethostname(hostname,sizeof(hostname));  
    if (ret==SOCKET_ERROR){  return false; }  

    //獲取主機ip  
    HOSTENT* host=gethostbyname(hostname);  
    if (host==NULL){  return false; }

    //轉化為char*並拷貝返回  
    //注意這裡,如果你本地IP多於32個就可能出問題了
    *cnt=host->h_length<maxCnt? host->h_length:maxCnt;  
    for (int i=0;i<*cnt;i++)  {  
        in_addr* addr =(in_addr*)*host->h_addr_list;  
        strcpy_s(ips[i], 16, inet_ntoa(addr[i]));  
    }  

    WSACleanup();
    return true;  
}  

/*
    TCP Online Test 
*/
int TcpOnlineTest(char *IP,int Port)
{
    //載入庫 
    //啟動SOCKET庫,版本為2.0 
    WSADATA wsdata;  
    WSAStartup(0x0202,&wsdata);  

    SOCKET PtcpFD=socket(AF_INET,SOCK_STREAM,0);

    SOCKADDR_IN addrSrv;
    addrSrv.sin_addr.S_un.S_addr=inet_addr(IP); //伺服器端的地址
    addrSrv.sin_family=AF_INET;
    addrSrv.sin_port=htons(Port);               //伺服器端的埠
    //connect(PtcpFD, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));   //會阻塞很久才反應過來

    //設定為非阻塞模式
    //這樣,在connect時,才會立馬跳過,不至於等太長時間
    int error = -1;    
    int len = sizeof(int);
    timeval tm;    
    fd_set set;    
    unsigned long ul = 1;    
    ioctlsocket(PtcpFD, FIONBIO, &ul); 

    int err =connect(PtcpFD, (SOCKADDR*)&addrSrv, sizeof(SOCKADDR));
    if(err==-1){
        tm.tv_sec  = 3;        
        tm.tv_usec = 0;        
        FD_ZERO(&set);        
        FD_SET(PtcpFD, &set);        
        if( select(PtcpFD+1, NULL, &set, NULL, &tm) > 0){            
            getsockopt(PtcpFD, SOL_SOCKET, SO_ERROR, (char *)&error, /*(socklen_t *)*/&len); 

            if(error == 0){ err = 0; }      
            else { err = -1; }   
        }        
        else { err = -1; }
    }//end if(err==-1)

    if(err==0){
        //設定為阻塞模式
        ul = 0;    
        ioctlsocket(PtcpFD, FIONBIO, &ul); 

        char buf[16]={0};
        err = send(PtcpFD, buf, 16, 0); //Online Test 
        if(err!=16){
            err = -1 ;  //此IP不線上,IP或Port有誤
        }
        else{ err = 0 ; }
    }

    closesocket(PtcpFD);
    WSACleanup();
    return err;
}

/*
    連線UDP
    返回: 0   -   成功
            -1  -   失敗
            -2  -   繫結失敗
*/
int UdpConnect(char *IP, int UdpPort, int *UdpFD)
{
    //載入庫
    //啟動SOCKET庫,版本為2.0  
    WSADATA wsdata;  
    WSAStartup(0x0202,&wsdata);  

    //然後賦值  
    sockaddr_in  serv; 
    serv.sin_family     =   AF_INET         ;  
    serv.sin_addr.s_addr=   inet_addr(IP)   ;   //INADDR_ANY ;  
    serv.sin_port       =   htons(UdpPort)  ;  

    //用UDP初始化套接字  
    *UdpFD = socket(AF_INET,SOCK_DGRAM,0); 
    if(!(*UdpFD)){ return -1; }

    // 設定該套接字為廣播型別,  
    bool optval =0 ;
    int res =0;
    //res =setsockopt(*UdpFD,SOL_SOCKET,SO_REUSEADDR,(char FAR *)&optval,sizeof(optval)); 

    //時限
    int nNetTimeout=100;//m秒
    //setsockopt(PudpFD,SOL_S0CKET,SO_SNDTIMEO,(char *)&nNetTimeout,sizeof(int));
    res|= setsockopt(*UdpFD,SOL_SOCKET,SO_RCVTIMEO,(char *)&nNetTimeout,sizeof(int));

    // 緩衝區
    int nBuf=100*1024;//設定為xK
    res|= setsockopt(*UdpFD,SOL_SOCKET,SO_SNDBUF,(const char*)&nBuf,sizeof(int));
    res|= setsockopt(*UdpFD,SOL_SOCKET,SO_RCVBUF,(const char*)&nBuf,sizeof(int));

    if(res){ return -1; }

    // 把該套接字繫結在一個具體的地址上  !!!!!!! 注意這裡 !!!!!!!
    // 這是與SOAP框架不同的地方,也是之所以可以跨網段的原因!
    res =bind(*UdpFD,(sockaddr *)&serv,sizeof(sockaddr_in));  
    if(res==-1){
        int errorcode =::GetLastError();
        return -2;
    }

    return 0 ;
}

/*
    關閉UDP
*/
int UdpClose(int UdpFD)
{
    closesocket(UdpFD);
    WSACleanup();
    return 0 ;
}

/*
    UDP SEND
*/
int UdpSend(int UdpFD,char*msg,int len)
{
    int ret =send(UdpFD, msg, len, 0 );
    return ret ;
}

/*
    UDP SEND TO
*/
int UdpSendto(int UdpFD,char *msg, int len, char *toIP, int toPort)
{
    int tolen = sizeof(struct sockaddr_in);  

    sockaddr_in to;
    to.sin_family       =   AF_INET         ;  
    to.sin_addr.s_addr  =   inet_addr(toIP) ;   //INADDR_ANY ;  
    to.sin_port         =   htons(toPort)   ;  

    int ret =sendto(UdpFD, msg, len, 0, (const sockaddr *)&to, tolen);

    return ret ;
}

/*
    UDP RECV from
    返回: -1  -   沒有關鍵欄位
            len -   nonSoap_XAddrs 空間不足,返回當前XAddrs長度
*/
int UdpRecvfrom(int UdpFD,char *fromIP,char *buf,int size)
{
    //引數配置
    sockaddr_in  afrom ;
    afrom.sin_family        =   AF_INET         ;  
    afrom.sin_addr.s_addr   =   INADDR_BROADCAST; //inet_addr(fromIP)   ; //INADDR_BROADCAST;  
   // afrom.sin_port            =   htons(fromPort) ;  

    //接收
    int fromlength = sizeof(SOCKADDR);  
    memset(buf, 0, size);
    int res =recvfrom(UdpFD,buf,size,0,(struct sockaddr FAR *)&afrom,(int FAR *)&fromlength);  

    //如果收到的不是指定的IP,將放棄
    //如果你要發現所有裝置,這裡需要修改
    if( res && 
        (afrom.sin_addr.S_un.S_addr != inet_addr(fromIP)) 
       ){
        //char *afIP =inet_ntoa(afrom.sin_addr); //only for debug
        res = 0 ;
    }

    return res ;
}

/*
    得到引數,解析報文
    返回: 0   -   成功
            -1  -   失敗
            >0  -   nonSoap_XAddrs空間不足
*/
int GetDiscoveryParam(char *buf,int size, char *MessageID, char *nonSoap_XAddrs,int xaddr_len)
{
    //規定搜尋範圍
    const char *type   = "NetworkVideoTransmitter";
    const char *Scopes = "www.onvif.org";

    //是否啟用搜索範圍
    int isUse_Type   = 0 ;
    int isUse_Scopes = 0 ;

    // type
    if(isUse_Type){
        char *find_type =strstr(buf, type);
        if(find_type==NULL){
            return -1 ; //不是這個型別的不必解析
        }
    }

    // Scopes
    if(isUse_Type){
        char *find_Scopes =strstr(buf, Scopes);
        if(find_Scopes==NULL){
            return -1 ; //不是這個型別的不必解析
        }
    }

    // MessageID
    char *find_uuid =strstr(buf, MessageID);
    if(find_uuid==NULL){
        return -1 ; //MessageID不相等的,不是廣播給你的,不必解析
    }

    // 找到XAddrs的開始和結束處
    const char *wsddXAddrs_b = "XAddrs>";//"<wsdd:XAddrs>";
    const char *wsddXAddrs_e = "</";//"</wsdd:XAddrs>";
    char *find_XAddrs_b =strstr(buf, wsddXAddrs_b);
    char *find_XAddrs_e =strstr(find_XAddrs_b, wsddXAddrs_e);
    if(find_XAddrs_b==NULL || wsddXAddrs_e==NULL){
        return -1 ;
    }

    //取出一個XAddrs
    int len_b = strlen(wsddXAddrs_b);               //找到的XAddrs字串頭長度
    int len = find_XAddrs_e -find_XAddrs_b -len_b;  //此區間裡可能包含多個XAddrs

    char *tmp = new char[len+1];
    memset(tmp, 0, len+1);
    for(int i=0;i<len;i++){                     //分離出一個XAddrs
        if(find_XAddrs_b[len_b+i]!=' '){        //多個XAddrs用空格隔開
            tmp[i] = find_XAddrs_b[len_b+i] ;   //找到的字串,除去開頭
        }
        else{
            len = i ;   //len重新賦值為單個XAddrs長度
            break;
        }
    }

    // XAddrs 如果比申請空間要長 
    if(len>xaddr_len){
        return len ;
    }

    // 得到 XAddrs 
    memset(nonSoap_XAddrs, 0, xaddr_len);
    memcpy(nonSoap_XAddrs, tmp, len);

    delete tmp ;
    return 0 ;
}


/*
    非SOAP搜尋
    返回: -4  -   本地沒有新增裝置對應的網段
            0   -   成功
*/
int GetVideo::FindONVIFservers_nonSoap()
{
    //清除錯誤
    this->error = 0 ;
    memset(this->error_info_, '\0',1024);
    memset(this->error_info,  '\0',1024);

    /////////////////////////////////////////////////////////////
    //得到裝置網段
    int a,b,c,d;
    char IPD[32];
    sscanf_s(this->IP,"%d.%d.%d.%d",&a,&b,&c,&d);
    sprintf_s(IPD,32,"%d.%d.%d.",a,b,c);

    //本地是否已添加當前網段
    int isAddIPD = 0 ;
    char lips[64][32];
    char *UseIP;
    int cnt = 0 ;
    GetLocalIPs(lips, 64, &cnt);

    for(int i=0;i<cnt;i++){ //歷遍所有本地IP
        isAddIPD = 0 ;
        UseIP = strstr(lips[i], IPD);
        if(UseIP!=NULL){    //已新增
            isAddIPD = 1 ;
            memcpy(this->LocalIP, UseIP, 64); //得到本地IP
            break;
        }
    }

    if(isAddIPD==0){
        this->error = NOTADDIP ;
        memcpy(this->error_info_, "沒有新增該網段",32);
        memcpy(this->error_info,  "請先新增該網段",32);
        return -4;  //返回沒有該網段
    }

    //測試裝置IP:Port是否可以連線
    int isConnecting = TcpOnlineTest(this->IP, this->Port);
    if(isConnecting==-1){
        this->error = NOCONNECT ;
        memcpy(this->error_info_, "該IP/Port無法連線,請檢查是否填寫正確",128);
        memcpy(this->error_info,  "ONVIF:Discovery IP/Port error\n",128);
        return -1;
    }

    //繫結本地IP,連線UDP
    int Dis_UdpFD = 0 ;
    int UsePort = this->FIND_BASE_UDP_PORT ;    //使用預定的埠,向下查詢合適埠
    int res =0;
    for(int i=0;i<512;i++){ //查詢一定次數
        res =UdpConnect(UseIP, UsePort, &Dis_UdpFD);
        if(res==-1){
            this->error = NOCONNECT ;
            memcpy(this->error_info_, "該IP無法建立UDP連線",64);
            memcpy(this->error_info,  "ONVIF:Discovery UDP SOCKET ERROR(-1)\n",128);
            UdpClose(Dis_UdpFD);
            return -1;
        }
        else if(res==-2){   //bind error
            UsePort+=2;     //UDP PORT 為偶數比較好
            UdpClose(Dis_UdpFD);
            Sleep(10);
        }
        else if(res==0){
            break;
        }
    }
    //如果依然繫結連線失敗
    if(res==-2){
        this->error = NOCONNECT ;
        memcpy(this->error_info_, "Can't bind UDP PORT 5340-5596.(used by others now)",128);
        memcpy(this->error_info,  "ONVIF:Discovery UDP bind ERROR\n",128);
        //UdpClose(Dis_UdpFD);  //已經在上面關閉了,此處註釋掉
        return -1;
    }

    /////////////////////////////////////////////////////////////////
    //準備報文
    char *cxml = {"<?xml version=\"1.0\" encoding=\"UTF-8\"?><SOAP-ENV:Envelope x"
                        "mlns:SOAP-ENV=\"http://www.w3.org/2003/05/soap-envelope\" x"
                        "mlns:SOAP-ENC=\"http://www.w3.org/2003/05/soap-encoding\" x"
                        "mlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" x"
                        "mlns:xsd=\"http://www.w3.org/2001/XMLSchema\" x"
                        "mlns:wsa=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\" x"
                        "mlns:wsdd=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\" x"
                        "mlns:chan=\"http://schemas.microsoft.com/ws/2005/02/duplex\" x"
                        "mlns:wsa5=\"http://www.w3.org/2005/08/addressing\" x"
                        "mlns:c14n=\"http://www.w3.org/2001/10/xml-exc-c14n#\" x"
                        "mlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\" x"
                        "mlns:xenc=\"http://www.w3.org/2001/04/xmlenc#\" x"
                        "mlns:wsc=\"http://schemas.xmlsoap.org/ws/2005/02/sc\" x"
                        "mlns:ds=\"http://www.w3.org/2000/09/xmldsig#\" x"
                        "mlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\" x"
                        "mlns:xmime=\"http://tempuri.org/xmime.xsd\" x"
                        "mlns:xop=\"http://www.w3.org/2004/08/xop/include\" x"
                        "mlns:tt=\"http://www.onvif.org/ver10/schema\" x"
                        "mlns:wsrfbf=\"http://docs.oasis-open.org/wsrf/bf-2\" x"
                        "mlns:wstop=\"http://docs.oasis-open.org/wsn/t-1\" x"
                        "mlns:wsrfr=\"http://docs.oasis-open.org/wsrf/r-2\" x"
                        "mlns:tad=\"http://www.onvif.org/ver10/analyticsdevice/wsdl\" x"
                        "mlns:tan=\"http://www.onvif.org/ver20/analytics/wsdl\" x"
                        "mlns:tdn=\"http://www.onvif.org/ver10/network/wsdl\" x"
                        "mlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" x"
                        "mlns:tev=\"http://www.onvif.org/ver10/events/wsdl\" x"
                        "mlns:wsnt=\"http://docs.oasis-open.org/wsn/b-2\" x"
                        "mlns:timg=\"http://www.onvif.org/ver20/imaging/wsdl\" x"
                        "mlns:tls=\"http://www.onvif.org/ver10/display/wsdl\" x"
                        "mlns:tmd=\"http://www.onvif.org/ver10/deviceIO/wsdl\" x"
                        "mlns:tptz=\"http://www.onvif.org/ver20/ptz/wsdl\" x"
                        "mlns:trc=\"http://www.onvif.org/ver10/recording/wsdl\" x"
                        "mlns:trp=\"http://www.onvif.org/ver10/replay/wsdl\" x"
                        "mlns:trt=\"http://www.onvif.org/ver10/media/wsdl\" x"
                        "mlns:trv=\"http://www.onvif.org/ver10/receiver/wsdl\" x"
                        "mlns:tse=\"http://www.onvif.org/ver10/search/wsdl\">"
                        "<SOAP-ENV:Header><wsa:MessageID>urn:uuid:9D7A37A4-DBFE-4bdd-A79C-74998B7A375D</wsa:MessageID>"
                        "<wsa:To SOAP-ENV:mustUnderstand=\"true\">urn:schemas-xmlsoap-org:ws:2005:04:discovery</wsa:To>"
                        "<wsa:Action SOAP-ENV:mustUnderstand=\"true\">http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</wsa:Action>"
                        "</SOAP-ENV:Header><SOAP-ENV:Body><wsdd:Probe><wsdd:Types></wsdd:Types><wsdd:Scopes></wsdd:Scopes>"
                        "</wsdd:Probe></SOAP-ENV:Body></SOAP-ENV:Envelope>" };

    // 生成GUID , Linux下叫UUID
    char MessageID[128];
    GUID guid; 
    if(CoCreateGuid(&guid)==S_OK){
        _snprintf_s(MessageID, sizeof(MessageID) 
            ,"urn:uuid:%08X-%04X-%04x-%02X%02X-%02X%02X%02X%02X%02X%02X"
            , guid.Data1   , guid.Data2   , guid.Data3
            , guid.Data4[0], guid.Data4[1]
            , guid.Data4[2], guid.Data4[3], guid.Data4[4],guid.Data4[5]
            , guid.Data4[6], guid.Data4[7] );
    }
    else{
        _snprintf_s(MessageID, sizeof(MessageID),"a5e4fffc-ebb3-4e9e-913e-7eecdf0b05e8");
    }

    //跳轉到關鍵欄位
    int xml_len = strlen(cxml);
    char *xml = new char[xml_len+2] ;   //從const char 到 char
    memset(xml, 0, xml_len+2);
    memcpy(xml, cxml, xml_len);
    char *tmp_xml = strstr(xml,"urn:uuid:");
    //寫入MessageID
    int idlen = strlen(MessageID);
    for(int i=0;i<idlen;i++){
        tmp_xml[i] = MessageID[i] ;
    }

    /////////////////////////////////////////////////////////////////
    //傳送報文 

    res =UdpSendto(Dis_UdpFD, xml, xml_len,"239.255.255.250",3702); 

/*  if(xml_len>1500){
        res  =UdpSendto(Dis_UdpFD,  xml,      1400,         "239.255.255.250",3702);    
        res +=UdpSendto(Dis_UdpFD,  xml+1400, xml_len-1400, "239.255.255.250",3702);    
    }
    else{
        res =UdpSendto(Dis_UdpFD, xml, xml_len,"239.255.255.250",3702); 
    }
*/  if(res!=xml_len){
        this->error = -1 ;
        char tmp[128];
        sprintf_s(tmp,128,"UDP send(%d)!=res(%d) ERROR",xml_len,res);
        memcpy(this->error_info_, tmp,128);
        memcpy(this->error_info,  "ONVIF:Discovery UDP send ERROR\n",128);
        UdpClose(Dis_UdpFD);
        delete xml;
        return -1;
    }

    /////////////////////////////////////////////////////////////////
    //接收報文 
    char buf[4096] ;
    int size = 4096 ;
    //接收10次後依然沒有報文,則退出
    for(int j=0,res=0; res==0 && j<100; j++){
        //memset(buf, 0, 4096); //2016年11月11日
        res =UdpRecvfrom(Dis_UdpFD, this->IP, buf+res, size-res);
        Sleep(10);
    }
    //如果沒有收到,再次接收
    //if(res==-1){  
    //  for(res=0; res!=0; ){
    //      memset(buf, 0, 4096);
    //      res =UdpRecvfrom(Dis_UdpFD, this->IP, buf, size);
    //      Sleep(10);
    //  }
    //}
    //再次沒有收到,認為出錯了
    if(res==-1){    
        this->error = -1 ;
        char tmp[128];
        sprintf_s(tmp,128,"UDP recv(%d) ERROR",res);
        memcpy(this->error_info_, tmp,128);
        memcpy(this->error_info,  "ONVIF:Discovery UDP recv ERROR\n",128);
        UdpClose(Dis_UdpFD);
        delete xml;
        return -1;
    }

    //////////////////////////////////////////////////////////////////////////
    //解釋報文
    int resGDP =GetDiscoveryParam(buf, res, MessageID, this->XAddrs, 256);
    //XAddrs空間不足
    if(resGDP>0){
        this->error = -1 ;
        char tmp[128];
        sprintf_s(tmp,128,"GetVideo: XAddrs buf overflow(256<%d) ERROR",res);
        memcpy(this->error_info_, tmp,128);
        memcpy(this->error_info,  "GetVideo: 請增大XAddrs空間\n",128);
        UdpClose(Dis_UdpFD);
        delete xml;
        return -1;
    }
    //沒有發現此裝置
    if(resGDP==-1){
        this->error = NO_DEVICE ;
        memcpy(this->error_info_, "ONVIF:  this device NOT found\n", 128);
        memcpy(this->error_info,  "ONVIF:non Soap Discovery NO Device\n",128);
        UdpClose(Dis_UdpFD);
        delete xml;
        return -1;
    }

    // 關閉
    UdpClose(Dis_UdpFD);
    delete xml;

    return 0 ;
}
    其實裝置發現,就是要得到一個XAddrs。同時看一下裝置是否線上。如果你確認裝置線上,並已知XAddrs,那不用發現這一步了。直接可以進行下面的操作。

ONVIF獲取裝置能力

    先看程式碼
/*
    獲取能力
*/
void UserGetCapabilities(   struct soap *soap   ,
        struct __wsdd__ProbeMatches *resp,
        class _tds__GetCapabilities *capa_req,
        class _tds__GetCapabilitiesResponse *capa_resp,
        char *ONVIF_USER,
        char *ONVIF_PASSWORD)
{
    //申請空間
    capa_req->Category = (enum tt__CapabilityCategory *)soap_malloc(soap, sizeof(int));
    capa_req->__sizeCategory = 1;
    capa_resp->Capabilities = (class tt__Capabilities*)soap_malloc(soap,sizeof(class tt__Capabilities)) ;

    //選擇要獲取的裝置能力
    *(capa_req->Category) = (enum tt__CapabilityCategory)(tt__CapabilityCategory__All);

    //身份認證,鑑權
    if(soap->error){ return; }
    COnvifDigest cTokenDigest(ONVIF_USER, ONVIF_PASSWORD);
    cTokenDigest.Authentication(soap,TRUE,"user");

    //獲取裝置能力
    if(soap->error){ return; }
    int result = soap_call___tds__GetCapabilities(soap, resp->wsdd__ProbeMatches->ProbeMatch->XAddrs, NULL, capa_req, capa_resp);

}
    小白都會問:這段程式碼怎麼得來的呢?請看解析。
    裝置發現之後是能力獲取,這可以從ONVIF官方資料得知。
    能力獲取是用GetCapabilities()函式,這可以從ONVIF官網的操作說明看到,也就是上那個D網址。 
    那怎麼知道soap的字首呢,soap_call___tds__XXX也不是亂加的。最保守是在soapClient.cpp裡Ctrl+F查詢GetCapabilities關鍵字,就可以找到相應的函式。到你熟悉之後,你會發現tds是GetCapabilities()的名稱空間,其他函式有其他的名稱空間,代替一下就可以。
    關於函式呼叫之前,輸入輸出變數是什麼意思,可以看ONVIF官網的函式說明。

ONVIF鑑權

鑑權可以用函式:
soap_wsse_add_UsernameTokenDigest(soap,"user", ONVIF_USER, ONVIF_PASSWORD); 
    這個函式要用到openssl庫,要自己編譯加進去,有點麻煩。
    這裡的鑑權也就是使用者名稱、密碼、隨機碼/時間碼的base64加密,自己也可能實現。所以我選擇了不用openssl庫的方法。推薦連結:
http://blog.csdn.net/pony_maggie/article/details/8588888
http://blog.csdn.net/qiaowei0/article/details/42024703
    (後面的連結,一開始可以用,後來裝置多了情況複雜了就似乎有點問題,我修改過,但忘記改了哪裡,程式碼就不放上來了 so sorry)

ONVIF流程

    可以實現能力獲取這一步,就已經可以舉一反三完成所有的ONVIF操作了。下面看看整體流程:
    發現裝置 >> 獲取能力 >> 獲取媒體資訊 >> 獲取視訊編碼配置 >> 設定視訊編碼配置 >> 獲取URI >> ONVIF完成 ->  RTSP播放 -> 解碼 

RTSP/RTP協議實現

增加準備資料,推薦以下連結:

http://blog.csdn.net/chenyanxu/article/details/2728427
http://www.cnblogs.com/lidabo/p/3701068.html
http://blog.csdn.net/baby313/article/details/7353605
http://www.faqs.org/rfcs/
http://blog.csdn.net/gavinr/article/details/7035966
http://blog.csdn.net/nkmnkm/article/category/1066093/2
http://blog.csdn.net/cjj198561/article/details/30248963
https://my.oschina.net/u/1431835/blog/393315

都是關於RTSP及RTP包及H264等相關資料

RTSP通訊

理論自已看上面的連結,不多說,直接上程式碼。
int C_RTSP_SendCMD::OPTIONS()
{
    //初始化
    memset(Authorization_Basic, '\0', 128); //用於鑑權

    //得到命令
    char *cmd = new char[1024];
    memset(cmd, 0, 1024);

    sprintf_s(cmd,1024, "OPTIONS %s RTSP/1.0\r\n"
                        "CSeq:1\r\n"
                        "User-Agent:HaoYun"
                        "\r\n"
                        "\r\n", C_RTSP_SendCMD::URI );

    //傳送
    int sLenth = strlen(cmd);
    int res = send(tcpFD, cmd, sLenth, 0);

    delete cmd ;

    return res ;
}
    這裡用到變數是:URI由ONVIF獲取的。tcpFD是socket值。由LocalIP與裝置建立的一個tcp連結,注意RTSP協議規定埠為554(有些也可能用8554)。Authorization_Basic是關於使用者名稱和密碼的一個basic64加密認證。 
    這裡要注意的是命令裡面不要有多餘的空格和多餘的轉義,否則不行。
    傳送完命令後,馬上接收伺服器返回的資料,然後就可以看到資料裡所說的過程了。例如:
        OPTIONS rtsp://192.168.20.136:5000/xxx666  RTSP/1.0 
        CSeq: 1 //每個訊息都有序號來標記,第一個包通常是option請求訊息 
        User-Agent: HaoYun

    伺服器的迴應資訊包括提供的一些方法,例如:
        RTSP/1.0 200 OK 
        Server: HaoYun IPC
        Cseq: 1 //每個迴應訊息的cseq數值和請求訊息的cseq相對應 
        Public: OPTIONS, DESCRIBE, SETUP, TEARDOWN, PLAY, PAUSE, SCALE, GET_PARAMETER   //伺服器提供的可用的方法 

再來一個要認證身份的例子

看下面程式碼:
int C_RTSP_SendCMD::DESCRIBE()
{
    //得到命令
    char *cmd = new char[1024];
    memset(cmd, 0, 1024);

    sprintf_s(cmd,1024, "DESCRIBE %s RTSP/1.0\r\n"
                        "CSeq:2\r\n"
                        "Accept: application/sdp\r\n"
                        "User-Agent:HaoYun"
                        "\r\n"
                        "\r\n", C_RTSP_SendCMD::URI );


    //要認證
    if(this->Authorization_Basic[1]!='\0'){
        char *cmd_tmp = strstr(cmd, "HaoYun");
        sprintf_s(cmd_tmp,512,  "HaoYun\r\n"
                                "Authorization: Basic %s"
                                "\r\n"
                                "\r\n", this->Authorization_Basic );

    }//end if

    //傳送
    int sLenth = strlen(cmd);
    int res = send(tcpFD, cmd, sLenth, 0);

    delete cmd ;

    return res ;
}
    這裡的Authorization_Basic是如下程式碼算出的。其中base64_bits_to_64()網上好多都有就不復制上來了。SOAP裡也有,在那個鑑權函式裡面,修改一下soap_malloc()就可以用了。
int C_RTSP_SendCMD::Calc_Authorization_Basic(char *user, char *passwd)
{
    unsigned char *user_passwd = new unsigned char[600];
    memset(user_passwd, '\0', 600);
    //合併兩個
    int u_len = strlen(user);
    int p_len = strlen(passwd);
    memcpy(user_passwd,         user,   u_len);
    *(user_passwd+u_len) = ':';
    memcpy(user_passwd+u_len+1, passwd, p_len);

    //base64轉碼
    base64_bits_to_64((unsigned char *)this->Authorization_Basic, user_passwd, u_len+p_len+1);

    delete user_passwd;
    return 0 ;
}
1.接收到DESCRIBE的sdp報文後,就在sdp裡解析出 control 和 transport 來。
2.接著就是用上面得到的資訊進行SETUP
3.接收到SETUP的返回資訊後,解析出Session,每個通話都有一個固定的Session。
4.用Session值傳送PLAY命令,成功的話,就有視訊流了。這時候的流是RTP包(如果用RTP協議的話)。
5.還有一個關閉的命令TEARDOWN,在關閉的時候呼叫。

RTP協議實現接收

    看完上面RTSP的連結後,就可以看下面的程式碼了。(如果一頭霧水,請對照著那兩個RTP網址來看,或對照著RTP協議也可以)
    理論不說,分析程式碼去。
    其中兩個結構體是關於RTP頭的,GetHeader()就是解析填充這兩個頭,程式碼和協議有一一對應的關係。而GetNALUnit_OneFrame()是從視訊流中把一個個小RTP包拆解成NAL包,再一個個的合併成一個大幀,合併得到的幀可以送到解碼器解碼了。有些解碼器也許不需要合併幀這一步,直接把NAL流送過去就可以解碼的。
//////////////////////////////////////////////////////////////////////////////////////////  
// 2 class CH264_RTP_UNPACK_2 start  

class CH264_RTP_UNPACK_2
{
#define RTP_VERSION     2  
#define BUF_SIZE        (1024 * 500)  
    typedef struct   
    {  
        //LITTLE_ENDIAN  
        unsigned char   CC;      /* 4 CSRC count                 */  
        unsigned char   X;       /* 1 header extension flag      */  
        unsigned char   P;       /* 1 padding flag               */  
        unsigned char   V;       /* 2 packet type                */  
        unsigned char   PT;      /* 7 payload type               */  
        unsigned char   M;       /* 1 marker bit                 */  

        unsigned short   seq_number;        /* 16 sequence number            */  
        unsigned int     time_stamp;        /* 32 timestamp                  */  
        unsigned int     SSRC;              /* 32 synchronization source     */ 
        unsigned int     size;
    } RTP_Header_t; 

    typedef struct 
    {
        unsigned char   Type;       // 5     
        unsigned char   NRI;        // 2    
        unsigned char   F;          // 1 在 H.264 規範中規定了這一位必須為 0
        unsigned char   FUA_Type;   // 5 
        unsigned char   FUA_R;      // 1
        unsigned char   FUA_E;      // 1 置1 NAL單元的結束
        unsigned char   FUA_S;      // 1 置1 NAL單元的開始
        unsigned char   isMajor578;
        unsigned int    size;
    } NAL_Header_t; 

public:
    RTP_Header_t  m_RTP_Header;
    NAL_Header_t  m_NAL_Header;

    unsigned char *H264_OneFrame;
    int H264_1f_size;

    int pic_size3;

public:
    CH264_RTP_UNPACK_2()
    {
    }

    ~CH264_RTP_UNPACK_2()
    {
        delete H264_OneFrame ;
    }

    void CH264_RTP_UNPACK_2_Init()
    {
        memset(&m_RTP_Header, 0, sizeof(RTP_Header_t));
        memset(&m_NAL_Header, 0, sizeof(NAL_Header_t));
        H264_OneFrame = new unsigned char[pic_size3];
        memset(H264_OneFrame, 0, pic_size3);
        H264_1f_size = 0 ;
    }

    // 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 
    //*-+-+-+-+-+-+-+-+*+-+-+-+-+-+-+-+*+-+-+-+-+-+-+-+*+-+-+-+-+-+-+-+
    //|V=2|P|X|  CC   |M|     PT      |       sequence number         |
    //|                         timestamp                             |
    //|                synchronization source(SSRC)                   |
    //+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
    //                      ..........
    int GetHeader(unsigned char *RTP_PackegRawBuf,int size)
    {
        if(size<13){ return -2; }   //包頭不會小於13的

        unsigned int ba,bb,bc,bd;   //4個位元組中間變數

        m_RTP_Header.V  =  RTP_PackegRawBuf[0] >>6 ;
        m_RTP_Header.P  = (RTP_PackegRawBuf[0] &0x20)? 1:0 ;
        m_RTP_Header.X  = (RTP_PackegRawBuf[0] &0x10)? 1:0 ;
        m_RTP_Header.CC =  RTP_PackegRawBuf[0] &0x0f ;

        m_RTP_Header.M  = RTP_PackegRawBuf[1] >>7 ;
        m_RTP_Header.PT = RTP_PackegRawBuf[1] &0x7f ;

        ba = RTP_PackegRawBuf[2] ;
        bb = RTP_PackegRawBuf[3] ;
        m_RTP_Header.seq_number = (ba<<8) + bb ;

        ba = RTP_PackegRawBuf[4] ;
        bb = RTP_PackegRawBuf[5] ;
        bc = RTP_PackegRawBuf[6] ;
        bd = RTP_PackegRawBuf[7] ;
        m_RTP_Header.time_stamp = (ba<<24) +(bb<<16) +(bc<<8) +bd ;

        ba = RTP_PackegRawBuf[ 8] ;
        bb = RTP_PackegRawBuf[ 9] ;
        bc = RTP_PackegRawBuf[10] ;
        bd = RTP_PackegRawBuf[11] ;
        m_RTP_Header.SSRC = (ba<<24) +(bb<<16) +(bc<<8) +bd ;

        m_RTP_Header.size = 12 + m_RTP_Header.CC ;

        //NAL包
        //RTP版本不同
        if(m_RTP_Header.V!=RTP_VERSION){    
            return -2;
        }
        //存在特殊報頭,此處要加入特殊處理,此版本不支援固直接返回
        if(m_RTP_Header.X){
            return -2;
        }
        //《RFC3551 RTP A/V Profile》中定義動態標識區(一般96標識H264)
        //if(m_RTP_Header.PT<96 || m_RTP_Header.PT>127){
        //(!!!!! 此處假定96(0X60)標識H264 並僅對H264進行處理 !!!!!)
        if(m_RTP_Header.PT != 96){
            return -2;
        }

        //提取H264 NAL頭
        int nal_h_offset  = 12+m_RTP_Header.CC ;
        m_NAL_Header.F    =  RTP_PackegRawBuf[nal_h_offset] >>7;
        m_NAL_Header.NRI  = (RTP_PackegRawBuf[nal_h_offset] &0x60)>>5;
        m_NAL_Header.Type =  RTP_PackegRawBuf[nal_h_offset] &0x1f;

        //重要型別為 5,7,8,1? (25,27,28,65,67,68,21?...)
        m_NAL_Header.isMajor578 = 0 ;
        if(m_NAL_Header.Type==5 || m_NAL_Header.Type==7 || 
           m_NAL_Header.Type==8 || m_NAL_Header.Type==1 )
        {
            m_NAL_Header.isMajor578 = 1 ; 
        }

        //此處僅處理多片FU-A(28)及單片的情況,其他情況直接返回
        if(m_NAL_Header.Type>23 && m_NAL_Header.Type!=28){
            return -2;
        }

        // 分片 FU-A 
        if(m_NAL_Header.Type==28){
            m_NAL_Header.FUA_S    =  RTP_PackegRawBuf[nal_h_offset+1] >>7 ;
            m_NAL_Header.FUA_E    = (RTP_PackegRawBuf[nal_h_offset+1] &0x40)>>6;
            m_NAL_Header.FUA_R    = 0 ;
            m_NAL_Header.FUA_Type =  RTP_PackegRawBuf[nal_h_offset+1] &0x1f ;
            m_NAL_Header.size     = 2 ;
        }
        else{ //單片
            m_NAL_Header.FUA_S    = 0 ;
            m_NAL_Header.FUA_E    = 0 ;
            m_NAL_Header.FUA_R    = 0 ;
            m_NAL_Header.FUA_Type = 0 ;
            m_NAL_Header.size     = 1 ;
        }

        return 0;
    }

    //此函式未測試 - 2016年11月11日
    int CheckLostMajorPackeg(float *fLossRate)
    {
        bool isLost =0;

        static bool isFrame_loss[256] = {0} ;
        static unsigned char i = 0 ;
        static unsigned short seq_number_old = 0;

        //重要型別為 5,7,8 (25,27,28,65,67,68,...)
        bool isMajor = 0 ;
        unsigned char Type[2];
        Type[0] = m_NAL_Header.Type &0x0f ;
        Type[1] = m_NAL_Header.FUA_Type & 0x0f ;

        //判斷是否有5 7 8 
        for(int j=0;j<2 && isMajor==0;j++){
            switch(Type[j]){
                case 5:
                case 7:
                case 8: isMajor = 1 ; break;
                default: isMajor = 0 ;
            }
        }
        if(isMajor==0){ 
            //更新資料
            seq_number_old = m_RTP_Header.seq_number ;
            return 0; 
        }

        //如果是重要幀
        //序列號應該遞增
        isLost = 1 ;
        if(m_RTP_Header.seq_number-seq_number_old == 1){
            isLost = 0 ;
        }
        if(seq_number_old==0xffff && m_RTP_Header.seq_number==0x0000){
            isLost = 0 ;
        }

        //計算丟包率
        float lossnum = 0.0 ;
        isFrame_loss[i++] = isLost ;
        for(int j=0;j<256;j++){
            if(isFrame_loss[j])
                lossnum += 1.0 ;
        }
        *fLossRate = lossnum/256.0f ;

        //更新資料
        seq_number_old = m_RTP_Header.seq_number ;

        return (int)isLost;
    }

    int GetNALUnit_OneFrame(unsigned char *RTP_PackegRawBuf,int size)
    {
        int reValue = 0 ;

        float fLossRate =0.0;
        static int isLossPackeg = 0 ;

        //引數斷定
        if(RTP_PackegRawBuf==NULL || size<13){return -1;}

        GetHeader(RTP_PackegRawBuf,size);
        /*if(!isLossPackeg && CheckLostMajorPackeg(&fLossRate)){
            isLossPackeg = 1 ;
            printf("loss packeg: LossRate=%f ... \n",fLossRate);
        }

        //如果丟包,丟掉這一幀
        if(isLossPackeg){
            memset(H264_OneFrame, 0, H264_1f_size);
            H264_1f_size = 0 ;
            if(m_RTP_Header.M){ //如果幀結束,清零標記位
                isLossPackeg = 0 ;
            }
            return -1;
        }
        */
        //去殼、去頭去尾
        unsigned char *playload = RTP_PackegRawBuf ;
        int playload_size = 0 ;
        int padding_size = 0 ;

        if(m_RTP_Header.P){  //尾部有多餘資料
            padding_size = RTP_PackegRawBuf[size-1] ;
        }
        //資料大小及資料地址重新指向有效資料區域
        playload_size = size -(m_RTP_Header.size+m_NAL_Header.size) -padding_size +1;
        playload += m_RTP_Header.size + m_NAL_Header.size -1;

        //單片
        if(m_NAL_Header.size==1 && !m_NAL_Header.isMajor578){
            playload -= 3 ;
            playload_size += 3 ;
            playload[0] =0x00; playload[1] =0x00; //寫入頭
            playload[2] =0x01; 
        }
        else if(m_NAL_Header.size==1 && m_NAL_Header.isMajor578){
            playload -= 4 ;
            playload_size += 4 ;
            playload[0] =0x00; playload[1] =0x00; 
            playload[2] =0x00; playload[3] =0x01;
        }
        else if(m_NAL_Header.size==2){  //多片
            if(m_NAL_Header.FUA_S){     //分片開始
                playload -= 4 ;
                playload_size += 4 ;
                playload[4] = (playload[3]&0xe0) + (playload[4]&0x1f);//取FU indicator的前三位和FU Header的後五位為NAL型別。
                playload[0] =0x00; playload[1] =0x00; //寫入頭
                playload[2] =0x00; playload[3] =0x01;
            }
            else{
                playload += 1 ;         //除去 FU Header
                playload_size -= 1 ;
            }
        }
        //合併成一幀
        memcpy(H264_OneFrame+H264_1f_size, playload, playload_size);
        H264_1f_size += playload_size ;

        //幀結束標誌
        if(m_RTP_Header.M){
            if(H264_1f_size<pic_size3){ //size大小是否異常
                reValue = 1 ;
            }
            else{   //出錯則清零
                memset(H264_OneFrame, 0, pic_size3);
                H264_1f_size = 0 ;
            }

            //以下僅用於檔案測試
            //得到的視訊檔案可以用播放器開啟
            //static int i=0;
            //printf("running %d... \n",i++);
/*          FILE *fp = fopen("test.264","ab+");
            int a = fwrite(H264_OneFrame,1,H264_1f_size,fp);
            if(a!=H264_1f_size){
                printf("write file test.264 ERROR ! \n");
            }
            fflush(fp);
            fclose(fp);
*/          
            //memset(H264_OneFrame, 0, H264_1f_size);
            //H264_1f_size = 0 ;
            //以上僅用於測試

        }// end if(m_RTP_Header.M)

        return reValue ;
    }

};

// 2 class CH264_RTP_UNPACK_2 end  
//////////////////////////////////////////////////////////////////////////////////////////  

FFMPEG解碼實現

    終於到最後一步了。請編譯ffmpeg與x264。網上雖然很多資料,但大多不行,要自己綜合各方資料。請看上面的推薦連結。也許編譯這一步會有點麻煩,耐心一點總可以解決。

關於FFMPEG多執行緒問題:

    這裡注意多執行緒問題,如果你用FFMPEG解多路視訊的話,編譯的時候請--enable-w32thread. 否則不好說,網上很多人都說多執行緒會出現問題,但也有人沒有問題。而且就算你enable了多執行緒,裡面有些函式也是不支援多執行緒的。比如說:
/**
 ......
 * @warning This function is not thread safe!
 *
 * @note Always call this function before using decoding routines (such as
 .......
 */
int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

關於FFMPGE的例子:

    官方的資料裡有PLAYER的例子,還有test資料夾下的decode例子。推薦一個檔案api-h264-test.cpp,這個參考性很高。
    民間例子更多了,比如《最簡單的基於FFmpeg的視訊播放器 Simplest FFmpeg Player》雷霄驊。

    下面看一個由api-h264-test.cpp更裝過來的函式。
/*
    僅作測試
    功能:開啟一個filepath檔案並解碼
*/
int C_FFMPEG_Decode::onlyforfun_and_test()
{
    AVFormatContext *pFormatCtx;
    int             i, videoindex;
    AVCodecContext  *pCodecCtx;
    AVCodec         *pCodec;

    char filepath[]="test.264";
    //char filepath[]="rtsp://192.168.70.231/majorvideo_and_audio";
    //char filepath[]="rtsp://192.168.70.179/majorvideo_and_audio";

    av_register_all();
    //avformat_network_init();
    pFormatCtx = avformat_alloc_context();


    if(avformat_open_input(&pFormatCtx,filepath,NULL,NULL)!=0){
        printf("Couldn't open input stream.(無法開啟輸入流)\n");
        return -1;
    }
    if(avformat_find_stream_info(pFormatCtx,NULL)<0){
        printf("Couldn't find stream information.(無法獲取流資訊)\n");
        return -1;
    }
    videoindex=-1;
    for(i=0; i<pFormatCtx->nb_streams; i++) 
        if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO){
            videoindex=i;
            break;
        }
    if(videoindex==-1){
        printf("Didn't find a video stream.(沒有找到視訊流)\n");
        return -1;
    }
    pCodecCtx=pFormatCtx->streams[videoindex]->codec;
    pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
    if(pCodec==NULL)
    {
        printf("Codec not found.(沒有找到解碼器)\n");
        return -1;
    }
    if(avcodec_open2(pCodecCtx, pCodec,NULL)<0)
    {
        printf("Could not open codec.(無法開啟解碼器)\n");
        return -1;
    }



    //初始化引數,下面的引數應該由具體的業務決定  
    //pCodec    = avcodec_find_decoder(CODEC_ID_H264);  
    //pCodecCtx = avcodec_alloc_context3(pCodec); 
    //avcodec_open2(pCodecCtx, pCodec, NULL);

    pCodecCtx->time_base.num    = 1         ;  
    pCodecCtx->frame_number     = 1         ; //每包一個視訊幀  
    pCodecCtx->codec_type       = AVMEDIA_TYPE_VIDEO;  
    pCodecCtx->bit_rate         = 0         ;  
    pCodecCtx->time_base.den    = 25        ; //幀率  
    pCodecCtx->width            = 384       ; //視訊寬  
    pCodecCtx->height           = 288       ; //視訊高  



    AVFrame *pFrame,*pFrameYUV;
    pFrame=av_frame_alloc();
    pFrameYUV=av_frame_alloc();
    uint8_t *out_buffer=(uint8_t *)av_malloc(avpicture_get_size(AV_PIX_FMT_YUV444P, pCodecCtx->width, pCodecCtx->height));
    avpicture_fill((AVPicture *)pFrameYUV, out_buffer, AV_PIX_FMT_YUV444P, pCodecCtx->width, pCodecCtx->height);

    int ret, got_picture;

    AVPacket *packet=(AVPacket *)av_malloc(sizeof(AVPacket));
    //輸出一下資訊-----------------------------
    printf("File Information(檔案資訊)---------------------\n");
    av_dump_format(pFormatCtx,0,filepath,0);
    printf("-------------------------------------------------\n");

    struct SwsContext *img_convert_ctx;
    img_convert_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV444P, SWS_BICUBIC, NULL, NULL, NULL); 
    //--------------


    //Event Loop
    for (;;) {
        //Wait
        if(1){
            //------------------------------
            Sleep(40);
            if(av_read_frame(pFormatCtx, packet)>=0){
                if(packet->stream_index==videoindex){
                    ret = avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet);
                    if(ret < 0){
                        printf("Decode Error.(解碼錯誤)\n");
                        return -1;
                    }
                    if(got_picture){
                        sws_scale(img_convert_ctx, (const uint8_t* const*)pFrame->data, pFrame->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);

                        /////
                        int rows = pCodecCtx->height ;
                        int cols = pCodecCtx->width ;
                        if(rows && cols){

                            /* 請加入opencv
/*                          Mat imgBGR = Mat(rows,cols,CV_8UC3);
                            Mat img[3];
                            img[0] = Mat(rows,cols,CV_8UC1);
                            img[1] = Mat(rows,cols,CV_8UC1);
                            img[2] = Mat(rows,cols,CV_8UC1);

                            int size = rows*cols;
                            memcpy(img[0].data, pFrameYUV->data[0], size);
                            memcpy(img[1].data, pFrameYUV->data[1], size);
                            memcpy(img[2].data, pFrameYUV->data[2], size);

                            merge(img,3,imgBGR);

                            cvtColor(imgBGR,imgBGR,CV_YUV2BGR);
                            imshow("opencv",imgBGR);
                            waitKey(1);
*/                      }
                        ///////


                    }
                }
                av_free_packet(packet);
            }else{
                //Exit Thread
                break;
            }
        }

    }

    sws_freeContext(img_convert_ctx);

    //--------------
    av_free(out_buffer);
    av_free(pFrameYUV);
    avcodec_close(pCodecCtx);
    avformat_close_input(&pFormatCtx);

    return 0;
}
    這裡需要注意的是,視訊的寬、高、幀率、輸入格式、輸出格式等。
    細心者可以看到,這裡可以直接開啟URL。是的,但是不能開啟所有URL。如果自己實現RTSP協議,則可以開啟足夠多型別的URL。
    如果是開啟URL,那接收buffer要足夠大,不然會花屏、卡、跳幀等。

如果自己實現RTSP/RTP,那麼就要自己填充AVPacket了(AVPacket是FFMPEG中一個為數不多的重要結構體!)。參考程式碼如下:

//申請輸入碼流包空間
    //packet = (AVPacket *)av_malloc(sizeof(AVPacket));
    packet = av_packet_alloc();
    av_init_packet(packet);
    packet->data = (uint8_t *)av_malloc(width*height*3+64); //編碼包資料空間(不可能比原圖更大了)
    memset(packet->data, 0, width*height*3+64);             //加64是因為有些CPU是每次讀32/64位
    packet->size = 0 ;  //未有資料設為0
.....
packet->size = nal_size ;
memcpy(packet->data, packet->size,nal_data);
......
avcodec_decode_video2(...)
.....
memset(packet->data, 0, width*height*3+64);
packet->size = 0 ;
......

看看總體效果圖 
這裡寫圖片描述 
這裡寫圖片描述