1. 程式人生 > >App與終端裝置通訊經驗五(流媒體傳輸對碼流的解析)

App與終端裝置通訊經驗五(流媒體傳輸對碼流的解析)

模組一(為什麼使用RTP協議):
一提到流媒體傳輸、一談到什麼視訊監控、視訊會議、語音電話(VOIP),都離不開RTP協議的應用,但當大家都根據經驗或者別人的應用而選擇RTP協議的時候,你可曾想過,為什麼我們要使用RTP來進行流媒體的傳輸呢?為什麼我們一定要用RTP?難道TCP、UDP或者其他的網路協議不能達到我們的要求麼?

RTP與TCP的比較

像TCP這樣的可靠傳輸協議,通過超時和重傳機制來保證傳輸資料流中的每一個bit的正確性,但這樣會使得無論從協議的實現還是傳輸的過程都變得非常的複雜。而且,當傳輸過程中有資料丟失的時候,由於對資料丟失的檢測(超時檢測)和重傳,會資料流的傳輸被迫暫停和延時。

或許你會說,我們可以利用客戶端構造一個足夠大的緩衝區來保證顯示的正常,這種方法對於從網路播放音視訊來說是可以接受的,但是對於一些需要實時互動的場合(如視訊聊天、視訊會議等),如果這種緩衝超過了200ms,將會產生難以接受的實時性體驗。

RTP協議是一種基於UDP的傳輸協議,RTP本身並不能為按順序傳送資料包提供可靠的傳送機制,也不提供流量控制或擁塞控制,它依靠RTCP提供這些服務。這樣,對於那些丟失的資料包,不存在由於超時檢測而帶來的延時,同時,對於那些丟棄的包,也可以由上層根據其重要性來選擇性的重傳。比如,對於I幀、P幀、B幀資料,由於其重要性依次降低,故在網路狀況不好的情況下,可以考慮在B幀丟失甚至P幀丟失的情況下不進行重傳,這樣,在客戶端方面,雖然可能會有短暫的不清晰畫面,但卻保證了實時性的體驗和要求。

RTP協議支援多播技術,節省了頻寬
(1)RTP協議在設計上考慮到安全功能,支援加密資料和身份驗證功能。
(2)有較少的首部開銷
TCP和XTP相對RTP來說具有過多的首部開銷(TCP和XTP3.6是40位元組,XTP4.0是32位元組,而RTP只有12位元組)

模組二:本專案的RTP詳情。

在這裡插入圖片描述
在這裡插入圖片描述

結合實際的程式碼,來看看解析的過程。

SClientInputThreadTM{
......
    public void run() {
      while (isStart) {
                int result = dis.read(b);
                if (result==-1){
                    Log.e("@@","@@接受資料的長度---------:"+result);
                    //出現-1為沒資料情況  長時間沒資料斷開伺服器  暫定10000ms
                    if (outtime==0) {
                        outtime = new Date().getTime();
                    }else{
                        if (new Date().getTime()-outtime>=10000){
                            messageListener.Message("outTime");
                            return;
                        }
                    }
                }else {
                    Log.e(TAG,"@@@接受資料的長度--%"+result+"碼流返回"+""+StringUtils.bytesToHexString(b));
                    outtime=0;
                    setdata(b, result);
                }
             }
	}

}

當read方法接收到資料後,會傳入接收資料的長度和相應的位元組陣列,執行setdata()方法。

    private void setdata(byte[] data,int len){
    
            int alllen=len+frontdatalength;//待處理的資料長度  包括上一次快取的長度
            byte[] temp=new byte[alllen];//新的位元組陣列的長度
            System.arraycopy(frontdata,0,temp,0,frontdatalength);//先把快取的位元組陣列放入到temp中
            System.arraycopy(data,0,temp,frontdatalength,len);//再把來的待處理的資料也加入進來
            frontdatalength=0;//清空掉快取資料
            int header=isHeader(temp);//30316364出現的位置,根據前面的文件定義,知道這個方法是用來判斷是否是幀頭的。如果是幀頭,那麼返回的是幀頭在temp陣列的位置
            Log.e("@@@","@@@header"+header+"--alllen:"+alllen+"--frontdatalength:"+frontdatalength);//實時的打印出幀頭的位置header,資料的總長度alllen,前一幀的快取長度。
             if (header==-1){//如果沒有找到幀頭,那麼就說明這包資料沒有幀頭資訊,把資料反向拷貝到frontdata中
            frontdatalength=alllen;
            System.arraycopy(temp,0,frontdata,0,alllen);
            return;
        }
         int headers=15+header;//RTP頭能夠拿到資料型別,這裡加上15是為了直接到達時間戳的位置
                 if (alllen>headers){//待處理的資料長度大於當前索引到的長度
            String stypeF = getData(temp, header+15, 1);//rtp頭資料型別,只有4bit,前4bits用於標記資料型別,另外後面的4bit用於分包標記,當前未處理分包
  			String stype=stypeF.substring(0,1);//資料型別。這裡拿到的應該是二進位制0000對應的十進位制資料
            String fbtype=stypeF.substring(1,2);//分包型別,分包資料暫時沒有做處理
              int length=0;//資料長度
              int alllength=0;
                if (stype.equals("3")) {//為音訊),RTP頭26位元組,長度標誌位在第24位元組
                headers=header+26;
                 if(alllen>(headers)) {
                    String slength = getData(temp, header + 24, 2);//rtp頭資料長度資訊
                    length = Integer.parseInt(slength, 16);//資料長度
                    alllength=length+headers;//資料從開始到結尾的長度
                }else {//如果長度沒有達到資料體,那麼繼續作為快取
                    frontdatalength=alllen;
                    System.arraycopy(temp,0,frontdata,0,alllen);
                    return;
                }else{//為視訊RTP頭30位元組,長度標誌位在第28位元組
                headers=header+30;
                if(alllen>(headers)) {
                    String slength = getData(temp, header + 28, 2);//rtp頭資料長度資訊
                    length = Integer.parseInt(slength, 16);//資料長度
                    alllength=length+headers;//資料從開始到結尾的長度
                }else {//如果長度沒有達到資料體,那麼繼續作為快取
                    frontdatalength=alllen;
                    System.arraycopy(temp,0,frontdata,0,alllen);
                    return;
                }
                //以上都是一些對資料長度檢測的判斷,那麼接下來會把資料加進來
                 if(alllength==alllen){//剛剛好為一個數據段,接收執行緒接收到的資料的長度,恰好等同於擷取到的head和資料體的長度。那麼恰好為一個數據段
					  if (stype.equals("3")){//為音訊
					  String  currenttimestamp = getData(temp, header + 16, 8);//拿到時間戳
					    timestamp = currenttimestamp;
                        if (oneg726length>0){
                            byte[] g726temp = new byte[oneg726length];
                            System.arraycopy(oneg726, 0, g726temp, 0, oneg726length);
                            allg726.add(g726temp);
                            oneg726length = 0;
                        }
                           System.arraycopy(temp,header+26,oneg726,oneg726length,alllen-header-26);//4位元組的海思頭在這去掉了
                    oneg726length=oneg726length+alllen-header-26;
					  }else{//為視訊
					      System.arraycopy(temp,header+30,onemp4,onemp4length,alllen-header-30);
                    onemp4length=onemp4length+alllen-header-30;
                    String  currenttimestamp = getData(temp, header + 16, 8);
                    if (!currenttimestamp.equals(timestamp)) {//新的一個(音視訊包,幀)開始
                        timestamp = currenttimestamp;
                    }
                    if (fbtype.equals("0")||fbtype.equals("2")){//原子包或最後一個包
                        byte[] mp4temp = new byte[onemp4length];
                        System.arraycopy(onemp4, 0, mp4temp, 0, onemp4length);
                        allmp4.add(mp4temp);
                        onemp4length = 0;
                    }
			}else if(alllength>alllen){//不足一個數據段  留到下次解析
                frontdatalength=alllen;
                System.arraycopy(temp,0,frontdata,0,alllen);
				 }else{//超過一個數據段  繼續解析
                if (stype.equals("3")){//為音訊
                    String  currenttimestamp = getData(temp, header + 16, 8);
                    if (!currenttimestamp.equals(timestamp)) {//新的一個(音視訊包,幀)開始
                        timestamp = currenttimestamp;
                        if (oneg726length>0){
                            byte[] g726temp = new byte[oneg726length];
                            System.arraycopy(oneg726, 0, g726temp, 0, oneg726length);
                            allg726.add(g726temp);
                            oneg726length = 0;
                        }
                    }
                    System.arraycopy(temp,header+26,oneg726,oneg726length,length);
                    oneg726length=oneg726length+length;

                }else{//為視訊
                    System.arraycopy(temp,header+30,onemp4,onemp4length,length);
                    onemp4length=onemp4length+length;
                    String  currenttimestamp = getData(temp, header + 16, 8);
                    if (!currenttimestamp.equals(timestamp)) {//新的一個(音視訊包,幀)開始
                        timestamp = currenttimestamp;
                    }
                    if (fbtype.equals("0")||fbtype.equals("2")){//原子包或最後一個包
                        byte[] mp4temp = new byte[onemp4length];
                        System.arraycopy(onemp4, 0, mp4temp, 0, onemp4length);
                        allmp4.add(mp4temp);
                        onemp4length = 0;
                    }
//                    allmp4.add(ttemp);
                  }
                  //繼續解析剩餘的,有個遞迴的思想
                byte[] surplustemp=new byte[alllen-alllength];
                System.arraycopy(temp,alllength,surplustemp,0,alllen-alllength);
                setdata(surplustemp,alllen-alllength);
                }

                }
 
}                 
//成員變數
  byte[] onemp4=new byte[bytelength];//存放一幀視訊流
    byte[] oneg726=new byte[audiobyteLength];//存放一幀音訊流
    int onemp4length=0;//存放一幀視訊流當前長度
    int oneg726length=0;//存放一幀音訊流當前長度
    String timestamp;//音視訊的時間戳
 /**
     * 判斷資料頭
     * @param temp
     * @return
     */
    private int isHeader(byte[] temp) {
        int i = 0, j = 1;
        byte[] header = {0x30, 0x31, 0x63, 0x64};
        if (temp.length > 4) {
//            Log.v("收到訊息", "@@msg" +"  "+temp[0]+" "+temp[1]+" "+temp[2]+" "+temp[3]);
            for (i = 0; i < temp.length - 4; i++) {
                if (temp[i] == header[0]) {
                    for (j = 1; j < header.length; j++) {
                        if (temp[i + j] != header[j]) {
                            break;
                        }
                        if (j == header.length - 1) {
                            return i;
                        }
                    }
                }
            }
        } else {
            return -1;
        }
        return -1;
    }

			
    }

整個的解析過程有個遞迴的思想。通過比對當前ServerSocket收到的長度和。資料項的長度。如果兩項長度相同的話就就正常的解析。
這裡需要考慮終端傳送資料的各種情況 。一下可能接受到幾包或者一包中的一個片段。時間戳增量作為一個包和的分割。通過分包標誌(是否是原子包或者最後一包),作為一幀資料的標誌

下片介紹對解析到的資料的進一步的處理