做爲一個認爲啥都想懂一點的小開發,一直都對WebRTC
很感興趣,這個興趣來源於幾年前公司但願作一個即時通信的小功能在APP
上,不過最終因爲項目最終需求更改而擱置。雖然如此,可是我仍是瞭解了一些關於該技術的技術背景,例如P2P
通信、內網打洞等等。經過幾個晚上的學習和實驗,大致上瞭解WebRTC
的原理和使用方法,如今分享一下個人學習過程吧。javascript
做爲一個文檔黨,歷來都要先看官方文檔和文章,這樣才能保證本身拿到最新,最好的一手信息。WebRTC
官網文檔也還算是比較全面,不過貌似都很久沒更新了。推測是,大概好久沒有作功能升級了吧。我此次學習,參考了一些官方例子,加上了本身的理解。有錯誤的地方你們能夠指出來呀,一塊兒學習。參考的文章會在文章結尾加上。廢話很少說了,開始吧。html
WebRTC
是谷歌開發的,目標是創造一個高質量的、可靠的通信框架,從字面的意咱們能夠拆分爲了Web
跟RTC
兩部分,Web
很好理解啊,就是基於網絡,而RTC
全稱爲Real Time Communications
(實時通信),所以它的做用就是讓咱們能夠利用瀏覽器(也能用於APP
),進行實時的通信的一個框架。既然是通信媒介固然是多種的,包括視頻,語音,文本等多種多媒體信息,甚至你還能利用它來傳輸各類文件。下面,咱們用最直觀的,視頻通信來開始咱們的學習吧。前端
用瀏覽器打開攝像頭很簡單,咱們能夠直接調用JS API
實現。java
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<h1>得到視頻流</h1>
<!-- 設置自動播放 -->
<video autoplay playsinline></video>
<script src="js/main.js"></script>
</body>
</html>
複製代碼
// 媒體流配置
const mediaStreamConstraints = {
video: true
};
// 得到 video 標籤元素
const localVideo = document.querySelector("video");
// 媒體流對象
let localStream;
// 回調保存視頻流對象並把流傳到 video 標籤
function gotLocalMediaStream(mediaStream) {
localStream = mediaStream;
localVideo.srcObject = mediaStream;
}
// handle 錯誤信息
function handleLocalMediaStreamError(error) {
console.log("打開本地視頻流錯誤: ", error)
}
// fire!!
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream)
.catch(handleLocalMediaStreamError);
複製代碼
代碼主要分2步git
navigator.mediaDevices.getUserMedia
中得到視頻設備。then
的回調中把視頻流傳到 video
標籤。很是簡單吧github
值得注意的是,我用的是Chrome
瀏覽器,新版本的Chrome
增強了獲取設備的安全策略。若是你想要打開攝像頭等設備,你的域名若是不是本地文件或者 localhost
那必須經過https
訪問。web
既然視頻流咱們獲得了,第二步,咱們來使用WebRTC
的 RTCPeerConnection
來進行本地傳輸吧。這個Demo
不是真實的使用場景,由於不涉及到真實世界的網絡傳輸,咱們僅僅是在同一個頁面,打開了兩個 RTCPeerConnection
把一個的內容傳輸到另外一個,從而進行通信。在貼代碼以前,咱們先來簡單的描述一下建立鏈接的過程吧。瀏覽器
假設如今是A想跟B視頻。他們的 offer/answer (申請?/ 應答?), 機制是這樣的:安全
1. `A `建立了一個 `RTCPeerConnection` 對象
2. `A` 利用`RTCPeerConnection` 的 `createOffer()` 方法建立了一個 `offer` (一個` SDP` 的會話描述)
3. `A` 在 `offer` 的回調中使用 `setLocalDescription()` 方法存儲他的 `offer`
4. `A` 把他的 `offer` 字符串化,而後經過某一種信令機制發給 `B`
5. `B` 收到 `A` 的 `offer` 後用`setRemoteDescription()` 存起來,如此一來他的 `RTCPeerConnection` 就知道了 `A` 的配置。
6. `B` 調用 `createAnswer()` 並用他的成功回調的傳送他的本地會話描述:這就是 `B` 的`answer`
7. `B` 用 `setLocalDescription()` 設置了他的 `answer` 到本地的會話描述
8. 而後 `B` 用某一種信令機制把他的 `answer` 字符串化以後返回給 `A`
9. `A` 把 `B` 的 `answer` 利用`setRemoteDescription()`方法存取爲遠程會話描述
複製代碼
過程看上去很麻煩,不過其實他們就作了個事情服務器
SDP
)SDP
)有關 SDP
的格式,能夠參看文章後面的連接
下面讓咱們看代碼,走起
<!DOCTYPE html>
<html lang="en">
<head>
...
</head>
<body>
<h1>RTCPeerConnection 傳輸視頻流</h1>
<!-- 設置自動播放 -->
<video autoplay playsinline id="localVideo"></video>
<video autoplay playsinline id="remoteVideo"></video>
<div>
<button id="startBtn">開始</button>
<button id="callBtn">撥打</button>
<button id="hangupBtn">掛機</button>
</div>
<!-- 墊片,用於統一瀏覽器 API -->
<script src="js/adapter.js"></script>
<script src="js/main.js"></script>
</body>
</html>
複製代碼
HTML 代碼比較簡單,咱們建立了兩個 video
,一個顯示遠程一個顯示本地,而且加入了三個按鈕進行模擬撥打。細心的同窗可能已經發現了,咱們引入了一個墊片adapter.js
。常常寫前端的同窗對墊片可能熟悉不過了,由於世界上不只僅只有谷歌的瀏覽器,還有各類各樣別的。而後命名,API
也是各類各樣,因此咱們會利用各類墊片,統一咱們的API
。再也不忍受兼容之苦。adapter.js
就是這樣的存在。他是谷歌官方提供給咱們的。引入它咱們即可以用統一套API
操做。
因爲代碼比較長,就只貼關鍵代碼了。所有代碼連接我會在文章後面貼上。
// 開始按鈕,打開本地媒體流
function startAction() {
startButton.disabled = true;
navigator.mediaDevices.getUserMedia(mediaStreamConstraints)
.then(gotLocalMediaStream).catch(handleLocalMediaStreamError);
trace('本地媒體流打開中...');
}
複製代碼
這是響應開始
按鈕的函數。跟第一個例子同樣,主要是用來打開攝像頭,而且把視頻流傳到id
爲localVideo
的視頻標籤。
// 撥打按鈕, 建立 peer connection
function callAction() {
callButton.disabled = true;
hangupButton.disabled = false;
trace("開始撥打...");
startTime = window.performance.now();
// ...
const servers = null; // RTC 服務器配置
// 建立 peer connetcions 並添加事件
localPeerConnection = new RTCPeerConnection(servers);
trace("建立本地 peer connetcion 對象");
localPeerConnection.addEventListener('icecandidate', handleConnection);
localPeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
remotePeerConnection = new RTCPeerConnection(servers);
trace("建立遠程 peer connetcion 對象");
remotePeerConnection.addEventListener('icecandidate', handleConnection);
remotePeerConnection.addEventListener('iceconnectionstatechange', handleConnectionChange);
remotePeerConnection.addEventListener('addstream', gotRemoteMediaStream);
// 添加本地流到鏈接中並建立鏈接
localPeerConnection.addStream(localStream);
trace("添加本地流到本地 PeerConnection");
trace("開始建立本地 PeerConnection offer");
localPeerConnection.createOffer(offerOptions)
.then(createdOffer).catch(setSessionDescriptionError);
}
複製代碼
這部份是撥打
按鈕的響應函數。在這個方法中,咱們作了個事情。
建立了用於通信的一對RTCPeerConnection
對象,localPeerConnection
和remotePeerConnection
分別給兩個RTCPeerConnection
對象註冊了icecandidate(重要)
和 iceconnectionstatechange
事件的響應函數
給remotePeerConnection
註冊了addstream
事件的響應。
把本地視頻流添加到localPeerConnection
localPeerConnection
建立offer
這裏有一個上面沒有說起的東西ICE Candidate
,ICE
是啥呢?哈哈,他的全稱是 Interactive Connectivity Establishment
交互式鏈接的創建。他是一個規範,說白了就是創建鏈接用的規範,因爲咱們的WebRTC
是要進行P2P
鏈接的,而咱們的網絡是很是複雜的,並且大部分都是在內網(須要打洞或者穿越防火牆)。因此咱們須要一個機制來創建內網鏈接。這個我會在後面的文章詳細來講說。如今,簡單理解成就是創建鏈接用的就行了。而icecandidate
的響應方法,則是當網絡可用的狀況下,用於存儲和交換各類網絡信息。
// 定義 RTC peer connection
function handleConnection(event) {
const peerConnection = event.target;
const iceCandidate = event.candidate;
if (iceCandidate) {
const newIceCanidate = new RTCIceCandidate(iceCandidate);
const otherPeer = getOtherPeer(peerConnection);
otherPeer.addIceCandidate(newIceCanidate)
.then(() => {
handleConnectionSuccess(peerConnection);
}).catch((error) => {
handleConnectionFailure(peerConnection, error);
});
trace(`${getPeerName(peerConnection)} ICE candidate:\n` +
`${event.candidate.candidate}.`);
}
}
複製代碼
這段代碼正是體現了網絡信息(ICE candidate
),的保存和交換過程。而保存Candidate
是經過調用RTCPeerConnection
對象的addIceCandidate
方法。這裏可能你們有疑問,這裏就交換了Candidate
信息了嗎?是的getOtherPeer
方法其實就是用於得到對方的RTCPeerConnection
對象,由於咱們的 Demo 是在同一頁面建立的。因此不需經過其餘載體交換。
好的,說完鏈接建立,咱們接着說建立offer
。在建立offer
前,咱們已經留意到,其實已經把本地的視頻流添加到RTCPeerConnection
對象中了,所以offer
所帶的SDP
會話描述,已經帶有相關信息。咱們先來createOffer
成功後的回調方法。
// 建立 offer
function createdOffer(description) {
trace(`Offer from localPeerConnection:\n${description.sdp}`);
trace('localPeerConnection setLocalDescription 開始.');
localPeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection setRemoteDescription 開始.');
remotePeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('remotePeerConnection createAnswer 開始.');
remotePeerConnection.createAnswer()
.then(createdAnswer)
}
複製代碼
簡單明瞭,對於localPeerConnection
來講是本地,因此就是調用 setLocalDescription
把offer
信息存儲。而對於對方就是遠程remotePeerConnection
就是用setRemoteDescription
進行存儲了。這裏跟我章節前說的第4步說的不同,這裏沒有轉成字符串。聰明的同窗可能猜到爲何了,由於這裏是同一個頁面,不須要傳輸呀。
緊接着立刻remotePeerConnection
就調用createAnswer
建立了一個 answer
,讓咱們繼續看,
// 建立 answer
function createdAnswer(description) {
trace(`Answer from remotePeerConnection:\n${description.sdp}.`);
trace('remotePeerConnection setLocalDescription 開始.');
remotePeerConnection.setLocalDescription(description)
.then(() => {
setLocalDescriptionSuccess(remotePeerConnection);
}).catch(setSessionDescriptionError);
trace('localPeerConnection setRemoteDescription 開始.');
localPeerConnection.setRemoteDescription(description)
.then(() => {
setRemoteDescriptionSuccess(localPeerConnection);
}).catch(setSessionDescriptionError);
}
複製代碼
這裏跟上面的createOffer
回調作的差很少,把answer
存儲到雙方對應的描述中。
到這裏爲止雙方的鏈接建好,offer
與 answer
也存儲穩當。因爲remotePeerConnection
在以前已經已經註冊好addStream
的響應方法了gotRemoteMediaStream
,而正如前文說的,由於建立offer
的時候已經把視頻流帶上了,因此gotRemoteMediaStream
此刻會回調,經過這個方法,把視頻流顯示在remoteVideo
標籤中。
// 回調保存遠程媒體流對象並把流傳到 video 標籤
function gotRemoteMediaStream(event) {
const mediaStream = event.stream;
remoteVideo.srcObject = mediaStream;
remoteStream = mediaStream;
trace("遠程節點連接成功,接收遠程媒體流中...");
}
複製代碼
如今,咱們應該能夠看到兩個如出一轍的畫面了。注意哦,右邊那個是經過RTC
傳輸過來的。撒花~
這一篇先到這裏吧,咱們下一篇繼續。下一篇會繼續繼續深刻WebRTC
架構和ICE
,signling
之類的內容。謝謝你們的閱讀,畢竟我也是個初學者,若是文中有不對的地方,你們能夠評論一下,而後一塊兒探討。再次謝過。
代碼和參考文檔