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收到的長度和。資料項的長度。如果兩項長度相同的話就就正常的解析。
這裡需要考慮終端傳送資料的各種情況 。一下可能接受到幾包或者一包中的一個片段。時間戳增量作為一個包和的分割。通過分包標誌(是否是原子包或者最後一包),作為一幀資料的標誌
下片介紹對解析到的資料的進一步的處理