PeerJS+Socket.ioで即席ボイスチャット

[PeerJS](http://peerjs.com/)を使用すると、相手にP2Pキーを伝えることで、音声や映像を共有できます。
ただ、[PeerJSCloud](http://peerjs.com/peerserver)を利用する場合、__ブラウザに表示されたP2Pキーを手動で入力しなければ__、共有することができません。

そこで、公式が提供している[PeerJSServer](https://github.com/peers/peerjs-server)を[Express4](http://expressjs.com/)上に設置し、ページにアクセスしたユーザーを無差別に共有するチャットルームを作成してみます。

# 環境

* NodeJS v5.7.0
* Npm v3.6.0
* [GoogleChrome](https://www.google.co.jp/chrome/browser/features.html) 48.0.2564.116 (OSX 10.11.3 64-bit)

下記`package.json`を使用します。

```bash:.
mkdir voicechat
cd voicechat

touch package.json #下記を copy&paste
```

```json:package.json
{
  "main": "server.js",
  "private": true,
  
  "dependencies": {
    "express": "^4.13.4",
    "peer": "^0.2.8",
    "socket.io": "^1.4.5"
  },
  "scripts": {
    "server": "node server.js"
  }
}
```

```bash
npm install
```

依存モジュールがインストールできたら、次へ進みます。

# 各サーバーの依存関係
まず、[Express4](http://expressjs.com/ja/api.html#app.listen)と[Socket.io](http://socket.io/docs/#using-with-node-http-server)、[peerServer](https://github.com/peers/peerjs-server#combining-with-existing-express-app)を下記のように依存させ、指定`PORT`にサーバーを起動するプログラムを作成します。

```js:server.js
// Dependencies
const express = require('express');
const createHttpServer = require('http').createServer;
const createIo = require('socket.io');
const createPeerServer = require('peer').ExpressPeerServer;

// Environment
const port = process.env.PORT || 59798;

// Routes
const app = express();
const httpServer = createHttpServer(app);
const io = createIo(httpServer);
const peerServer = createPeerServer(httpServer);

app.use('/api', peerServer);
app.use(express.static(__dirname));

// Boot
httpServer.listen(port, () => {
  console.log(`Boot on http://localhost:${port}`);
});
```

同時に、以下の`index.html`を用意し、必要なライブラリがグローバル変数に定義されているか確認してください。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Voicechat</title>
  <script src="/socket.io/socket.io.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/0.3.14/peer.js"></script>
  <script>
navigator.webkitGetUserMedia({audio:true}, function(stream) {
  var audio = new Audio;
  audio.src = URL.createObjectURL(stream);
  audio.controls = true;
  audio.play();
  document.body.appendChild(audio);
}, function(error) {
  alert('Failed to get local stream' ,error);
});
  </script>
</head>
<body>
  
</body>
</html>
```

`package.json`からサーバーを起動します。

```bash
npm start
# Boot on http://localhost:59798
```

# マイクの許可

![スクリーンショット_2016-02-24_22_01_13.png](https://qiita-image-store.s3.amazonaws.com/0/28576/b50f1bf0-d64d-ba69-60ea-da8b7f30e3da.png)

デフォルトでは、ブラウザからマイクへのアクセスが禁止されています。これを許可すると、右ウィンドウのように確認用のAudio要素が表示されます。
この状態でマイクに喋ると、マイクの音声がそのまま再生されるはずです。

# マイク接続→Socket.io接続→PeerJS接続

PeerJSにはP2Pキーを管理する機能がありません。代わりにSocket.ioでこれを通知できるように、クライアントの識別idである`socket.id`をP2Pキーとして使用します。

`index.html`を下記のように変更します。

```html:index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Voicechat</title>
  <script src="/socket.io/socket.io.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/0.3.14/peer.js"></script>
  <script>
navigator.webkitGetUserMedia({ audio: true }, function (stream) {
  var peer;
  var socket = io();
  socket.on('connect', function () {
    var options = {
      host: location.hostname,
      port: location.port,
      path: '/api', // app.use('/api', peerServer);と同じ位置になるように
    };
    peer = new Peer(socket.id, options);
  });
}, function(error) {
  alert('Failed to get local stream', error);
});
  </script>
</head>
<body>
  
</body>
</html>
```

クライアントが`new Peer`を実行すると、サーバーは`peerServer.on('connection')`[^1]からP2Pキーを受け取ること出来ます。

```js:server.js
// Dependencies
const express = require('express');
const createHttpServer = require('http').createServer;
const createIo = require('socket.io');
const createPeerServer = require('peer').ExpressPeerServer;

// Environment
const port = process.env.PORT || 59798;

// Routes
const app = express();
const httpServer = createHttpServer(app);
const io = createIo(httpServer);
const peerServer = createPeerServer(httpServer);

app.use('/api', peerServer);
app.use(express.static(__dirname));

// Boot
httpServer.listen(port, () => {
  console.log(`Boot on http://localhost:${port}`);
});

// Manage p2p keys
const keys = [];
peerServer.on('connection', (key) => {
  keys.push(key);

  console.log('connected', keys);

  io.emit('keys', keys);// 接続中の全てのクライアントへ`keys`イベントを発行
});
peerServer.on('disconnect', (key) => {
  const index = keys.indexOf(key);
  if (index > -1) {
    keys.splice(index, 1);
  }
  console.log('disconnect', keys);

  io.emit('keys', keys);// 同上
});
```

```bash
npm start
# Boot on http://localhost:59798
# connected [ 'ZJ4vZ-bmXXE405AyAAAA' ]
```

# →待機→共有

前述の`server.js`により、接続が更新される度に`keys`イベントを受信しますので、受信したP2Pを元にAudio要素を生成すれば、マイク音声の共有が可能です。

```index.html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Voicechat</title>
  <script src="/socket.io/socket.io.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/peerjs/0.3.14/peer.js"></script>
  <script>
navigator.webkitGetUserMedia({ audio: true }, function (stream) {
  var peer;
  var socket = io();
  socket.on('connect', function () {
    var options = {
      host: location.hostname,
      port: location.port,
      path: '/api', // app.use('/api', peerServer);と同じ位置になるように
    };
    peer = new Peer(socket.id, options);
    peer.on('call', function (call) {
      console.log('%sにcallされました', call.peer);
      call.answer(stream);
    });
  });

  // 受信した全てのP2Pkeyをaudio要素に変換する
  socket.on('keys', function (keys) {
    var audios = document.querySelector('#audios');
    audios.innerHTML = '';
    
    var index = keys.indexOf(socket.id);
    if (index > -1) {
      keys.splice(index, 1);// 自分自身を無視
    }
    keys.forEach(function (key) {
      var call = peer.call(key, stream);
      if (call === undefined) {
        console.log(key + 'なんて居ません');
        return;
      }
      call.on('stream', function (remoteStream) {
        var audio = new Audio;
        audio.src = URL.createObjectURL(remoteStream);
        audio.controls = true;
        audio.play();

        audios.appendChild(audio);
      });
    });
  });
}, function (error) {
  alert('Failed to get local stream', error);
});
  </script>
</head>
<body>
  
<section id="audios">
  <p>他のユーザーを待っています…</p>
</section>

</body>
</html>
```

![ぼっち](https://cloud.githubusercontent.com/assets/1548478/13287700/bb8b780a-db4b-11e5-9ef7-5cfda6ad8450.gif)

# その他注意事項
`https://`か`localhost`上でなければ、getUserMediaは動作しません。下記のような警告文が表示されて失敗します。

```js:http上で開発コンソールを開いて入力する
navigator.webkitGetUserMedia({audio:true},x => null,x => null)
// getUserMedia() no longer works on insecure origins. To use this feature, you should consider switching your application to a secure origin, such as HTTPS. See https://goo.gl/rStTGz for more details.
```

そのため、https上でデプロイ出来る環境が必要になります。[Heroku](https://www.heroku.com/)や[c9](http://c9.io/)など。

おわりに
---
> 動作確認用リポジトリ： [59naga/peerjs-voicechat - github](https://github.com/59naga/peerjs-voicechat)

友達が居なくて３機以上の動作確認ができませんでした (◞‸◟）

[^1]: [ExpressPeerServer()](https://github.com/peers/peerjs-server/blob/0.2.8/lib/index.js#L8) < [express()](https://github.com/expressjs/express/blob/4.9.8/lib/express.js#L32) のような依存。…接続確立時に[.emit('connection')](https://github.com/peers/peerjs-server/blob/0.2.8/lib/server.js#L107)