在本章中,我们将构建一个客户端应用程序,允许不同设备上的两个用户使用WebRTC音频流进行通信.我们的申请将有两页.一个用于登录,另一个用于向另一个用户进行音频呼叫.
这两个页面将是 div 标签.大多数输入是通过简单的事件处理程序完成的.
信令服务器
要创建WebRTC连接,客户端必须能够在不使用WebRTC的情况下传输邮件同伴连接.这是我们将使用HTML5 WebSockets : 两个端点之间的双向套接字连接 : Web服务器和Web浏览器.现在让我们开始使用WebSocket库.创建 server.js 文件并插入以下代码 :
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //when a user connects to our sever wss.on('connection', function(connection) { console.log("user connected"); //when server gets a message from a connected user connection.on('message', function(message) { console.log("Got message from a user:", message); }); connection.send("Hello from server"); });
第一行需要我们已安装的WebSocket库.然后我们在端口9090上创建一个套接字服务器.接下来,我们监听 connection 事件.当用户与服务器建立WebSocket连接时,将执行此代码.然后我们收听用户发送的任何消息.最后,我们向连接的用户发送一个回复,说"来自服务器的Hello".
在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道在哪里发送消息.让我们稍微改变我们的连接处理程序;
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } });
这样我们只接受JSON消息.接下来,我们需要将所有连接的用户存储在某个地方.我们将使用一个简单的Javascript对象.更改我们文件的顶部 :
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};
我们将为来自客户端的每条消息添加类型字段.例如,如果用户想要登录,则他发送登录类型的消息.让我们定义它 :
connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged:", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; default: sendTo(connection, { type: "error", message: "Command no found: " + data.type }); break; } });
如果用户发送带有登录类型的消息,我们 :
检查是否有人已使用此用户名登录.
如果是,请告诉用户他尚未成功登录.
如果没有人使用此用户名,我们会将用户名添加为连接对象的密钥.
如果命令无法识别,我们发送一个错误.
以下代码是用于向连接发送消息的辅助函数.将其添加到 server.js 文件 :
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
当用户断开连接时,我们应该清理它的连接.我们可以在触发关闭事件时删除用户.将以下代码添加到连接处理程序 :
connection.on("close", function() { if(connection.name) { delete users[connection.name]; } });
成功登录后,用户想要拨打另一个.他应该向另一个用户提出 offer 来实现它.添加 offer 处理程序 :
case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break;
首先,我们得到我们试图呼叫的用户的连接.如果它存在,我们发送 offer 详细信息.我们还将 otherName 添加到 connection 对象.这是为了以后查找它的简单性.
回答响应的模式类似于我们在 offer 处理程序中使用的模式.我们的服务器只是将回答的所有消息传递给另一个用户.在 offer 处理程序 :
case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break;
最后一部分是处理用户之间的ICE候选者.我们使用相同的技术只是在用户之间传递消息.主要区别在于候选消息可能以任何顺序每个用户多次发生.添加候选人处理程序 :
case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break;
为了让我们的用户与其他用户断开连接,我们应该实现挂机功能.它还会告诉服务器删除所有用户引用.添加离开处理程序 :
case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break;
这也将向其他用户发送离开事件,以便他可以相应地断开他的对等连接.当用户从信令服务器断开连接时,我们也应该处理这种情况.让我们修改我们的关闭处理程序 : 去;
connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } });
以下是我们信令服务器的整个代码 :
//require our websocket library var WebSocketServer = require('ws').Server; //creating a websocket server at port 9090 var wss = new WebSocketServer({port: 9090}); //all connected to the server users var users = {};//when a user connects to our sever wss.on('connection', function(connection) { console.log("User connected"); //when server gets a message from a connected user connection.on('message', function(message) { var data; //accepting only JSON messages try { data = JSON.parse(message); } catch (e) { console.log("Invalid JSON"); data = {}; } //switching type of the user message switch (data.type) { //when a user tries to login case "login": console.log("User logged", data.name); //if anyone is logged in with this username then refuse if(users[data.name]) { sendTo(connection, { type: "login", success: false }); } else { //save user connection on the server users[data.name] = connection; connection.name = data.name; sendTo(connection, { type: "login", success: true }); } break; case "offer": //for ex. UserA wants to call UserB console.log("Sending offer to: ", data.name); //if UserB exists then send him offer details var conn = users[data.name]; if(conn != null) { //setting that UserA connected with UserB connection.otherName = data.name; sendTo(conn, { type: "offer", offer: data.offer, name: connection.name }); } break; case "answer": console.log("Sending answer to: ", data.name); //for ex. UserB answers UserA var conn = users[data.name]; if(conn != null) { connection.otherName = data.name; sendTo(conn, { type: "answer", answer: data.answer }); } break; case "candidate": console.log("Sending candidate to:",data.name); var conn = users[data.name]; if(conn != null) { sendTo(conn, { type: "candidate", candidate: data.candidate }); } break; case "leave": console.log("Disconnecting from", data.name); var conn = users[data.name]; conn.otherName = null; //notify the other user so he can disconnect his peer connection if(conn != null) { sendTo(conn, { type: "leave" }); } break; default: sendTo(connection, { type: "error", message: "Command not found: " + data.type }); break; } }); //when user exits, for example closes a browser window //this may help if we are still in "offer","answer" or "candidate" state connection.on("close", function() { if(connection.name) { delete users[connection.name]; if(connection.otherName) { console.log("Disconnecting from ", connection.otherName); var conn = users[connection.otherName]; conn.otherName = null; if(conn != null) { sendTo(conn, { type: "leave" }); } } } }); connection.send("Hello world"); }); function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
客户端应用程序
测试此应用程序的一种方法是打开两个浏览器选项卡并尝试互相进行音频通话.
首先,我们需要安装 bootstrap 库. Bootstrap是用于开发Web应用程序的前端框架.您可以在 http://getbootstrap.com/.了解更多信息.创建一个名为的文件夹,例如,"audiochat".这将是我们的根应用程序文件夹.在此文件夹中创建一个文件 package.json (这是管理npm依赖项所必需的)并添加以下 :
{ "name": "webrtc-audiochat", "version": "0.1.0", "description": "webrtc-audiochat", "author": "Author", "license": "BSD-2-Clause" }
然后运行 npm install bootstrap .这将在 audiochat/node_modules 文件夹中安装引导程序库.
现在我们需要创建一个基本的HTML页面.在根文件夹中使用以下代码创建 index.html 文件 :
WebRTC Voice Demo WebRTC Voice Demo. Please sign in
Local audio:Remote audio:
您应该熟悉此页面.我们添加了 bootstrap css文件.我们还定义了两个页面.最后,我们创建了几个文本字段和按钮,用于从用户获取信息.您应该看到本地和远程音频流的两个音频元素.请注意,我们已添加指向 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)); };
现在通过节点服务器运行我们的信令服务器.然后,在根文件夹内运行 static 命令并在浏览器中打开页面.您应该看到以下控制台输出 :
下一步是使用唯一的用户名实现用户登录.我们只需向服务器发送一个用户名,然后告诉我们是否采用了该用户名.将以下代码添加到 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 //********************** } };
首先,我们选择对页面元素的一些引用.我们隐藏了呼叫页面.然后,我们在登录按钮上添加一个事件监听器.当用户点击它时,我们将他的用户名发送到服务器.最后,我们实现了handleLogin回调.如果登录成功,我们将显示呼叫页面并开始设置对等连接.
要启动对等连接,我们需要 :
从麦克风获取音频流
创建RTCPeerConnection对象
将以下代码添加到"UI选择器块" :
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); }); } };
现在,如果您运行代码,该页面应该允许您登录并在页面上显示本地音频流.
现在我们已准备好发起呼叫.首先,我们向另一个用户发送报价.一旦用户获得报价,他就会创建回答并开始交易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)); };
我们在"呼叫"按钮上添加了一个单击处理程序,该按钮启动了优惠.然后我们实现了 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;};
当用户点击挂断按钮 : 去;
它将向其他用户发送"离开"消息
它将关闭RTCPeerConnection并在本地销毁连接
现在运行代码.您应该能够使用两个浏览器选项卡登录服务器.然后,您可以拨打音频呼叫并挂断电话.
以下是整个 client.js file :
//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 uphangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; remoteAudio.src = null; yourConn.close(); yourConn.onicecandidate = null; yourConn.onaddstream = null; };