WebRTC メモ

だいぶ前にいろいろ試していた時に適当に書いたもの（フルメッシュです）をあげておきます

![Screenshot_2015-06-03-22-53-19.jpg](https://qiita-image-store.s3.amazonaws.com/0/36202/25296df4-f510-d23d-dbeb-55097988dac9.jpeg)


```html
<html>
  <head>
    <title></title>
    <style type="text/css">
      <!--
        video {
          width: 200px;
          height: 200px;
        }
      -->
    </style>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js"></script>
  </head>
  <body>
    <div>
      <strong id="srcId" ></strong>
    </div>
    <div>
      <input id="dstId" type="text" />
      <input type="button" value="connect" onclick="connect()" />
    </div>
    <div id="local">
      <video id="localVideo" autoplay />
    </div>
    <hr />
    <div id="remote">
    </div>

    <script>
      var RTCPeerConnection = window.RTCPeerConnection || window.webkitRTCPeerConnection;
      var RTCSessionDescription = window.RTCSessionDescription;
      var RTCIceCandidate = window.RTCIceCandidate || window.RTCIceCandidate;

      navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;

      var ws;
      var pcs = {};
      var dcs = {};
      var dcLabel = 'label';

      var localStream;

      function getMedia() {
        navigator.getUserMedia(
          {video: true},
          function(stream) {
            localStream = stream
            localVideo.src = window.URL.createObjectURL(stream)
            localVideo.play();
          },
          function(error) {
            console.error(error);
          }
        )
      }

      getMedia();


      function getId() {
        var url = 'http://' + location.host + '/id';
        var xhr = new XMLHttpRequest();
        xhr.open('get', url)
        xhr.onreadystatechange = function() {
          if (this.readyState == xhr.DONE && this.status == 200) {
            var message = JSON.parse(this.responseText);
            var id = message.id
            var srcId = $('#srcId');
            srcId.text(id);

            wsConnect(id);
          }
        };

        xhr.send(null);
      }

      function wsConnect(id) {
        ws_url = 'ws://' + location.host + '/ws/' + id;
        ws = new WebSocket(this.ws_url);
        ws.onopen = function() {
          console.log('WebSocket open');
        };

        ws.onmessage = handleMessage;
        ws.onclose = function(event) {
          console.log('close');
        };
      }

      function handleMessage(event) {
        var message = JSON.parse(event.data);
        var id = message.id;
        if (message.type === 'offer') {
          var config = {'iceServers': []};
          var pc = new RTCPeerConnection(config);
          var dc = pc.createDataChannel(dcLabel);
          pcs[id] = pc;
          dcs[id] = dc;

          var sessionDescription = new RTCSessionDescription({type: message.type,
                                                              sdp: message.sdp})
          pc.setRemoteDescription(sessionDescription);
          pc.onicecandidate = function(event) {
            var candidate = event.candidate;
            if (candidate) {
              pc.addIceCandidate(candidate);
              var message = JSON.stringify({type: "candidate",
                                            id: id,
                                            sdpMid: candidate.sdpMid,
                                            candidate: candidate.candidate});
              ws.send(message);
            }
          };
          pc.onclose = function(event) {
            console.log('close');
          };
          pc.onerror = function(error) {
            console.error(error);
          };

          pc.addStream(localStream);
          pc.onaddstream = function(event) {
            var remote = $('#remote');
            var remoteVideo = $('<video>');
            remoteVideo.attr('src', window.URL.createObjectURL(event.stream));
            remoteVideo.attr('autoplay', '');
            remote.append(remoteVideo);
          };

          sendAnswer(pc, ws, id);
        } else if (message.type === 'answer') {
          var sessionDescription = new RTCSessionDescription({type: message.type,
                                                              sdp: message.sdp})
          pc = pcs[id];
          pc.setRemoteDescription(sessionDescription);
        } else if (message.type === 'candidate') {
          var iceCandidate = new RTCIceCandidate({sdpMLineIndex: message.sdpMLineIndex,
                                                  candidate: message.candidate})
          pc = pcs[id];
          pc.addIceCandidate(iceCandidate);
        } else if (message.type === 'connections') {
          console.log(message)
        }
      }

      function connect(id) {
        var id = id || $("#dstId").val();
        var config = {'iceServers': []};
        var pc = new RTCPeerConnection(config);
        pcs[id] = pc;
        pc.onicecandidate = function(event) {
          var candidate = event.candidate
          if (candidate) {
            var message = JSON.stringify({type: "candidate",
                                          id: id,
                                          sdpMLineIndex: candidate.sdpMLineIndex,
                                          candidate: candidate.candidate});
            ws.send(message);
          }
        };
        pc.onclose = function(event) {
          console.log('close');
        };
        pc.onerror = function(error) {
          console.error(error);
        };
        pc.addStream(localStream);
        pc.onaddstream = function(event) {
          var remote = $('#remote');
          var remoteVideo = $('<video>');
          remoteVideo.attr('src', window.URL.createObjectURL(event.stream));
          remoteVideo.attr('autoplay', '');
          remote.append(remoteVideo);
        };

        var dc = pc.createDataChannel(dcLabel);
        dcs[id] = dc;
        dc.onmessage = function(event) {
          var message = JSON.parse(event.data);
          if (message.type === 'connections') {
            var srcId = $('#srcId').text();
            var dcIds = Object.keys(dcs);
            var ids = message.ids.filter(function(e) { return dcIds.indexOf(e) < 0 });
            ids.forEach(function(channelId) {
              if (channelId !== srcId) {
                connect(channelId);
              }
            })
          }
        };
        dc.onopen = function(event) {
          console.log('open');
          var ids = Object.keys(dcs);
          dc.send(JSON.stringify({type: 'connections', ids: ids}));
        };
        dc.onclose = function(event) {
          console.log('close');
          for (id in dcs) {
            if (this === dcs[id]) {
              delete dcs[id];
            }
          }
        };
        pcs[id] = pc;
        dcs[id] = dc;

        sendOffer(pc, ws, id);
      };

      function sendOffer(pc, ws, id)  {
        var options = {};
        pc.createOffer(options)
          .then(sessionDescription => {
            return pc.setLocalDescription(sessionDescription)
          })
          .then(() => {
            var message = JSON.stringify({type: pc.localDescription.type,
                                          id: id,
                                          sdp: pc.localDescription.sdp});
            ws.send(message);
          })
          .catch(reason => console.error(reason));
      }

      function sendAnswer(pc, ws, id) {
        var options = {};
        pc.createAnswer(options)
          .then(sessionDescription => {
            return pc.setLocalDescription(sessionDescription)
          })
          .then(() => {
            pc.ondatachannel = function(event) {
              var dc = event.channel;
              dcs[id] = dc;
              dc.onmessage = function(event) {
                var message = JSON.parse(event.data);
                if (message.type === 'connections') {
                  var srcId = $('#srcId').text();
                  var dcIds = Object.keys(dcs);
                  var ids = message.ids.filter(function(e) { return dcIds.indexOf(e) < 0 });
                  ids.forEach(function(channelId) {
                    if (channelId !== srcId) {
                      connect(channelId);
                    }
                  })
                }
              };
              dc.onopen = function(event) {
                var ids = Object.keys(dcs);
                dc.send(JSON.stringify({type: 'connections', ids: ids}));
              };
              dc.onclose = function(event) {
                console.log('close');
                for (id in dcs) {
                  if (this === dcs[id]) {
                    delete dcs[id];
                  }
                }
              };
            };

            var message = JSON.stringify({type: pc.localDescription.type,
                                          id: id,
                                          sdp: pc.localDescription.sdp});
            ws.send(message);
          })
          .catch(reason => console.error(reason));
      }

      getId();
    </script>
  </body>
</html>
```


手動シグナリングは面倒だったので、下記のような簡単なシグナリングサーバを用意しました

```py
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import hashlib
import simplejson
import tornado.web
import tornado.websocket

CONNECTIONS = {}

class BasicHandler(tornado.web.RequestHandler):
    pass

class MainHandler(BasicHandler):
    def get(self):
        self.render('index.html')

class IdHandler(BasicHandler):
    def generate_id(self):
        return hashlib.sha1(str(id(self))).hexdigest()

    def get(self):
        id = self.generate_id()
        res = simplejson.dumps({'id': id})
        self.write(res)

class WebSocketHandler(tornado.websocket.WebSocketHandler):

    def initialize(self):
        self.source_id = ''

    def push_handler(self, id, handler):
        CONNECTIONS[id] = handler

    def remove_handler(self, id):
        del CONNECTIONS[id]

    def get_handler(self, id):
        return CONNECTIONS.get(id)

    def open(self, id):
        self.source_id = id
        self.push_handler(id, handler = self)
        print('open: ' + id)

    def on_message(self, message):
        payload = simplejson.loads(message)
        id = payload['id']
        handler = self.get_handler(id)
        payload['id'] = self.source_id
        json = simplejson.dumps(payload)
        handler.write_message(json)

    def on_close(self):
        print('close')

    def close(self):
        self.write_message('close')

if __name__ == '__main__':
    application = tornado.web.Application([
        (r'/', MainHandler),
        (r'/id', IdHandler),
        (r'/ws/(.+)', WebSocketHandler),
    ], debug=True)
    application.listen(9999)
    tornado.ioloop.IOLoop.instance().start()
```