メモ：WebRTCを使ったブラウザ間映像送信

映像などをリアルタイムで送受信できるWebRTCは触ったことがないのでざっと概要を見てみる。
少し古いが[ここ](https://qiita.com/massie_g/items/fc5e82d192f6767123bc)とか、[node-webrtc](https://qiita.com/hachicomb/items/d028012fefa69f5f24d8)とかで内容を確認しつつ
[これ](https://ichi.pro/node-js-socket-io-webrtc-de-bideo-chatto-apuri-o-kochikusuru-42831211212823)を試す。

# OBSの設定
試そうとしている環境は仮想サーバでカメラが付いていないのでOBS+仮想カメラプラグインを利用する。
[OBS](https://obsproject.com/ja/download)からダウンロード
使い方は[ここ](https://delaymania.com/202102/premiere/obs-virtual-camera-zoom/)
最新バージョンは仮想カメラプラグインが最初から組み込まれているが、インストーラーからインストールしたほうが良い。

Enable CoreAudio AAC encoder (Windows)が出る場合
[この手順](https://obsproject.com/forum/resources/obs-studio-enable-coreaudio-aac-encoder-windows.220/)を試してみる。
iTunesのExeダウンロードして、7Zipで解凍、AppleApplicationSupport.msiとAppleApplicationSupport.msiを実行
※12.10.8がこれが出来るiTunesの最後のバージョンらしい。[ここ](https://discussions.apple.com/docs/DOC-6562#versions)から旧バージョンをDLできる。

# node.js
フォルダを作成して'npm install express'
普通の画面を出すまでは単なるExpressの使い方なのでサンプルにあるファイルを作成する。
ejsは触った事がないが、今覚えるつもりもないので'npm install ejs'、ソースはコピーして対応。
見た感じJSPのjavascript版といった感じ。

```
<script>
  const ROOM_ID = "<%= roomId %>";
</script>
```
ここでroomIdが未定義とのエラーが出たのでとりあえず固定値で対応。
実際には後からルームIDを一意に取得する処理を追加する。

cssとjsはページだけでは中身が分からないのでサンプル画面から引っこ抜くか、
[git](https://github.com/itstaranarora/video-chat-v1)から取得する。

ここまでで画面は綺麗に表示はされる。
続いてUUIDを用いたルームID。
'npm install uuid'して.jsを変更。.ejsのroomIdも元に戻す。

```javascript
const { v4: uuidv4 } = require('uuid');
app.get('/', (req, res) => {
    res.redirect(`/${uuidv4()}`);
});

app.get('/:room', (req, res) => {
    res.render('room', { roomId: req.param.room });
});
```

http://localhost:3000/a06b02fb-4780-403f-963e-a05f559decb6
のようなURLにリダイレクトされるようになった。

# ブラウザでのカメラ取得
OBSの仮想カメラをブラウザから取得する方法。
今回は仮想カメラのみで音声はないのでaudioはfalseにした。

流れとしては
navigator.mediaDevicesのオブジェクトでgetUserMediaで取得する。
audio,videoの設定が可能で、`{ audio: true, video: { width: 1280, height: 720 } }`のようにして解像度も設定出来る。
`video: { width: {min: 640, ideal: 1280}, height: {min: 480,ideal: 720} }`で可能なら高画質でといった指定も可能。
facingModeのようにフロントカメラを使って欲しいという指示も出せる。
getUserMediaはpromiseを返すのでthen,catchでコントロールできる。

取得したstreamをvideo要素のsrcObjectに放り込めばOK
autoplayを設定していなければvideo.play()で明示的な再生をする。
明示的な再生の方が良いが、[ここ](https://neos21.net/blog/2020/07/18-02.html)とかを見るとplay出来ない場合もあるので`laysinline muted autoplay`を設定してもいいのかも。

streamは.getVideoTracks()[0].enabled、getAudioTracks()[0].enabledで有効無効を切り替え可能。

という感じだが、ここでは以下のコードになっていた。(catchはしていない)
socket(WebSocket)とpeer(WebRTC)については次の節。

```javascript
const myVideo = document.createElement("video");
let myVideoStream;
navigator.mediaDevices
  .getUserMedia({
    audio: false,
    video: true,
  })
  .then((stream) => {
    myVideoStream = stream;
    addVideoStream(myVideo, stream);

    peer.on("call", (call) => {
      略;
    });

    socket.on("user-connected", (userId) => {
      略;
    });
  });

const addVideoStream = (video, stream) => {
  video.srcObject = stream;
  video.addEventListener("loadedmetadata", async () => {
    await video.play();
    videoGrid.append(video);
  });
};
```


あまり関係はないがscript.js上でbackBtnとshowChatは存在しておらずエラーが出たのでコメントアウト。

# WebRTC
既に上のコードでWebSocketsとWebRTCが出ている。これはnode.js側と連携して使う。
サーバ側WebSocketはsoket.ioを使うが、RTCはpeerjs。
[ここ](https://reffect.co.jp/html/webrtcpeerjs)とか[ここ](https://blog.bitsrc.io/simplified-peer-to-peer-communication-with-peerjs-e37244267723)で解説している。

WebRTCはブラウザ間で通信を行うが、その際のIPアドレスなどを伝える手段としてサーバが必要となる。
これはシグナリングサーバと呼ばれている。
そのシグナリングを行うのにWebSocketを使う、というのが大まかな流れ。

クライアント側から見る。
本体の流れは[こんな感じ]([](https://neos21.net/blog/2020/07/18-02.html)
)のようだが、peerjsを使っているので簡略化されている、はず。[解説](https://reffect.co.jp/html/webrtcpeerjs)もある。

クライアント側ではpeerjs.min.jsを読み込んだ上でPeerで接続情報を作成する。

```javascript
var peer = new Peer(undefined, {
  path: "/peerjs",
  host: "/",
  port: "3000",
});
```
最初の引数を空にしておくことで、Peerが自動的にIDを割り振ってくれる。
指定する事も出来る。


接続が正常に行われた場合の処理はpeer.on("open")で設定。
このスクリプトではWebSocketでルームにJOINして、参加している人(自分以外）にPeerのIDを送信する処理がサーバで行われる。

```javascript
peer.on("open", (id) => {
  socket.emit("join-room", ROOM_ID, id, user);
});
```

bradcast等の話は[ここ](https://qiita.com/uranesu/items/8ee0dbe4e472f9fffa49)参考
`socket.to(roomId).broadcast.emit`はエラーになったので
`socket.broadcast.to(roomId).emit`に変更。

```javascript
io.on('connection', (socket) => {
    socket.on('join-room', (roomId, userId) => {
        socket.join(roomId);
        socket.broadcast.to(roomId).emit('user-connected', userId);
    });
});
```

ここで配信されたソケットはsocket.onで受け取られて、接続要求が投げられる。

```javascript
socket.on("user-connected", (userId) => {
  connectToNewUser(userId, stream); //このstreamはmediaDevicesのもの
});

const connectToNewUser = (userId, stream) => {
  const call = peer.call(userId, stream);
  const video = document.createElement("video");
  call.on("stream", (userVideoStream) => {
    addVideoStream(video, userVideoStream);
  });
};
```
で処理される。
peer.callで新規接続ユーザに対して映像送信を設定。
`call.on("stream"`でcallするが、応答があった場合にcallbackでストリーム受信時の処理を書く。
ここでは受信したストリームをvideo要素にセット。


なお、自分自身のカメラ取得が成功した直後の処理で
`peer.on("call")`で誰かからcallがあった場合の処理を記述している。
ここでは自分のstream＝カメラ映像を返す。
同時に相手にもCallして、相手の映像を受け取っている。

```javascript
peer.on("call", (call) => {
  call.answer(stream);
  const video = document.createElement("video");
  call.on("stream", (userVideoStream) => {
    addVideoStream(video, userVideoStream);
  });
});
```

微妙にうまくいかない場合もあるが、一応2つのクライアント間で映像のやりとりは出来た。

今回は動画なのでCallを利用しているが、connectionで普通にテキストメッセージもP2Pで送信する事が可能。らしい。

```javascript
peer.on('connection', conn =>{
  conn.on('data', data => {
    console.log(`別ユーザからのメッセージ：${data}`);
    conn.send('メッセージ返送');
  });
});
```

# その他
デバイスを選択したい場合は[ここ](https://qiita.com/massie_g/items/b9863e4366cfed339528)にあるが、
`navigator.mediaDevices.enumerateDevices().then(function(devices) {...}`
でデバイス一覧を取得出来る。
deviceId,kind,labelの情報を持っている。

IDを指定して選択する場合は以下のように指定すれば良さそう。

```
  var constraints = {
    audio: { deviceId: audioId },
    video: { deviceId: videoId }
  };

  navigator.mediaDevices.getUserMedia(
   constraints
  ).then(function(stream) {
  }
```

また、切断処理については[ここ](https://qiita.com/yusuke84/items/43a20e3b6c78ae9a8f6c)が生のRTCの場合は参考になる。
peerjsの場合は[ここ](https://qiita.com/yusuke84/items/54dce88f9e896903e64f)を見るに
一般的には他人が抜けた場合にはpeerLeaveで捉える模様。
自分の場合はcall.on('close')で切断を検知する。call.close()時に発火？

```
call.on('peerLeave', function(peerId){
    removeVideo(peerId);
});
```
window.closeでwebSocketで飛ばすとかしてleaveを通知した方が確実なのかはまでは調べ切れていない。
（あと無応答になってからのタイムアウト時の挙動とか）

peerjsしか使ってないけど、基本的な部分はなんとなくわかったのでおわり。