调用时序错误导致webrtc无法建立链接

这段时间帮朋友做一个简单的HTML的webrtc连接,单对单,一端推流,一端拉流,东西极其简单,偏偏还遇到了几个坑,这里写出来供大家参考,顺便分享下代码。

要建立webrtc连接,首先需要一个模块帮助连接双方交换SDP和ICE,通常我们采用一个websocket服务器来做这个事,不过受限朋友原来代码,考虑用一个简单的HTTP服务来做这个事,连tomcat容器都不用,服务器用java写的,就是对每一个流名建立一个两个消息队列,A发的消息放A队列,B发的消息放B队列,A和B会有个定时器定时获取消息,如果没有消息就会收到nomessage, 放核心代码代码参考:

public class HttpServer implements Runnable {
    
    static public void StartHttpServer(Integer port) {
        Executors.newSingleThreadScheduledExecutor().execute(new HttpServer(port));
    }

    
    @Override
    public void run() {        
        ServerSocket server=null;
        // TODO Auto-generated method stub
        ExecutorService ex=Executors.newFixedThreadPool(200);
        try {
            //建立服务器监听端口
            server=new ServerSocket(port_);
            while(true) {
                Socket s=server.accept();
                //然后把接收到socket传给SocketServer并执行该线程
                ex.execute(new SocketServer(s));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    private class SocketServer implements Runnable{
        Socket s;
        private byte[] header;  
        
        //构造方法
        public SocketServer(Socket s) {
            this.s=s;
        }
        
        @SuppressWarnings("unused")
        private String getHeader(Integer datalen) {
            String header="HTTP/1.0 200 OK\r\n"+  
                    "Server: RtcServer 1.0\r\n"+  
                    "Content-length: "+datalen+"\r\n"+  
                    "Content-type: text/json\r\n"+
                    "Access-Control-Allow-Origin:*\r\n"+
                    "Access-Control-Allow-Methods:*\r\n"+
                    "Access-Control-Allow-Headers:*\r\n\r\n";  
             return header;
        }

        @Override
        public void run() {
            // TODO Auto-generated method stub
            //输入输出流
            OutputStream os=null;
            InputStream in=null;
            try 
            {    //打开socket输入流,并转为BufferedReader流
                in=s.getInputStream();
                BufferedReader br=new BufferedReader(new InputStreamReader(in));
                 
                //接收第一行,得到请求路径
                String strRequest=br.readLine();
                do{  
                    String newline=br.readLine();    
                    if (newline==null || newline.length()==0) {  
                        break;  
                    }  
                    //strRequest += "\r\n" +newline;
                }while (true);
                
                String stretcontent = ProccessRequest(strRequest);
                if(stretcontent==null)
                    stretcontent="";
                //打开socket对象的输出流,写入响应头
                os = s.getOutputStream();
                String heaner = getHeader(stretcontent.length());
                os.write(heaner.getBytes("ASCII"));
                os.write(stretcontent.getBytes("ASCII"));

                os.flush();
                //如果os流没有关闭的话,浏览器会以为内容还没传输完成,将一直显示不了内容
                os.close();
            }catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        private String ProccessRequest(String strRequest)
        {
            String strRetContent = "{\"retcode\":\"000010\"}";
            if(strRequest==null) {
                return strRetContent;
            }            
            int begin = strRequest.indexOf("GET /");
            if(begin==-1) {
                return strRetContent;
            }
            
            int end = strRequest.indexOf("HTTP/");
            if(end==-1) {
                return strRetContent;
            }
            String strReqCmd=strRequest.substring(begin+5, end); 
            if(strReqCmd==null) {
                return strRetContent;
            }
            
            int key = strReqCmd.indexOf("?");
            String strcmd = strReqCmd;
            if(key!=-1) {
                strcmd = strReqCmd.substring(0, key).toLowerCase().trim();
            }

            String parastr = strReqCmd.substring(key+1, strReqCmd.length()).trim();
            
            if(strcmd.contains("rtc_sdp") ){
                return ProccessRtcSdpMsg(parastr);
            }
            else if(strcmd.contains("rtc_icefinish")) {
                return ProccessRtcIceFinshMsg(parastr);    
            }            
            else if(strcmd.contains("rtc_ice")) {
                return ProccessRtcIceMsg(parastr);    
            }            
            else if(strcmd.contains("rtc_get")) {
                return ProccessRtcGetMsg(parastr);    
            }
            else if(strcmd.contains("start_stream")) {
                return ProccessStartStream(parastr);    
            }
            else {                
            }
            return strRetContent;            
        }
    }

推流端定时器定时获取消息,收到startstream消息,就开始创建connect和offer sdp, 外部只要调用connectToRtcChannel,参数为服务器地址,流的名称,video控件即可,Js代码:

var pcConfig = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var pcOptions = {
    optional: [
        {DtlsSrtpKeyAgreement: true}
    ]
}
var mediaConstraints = {'mandatory': {
    'OfferToReceiveAudio': true,
    'OfferToReceiveVideo': true }};
    
RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;

function connectToRtcChannel(server, channelname, videoelement)
{
	var channelobj={__server:server, __channelname: channelname, __videoele:videoelement,pc:null}
  
	setTimeout(getNewMessage,1000,server, channelname,channelobj);
	
	return channelobj;
}


function getNewMessage(server, channelname, channelobj){
	 $.ajax({
      type: "GET",
      url: server+'/rtc_get?from=server&channel='+channelname,
      dataType: 'json',
      success: function (data) {
      	
      	if(data['msgtype'] == 'remotesdp'){
      		var sdpstr = decodeURIComponent(data['sdp']);
      		var sdp = JSON.parse(sdpstr);
      		channelobj.pc.setRemoteDescription(new RTCSessionDescription(sdp), onRemoteSdpSucces, onRemoteSdpError);   
      		setTimeout(getNewMessage,500,server, channelname,channelobj);  
      	}
      	else if(data['msgtype'] == 'remoteice'){
      		var icestr = decodeURIComponent(data['ice']);
      		var ice = JSON.parse(icestr);
      		var candidate = new RTCIceCandidate({sdpMLineIndex: ice.sdpMLineIndex, candidate: ice.candidate,sdpMid:ice.sdpMid});
      		channelobj.pc.addIceCandidate(candidate, aic_success_cb, aic_failure_cb);
      		setTimeout(getNewMessage,500,server, channelname,channelobj);
      	}
        else if(data['msgtype'] == 'icefinish'){

        	return;
      	}
      	else if(data['msgtype'] == 'statstream'){
      		connectToRtcChannelInner(server,channelname,channelobj);
      		setTimeout(getNewMessage,500,server, channelname,channelobj);
      	}
      	else if(data['msgtype'] == 'nomessage'){
      		setTimeout(getNewMessage,1000,server, channelname,channelobj);
      	}
      }
  });
}


function connectToRtcChannelInner(server, channelname, channelobj)
{	  
	var	pc = new RTCPeerConnection(pcConfig, pcOptions);
	channelobj.pc=pc;

	pc.onicecandidate = function(event) {
    if (event.candidate) {
        var candidate = {
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            sdpMid: event.candidate.sdpMid,
            candidate: event.candidate.candidate
        };
        var data = JSON.stringify(candidate);
        console.log("onicecandidate ",data);
        sendChannelIceCandidate(server, channelname, data);
    } else {
      console.log("End of candidates.");
      sendChannelIceCandidateFinish(server,channelname);
    }
  };
  pc.onconnecting = onSessionConnecting;
  pc.onopen = onSessionOpened;
  pc.ontrack  = onRemoteStreamAdded;
  pc.onremovestream = onRemoteStreamRemoved;
	navigator.getUserMedia({ video: true, audio: true },
   stream => {       
       //window.stream = stream;        
       for (const track of stream.getTracks()) {
        channelobj.pc.addTrack(track, stream);
       } 
       
       channelobj.__videoele.srcObject = stream;    
       channelobj.__videoele.play();     
       
	     channelobj.pc.createOffer(function(sessionDescription) {
		      console.log("Create answer:", sessionDescription);
		      pc.setLocalDescription(sessionDescription,sld_success_cb,sld_failure_cb);
		      var data = JSON.stringify(sessionDescription);

		      sendChannelOfferSdp(server, channelname, data);
		      console.log(data);          
			  }, 
				function(error) { // error
				      console.log("Create answer error:", error);          
				}, mediaConstraints);	
   },
   err => {
        console.log(err);
   })   
     
  return pc;
}

    
function sendChannelOfferSdp(server, channelname,  sdp)
{	
	 $.ajax({
      type: "GET",
      url: server+'/rtc_sdp?from=server&channel='+channelname+'&sdp='+encodeURIComponent(sdp),
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,pc);
      }
  });
}

function sendChannelIceCandidate(server, channelname,  ice)
{
		 $.ajax({
      type: "GET",
      url: server+'/rtc_ice?from=server&channel='+channelname+'&ice='+ encodeURIComponent(ice),
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,pc);
      }
  });
}

function sendChannelIceCandidateFinish(server, channelname)
{
		 $.ajax({
      type: "GET",
      url: server+'/rtc_icefinish?from=server&channel='+channelname,
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,pc);
      }
  });
}

拉流端连到服务器,定时获取消息,收到SDP后设置并创建answer即可:

function connectToRtcChannel(server, channelname, videoshow)
{
	var channelobj={__server:server, __channelname: channelname, __videoele:videoshow,pc:null}
  
  $.ajax({
      type: "GET",
      url: server+'/start_stream?channel='+channelname,
      dataType: 'text',
      success: function (data) {      	

      }
  });
	setTimeout(getNewMessage,500,server, channelname,channelobj);
	return channelobj;
}

//-------------------------inner function----------------------------
var pcConfig = {"iceServers": [{"url": "stun:stun.l.google.com:19302"}]};
var pcOptions = {
    optional: [
        {DtlsSrtpKeyAgreement: true}
    ]
}
var mediaConstraints = {'mandatory': {
    'OfferToReceiveAudio': true,
    'OfferToReceiveVideo': true }};
    
RTCPeerConnection = window.mozRTCPeerConnection || window.webkitRTCPeerConnection;
RTCSessionDescription = window.mozRTCSessionDescription || window.RTCSessionDescription;
RTCIceCandidate = window.mozRTCIceCandidate || window.RTCIceCandidate;
getUserMedia = navigator.mozGetUserMedia || navigator.webkitGetUserMedia;

function getNewMessage(server, channelname, channelobj){
	 $.ajax({
      type: "GET",
      url: server+'/rtc_get?channel='+channelname,
      dataType: 'json',
      success: function (data) {
      	
      	if(data.msgtype == 'remotesdp'){
      		var sdpstr = decodeURIComponent(data['sdp']);
      		var sdp = JSON.parse(sdpstr);      		
      		connectToRtcChannelInner(server,channelname,channelobj,sdp);
      		setTimeout(getNewMessage,500,server, channelname,channelobj);
      	}
      	else if(data.msgtype == 'remoteice'){
      		var icestr = decodeURIComponent(data['ice']);
      		var ice = JSON.parse(icestr);
      		var candidate = new RTCIceCandidate({sdpMLineIndex: ice.sdpMLineIndex, candidate: ice.candidate,sdpMid:ice.sdpMid});
      		channelobj.pc.addIceCandidate(candidate, aic_success_cb, aic_failure_cb);
      		setTimeout(getNewMessage,500,server, channelname,channelobj);
      	}
        else if(data.msgtype == 'icefinish'){

        	return;      		
      	}
      	else if(data.msgtype == 'error'){
      		
      	}
      	else if(data.msgtype == 'nomessage'){
      		setTimeout(getNewMessage,1000,server, channelname,channelobj);
      	}

      }
  });
}

function connectToRtcChannelInner(server, channelname, channelobj,sdp)
{	 
	var	pc = new RTCPeerConnection(pcConfig, pcOptions);
	channelobj.pc = pc;
	
	pc.onicecandidate = function(event) {
      if (event.candidate) {
          var candidate = {
              sdpMLineIndex: event.candidate.sdpMLineIndex,
              sdpMid: event.candidate.sdpMid,
              candidate: event.candidate.candidate
          };
          var data = JSON.stringify(candidate);
          console.log("onicecandidate ",data);
          sendChannelIceCandidate(server, channelname,channelobj, data);
      } else {
        console.log("End of candidates.");
        sendChannelIceCandidateFinish(server,channelname);
      }
  };
  pc.ontrack  = function(event){
  	  if(event.track.kind=='video'){
			  channelobj.__videoele.srcObject = event.streams[0];
			  channelobj.__videoele.play();
		  }
  };
  pc.onconnecting = onSessionConnecting;
  pc.onopen = onSessionOpened;
  pc.onremovestream = onRemoteStreamRemoved;
  
  pc.setRemoteDescription(new RTCSessionDescription(sdp), onRemoteSdpSucces, onRemoteSdpError); 
  
  pc.createAnswer(function(sessionDescription) {
          console.log("Create answer:", sessionDescription);
          pc.setLocalDescription(sessionDescription,sld_success_cb,sld_failure_cb);
          var data = JSON.stringify(sessionDescription);

          sendChannelOfferSdp(server, channelname, channelobj, data);
          console.log(data);          
      }, 
      function(error) { // error
          console.log("Create answer error:", error);
  }, mediaConstraints);
  
  return pc;
}

function onRemoteStreamAdded(event) {
  console.log("Remote stream added:");
}

function onSessionConnecting(message) {
    console.log("Session connecting.");
}

function onSessionOpened(message) {
    console.log("Session opened.");
}

function onRemoteStreamRemoved(event) {
    console.log("Remote stream removed.");
}

function onRemoteSdpError(event) {
    console.error('onRemoteSdpError', event.name, event.message);
}

function onRemoteSdpSucces() {
    console.log('onRemoteSdpSucces');
} 

function sld_success_cb() {
	console.log("setLocalDescription success");
}

function sld_failure_cb() {
  console.log("setLocalDescription failed");
}

function aic_success_cb() {
	console.log("addIceCandidate success.");
}

function aic_failure_cb() {
  console.log("addIceCandidate failed");
}

function sendChannelOfferSdp(server, channelname, channelobj, sdp)
{	
	 $.ajax({
      type: "GET",
      url: server+'/rtc_sdp?channel='+channelname+'&sdp='+encodeURIComponent(sdp),
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,channelobj);
      }
  });
}

function sendChannelIceCandidate(server, channelname, channelobj, ice)
{
		 $.ajax({
      type: "GET",
      url: server+'/rtc_ice?channel='+channelname+'&ice='+ encodeURIComponent(ice),
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,channelobj);
      }
  });
}

function sendChannelIceCandidateFinish(server, channelname)
{
		 $.ajax({
      type: "GET",
      url: server+'/rtc_icefinish?channel='+channelname,
      dataType: 'text',
      success: function (data) {
				//setTimeout(getNewMessage,1000,server, channelname,pc);
      }
  });
}

遇到的坑1:

推流端一定要在getUserMedia函数回调里才能调用createOffer, 跟在getUserMedia调用后面createOffer,可能sdp都发出去了,stream还没有add

遇到的坑2:

RTCPeerConnection的onaddstream回调已经废弃,使用ontrack代替了,网上一些比较老的例子还在用onstreamadd

遇到的坑3:

SDP和ICE是区分大小写的,服务器里把收到的消息做了个toLowerCase,导致sdp设置失败,失败原因也莫名其妙

 

你可能感兴趣的:(调用时序错误导致webrtc无法建立链接)