[memo] WebRTC + dataChannel + VideoStream の接続フロー

生 WebRTC API で dataChannel での文字列チャットの接続フロー、何回か調べてる気がするので張っておく。
流れを思い出すためのものなので、適当に動くだけで細かい例外処理や、ハンドリングしてないイベントや、構造化はしてない。ちょっといじれば Video も送れる。一対一しかできない。

かける側と受ける側の peer は別オブジェクトにしてるのも、流れを思い出しやすくするため。

サーバ側は socket.io で適当に返している。

## client.js

```js
function prittysdp(sdp) {
  var text = 'type:' + sdp.type + '\n';
  text += sdp.sdp;
  return text
}

function prittyice(ice) {
  var text = JSON.stringify(ice, null, "    ");
  return text;
}

var config = {
  media: {
    video: true,
    audio: false
  },
  peer: {
    iceServers: []
  },
  rtcoption: {
    mandatory: {
      OfferToReceiveAudio:true,
      OfferToReceiveVideo:true
    }
  },
  channel: {
    ordered: false,
    maxRetransmitTime: 3000
  }
}

// サーバに接続(localhost)
var socket = io.connect();

var requester = null;
var responser = null;
var localStream = null;

function chat(dataChannel) {
  var $chatView = $('#chatView');
  var $chatInput = $('#chatInput');
  var $chatSubmit = $('#chatSubmit');

  dataChannel.onopen = function () {
    console.log('chat open');
    $chatInput.prop('disabled', false);
  };

  dataChannel.onmessage = function (e) {
    $chatView.val($chatView.val() + e.data + '\n');
    console.log("Got Data Channel Message:", e.data);
  };

  dataChannel.onerror = function (error) {
    $chatInput.prop('disabled', true);
    console.log("Data Channel Error:", error);
  };

  dataChannel.onclose = function () {
    $chatInput.prop('disabled', true);
    console.log("The Data Channel is Closed");
  };

  $chatSubmit.click(function() {
    var message = $chatInput.val();
    dataChannel.send(message);
    $chatView.val($chatView.val() + message + '\n');
  });
}

$(function() {
  var $localVideo = $('#local-video').get(0);
  var $remoteVideo = $('#remote-video').get(0);

  socket.on('offer', function(offer) {
    // create peer responser
    responser = new webkitRTCPeerConnection(config.responser);
    responser.setRemoteDescription(new RTCSessionDescription(offer));

    // add stream
    if(localStream) {
      responser.addStream(localStream);
    }
    responser.onaddstream = function(e) {
      $remoteVideo.src = URL.createObjectURL(e.stream);
    }
    responser.onremovestream = function(e) {
      $remoteVideo.src = '';
    }

    // candidate
    responser.onicecandidate = function(ice) {
      if (ice.candidate) {
        console.log(prittyice(ice.candidate));
        socket.emit('ice', ice.candidate);
      } else {
        console.log('==END CANDIDATE==');
      }
    }

    // answer
    responser.createAnswer(function success(ans) {
      responser.setLocalDescription(ans);

      responser.ondatachannel = function(e) {
        // data channel
        var dataChannel = e.channel;
        chat(dataChannel);

      };
      console.log(prittysdp(ans));
      socket.emit('answer', ans);
    }, console.error, config.rtcoption);
  });

  socket.on('answer', function(sdp) {
    requester.setRemoteDescription(new RTCSessionDescription(sdp));
  });

  socket.on('ice', function(ice) {
    var candidate = new RTCIceCandidate(ice);
    if (requester) {
      requester.addIceCandidate(candidate);
    } else {
      responser.addIceCandidate(candidate);
    }
  });

  socket.on('stop', function() {
    $('#stop').click();
  });

  $('#connect').click(function() {
    // create peer requester
    requester = new webkitRTCPeerConnection(config.requester);

    // data channel
    var dataChannel = requester.createDataChannel('RTCDataChannel', config.channel);
    chat(dataChannel);

    // add stream
    if (localStream) {
      requester.addStream(localStream);
    }
    requester.onaddstream = function(e) {
      $remoteVideo.src = URL.createObjectURL(e.stream);
    }
    requester.onremovestream = function(e) {
      $remoteVideo.src = '';
    }

    // candidate
    requester.onicecandidate = function(ice) {
      if (ice.candidate) {
        console.log(prittyice(ice.candidate));
        socket.emit('ice', ice.candidate);
      } else {
        console.log('==END CANDIDATE==');
      }
    }

    // offer
    requester.createOffer(function success(offer) {
      console.log(prittysdp(offer));
      requester.setLocalDescription(offer);
      socket.emit('offer', offer);
    }, console.error, config.rtcoption);
  });

  // stop
  $('#stop').click(function() {
    $localVideo.src = '';
    $remoteVideo.src = '';
    if (localStream) {
      localStream.stop();
    }
    socket.disconnect();
    if (requester) {
      requester.close();
      requester = null;
    } else if (responser) {
      responser.close();
      responser = null;
    }
  });

  $('#video').click(function() {
    // start video
    navigator.webkitGetUserMedia(config.media, function success(stream) {
      localStream = stream;
      $localVideo.src = URL.createObjectURL(stream);
      $localVideo.play();
      $localVideo.volume = 0;
    }, console.error);
  });
});
```


## server.js

```js
// Express
var express = require('express')
  , http = require('http')
  , app = express();

app.use(express.static(__dirname + '/public'));

var server = http.createServer(app).listen(3000);
console.log('server start:', 3000);

// Socket.IO
var io = require('socket.io')
  , io = io.listen(server);

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

  socket.on('answer', function(data) {
    socket.broadcast.emit('answer', data);
  });

  socket.on('ice', function(data) {
    socket.broadcast.emit('ice', data);
  });

  socket.on('disconnect', function(data) {
    socket.broadcast.emit('stop', true);
  });
});
```