开发学院,分享开发教程和最新动态

WebRTC 实例之语音演示(二):客户端

WebRTC 实例之语音演示(二):客户端

  测试该应用程序的一种方法是打开两个浏览器选项卡并尝试彼此调用。

 首先,我们需要安装bootstrap库。bootstrap是开发web的前端框架。你可以在http://getbootstrap.com/学到更多. 创建一个名为videochat的目录, 这是我们的应用程序根目录. 在文件中创建package.json (它是管理NPM依赖关系的必要条件)并添加如下内容:

{ 
   "name": "webrtc-audiochat", 
   "version": "0.1.0", 
   "description": "webrtc-audiochat", 
   "author": "Author", 
   "license": "BSD-2-Clause" 
}

 运行npm install bootstrap,将在videochat/node_modules文件中安装bootstrap库

  现在我们需要创建一个基本的html页面。在根文件夹中创建一个具有以下代码的index .html文件.

<html>
   <head> 
      <title>WebRTC Voice Demo</title> 
      <link rel = "stylesheet" href = "node_modules/bootstrap/dist/css/bootstrap.min.css"/> 
   </head>
 
   <style> 
      body { 
         background: #eee; 
         padding: 5% 0; 
      } 
   </style>
   <body> 
      <div id = "loginPage" class = "container text-center"> 
         <div class = "row"> 
            <div class = "col-md-4 col-md-offset-4">
               <h2>WebRTC Voice Demo. Please sign in</h2>
               <label for = "usernameInput" class = "sr-only">Login</label> 
               <input type = "email" id = "usernameInput" 
                  class = "form-control formgroup"
                  placeholder = "Login" required = "" autofocus = ""> 
               <button id = "loginBtn" class = "btn btn-lg btn-primary btnblock">
                  Sign in</button> 
            </div> 
         </div> 
      </div>
      <div id = "callPage" class = "call-page">
         <div class = "row"> 
            <div class = "col-md-6 text-right"> 
               Local audio: <audio id = "localAudio" 
               controls autoplay></audio> 
            </div>
            <div class = "col-md-6 text-left"> 
               Remote audio: <audio id = "remoteAudio" 
                  controls autoplay></audio> 
            </div> 
         </div> 
         <div class = "row text-center"> 
            <div class = "col-md-12"> 
               <input id = "callToUsernameInput" 
                  type = "text" placeholder = "username to call" /> 
               <button id = "callBtn" class = "btn-success btn">Call</button> 
               <button id = "hangUpBtn" class = "btn-danger btn">Hang Up</button> 
            </div> 
         </div>
      </div> 
      <script src = "client.js"></script> 
   </body>
</html>

  这个页面大家该很熟悉,我们添加了bootstrap的CSS文件。我们还定义了两个div。最后,我们创建了几个文本字段和按钮,以便从用户获取信息。您应该看到本地和远程视频流的两个视频元素。请注意,我们已添加了指向client.js文件的链接。

  现在我们需要与我们的信令服务器建立连接。使用以下代码在根目录中创建client.js文件:

//our username 
var name; 
var connectedUser;
  
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
  
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
}; 
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data);  
   var data = JSON.parse(msg.data);  
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break;
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
};
  
conn.onerror = function (err) { 
   console.log("Got error", err); 
};
  
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
   conn.send(JSON.stringify(message)); 
};

  现在使用node运行我们的信令服务器,然后,在在根目录中运行static命令并在浏览器中打开页面。您应该看到以下控制台输出:

run_signalling_server.jpg

  下一步是使用唯一的用户名实现用户登录。我们向服务器发送一个用户名,然后服务器告诉我们是否接受了它。将以下代码添加到您的client.js文件中:

//****** 
//UI selectors block 
//******

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn');
 
var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn');
 
var hangUpBtn = document.querySelector('#hangUpBtn');
  
callPage.style.display = "none";
  
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value;
   if (name.length > 0) { 
      send({
         type: "login", 
         name: name 
      }); 
   } 
}); 
 
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
      //********************** 
      //Starting a peer connection 
      //**********************
        
   } 
};

  首先,我们选择对页面元素的引用。我们隐藏名字为call page的div元素。然后,我们在登录按钮上添加一个事件侦听器,当用户单击它时,我们将他的用户名发送到服务器。最后,实现了handleLogin回调。如果登录成功,我们将显示名为call page的div,并开始设置对等连接。

  要启动对等连接,我们需要:

  •   从麦克风获取流。

  •   创建rtcpeerconnection对象。

  将以下代码添加到“UI selectors block”中:

var localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

var yourConn; 
var stream;
  修改handleLogin方法
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block";
      //********************** 
      //Starting a peer connection 
      //********************** 
      //getting local audio stream 
      navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { 
         stream = myStream; 
         //displaying local audio stream on the page
         localAudio.src = window.URL.createObjectURL(stream);
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
         }; 
         yourConn = new webkitRTCPeerConnection(configuration); 
         // setup stream listening 
         yourConn.addStream(stream); 
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteAudio.src = window.URL.createObjectURL(e.stream); 
         }; 
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
               }); 
            } 
         };  
      }, function (error) { 
         console.log(error); 
      }); 
   } 
};

  现在运行代码,页面应该显示名字输入框并在页面上显示本地音频流。

local_audio_stream.jpg

  现在我们准备发起一个呼叫请求。首先,我们向另一个用户发送一个询问。一旦用户获得这个请求,他就会创建一个应答,并开始选择ice候选。将以下代码添加到client.js文件中:

//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
   if (callToUsername.length > 0) { 
      connectedUser = callToUsername; 
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({ 
            type: "offer", 
            offer: offer 
         }); 
         yourConn.setLocalDescription(offer); 
      }, function (error) { 
         alert("Error when creating an offer"); 
      }); 
   } 
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer)); 
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
      send({ 
         type: "answer",
         answer: answer 
      }); 
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
};
 
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};

  我们将单击处理添加到call按钮,该按钮将启动一个请求。然后我们实现了onmessage处理程序期望的几个处理程序。它们将异步处理,直到用户建立连接为止。

  最后一步是实现挂起功能。这将停止传输数据,并告诉其他用户关闭该呼叫。添加以下代码:

//hang up 
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   });  
   handleLeave(); 
});
  
function handleLeave() { 
   connectedUser = null; 
   remoteAudio.src = null;
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null;
};

当用户单击“hangup”按钮时:

  •   它将向其他用户发送“leave”消息

  •   它将关闭rtcpeerconnection并在本地销毁连接

  现在运行代码。您应该能够正常使用功能了。

login_to_the_server.jpg

voice_call_and_hang_up.jpg

  下面是完整的client.js代码

//our username 
var name; 
var connectedUser;
 
//connecting to our signaling server 
var conn = new WebSocket('ws://localhost:9090');
 
conn.onopen = function () { 
   console.log("Connected to the signaling server"); 
};
 
//when we got a message from a signaling server 
conn.onmessage = function (msg) { 
   console.log("Got message", msg.data); 
   var data = JSON.parse(msg.data); 
   switch(data.type) { 
      case "login": 
         handleLogin(data.success); 
         break; 
      //when somebody wants to call us 
      case "offer": 
         handleOffer(data.offer, data.name); 
         break; 
      case "answer": 
         handleAnswer(data.answer); 
         break; 
      //when a remote peer sends an ice candidate to us 
      case "candidate": 
         handleCandidate(data.candidate); 
         break; 
      case "leave": 
         handleLeave(); 
         break; 
      default: 
         break; 
   } 
}; 

conn.onerror = function (err) { 
   console.log("Got error", err); 
};
 
//alias for sending JSON encoded messages 
function send(message) { 
   //attach the other peer username to our messages 
   if (connectedUser) { 
      message.name = connectedUser; 
   } 
   conn.send(JSON.stringify(message)); 
};
 
//****** 
//UI selectors block 
//****** 

var loginPage = document.querySelector('#loginPage'); 
var usernameInput = document.querySelector('#usernameInput'); 
var loginBtn = document.querySelector('#loginBtn');

var callPage = document.querySelector('#callPage'); 
var callToUsernameInput = document.querySelector('#callToUsernameInput');
var callBtn = document.querySelector('#callBtn'); 

var hangUpBtn = document.querySelector('#hangUpBtn'); 
var localAudio = document.querySelector('#localAudio'); 
var remoteAudio = document.querySelector('#remoteAudio'); 

var yourConn; 
var stream; 

callPage.style.display = "none";
 
// Login when the user clicks the button 
loginBtn.addEventListener("click", function (event) { 
   name = usernameInput.value; 
   if (name.length > 0) { 
      send({ 
         type: "login", 
         name: name 
      }); 
   } 
});
 
function handleLogin(success) { 
   if (success === false) { 
      alert("Ooops...try a different username"); 
   } else { 
      loginPage.style.display = "none"; 
      callPage.style.display = "block"; 
      //********************** 
      //Starting a peer connection 
      //********************** 
      //getting local audio stream 
      navigator.webkitGetUserMedia({ video: false, audio: true }, function (myStream) { 
         stream = myStream; 
         //displaying local audio stream on the page 
         localAudio.src = window.URL.createObjectURL(stream);
         //using Google public stun server 
         var configuration = { 
            "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] 
         }; 
         yourConn = new webkitRTCPeerConnection(configuration); 
         // setup stream listening 
         yourConn.addStream(stream); 
         //when a remote user adds stream to the peer connection, we display it 
         yourConn.onaddstream = function (e) { 
            remoteAudio.src = window.URL.createObjectURL(e.stream); 
         }; 
         // Setup ice handling 
         yourConn.onicecandidate = function (event) { 
            if (event.candidate) { 
               send({ 
                  type: "candidate", 
                  candidate: event.candidate 
               }); 
            } 
         }; 
      }, function (error) { 
         console.log(error); 
      }); 
   } 
};
 
//initiating a call 
callBtn.addEventListener("click", function () { 
   var callToUsername = callToUsernameInput.value; 
   if (callToUsername.length > 0) { 
      connectedUser = callToUsername; 
      // create an offer 
      yourConn.createOffer(function (offer) { 
         send({
            type: "offer", 
            offer: offer 
         }); 
         yourConn.setLocalDescription(offer); 
      }, function (error) { 
         alert("Error when creating an offer"); 
      }); 
   } 
});
 
//when somebody sends us an offer 
function handleOffer(offer, name) { 
   connectedUser = name; 
   yourConn.setRemoteDescription(new RTCSessionDescription(offer)); 
   //create an answer to an offer 
   yourConn.createAnswer(function (answer) { 
      yourConn.setLocalDescription(answer); 
      send({ 
         type: "answer", 
         answer: answer 
      });
   }, function (error) { 
      alert("Error when creating an answer"); 
   }); 
};
 
//when we got an answer from a remote user 
function handleAnswer(answer) { 
   yourConn.setRemoteDescription(new RTCSessionDescription(answer)); 
};
 
//when we got an ice candidate from a remote user 
function handleCandidate(candidate) { 
   yourConn.addIceCandidate(new RTCIceCandidate(candidate)); 
};
 
//hang up
hangUpBtn.addEventListener("click", function () { 
   send({ 
      type: "leave" 
   }); 
   handleLeave(); 
});
 
function handleLeave() { 
   connectedUser = null; 
   remoteAudio.src = null; 
   yourConn.close(); 
   yourConn.onicecandidate = null; 
   yourConn.onaddstream = null; 
};