m_shige1979のときどきITブログ

プログラムの勉強をしながら学習したことや経験したことをぼそぼそと書いていきます

Github(変なおっさんの顔でるので気をつけてね)

https://github.com/mshige1979

WebRTCでStunサーバを経由して接続

WebRTCで接続する場合

Webサーバ

通信を実際に行う画面。これがないと始まらない

シグナリングサーバ

ピアツーピア通信を行うもの。基本的にはこれがあれば通信を行うことが可能。ローカルネットワーク内ではこれだけで行けるんじゃないかな?

Stunサーバ

シグナリングサーバでは相手側のグローバルipなどがわからないのでそれを調べたりするのにこれが必要。これとシグナリングサーバを経由して通信する。
一度通信を行えばネットワーク負荷はかからないので通信料はあまりネックにならないとか

Turnサーバ

なんかStunサーバとか使っても通信できない場合の最後の手段的なもの、これはなんかネットワーク負荷がかかるらしい。

環境

さくらのクラウドで以下を用意

Webサーバ
yum update -y
rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
yum -y update
yum -y groupinstall "Development Tools"

cd /etc/yum.repos.d/
wget http://wing-repo.net/wing/6/EL6.wing.repo
wget http://wing-repo.net/wing/extras/6/EL6.wing-extras.repo
yum clean all
yum -y install yum-priorities

yum -y remove git
yum -y install git --enablerepo=wing

rpm -Uvh http://mirror.webtatic.com/yum/el6/latest.rpm
yum -y --enablerepo=epel install re2c libmcrypt libmcrypt-devel
yum -y install libxml2-devel bison bison-devel openssl-devel curl-devel libjpeg-devel libpng-devel libmcrypt-devel readline-devel libtidy-devel libxslt-devel httpd-devel enchant-devel libXpm libXpm-devel freetype-devel t1lib t1lib-devel gmp-devel libc-client-devel libicu-devel oniguruma-devel net-snmp net-snmp-devel  bzip2-devel
yum -y install php55w php55w-bcmath php55w-cli php55w-common php55w-dba php55w-devel php55w-embedded php55w-enchant php55w-fpm php55w-gd php55w-imap php55w-interbase php55w-intl php55w-ldap php55w-mbstring php55w-mcrypt php55w-mssql php55w-mysql php55w-odbc php55w-opcache php55w-pdo php55w-pear.noarch php55w-pecl-apcu php55w-pecl-apcu-devel php55w-pecl-memcache php55w-pecl-xdebug php55w-pgsql php55w-process php55w-pspell php55w-recode php55w-snmp php55w-soap php55w-tidy php55w-xml php55w-xmlrpc
service php-fpm start
chkconfig php-fpm on

rpm -ivh http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm
yum -y install nginx --enablerepo=nginx
service nginx start
chkconfig nginx on
シグナリングサーバ
yum update -y
rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
yum -y update
yum -y groupinstall "Development Tools"

cd /etc/yum.repos.d/
wget http://wing-repo.net/wing/6/EL6.wing.repo
wget http://wing-repo.net/wing/extras/6/EL6.wing-extras.repo
yum clean all
yum -y install yum-priorities

yum -y remove git
yum -y install git --enablerepo=wing

yum -y install nodejs npm --enablerepo=epel

mkdir -p /var/www/app1
cd /var/www/app1
npm install socket.io@0.9
npm install -g pm2
signaling.js
var port = 9001;
var io = require('socket.io').listen(port);
console.log((new Date()) + " Server is listening on port " + port);

io.sockets.on('connection', function(socket) {
  socket.on('message', function(message) {
    socket.broadcast.emit('message', message);
  });

  socket.on('disconnect', function() {
    socket.broadcast.emit('user disconnected');
  });
});

起動

m2 start signaling.js
STUNサーバ
yum update -y
rpm -Uvh http://dl.fedoraproject.org/pub/epel/6/x86_64/epel-release-6-8.noarch.rpm
rpm -Uvh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
yum -y update
yum -y groupinstall "Development Tools"

cd /etc/yum.repos.d/
wget http://wing-repo.net/wing/6/EL6.wing.repo
wget http://wing-repo.net/wing/extras/6/EL6.wing-extras.repo
yum clean all
yum -y install yum-priorities

yum -y remove git
yum -y install git --enablerepo=wing

mkdir -p /var/www/stun1
cd /var/www/stun1
wget http://turnserver.open-sys.org/downloads/v3.2.4.1/turnserver-3.2.4.1-CentOS6.5-x86_64.tar.gz
tar zxf turnserver-3.2.4.1-CentOS6.5-x86_64.tar.gz
cd turnserver-3.2.4.1
./install.sh

/etc/turnserver/turnserver.conf

stun-only

※stunのみ使用にする
※他にも設定はあるけど変更点はこれだけ

起動

/usr/bin/turnserver -o -v -c /etc/turnserver/turnserver.conf

index.htmlの修正

<!DOCTYPE html>
<html>
<head>
  <title>WebRTC 1 to 1 signaling</title>
</head>
<body>
  <h1>外部テスト</h1>
  <button type="button" onclick="startVideo();">Start video</button>
  <button type="button" onclick="stopVideo();">Stop video</button>
  &nbsp;&nbsp;&nbsp;&nbsp;
  <button type="button" onclick="connect();">Connect</button>
  <button type="button" onclick="hangUp();">Hang Up</button>
  <br />
  <div>
   <video id="local-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video>
   <video id="remote-video" autoplay style="width: 240px; height: 180px; border: 1px solid black;"></video>
  </div>

  <p>
   SDP to send:<br />
   <textarea id="text-for-send-sdp" rows="5" cols="100" disabled="1">SDP to send</textarea>
  </p>
  <p>
   SDP to receive:<br />
   <textarea id="text-for-receive-sdp" rows="5" cols="100"></textarea><br />
   <button type="button" onclick="onSDP();">Receive SDP</button>
  </p>

  <p>
   ICE Candidate to send:<br />
   <textarea id="text-for-send-ice" rows="5" cols="100" disabled="1">ICE Candidate to send</textarea>
  </p>
  <p>
   ICE Candidates to receive:<br />
   <textarea id="text-for-receive-ice" rows="5" cols="100"></textarea><br />
   <button type="button" onclick="onICE();">Receive ICE Candidates</button>
  </p>

  <!---- socket ------>
  <script src="http://133.242.53.63:9001/socket.io/socket.io.js"></script>

  <script>
  var localVideo = document.getElementById('local-video');
  var remoteVideo = document.getElementById('remote-video');
  var localStream = null;
  var peerConnection = null;
  var peerStarted = false;
  var mediaConstraints = {'mandatory': {'OfferToReceiveAudio':false, 'OfferToReceiveVideo':true }};


  // ---- socket ------
  // create socket
  var socketReady = false;
  var port = 9001;
  var socket = io.connect('http://133.242.53.63:' + port + '/');
  // socket: channel connected
  socket.on('connect', onOpened)
        .on('message', onMessage);

  function onOpened(evt) {
    console.log('socket opened.');
    socketReady = true;
  }

  // socket: accept connection request
  function onMessage(evt) {
    if (evt.type === 'offer') {
      console.log("Received offer, set offer, sending answer....")
      onOffer(evt);
    } else if (evt.type === 'answer' && peerStarted) {
      console.log('Received answer, settinng answer SDP');
          onAnswer(evt);
    } else if (evt.type === 'candidate' && peerStarted) {
      console.log('Received ICE candidate...');
          onCandidate(evt);
    } else if (evt.type === 'user dissconnected' && peerStarted) {
      console.log("disconnected");
      stop();
    }
  }



  // ----------------- handshake --------------
  var textForSendSDP = document.getElementById('text-for-send-sdp');
  var textForSendICE = document.getElementById('text-for-send-ice');
  var textToReceiveSDP = document.getElementById('text-for-receive-sdp');
  var textToReceiveICE = document.getElementById('text-for-receive-ice');
  var iceSeparator = '------ ICE Candidate -------';
  var CR = String.fromCharCode(13);

  function onSDP() {
    var text = textToReceiveSDP.value;
        var evt = JSON.parse(text);
        if (peerConnection) {
          onAnswer(evt);
        }
        else {
          onOffer(evt);
        }

        textToReceiveSDP.value ="";
  }

  //--- multi ICE candidate ---
  function onICE() {
    var text = textToReceiveICE.value;
        var arr = text.split(iceSeparator);
        for (var i = 1, len = arr.length; i < len; i++) {
      var evt = JSON.parse(arr[i]);
          onCandidate(evt);
    }

        textToReceiveICE.value ="";
  }


  function onOffer(evt) {
    console.log("Received offer...")
        console.log(evt);
    setOffer(evt);
        sendAnswer(evt);
        peerStarted = true;  // ++
  }

  function onAnswer(evt) {
    console.log("Received Answer...")
        console.log(evt);
        setAnswer(evt);
  }

  function onCandidate(evt) {
    var candidate = new RTCIceCandidate({sdpMLineIndex:evt.sdpMLineIndex, sdpMid:evt.sdpMid, candidate:evt.candidate});
    console.log("Received Candidate...")
        console.log(candidate);
    peerConnection.addIceCandidate(candidate);
  }

  function sendSDP(sdp) {
    var text = JSON.stringify(sdp);
        console.log("---sending sdp text ---");
        console.log(text);
        textForSendSDP.value = text;

        // send via socket
        socket.json.send(sdp);
  }

  function sendCandidate(candidate) {
    var text = JSON.stringify(candidate);
        console.log("---sending candidate text ---");
        console.log(text);
        textForSendICE.value = (textForSendICE.value + CR + iceSeparator + CR + text + CR);
        textForSendICE.scrollTop = textForSendICE.scrollHeight;

        // send via socket
        socket.json.send(candidate);
  }

  // ---------------------- video handling -----------------------
  // start local video
  function startVideo() {
        navigator.webkitGetUserMedia({video: true, audio: false},
    function (stream) { // success
      localStream = stream;
      localVideo.src = window.webkitURL.createObjectURL(stream);
      localVideo.play();
          localVideo.volume = 0;
    },
    function (error) { // error
      console.error('An error occurred: [CODE ' + error.code + ']');
      return;
    }
        );
  }

  // stop local video
  function stopVideo() {
    localVideo.src = "";
    localStream.stop();
  }

  // ---------------------- connection handling -----------------------
  function prepareNewConnection() {
//    var pc_config = {"iceServers":[]};
    var pc_config = {"iceServers":[ {"url":"stun:133.242.48.9:3478"} ]};
    var peer = null;
    try {
      peer = new webkitRTCPeerConnection(pc_config);
    } catch (e) {
      console.log("Failed to create peerConnection, exception: " + e.message);
    }

    // send any ice candidates to the other peer
    peer.onicecandidate = function (evt) {
      if (evt.candidate) {
        console.log(evt.candidate);
        sendCandidate({type: "candidate",
                          sdpMLineIndex: evt.candidate.sdpMLineIndex,
                          sdpMid: evt.candidate.sdpMid,
                          candidate: evt.candidate.candidate}
                );
      } else {
        console.log("End of candidates. ------------------- phase=" + evt.eventPhase);
      }
    };

    console.log('Adding local stream...');
    peer.addStream(localStream);

    peer.addEventListener("addstream", onRemoteStreamAdded, false);
    peer.addEventListener("removestream", onRemoteStreamRemoved, false)

    // when remote adds a stream, hand it on to the local video element
    function onRemoteStreamAdded(event) {
      console.log("Added remote stream");
      remoteVideo.src = window.webkitURL.createObjectURL(event.stream);
    }

    // when remote removes a stream, remove it from the local video element
    function onRemoteStreamRemoved(event) {
      console.log("Remove remote stream");
      remoteVideo.src = "";
    }

    return peer;
  }

  function sendOffer() {
    peerConnection = prepareNewConnection();
    peerConnection.createOffer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Offer failed");
    }, mediaConstraints);
  }

  function setOffer(evt) {
    if (peerConnection) {
          console.error('peerConnection alreay exist!');
        }
    peerConnection = prepareNewConnection();
    peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }

  function sendAnswer(evt) {
    console.log('sending Answer. Creating remote session description...' );
        if (! peerConnection) {
          console.error('peerConnection NOT exist!');
          return;
        }

    peerConnection.createAnswer(function (sessionDescription) { // in case of success
      peerConnection.setLocalDescription(sessionDescription);
      console.log("Sending: SDP");
      console.log(sessionDescription);
      sendSDP(sessionDescription);
    }, function () { // in case of error
      console.log("Create Answer failed");
    }, mediaConstraints);
  }

  function setAnswer(evt) {
    if (! peerConnection) {
          console.error('peerConnection NOT exist!');
          return;
        }
        peerConnection.setRemoteDescription(new RTCSessionDescription(evt));
  }

  // -------- handling user UI event -----
  // start the connection upon user request
  function connect() {
    if (!peerStarted && localStream && socketReady) { // **
        //if (!peerStarted && localStream) { // --
      sendOffer();
      peerStarted = true;
    } else {
      alert("Local stream not running yet - try again.");
    }
  }

  // stop the connection upon user request
  function hangUp() {
    console.log("Hang up.");
    stop();
  }

  function stop() {
    peerConnection.close();
    peerConnection = null;
    peerStarted = false;
  }

  </script>
</body>
</html>

確認

クライアント1:うぃんどws(れおねっと)
クライアント2:まc(うぃまx)
f:id:m_shige1979:20140809184808p:plain

クライアント1:スマホ(3g)
クライアント2:まc(うぃまx)
f:id:m_shige1979:20140809185841p:plain

※なんかスマホ経由のがちょっと怪しいけどまあなんとかできてる感じでした

時は金なり?

テストが終わったらクラウドサーバはすぐに止めたw
お金かかるしw

まとめ

最初はシグナリングサーバだけあれば通信できると思っていたけど外部同士の場合はstunサーバを必要とするのでサーバ構成が別途必要になる。現在はサンプルソースを流用させて頂いているのでそのうち自分でクライアントプログラムを組めるようにしないと。