模块一(为什么使用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收到的长度和。数据项的长度。如果两项长度相同的话就就正常的解析。
这里需要考虑终端发送数据的各种情况 。一下可能接受到几包或者一包中的一个片段。时间戳增量作为一个包和的分割。通过分包标志(是否是原子包或者最后一包),作为一帧数据的标志
下片介绍对解析到的数据的进一步的处理