【翻譯】第五章 - [Dan_Ristic]webrtc開發互動實踐

鏈接客戶

​ 如今,咱們實現了咱們本身的信令服務器,如今是時候創建一個應用展示它的力量了。在這一章,咱們將構建一個客戶端應用程序,該客戶端應用程序容許兩個用戶在不一樣的計算機上使用WebRTC進行實時鏈接和通訊。在本章的最後,咱們將爲大多數WebRTC應用程序的功能提供一個精心設計的工做示例。javascript

在這一章,咱們講解如下內容:css

  • 從客戶端鏈接到咱們的服務器
  • 識別鏈接兩端的用戶
  • 在兩個遠程用戶之間發起呼叫
  • 掛斷

​ 若是您尚未學習完第4章,Creating a Signaling Server,那麼如今是回頭再來的好時機。本章以咱們在該章中構建的服務器爲基礎,所以您將必須知道如何在計算機上搭建本地和運行服務器。html

客戶端應用

客戶端應用程序的目標是使兩個用戶可以從不一樣位置相互鏈接並進行通訊。這也是經常被看做WebRTC應用的hello world,在基於webrtc會議和活動中,咱們能夠看到大量的這類應用的案例。您可能已經應用了與本章內容類似的東西。html5

1574648206549

咱們的應用有兩個頁面:一個登錄界面和另外一個呼叫用戶界面。請記住,頁面自己將會很是簡單。咱們將主要集中於如何構建實際的WebRTC功能。如今,咱們將在構建應用程序以前查看最初的線框模型,以用做指導。java

你能夠說,從某種意義上講,這是一個不完整的應用。兩個頁面都是div標籤,咱們將用javascript動態實現。然而,大多的input都是經過簡單的事件實現。若是你懂一點html5Js編程,這一章的代碼應該會很熟悉。web

咱們將重點關注將咱們的應用程序集成到咱們的信令服務器上。這就意味着咱們採起本地事件,咱們在第三章「The RTCPeerConnection object subsection」的子部分WebRTC API建立的基礎webrtc應用,在兩個頁面之間發送消息,而不是在同一個頁面。測試的一種方式是打開瀏覽器的兩個tab, 以使兩個選項卡都指向同一頁面並使它們彼此調用。chrome

設置頁面

開始,咱們須要建立基礎的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('#theirusername'),
 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應用程序的安全性。

相關文章
相關標籤/搜索