almost 7 years ago

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 WebRTCcodelabs/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在實作上主要有兩個步驟

  1. signal:
    因為瀏覽器對於影音檔的支援度不同,所以必須交換雙方瀏覽器可以接受的資料,此多媒體資料格式為SDP規格
    這部分必須自己處理,我使用socketio傳送signal資料
  2. 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也會回傳資料,這裡主要就是兩份setRemoteDescriptionaddIceCandidate

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 之後佈建到遠端伺服器在上傳結果。

← 關於前端測試與Selenium實戰(Firefox/Chrome/Android)-使用Docker與Nodejs NodeJS使用HTTPS以及到GoDaddy購買憑證 →
 
comments powered by Disqus