- WebRTC 教程
- WebRTC - 首页
- WebRTC - 概述
- WebRTC - 架构
- WebRTC - 环境
- WebRTC - MediaStream API
- WebRTC - RTCPeerConnection API
- WebRTC - RTCDataChannel API
- WebRTC - 发送消息
- WebRTC - 信令
- WebRTC - 浏览器支持
- WebRTC - 移动设备支持
- WebRTC - 视频演示
- WebRTC - 语音演示
- WebRTC - 文本演示
- WebRTC - 安全性
- WebRTC 资源
- WebRTC - 快速指南
- WebRTC - 有用资源
- WebRTC - 讨论
WebRTC - 文本演示
在本章中,我们将构建一个客户端应用程序,允许两个在不同设备上的用户使用 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 连接时,将执行此代码。然后,我们监听用户发送的任何消息。最后,我们向已连接的用户发送一个“来自服务器的问候”响应。
在我们的信令服务器中,我们将为每个连接使用基于字符串的用户名,以便我们知道将消息发送到哪里。让我们稍微修改一下我们的connection处理程序 -
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 = {};
我们将为来自客户端的每条消息添加一个type字段。例如,如果用户想要登录,他发送login类型的消息。让我们定义它 -
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; } });
如果用户发送带有login类型的消息,我们将 -
- 检查是否有人已经使用此用户名登录。
- 如果是,则告诉用户他尚未成功登录。
- 如果没有人使用此用户名,我们将用户名作为键添加到连接对象中。
- 如果命令无法识别,我们将发送错误。
以下代码是用于向连接发送消息的辅助函数。将其添加到server.js文件中 -
function sendTo(connection, message) { connection.send(JSON.stringify(message)); }
当用户断开连接时,我们应该清理其连接。当触发close事件时,我们可以删除用户。将以下代码添加到connection处理程序中 -
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;
首先,我们获取我们尝试呼叫的用户connection。如果它存在,我们将向他发送offer详细信息。我们还在connection对象中添加otherName。这是为了简化以后查找它。
对响应的答复具有与我们在offer处理程序中使用的类似模式。我们的服务器只是将所有消息作为answer传递给另一个用户。在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 候选。我们使用相同的技术,只是在用户之间传递消息。主要区别在于候选消息可能每个用户多次发生,并且以任何顺序发生。添加candidate处理程序 -
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;
为了允许我们的用户断开与另一个用户的连接,我们应该实现挂断功能。它还会告诉服务器删除所有用户引用。添加leave处理程序 -
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;
这也会向另一个用户发送leave事件,以便他可以相应地断开其对等连接。我们还应该处理用户从信令服务器断开连接的情况。让我们修改我们的close处理程序 -
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 应用程序的前端框架。您可以在https://bootstrap.ac.cn/了解更多信息。创建一个名为“textchat”的文件夹。这将是我们的根应用程序文件夹。在此文件夹内创建一个package.json文件(它对于管理 npm 依赖项是必要的)并添加以下内容 -
{ "name": "webrtc-textochat", "version": "0.1.0", "description": "webrtc-textchat", "author": "Author", "license": "BSD-2-Clause" }
然后运行npm install bootstrap。这将在textchat/node_modules文件夹中安装 bootstrap 库。
现在我们需要创建一个基本的 HTML 页面。在根文件夹中创建一个index.html文件,其中包含以下代码 -
<html> <head> <title>WebRTC Text 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 Text 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 container"> <div class = "row"> <div class = "col-md-4 col-md-offset-4 text-center"> <div class = "panel panel-primary"> <div class = "panel-heading">Text chat</div> <div id = "chatarea" class = "panel-body text-left"></div> </div> </div> </div> <div class = "row text-center form-group"> <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 class = "row text-center"> <div class = "col-md-12"> <input id = "msgInput" type = "text" placeholder = "message" /> <button id = "sendMsgBtn" class = "btn-success btn">Send</button> </div> </div> </div> <script src = "client.js"></script> </body> </html>
此页面应该很熟悉。我们添加了bootstrap css 文件。我们还定义了两个页面。最后,我们创建了几个文本字段和按钮来获取用户的输入。在“聊天”页面上,您应该看到带有“chatarea”ID 的 div 标签,所有我们的消息都将显示在此处。请注意,我们添加了指向client.js文件的链接。
现在我们需要与我们的信令服务器建立连接。在根文件夹中创建client.js文件,其中包含以下代码 -
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://127.0.0.1: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 server运行我们的信令服务器。然后,在根文件夹中运行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 对象
- 在我们的 RTCPeerConnection 对象中创建数据通道
将以下代码添加到“UI 选择器块”中 -
var msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var chatArea = document.querySelector('#chatarea'); var yourConn; var dataChannel;
修改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 //********************** //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; //creating data channel dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); dataChannel.onerror = function (error) { console.log("Ooops...error:", error); }; //when we receive a message from the other peer, display it on the screen dataChannel.onmessage = function (event) { chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; }; dataChannel.onclose = function () { console.log("data channel is closed"); }; } };
如果登录成功,应用程序将创建RTCPeerConnection对象并设置onicecandidate处理程序,该处理程序将所有找到的 icecandidates 发送到另一个对等方。它还创建一个 dataChannel。请注意,在创建 RTCPeerConnection 对象时,构造函数中的第二个参数是可选的:[{RtpDataChannels: true}] 如果你正在使用 Chrome 或 Opera,则是强制性的。下一步是向另一个对等方发出 offer。一旦用户收到 offer,他就会创建一个answer并开始交换 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)); };
我们在“呼叫”按钮上添加了一个click处理程序,它会启动一个 offer。然后,我们实现了onmessage处理程序预期的几个处理程序。它们将异步处理,直到两个用户都建立了连接。
下一步是实现挂断功能。这将停止传输数据并告诉另一个用户关闭数据通道。添加以下代码 -
//hang up hangUpBtn.addEventListener("click", function () { send({ type: "leave" }); handleLeave(); }); function handleLeave() { connectedUser = null; yourConn.close(); yourConn.onicecandidate = null; };
当用户点击“挂断”按钮时 -
- 它将向另一个用户发送“leave”消息。
- 它将关闭 RTCPeerConnection 以及数据通道。
最后一步是向另一个对等方发送消息。将“click”处理程序添加到“发送消息”按钮 -
//when user clicks the "send message" button sendMsgBtn.addEventListener("click", function (event) { var val = msgInput.value; chatArea.innerHTML += name + ": " + val + "<br />"; //sending a message to a connected peer dataChannel.send(val); msgInput.value = ""; });
现在运行代码。您应该能够使用两个浏览器选项卡登录到服务器。然后,您可以与另一个用户建立对等连接并向他发送消息,以及通过点击“挂断”按钮关闭数据通道。
以下是完整的client.js文件 -
//our username var name; var connectedUser; //connecting to our signaling server var conn = new WebSocket('ws://127.0.0.1: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 msgInput = document.querySelector('#msgInput'); var sendMsgBtn = document.querySelector('#sendMsgBtn'); var chatArea = document.querySelector('#chatarea'); var yourConn; var dataChannel; 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 //********************** //using Google public stun server var configuration = { "iceServers": [{ "url": "stun:stun2.1.google.com:19302" }] }; yourConn = new webkitRTCPeerConnection(configuration, {optional: [{RtpDataChannels: true}]}); // Setup ice handling yourConn.onicecandidate = function (event) { if (event.candidate) { send({ type: "candidate", candidate: event.candidate }); } }; //creating data channel dataChannel = yourConn.createDataChannel("channel1", {reliable:true}); dataChannel.onerror = function (error) { console.log("Ooops...error:", error); }; //when we receive a message from the other peer, display it on the screen dataChannel.onmessage = function (event) { chatArea.innerHTML += connectedUser + ": " + event.data + "<br />"; }; dataChannel.onclose = function () { console.log("data channel is closed"); }; } }; //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; yourConn.close(); yourConn.onicecandidate = null; }; //when user clicks the "send message" button sendMsgBtn.addEventListener("click", function (event) { var val = msgInput.value; chatArea.innerHTML += name + ": " + val + "<br />"; //sending a message to a connected peer dataChannel.send(val); msgInput.value = ""; });