2

连接客户

​ 现在,我们实现了我们自己的信令服务器,现在是时候建立一个应用展现它的力量了。在这一章,我们将构建一个客户端应用程序,该客户端应用程序允许两个用户在不同的计算机上使用WebRTC进行实时连接和通信。在本章的最后,我们将为大多数WebRTC应用程序的功能提供一个精心设计的工作示例。

在这一章,我们讲解以下内容:

  • 从客户端连接到我们的服务器
  • 识别连接两端的用户
  • 在两个远程用户之间发起呼叫
  • 挂断

​ 如果您还没有学习完第4章,Creating a Signaling Server,那么现在是回头再来的好时机。本章以我们在该章中构建的服务器为基础,因此您将必须知道如何在计算机上搭建本地和运行服务器。

客户端应用

客户端应用程序的目标是使两个用户能够从不同位置相互连接并进行通信。这也是常常被看作WebRTC应用的hello world,在基于webrtc会议和活动中,我们可以看到大量的这类应用的案例。您可能已经应用了与本章内容相似的东西。

1574648206549

我们的应用有两个页面:一个登陆界面和另一个呼叫用户界面。请记住,页面本身将会非常简单。我们将主要集中于如何构建实际的WebRTC功能。现在,我们将在构建应用程序之前查看最初的线框模型,以用作指导。

你可以说,从某种意义上讲,这是一个不完整的应用。两个页面都是div标签,我们将用javascript动态实现。然而,大多的input都是通过简单的事件实现。如果你懂一点html5Js编程,这一章的代码应该会很熟悉。

我们将重点关注将我们的应用程序集成到我们的信令服务器上。这就意味着我们采取本地事件,我们在第三章“The RTCPeerConnection object subsection”的子部分WebRTC API创建的基础webrtc应用,在两个页面之间发送消息,而不是在同一个页面。测试的一种方式是打开浏览器的两个tab, 以使两个选项卡都指向同一页面并使它们彼此调用。

设置页面

开始,我们需要创建基础的html页面。下文是提供给我们的一些样例代码。将代码复制到index.html文档中:

<!DOCTYPE html>
<html lang="en">
 <head>
 <meta charset="utf-8" />
 <title>Learning WebRTC - Chapter 5: Connecting Clients 
Together</title>
 <style>
 body {
 background-color: #3D6DF2;
 margin-top: 15px;
 font-family: sans-serif;
 color: white;
 }
 video {
 background: black;
 border: 1px solid gray;
 }
 .page {
 position: relative;
 display: block;
 margin: 0 auto;
 width: 500px;
height: 500px;
 }
 #yours {
 width: 150px;
 height: 150px;
 position: absolute;
 top: 15px;
 right: 15px;
 }
 #theirs {
 width: 500px;
 height: 500px;
 }
 </style>
 </head>
<body>
 <div id="login-page" class="page">
 <h2>Login As</h2>
 <input type="text" id="username" />
 <button id="login">Login</button>
 </div>
 <div id="call-page" class="page">
 <video id="yours" autoplay></video>
 <video id="theirs" autoplay></video>
 <input type="text" id="their-username" />
 <button id="call">Call</button>
 <button id="hang-up">Hang Up</button>
 </div>
 <script src="client.js"></script>
 </body>
</html>

目前为止,这些代码看起来很熟悉。我们应用div标签标识两个页面,通过display属性显示隐藏。在此之上,我们创建一系列获取用户信息的按钮和input框。最后,你应该识别出两个video 元素,一个是你自己的视频流,另一个是远程视频流。如果你不喜欢默认的页面风格,你可以用css美化页面。

获取链接

首先,我们用我们自己的信令服务器建立连接。我们在第四章建立的信令服务器,“Creating a Signaling Server” ,完全建立在websocket协议之上。关于建立在该技术之上的技术,最妙的是它不需要额外的库即可连接到服务器。我们只需要今天浏览器内置的websocket功能。我们仅仅需要创建一个websocket对象,并立即连接到我们的服务器。

我们先创建HTML文件包含的client.js文件。你可以添加如下的链接代码:

var name,
 connectedUser;
var connection = new WebSocket('ws://localhost:8888');
connection.onopen = function () {
 console.log("Connected");
};
// Handle all messages through this callback
connection.onmessage = function (message) {
 console.log("Got message", message.data);
 var data = JSON.parse(message.data);
 switch(data.type) {
 case "login":
 onLogin(data.success);
 break;
case "offer":
 onOffer(data.offer, data.name);
 break;
 case "answer":
 onAnswer(data.answer);
 break;
 case "candidate":
 onCandidate(data.candidate);
 break;
 case "leave":
 onLeave();
 break;
 default:
 break;
 }
};
connection.onerror = function (err) {
 console.log("Got error", err);
};
// Alias for sending messages in JSON format
function send(message) {
 if (connectedUser) {
 message.name = connectedUser;
 }
connection.send(JSON.stringify(message));
};

首先,初始化连接到服务器。我们在URI添加ws://协议前缀,连接到我们的本地服务器。接下来,我们设置一系列事件。最主要的一个是onmessage事件,它能使我们接收基于webrtc的即时消息。switch语句调用不同类型方法,我们接下来章节中填写的内容。最后,我们创建一个简单的send方法,自动关联到用户ID,编码和发送我们的消息内容。我们还定义了一些用户名变量和其他用户的id以方便后面使用。当你打开这一文件,你应该看到一条简单的连接消息:

1574828087457

Websocket API是建立即时通信应用的坚实基础。正如你这章看到的,它可以使我们在浏览器和服务器之间来回发送即时消息。我们不但可以用来发送信令消息,也可以发送其他消息。Websocket已经被应用到不同的网站之中,比如多媒体游戏,股票经纪等等。

登陆应用

第一次和服务器交互是记录唯一用户名。这一记录识别我们自己同时也给呼叫方一个标识符。开始呼叫,我们简单地发送一个名字给服务器,服务器给我们返回用户名的校验消息。我们这个应用,允许用户选择任何他们喜欢的用户名。

为了实现这一功能,我们需要给我们的脚本文件添加一些功能。你可以添加如下Javascript代码:

var loginPage = document.querySelector('#login-page'),
 usernameInput = document.querySelector('#username'),
 loginButton = document.querySelector('#login'),
 callPage = document.querySelector('#call-page'),
 theirUsernameInput = document.querySelector('#their username'),
 callButton = document.querySelector('#call'),
 hangUpButton = document.querySelector('#hang-up');
callPage.style.display = "none";
// Login when the user clicks the button
loginButton.addEventListener("click", function (event) {
 name = usernameInput.value;
 if (name.length > 0) {
 send({
 type: "login",
 name: name
 });
}
});
function onLogin(success) {
 if (success === false) {
 alert("Login unsuccessful, please try a different name.");
 } else {
 loginPage.style.display = "none";
 callPage.style.display = "block";
 // Get the plumbing ready for a call
 startConnection();
 }
};

开始,我们选择页面元素的一些引用以便我们可以和元素交互并且提供各种各样的反馈方式。然后我们隐藏callPage区域以便用户看到登陆流程。然后,我们给登陆按钮绑定监听事件,使得用户在点击的时候,服务器可以监听到用户登陆消息。最后,我们参考早期的消息回调函数实现onLogin方法。如果登陆成功,应用将会展示一个callPage区域,设置一些建立webRTC链接的必要条件。

开始一个对等链接

startConnection是链接的第一部分。由于整个过程不依赖于呼叫其他人,所以,我们可以在尝试呼叫用户之前设置这些步骤,详细步骤包括:

  1. 从摄像头获取视频源。
  2. 验证用户浏览器是否支持webrtc
  3. 创建RTCPeerConnection对象。

这是通过以下Javascript实现的:

var yourVideo = document.querySelector('#yours'),
 theirVideo = document.querySelector('#theirs'),
 yourConnection, connectedUser, stream;
function startConnection() {
 if (hasUserMedia()) {
 navigator.getUserMedia({ video: true, audio: false }, function 
(myStream) {
stream = myStream;
 yourVideo.src = window.URL.createObjectURL(stream);
 if (hasRTCPeerConnection()) {
 setupPeerConnection(stream);
 } else {
 alert("Sorry, your browser does not support WebRTC.");
 }
 }, function (error) {
 console.log(error);
 });
 } else {
 alert("Sorry, your browser does not support WebRTC.");
 }
}
function setupPeerConnection(stream) {
 var configuration = {
 "iceServers": [{ "url": "stun:stun.1.google.com:19302" }]
 };
 yourConnection = new RTCPeerConnection(configuration);
 // Setup stream listening
 yourConnection.addStream(stream);
 yourConnection.onaddstream = function (e) {
 theirVideo.src = window.URL.createObjectURL(e.stream);
 };
 // Setup ice handling
 yourConnection.onicecandidate = function (event) {
 if (event.candidate) {
 send({
 type: "candidate",
 candidate: event.candidate
 });
 }
 };
}
function hasUserMedia() {
 navigator.getUserMedia = navigator.getUserMedia || 
navigator.webkitGetUserMedia || navigator.mozGetUserMedia || 
navigator.msGetUserMedia;
 return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
 window.RTCPeerConnection = window.RTCPeerConnection || 
window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
 window.RTCSessionDescription = window.RTCSessionDescription || 
window.webkitRTCSessionDescription || 
window.mozRTCSessionDescription;
 window.RTCIceCandidate = window.RTCIceCandidate || 
window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
 return !!window.RTCPeerConnection;
}

到现在开起来都比较熟悉了。大多数的代码都是复制第三章例子中的,"Creating a Basic WebRTC Application"。与往常一样,检查浏览器前缀处理相应的错误。如果你运行代码,你的页面将提醒你登录然后在硬件设备上查找摄像头权限。另外,你可能记得,我们把audio设置为false,是为了避免在同一设备上测试链接时反馈出大音频:

完成本节后,你应该拥有和前面屏幕截图类似的内容。到这一步,如果你有问题,返回预览前面章节并确保你的服务器正常运行。此外,确保你的文件部署在本地web服务器下,然后,正确应用了getUserMedia API

发起通话

现在,我们已经设置好了每一步,我们准备呼叫一个远程用户。给远程用户发送offer开始下面的流程。用户一旦接到offer,他将会创建一个回复并且开始交换候选人,知道他成功连接。这一过程和第三章"创建一个基础应用"是相同的,不同的是,现在我们可以通过信令服务器远程完成。为此,我们将以下代码添加到脚本中:

callButton.addEventListener("click", function () {
 var theirUsername = theirUsernameInput.value;
 if (theirUsername.length > 0) {
 startPeerConnection(theirUsername);
 }
});
function startPeerConnection(user) {
 connectedUser = user;
 // Begin the offer
 yourConnection.createOffer(function (offer) {
 send({
 type: "offer",
 offer: offer
 });
 yourConnection.setLocalDescription(offer);
 }, function (error) {
 alert("An error has occurred.");
 });
};
function onOffer(offer, name) {
 connectedUser = name;
 yourConnection.setRemoteDescription(new 
RTCSessionDescription(offer));
 yourConnection.createAnswer(function (answer) {
 yourConnection.setLocalDescription(answer);
 send({
 type: "answer",
answer: answer
 });
}, function (error) {
 alert("An error has occurred");
 });
};
function onAnswer(answer) {
 yourConnection.setRemoteDescription(new 
RTCSessionDescription(answer));
};
function onCandidate(candidate) {
 yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
};

我们给呼叫按钮添加点击事件,以启动该过程。然后,我们通过连接到我们服务器的消息处理实现了一系列期望的功能。这些将被异步处理,直到双方都成功建立了连接。大多数工作已经在服务器和WebSocket层中完成,这使得该部分的实现更加容易。

通过运行代码以后,你可以使用两个不同的用户名在浏览器tab中登录。然后,您可以使用调用功能调用另一个选项卡,该功能将成功在客户端之间建立WebRTC连接。

恭喜您开发出功能完善的WebRTC应用程序! 这是创建令人惊叹的基于Web的对等网络应用程序的重要一步。 具有如此强大功能的东西通常需要花费数本书和框架才能使用,但是我们仅在短短几章中就使用了强大的技术来做到这一点。

调试

调试实时应用程序可能是一个艰巨的过程。 由于同时发生许多事情,因此很难对任何给定时刻的情况进行全面了解。 这是使用具有WebSocket协议的现代浏览器的真正亮点。 当今大多数浏览器都将具有某种方式,不仅可以查看与服务器的WebSocket连接,还可以检查通过网络发送的每个数据包。

在我的例子中,我用chrome浏览器调试。通过View |Developer|Developer Tools打开调试工具。将使我能够访问用于调试Web应用程序的一系列工具。 然后,打开“网络”选项卡将显示该页面进行的所有网络请求。 如果看不到任何网络请求,请在打开“开发人员工具”的情况下刷新页面。 从那里,到本地主机的连接很容易在列表中看到。 选择它时,您可以选择查看使用WebSocket连接发送的帧。 它显示了以易于阅读的格式发送的每个数据包,以便于调试。

您应该能够在此视图中看到各个步骤。 上面的屏幕截图显示了loginofferanswer,以及通过连接发送的每个ICE候选人。 这样,我可以检查每个邮件中是否有错误,例如格式错误的数据。 调试Web应用程序时,最好始终尽可能利用内置工具。

还有许多其他方法可以从计算机获取此信息。 在服务器和客户端上使用控制台输出是获取少量信息的好方法。 您还可以研究使用网络代理和抓包工具来拦截从浏览器发送的数据包。 这很难设置,但是会提供有关客户端和服务器之间发送的数据的更多信息。 我将把它留给读者作为练习,以找出调试Web应用程序的多种方法。

挂断

我们将实现的最后一个功能是挂断正在进行的呼叫。 这将通知其他用户我们打算关闭呼叫并停止发送信息。 我们的JavaScript仅需要几行:

hangUpButton.addEventListener("click", function () {
 send({
 type: "leave"
 });
 onLeave();
});
function onLeave() {
 connectedUser = null;
 theirVideo.src = null;
 yourConnection.close();
 yourConnection.onicecandidate = null;
 yourConnection.onaddstream = null;
 setupPeerConnection(stream);
};

当用户单击“挂断”按钮时,它将向其他用户发送一条消息,并销毁本地连接。 要成功销毁连接并允许将来再进行另一个呼叫,需要做一些事情:

  1. 首先,我们需要通知服务器我们不再通信。
  2. 其次,我们需要关闭RTCPeerConnection,这将停止向其他用户传输流数据。
  3. 最后,我们再次建立连接。 这会将我们的连接实例化为打开状态,以便我们可以接受新的呼叫。

完整的WebRTC客户端

以下是客户端应用程序中使用的完整JavaScript代码。 这包括所有代码以连接UI,连接到信令服务器以及与另一个用户启动WebRTC连接:

var connection = new WebSocket('ws://localhost:8888'),
 name = "";
var loginPage = document.querySelector('#login-page'),
 usernameInput = document.querySelector('#username'),
 loginButton = document.querySelector('#login'),
 callPage = document.querySelector('#call-page'),
 theirUsernameInput = document.querySelector('#their￾username'),
 callButton = document.querySelector('#call'),
 hangUpButton = document.querySelector('#hang-up');
callPage.style.display = "none";
// Login when the user clicks the button
loginButton.addEventListener("click", function (event) {
 name = usernameInput.value;
 if (name.length > 0) {
 send({
 type: "login",
 name: name
 });
 }
});
connection.onopen = function () {
 console.log("Connected");
};
// Handle all messages through this callback
connection.onmessage = function (message) {
 console.log("Got message", message.data);
 var data = JSON.parse(message.data);
 switch(data.type) {
 case "login":
 onLogin(data.success);
 break;
 case "offer":
 onOffer(data.offer, data.name);
 break;
 case "answer":
onAnswer(data.answer);
 break;
 case "candidate":
 onCandidate(data.candidate);
 break;
 case "leave":
 onLeave();
 break;
 default:
 break;
 }
};
connection.onerror = function (err) {
 console.log("Got error", err);
};
// Alias for sending messages in JSON format
function send(message) {
 if (connectedUser) {
 message.name = connectedUser;
 }
 connection.send(JSON.stringify(message));
};
function onLogin(success) {
 if (success === false) {
 alert("Login unsuccessful, please try a different name.");
 } else {
 loginPage.style.display = "none";
 callPage.style.display = "block";
 // Get the plumbing ready for a call
 startConnection();
 }
};
callButton.addEventListener("click", function () {
 var theirUsername = theirUsernameInput.value;
 if (theirUsername.length > 0) {
 startPeerConnection(theirUsername);
 }
});
hangUpButton.addEventListener("click", function () {
 send({
 type: "leave"
 });
 onLeave();
});
function onOffer(offer, name) {
 connectedUser = name;
 yourConnection.setRemoteDescription(new 
RTCSessionDescription(offer));
 yourConnection.createAnswer(function (answer) {
 yourConnection.setLocalDescription(answer);
 send({
 type: "answer",
 answer: answer
 });
 }, function (error) {
 alert("An error has occurred");
 });
}
function onAnswer(answer) {
 yourConnection.setRemoteDescription(new 
RTCSessionDescription(answer));
}
function onCandidate(candidate) {
 yourConnection.addIceCandidate(new RTCIceCandidate(candidate));
}
function onLeave() {
 connectedUser = null;
 theirVideo.src = null;
 yourConnection.close();
 yourConnection.onicecandidate = null;
 yourConnection.onaddstream = null;
 setupPeerConnection(stream);
}
function hasUserMedia() {
 navigator.getUserMedia = navigator.getUserMedia || 
navigator.webkitGetUserMedia || navigator.mozGetUserMedia || 
navigator.msGetUserMedia;
 return !!navigator.getUserMedia;
}
function hasRTCPeerConnection() {
 window.RTCPeerConnection = window.RTCPeerConnection || 
window.webkitRTCPeerConnection || window.mozRTCPeerConnection;
 window.RTCSessionDescription = window.RTCSessionDescription || 
window.webkitRTCSessionDescription || 
window.mozRTCSessionDescription;
 window.RTCIceCandidate = window.RTCIceCandidate || 
window.webkitRTCIceCandidate || window.mozRTCIceCandidate;
 return !!window.RTCPeerConnection;
}
var yourVideo = document.querySelector('#yours'),
 theirVideo = document.querySelector('#theirs'),
 yourConnection, connectedUser, stream;
function startConnection() {
 if (hasUserMedia()) {
 navigator.getUserMedia({ video: true, audio: false }, function 
(myStream) {
 stream = myStream;
 yourVideo.src = window.URL.createObjectURL(stream);
 if (hasRTCPeerConnection()) {
 setupPeerConnection(stream);
 } else {
 alert("Sorry, your browser does not support WebRTC.");
 }
 }, function (error) {
 console.log(error);
 });
 } else {
 alert("Sorry, your browser does not support WebRTC.");
 }
}
function setupPeerConnection(stream) {
 var configuration = {
"iceServers": [{ "url": "stun:stun.1.google.com:19302" }]
 };
 yourConnection = new RTCPeerConnection(configuration);
 // Setup stream listening
 yourConnection.addStream(stream);
 yourConnection.onaddstream = function (e) {
 theirVideo.src = window.URL.createObjectURL(e.stream);
 };
 // Setup ice handling
 yourConnection.onicecandidate = function (event) {
 if (event.candidate) {
 send({
 type: "candidate",
 candidate: event.candidate
 });
 }
 };
}
function startPeerConnection(user) {
 connectedUser = user;
 // Begin the offer
 yourConnection.createOffer(function (offer) {
 send({
 type: "offer",
 offer: offer
 });
 yourConnection.setLocalDescription(offer);
 }, function (error) {
 alert("An error has occurred.");
 });
};

如果您在运行客户端时遇到问题,请确保仔细查看此代码几次以确保正确复制了所有内容。 要看的另一件事是特定于浏览器的实现。 浏览器之间存在细微差别,因此请注意控制台上可能出现的任何错误。

改善应用

在本章中,我们建立的应用是进入到技术的核心区,可以做更多更大的事情。它提供了几乎所有对等通信应用程序都需要的基本功能。 从这里开始,只需添加常见的Web应用程序功能即可增强体验。

登录是开始改善体验的一个地方。 有许多完善的服务,可通过Facebook和Google等常见平台进行用户识别。与这两种API的集成都非常简单明了,并提供了一种确保每个用户都是唯一的好方法。 它们还提供了好友列表功能,因此,即使这是他/她首次使用该应用程序,该用户也可以拥有一个要呼叫的人员列表。

最重要的是,该应用程序需要万无一失,以确保获得最佳体验。 客户端和服务器都应在各个部分检查用户输入。此外,在许多地方WebRTC连接可能会失败,例如不支持该技术,无法穿越防火墙以及没有足够的带宽来传输视频呼叫。 为了避免丢掉会话,为使普通电话通信平台稳定,已经进行了大量工作,而使任何WebRTC平台稳定都需要进行大量工作。

自测题

Q1. 在大多数浏览器中创建WebSocket连接需要安装多个框架才能正常工作。 对或错?

Q2. 用户的浏览器需要支持哪些技术才能成功运行本章中创建的示例?

 1. webrtc
   2. websockets
   3. Media Capture and Streams
   4. 以上所有

Q3. 该应用程序允许两个以上的用户在视频通话中相互联系。 对或错?

Q4. 使我们的应用程序更稳定,从而在尝试建立呼叫时减少错误的最好方法是添加:

    1. 更多的css样式
      2. Facebook登录集成
      3. 在整个过程中的每一步都添加错误检查和验证
      4. 非常酷的动画

总结

完成本章后,您应该退后一步,并祝贺自己取得了如此长的成就。在本章的整个过程中,我们通过功能完善的WebRTC应用程序将本书的上半部分带入了视野。点对点的连接是如此复杂,令人惊讶的是,我们能够在短短的五个章节中成功完成一个对等连接。 现在,您可以放下该聊天客户端,并使用自己的手工解决方案与世界各地的人们进行交流!

现在,您应该掌握任何WebRTC应用程序的总体体系结构。 我们不仅介绍了客户端的实现,还介绍了信令服务器。 我们甚至还集成了其他HTML5技术(例如WebSockets),以帮助我们建立远程对等连接。

如果你需要休息,想放下这本书,现在是时候了。 该应用程序是开始为您自己的WebRTC应用程序制作原型并添加新的创新功能的起点。 阅读完本文后,最好还是研究Web上的其他WebRTC应用程序以及它们在开发时所采用的方法。 全面了解WebRTC应用程序的内部工作原理之后,您应该能够通过查看Web上的其他开放源代码中的示例去学到很多东西。

在接下来的章节中,我们将在Web上创建点对点应用程序时扩展并涉及许多高级主题。 音频和视频通话只是WebRTC的转折点; 我们将探索该技术的许多其他功能。 我们还将介绍如何通过与多个用户建立联系来构建更强大的应用程序,移动端以及WebRTC应用程序的安全性。


zeronlee
112 声望13 粉丝