2015年5月28日 星期四

如何使用node.js 實作WebRTC

一、    Socket訊息傳遞

    兩個端點之間的連線建立時,兩個端點需要一直與socket Server互相傳遞的訊息,透過SDPOfferAnswer 的協定,建立雙方通道才能進行P2P資料傳輸。
以上圖供參考,如有錯誤,請見諒


二、    安裝Socket.IO








三、    Node.js 建立的Socket Server


var http = require('http'); 
var fs = require('fs');
var port = "8000";
var ip = "192.168.1.63";
var socket_io = require("socket.io");  //注入socket.io
function onRequest(request, response) {
    console.log("Request received.");   
    response.writeHead(200, { 'Content-Type': 'text/html' });
    response.write('<head><meta charset="utf-8"/></head>');          
    fs.readFile('index3.html', 'utf8', function (err, data) {
        if (err) {
            return console.log(err);
        }
        response.end(data);
    });
}

var server =http.createServer(onRequest).listen(port, ip); 
var io = socket_io.listen(server);  // socket.io 注入http ;  client端呼叫http 時,socket.io 也會啟動
io.sockets.on('connection', function (socket) {   
    // 如果有接收到offer 訊息,立即傳送廣播封包,發送offer 訊息給client
    socket.on('offer', function (data) {
        socket.broadcast.emit('offer', { sdp: data.sdp });
    });
    // 如果有接收到answer 訊息,立即傳送廣播封包,發送answer 訊息給client
    socket.on('answer', function (data) {
        socket.broadcast.emit('answer', { sdp: data.sdp });
    });
// 如果有接收到ice 訊息,立即傳送廣播封包,發送ice 訊息給client
    socket.on('ice', function (data) {
        socket.broadcast.emit('ice', { candidate: data.candidate });
    });  
   // 如果有接收到hangup 訊息,立即傳送廣播封包,發送hangup訊息給client
    socket.on('hangup', function () {
        socket.broadcast.emit('hangup', {});
    });
});

四、    WebRTC程式範例  
 
網頁要嵌入socket.io.js 檔案,版本有差異檔案路徑也有所不同,目前作者的路徑在\node_modules\socket.io\node_modules\socket.io-client\socket.io.js 將它copy\node_modules\socket.io\ 路徑

  <script src="/socket.io/socket.io.js"></script>


  <script>
        var socket = io.connect('http://192.168.1.63:8000');
        //針對moliza 有些仍未整合統一規格,所以針對各家瀏覽器,仍需特殊處理
        var RTCSessionDescription = window.mozRTCSessionDescription
|| window.webkitRTCSessionDescription || window.RTCSessionDescription;
        var RTCIceCandidate = window.mozRTCIceCandidate || window.webkitRTCIceCandidate
|| window.RTCIceCandidate;

        var mediaConstraints = {
            "mandatory": {
                "OfferToReceiveAudio": true,
                "OfferToReceiveVideo": true
            }
        };

        var localStream;
        var localPeerConnection;
        var remotePeerConnection;
        var localVideo = document.getElementById('localVideo');
        var remoteVideo = document.getElementById('remoteVideo');              
        var startButton = document.getElementById('startButton');
        var callButton = document.getElementById('callButton');
        var hangupButton = document.getElementById('hangupButton');
        startButton.disabled = false;
        callButton.disabled = true;
        hangupButton.disabled = true;
        startButton.onclick = start;
        callButton.onclick = call;
        hangupButton.onclick = hangup;

        function gotStream(stream) {
            localVideo.src = URL.createObjectURL(stream);
            localStream = stream;
            callButton.disabled = false;
        }

//針對moliza 的規格,所以要特殊處理
      function getPeerConnection() {
                if(navigator.userAgent.indexOf("Chrome") != -1 )
                {
                    localPeerConnection = new webkitRTCPeerConnection(null);
                    remotePeerConnection = new webkitRTCPeerConnection(null);
                }             
                else if(navigator.userAgent.indexOf("Firefox") != -1 )
                {
                    localPeerConnection = new mozRTCPeerConnection(null);
                    remotePeerConnection = new mozRTCPeerConnection(null);
                }
        }

        function start() {
            startButton.disabled = true;
            navigator.getUserMedia = navigator.getUserMedia ||
              navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
            navigator.getUserMedia({
                video: true
            }, gotStream,
              function (error) {
                  trace('navigator.getUserMedia error: ', error);
              });
        }

        function call() {
            callButton.disabled = true;
            hangupButton.disabled = false;          
         getPeerConnection(); //建立RTCPeerConnection 的函式(fireFox 瀏覽器另外處理)
         remotePeerConnection.onaddstream = gotRemoteStream; //產生串流
            localPeerConnection.addStream(localStream);
           
            //準備訊息交換的訊息
            localPeerConnection.createOffer(function (sdp) {
                localPeerConnection.setLocalDescription(sdp, function () {
                    socket.emit('offer', { sdp: sdp }); ,建立 offer 訊號, 等待socket 回傳offer
                },Error);
            }, function () {
                console.log('create offer error.');
            }, mediaConstraints);
        }
             
        function gotRemoteIceCandidate(event) {          
            if (!!event.candidate) {
                socket.emit('ice', { candidate: e.candidate });
            }
        }
    
        function hangup() {
            localPeerConnection.close();
            remotePeerConnection.close();
            localPeerConnection = null;
            remotePeerConnection = null;
            hangupButton.disabled = true;
            callButton.disabled = false;
        }

        function gotRemoteStream(event) {
            remoteVideo.src = URL.createObjectURL(event.stream);
        }
        // 建立 ice ,準備連線的前置作業
        socket.on('offer', function (data) {
            remotePeerConnection.onicecandidate = function (e) {
                if (!!e.candidate) {
                    socket.emit('ice', { candidate: e.candidate });  // 發送出ice 訊息等待socket回傳ice訊息
                }
            };
            var offer = new RTCSessionDescription(data.sdp);  // 預備要交換的訊息
            remotePeerConnection.setRemoteDescription(offer, function () {
// 遠端電腦接收offer訊息後,要回傳answer訊息,並開始進始多媒體的傳輸作業
            remotePeerConnection.createAnswer(function (sdp) { 
                    remotePeerConnection.setLocalDescription(sdp, function () {
                        socket.emit('answer', { sdp: sdp }); //送出socket answer 等待,socket 回傳answer 訊息
                    }, function (e) {
                        console.log('set local description error: ' + e);
                    });
                }, function (e) {
                    console.log('create answer error: ' + e);
                }, mediaConstraints);
            }, function (e) {
                console.log('set remote description error: ' + e);
            });
        });

        // 接收遠端電腦回傳的answer 訊息
        socket.on('answer', function (data) {
            localPeerConnection.setRemoteDescription(new RTCSessionDescription(data.sdp));  //準備多媒體傳輸通道
        });

       // 接收回傳ice 訊息,將自己本地端的ICE 訊息回傳
        socket.on('ice', function (data) {         
            localPeerConnection.addIceCandidate(new RTCIceCandidate(data.candidate));
        });
  }
    </script>

    目前作者想測試桌機瀏覽器Chrome 43版本 和Chrome for Android的連線,電腦無法接收到手機傳出的視訊,測試Firefox 38 firefox for Android 狀況也是一樣,截取自fireFox 的錯誤訊息為ICE failed, see about:webrtc for more details


手機卻成功接收來自電腦傳送的視訊串流…   這真是太神奇了傑克!








2 則留言:

  1. 您好,最近在學習webrtc這塊 看到您的文章使用了您的範例
    但似乎好像無法使用

    回覆刪除
  2. 這篇文章是很早之前寫的,後來的 webrtc 是否有改變呼叫的方法,你可能要去查文件,但後來的webrtc都需要建在https 安全模式之下,才可以連線成功

    回覆刪除