WebRTC,由Google最先提出的Web Realtime communication protocol,最主要是希望讓使用者在操作瀏覽器時,可以不用安裝任何插件就可以直接做Video/ Audio/ Data P2P Streaming,目前瀏覽器支援還有點低,可以參考CanIUse,大致上就Chrome/Opera/Firefox/Android有部分支援,iOS/Safari沒有實作比較麻煩一些,不過在市面上有許多服務都採用了WebRTC,例如Google Hangout、Zoom等等支援雲端視訊軟體。
網路上教學找起來也不太多,最主要還是這一篇Getting Started with WebRTC和codelabs/webrtc-web,不過裡頭的程式碼有點老舊,我有把它翻新過;如果想看WebRTC可以做到哪些效果,可以參考WebRTC Demo
整篇程式碼可以到Github: webrtc專案,下載後npm install & nodejs app.js
即可啟動,操作請看Readme.md
前言
WebRTC官方使用的函式庫為Adapter.js,也是後續範例所使用的,裡頭只有三個API
navigator.mediaDevices
取得使用者的攝影機與麥克風,並以stream方式設定到DOM中的<vedio>,瀏覽器會自動跳出要求權限的視窗
<video id="selfView" width="320" height="240" autoplay></video>
///
navigator.mediaDevices.getUserMedia({"audio": true, "video": true}).then((stream) => {
console.log("start streaming");
selfView.src = URL.createObjectURL(stream);
})
!! 注意:網站必須在HTTPS之下才能呼叫navigator.mediaDevices,不然Chrome會阻擋
RTCPeerConnection(configuration)
建立Peer(端點)的方法,configuration後續會補充
RTCDataChannel
可用來傳送任何資料,本篇不提
WebRTC在實作上主要有兩個步驟
- signal:
因為瀏覽器對於影音檔的支援度不同,所以必須交換雙方瀏覽器可以接受的資料,此多媒體資料格式為SDP規格
這部分必須自己處理,我使用socketio傳送signal資料
- exchange network information:
交換雙方的網路資訊並建立P2P連線,這部分使用的是ICE network Framework,裡頭又包含兩種協定 STUN和TURN,最主要是因為兩端點可能藏在NAT之後,此時就需要STUN/TURN來做NAT打動(Hole punching);
STUN比較簡單,單純提供端點查詢自己public IP,所以基本上端點串流還是靠自己。
TURN比較複雜,主要是遇到symmetric NAT時,僅透過STUN無法運作,必須改由TURN伺服器作為端點,接著在將資料relay給使用者;通常TURN Server都會包含STUN的功能
NAT(Network Address Translation),網路位置轉換
簡而言之就是一群用戶使用私有IP連接到NAT裝置,再由NAT裝置用有限的公共IP(Public IP)對外連線,其中IP映射的方式又有四種之分。
平常家用的Router上網或是連手機無線基地台都是使用NAT,這種壞處就是某些網路協定無法正常用做,WebRTC也是其中之一
實戰
客戶端原始碼參考webrtc.html
使用上開兩個瀏覽器,一個暫且叫做Alice,另一個Bob
此時我在local端執行一個SocketIO的Server
連線socket
在網頁載入後的第一步,就是先將socket連線到Server
var socket = io.connect('http://localhost:4200');
接著Alice按下"start"決定與Bob開始視訊
創建Peer
這時候第一步是創建RTCPeerConnection,在設定檔中可以設定ICE Server,接著RTCPeerConnection會嘗試每個在List中的server(STUN or TURN),後續WebRTC會處理
var configuration = {
iceServers: [
{urls: "stun:23.21.150.121"},
{urls: "stun:stun.l.google.com:19302"},
{urls: "turn:numb.viagenie.ca", credential: "turnserver", username: "sj0016092@gmail.com"}
]
};
pc = new RTCPeerConnection(configuration);
端點註冊一些基本的事件聆聽
//將取得的 ice candidates 送給對方
pc.onicecandidate = function (evt) {
socket.emit('candidate', {"candidate": evt.candidate});
};
// 如果遠端的stream成功就會觸發事件
pc.ontrack = function (evt) {
console.log("add remote stream");
console.log(evt);
//將stream綁定到vedio上,就可以開始串流了
remoteView.src = URL.createObjectURL(evt.streams[0]);
};
此時都還不會觸發任何事件
發送Offer
接著到這一步,就是取得本地端的視訊並綁定到video上,接著因為Alice是主動觸發,所以會呼叫pc.createOffer()
,也就是產生SDP中的多媒體資訊格式,接著就設定本地端pc.setLocalDescription(desc)
navigator.mediaDevices.getUserMedia({"audio": true, "video": true}).then((stream) => {
console.log("start streaming");
console.log(stream);
selfView.src = URL.createObjectURL(stream);
pc.addStream(stream);
if (isCaller)
pc.createOffer().then((desc)=>gotDescription(desc));
else
pc.createAnswer().then((desc)=> gotDescription(desc));
function gotDescription(desc) {
pc.setLocalDescription(desc);
socket.emit('sdp', {"sdp": desc});
}
}
);
註冊Socket事件
最後是註冊Socket事件,因為等等Bob也會回傳資料,這裡主要就是兩份setRemoteDescription
和addIceCandidate
socket.on('msg', function (data) {
console.log(data);
if (!pc)
start(false);
if (data.sdp)
pc.setRemoteDescription(new RTCSessionDescription(data.sdp));
else if (data.candidate)
pc.addIceCandidate(new RTCIceCandidate(data.candidate));
});
Bob
最一開始Bob會在Socket事件中收到Alice在createOffer()傳的SDP Description,接著就先創建RTCPeerConnection
,一樣綁定完事件後,Bob使用createAnswer()
回傳自己的SDP 多媒體資料格式,此時Alice和Bob都有雙方的SDP 多媒體資料格式了!
接著雙方交換candidate,WebRTC會自動將兩者連線
ontrack事件觸發
如果SDP與ICE都沒有出錯,Alice和Bob會同時觸發ontrack,此時就會在remoteView
中看到雙方的身影。
稍微整理一下
A 創建 RTCPeerConnection
A ---- createOffer()傳遞SDP Offer-------> B,收到後創建 RTCPeerConnection
A <--- createAnswer()傳遞SDP Answer------ B
====此時兩者都各自設定好setLocalDescription、setRemoteDescription====
====A、B也透過`navigator.mediaDevices.getUserMedia`先顯示自己的畫面====
A <--- 交換ICE Candidate -----> B
====剩下的就交給WebRTC,如果成功會觸發ontrack()事件====
流程圖長這樣
來源 MDN:Signaling_and_video_calling
關於Signal Server
Signal Server我是用socket io搭配NodeJS Express實作,相當簡單
基本上就是註冊事件,接著就轉發而已
const express = require('express');
const app = express();
const server = require('http').createServer(app);
const io = require('socket.io')(server);
app.use(express.static(__dirname + '/bower_components'));
app.get('/', function (req, res, next) {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', function (client) {
console.log('Client connected...');
client.on('candidate', function(data){
"use strict";
console.log(data);
client.broadcast.emit('msg', data);
});
client.on('sdp', function(data){
"use strict";
console.log(data);
client.broadcast.emit('msg', data);
});
});
server.listen(4200, ()=> {
"use strict";
console.log('server start at 4200');
});
ICE Servers
ICE比較麻煩,STUN公用的可以參考{urls: "stun:23.21.150.121"},{urls: "stun:stun.l.google.com:19302"}
,至於TURN可以到http://numb.viagenie.ca/
註冊一個免費的,想要測試ICE Servers是否正常可以到這裡測試STUN/TURN server connectivity test。
如果想要自己架設,Google有開源專案coturn,這部分我還在嘗試,成功在分享。
結語
目前跑在Local端,自己看自己有點奇怪XD 之後佈建到遠端伺服器在上傳結果。