WebRTC ハンズオン資料 ScreenCapture ＆ マルチストリーム編

# はじめに

この資料は、[WebRTC Beginners Tokyo](https://webrtc.connpass.com) が開催する、WebRTCハンズオン勉強会用の資料です。
※2019.02.06 WebRTC Meetup Tokyo / Osaka 向けに、アップデートしました。
※2019.05.31 Chrome 74 の状況をアップデートしました。

---

# ScreenCapture API 

- 2017年まで ... 各ブラウザ独自にスクリーンキャプチャーを実装
- 2018年以後 ... 仕様が固まりそれに合わせる動きが進んでいる

---

## 仕様

- Screen Capture ... https://w3c.github.io/mediacapture-screen-share/

```js:EXAMPLE1
try {
  let mediaStream = await navigator.mediaDevices.getDisplayMedia({video:true});
  videoElement.srcObject = mediaStream;
} catch (e) {
  console.log('Unable to acquire screen capture: ' + e);
}
```

仕様上は指定できるオプション（まだ効果がないものが多い）

- width, height, frameRate ... Chrome Canary 74 では有効
- aspectRatio
- resizeMode ... "none" / "crop-and-scale" 
- displaySurface ... "monitor" / "window" / "application" / "browser"
- logicalSurface ... true / false
- cursor ...   "never" / "always" / "motion"

---

## 以前のChrome

※詳細は省略

- 機能拡張を作成し、その中で chrome.desktopCapture.chooseDesktopMedia()を呼び出す
- 取得した streamID を使って、 navigator.mediaDevices.getUserMedia() で取得する

---

## Chrome 71 （2019.01.27現行）

- chrome://flags/#enable-experimental-web-platform-features を Enable にすると使える
- audioは取得できない（エラーになる）

```js
let screen = await navigator.getDisplayMedia( { video: true });
```

---

## Chrome 72 （近日登場→2019.01.30リリース済）

- フラグ不要。デフォルトで使えるように
- 仕様に沿ったAPIになる
    - 仕様上は  "monitor", "window", などを指定できるが、それは無視される
- audioは取得できない（エラーになる）
- ※2019.01.30 現在、72がリリース済

```js
let screen = await navigator.mediaDevices.getDisplayMedia( { video: true } );

// パラメーターは無視される
let win = await navigator.mediaDevices.getDisplayMedia( { video: { displaySurface: "window" } } );
```

---

Chrome 72 のキャプチャ対象選択ダイアログ

<img width="619" alt="chrome72_capture.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/46b6ecbb-3e6f-026a-d2a1-6e2c96c5a52c.png">

<!--
Chrome 73 Canary のキャプチャ対象選択ダイアログ

<img width="604" alt="chrome73_capture.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/86312053-06a9-456c-7f36-cdc95113d495.png">
-->

## Chrome 74 （2019.05.31現在）

- audioが取得できるように
    - audio単体では取得できない。videoとセット
- { video: true, audio: true } で　getDisplayMedia() を呼び出す
- 選択ダイアログの左下に　「音声を共有する」のチェックボックスが表示される
    - Windows の場合 ... 全画面 および Chromeタブ の場合
    - それ以外の場合 ... Chromeタブ の場合
- ユーザーが明示的に「音声を共有する」をチェックした場合は、音声も取れる

※音声を取るにはソースコードのオプションと、ユーザーの操作の2重の両方が必要

![screen_capture_with_audio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/19403/4d655395-ee36-ecc3-d592-8a9930c28233.png)




---

## Firefox 64/65（現行）

```js

let screen = await navigator.mediaDevices.getUserMedia({
  video: {mediaSource: "screen"}
});
```

- mediaSrouce には、"window" を指定することもできる

---

## Firefox Nightly 66

- 仕様に沿った実装がされている
    - 仕様上は  "monitor", "window", などを指定できるが、それは無視される
- audioは取得できない（無視される）


```js
let screen = await navigator.mediaDevices.getDisplayMedia( { video: true } );
```

---
Firefox Nightly 66 のキャプチャ対象選択ダイアログ

<img width="396" alt="firefox66_capture.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/3329a592-5fcb-f005-028c-c940dd3e9b59.png">

---

## Safari Technology Preview 73/74

- 開発オプションで有効にできる
    - Develop - Experimental Features - ScreenCapture
- 仕様に沿った実装がされている
- スクリーン全体だけ（ウィンドウは選択できない）
- audioは取得できない（無視される）

```js
let screen = await navigator.mediaDevices.getDisplayMedia( { video: true } );
```

---

Safari TP 73 のキャプチャ許可ダイアログ

<img width="559" alt="safari_tp73_capture.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/c66f351f-6ba1-dd15-b07a-ac9cf4bca422.png">


---

# スクリーンキャプチャーを試してみる

※2019.01.30追記

GitHub Page で次の3つのパターンが試せる

- https://mganeko.github.io/webrtc_screen_multistream/capture_test.html
- [getUserMedia]ボタン ... 現行Firefox 64用
- [navigator.getDisplayMedia]ボタン ... Chrome 71+フラグ設定
- [mediaDevices.getDisplayMedia]ボタン ... Chrome 72, Safari TP74+オプション


---



## ビデオチャット+スクリーンキャプチャー

すでにWebRTCでビデオチャットしている最中に、あとからScreenCapureで画面共有をする場合、3通りの方法が考えられる。

- マルチ Peer
- ビデオトラック差し替え
- マルチストリーム

---

# マルチ Peer

- 新たに PeerConnection を接続し、そちらで画面共有のMediaStreamを送る
- 利点：最新でないブラウザや、ライブラリでも動作する。異なるブラウザ間でも動作する
- 欠点：接続まで（少々）時間がかかる。ポートを消費する

![multi_peer2.png](https://qiita-image-store.s3.amazonaws.com/0/19403/30ad6c38-3aca-e095-1063-4ecd8cd39446.png)


---

## ビデオトラック差し替え

- 通信中の PeerConnection の、ビデオトラックを差し替える
    - RTCRtpSender.replaceTrack() を使う
- 利点：接続済のPeerConnectionをそのまま利用できる
- 欠点：カメラ映像と画面共有を同時には使えない（声はOK）

![replace_track2.png](https://qiita-image-store.s3.amazonaws.com/0/19403/edb51117-2f1b-bf76-f39b-ea805b16bf46.png)

---

## マルチストリーム

- 通信中の PeerConnection に、ビデオトラックを追加する
    - PeerConnection.addTrack()を使う
- 利点：接続済のPeerConnectionをそのまま利用できる
- 欠点：最新のブラウザでしか使えない、今までは異なるブラウザ間では通信できなかった
    - → 各ブラウザが Unified Plan という方式に統一されて、使えるようになる

![multistream.png](https://qiita-image-store.s3.amazonaws.com/0/19403/42952a5a-1ee7-6244-c7db-54aa2f86d3fd.png)


<!--

---

- マルチ Peer
    - 新たに PeerConnection を接続し、そちらで画面共有のMediaStreamを送る
    - 利点：最新でないブラウザや、ライブラリでも動作する。異なるブラウザ間でも動作する
    - 欠点：接続まで（少々）時間がかかる。ポートを消費する
- ビデオトラック差し替え
    - 通信中の PeerConnection の、ビデオトラックを差し替える
        - RTCRtpSender.replaceTrack() を使う
    - 利点：接続済のPeerConnectionをそのまま利用できる
    - 欠点：カメラ映像と画面共有を同時には使えない（声はOK）
- マルチストリーム
    - 通信中の PeerConnection に、ビデオトラックを追加する
        - PeerConnection.addTrack()を使う
    - 利点：接続済のPeerConnectionをそのまま利用できる
    - 欠点：最新のブラウザでしか使えない、今までは異なるブラウザ間では通信できなかった

※ いよいよ各ブラウザの対応が揃ってきて、マルチストリームが使えるようになる


---

![multi_peer.png](https://qiita-image-store.s3.amazonaws.com/0/19403/e2bccfea-1239-29ef-7634-916562069190.png)

---
![replace_track.png](https://qiita-image-store.s3.amazonaws.com/0/19403/3b2f0788-3f8d-eab2-4527-f5c0b211d34e.png)

-->
---

# Unified Plan と Plan B

- マルチストリーム
    - 複数のビデオトラックやオーディをトラック、さらにはデータチャネルをまとめて通信できる仕組み
- これまでは、ブラウザごとに異なる方式を利用
    - Firefox ... Unified Plan
    - Chrome, Safari ... Plan B
- 現在は Unified Plan に統一されるつつある
    - Firefox, Chrome 72〜, Safari TP 67〜（開発オプション） 
- 参考：[LT資料：Unified Plan時代のWebRTC（半）手動シグナリング](https://qiita.com/massie_g/items/9cc2133d30681664a447)


```js
// Chrome で 明示的にUnified Plan を使う場合
let peer = new RTCPeerConnection({sdpSemantics : "unified-plan"});
```

---

# SDPの違い: Unified Plan


```
a=group:BUNDLE sdparta_0 sdparta_1 sdparta_2 sdparta_3 sdparta_4
a=msid-semantic:WMS *

m=application 64895 DTLS/SCTP 5000
a=mid:sdparta_0

m=video 64922 UDP/TLS/RTP/SAVPF 120 121 126 97
a=mid:sdparta_1
a=msid:{968e2268-4336-4d48-xxx} {c40feace-2302-4ca6-yyy}

m=audio 64924 UDP/TLS/RTP/SAVPF 109 101 0 8 9
a=mid:sdparta_2
a=msid:{968e2268-4336-4d48-xxx} {5550b468-49a8-zzz}

m=video 0 UDP/TLS/RTP/SAVPF 120 121 126 97
a=mid:sdparta_3
a=msid:{5f905219-2416-4d96-bbb} {8798f142-402b-419d-ccc}

m=audio 0 UDP/TLS/RTP/SAVPF 109 9 0 8 101
a=mid:sdparta_4
a=msid:{0721cc21-5cb1-473d-cccc} {423518ba-9ca7-41e6-cccc}
```

- 各 video, audio 毎に m= の行がある。利用するコーデックなどはこの単位で選択できる
    - → 1つのPeerConnectionで、異なるコーデックのビデオを一緒に送ることができる（仕様上）

---

# SDPの違い: Plan B

```
a=group:BUNDLE data video audio

m=application 59191 DTLS/SCTP 5000
a=mid:data

m=video 59203 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 … 118 114
a=mid:video
a=ssrc:1934825139 msid:4QD5cPipYYHfxxxx 1bc7a4bb-545c-yyy
a=ssrc:1928081165 msid:5b439d1a-4c68-zzz 190251fa-7934-434c-aaa

m=audio 59207 UDP/TLS/RTP/SAVPF 111 103 104 9 0 8 106 105 13 110 112 113 126
a=mid:audio
a=sendrecv
a=ssrc:3639648018 msid:4QD5cPipYYHfxxx a69cca57-009f-442e-bbb
a=ssrc:3896373756 msid:037f3b64-feee-ccc 7c449c50-8607-ddd
```

- m=video, m=audio は1行ずつ。共通のコーデック指定になる

---

# マルチストリームを使ったビデオチャット+画面共有

マルトストリーム(Unified Plan)を用いて

- カメラ(Video)+マイク(Audio)のビデオチャットに
- 画面共有(Video)を追加する

Unified Plan が有効になったので、異なるブラウザ間でマルチストリーム可能

- Chrome 72 --> Firefox 64

---

## デモ

動かし方

- [WebRTCハンズオン 本編](https://qiita.com/yusuke84/items/43a20e3b6c78ae9a8f6c#step3)のシグナリングサーバーを起動
    - コード：GitHubのbranch_step3 [webrtc-handson-2016/server/signaling.js](https://github.com/yusuke84/webrtc-handson-2016/blob/handson_step3/server/signaling.js)
- クライアント用のコードを、ローカルにダウンロード(git cloneや、zipでダウンロード）
    - コード： GitHub [mganeko/webrtc_screen_multistream](https://github.com/mganeko/webrtc_screen_multistream)
- ローカルWebサーバーのフォルダーに配置
- 2つのブラウザで screen.html を開く
    - Chrome 72 Beta, Chrome Canary 73, Firefox Nightly 66, Safari TP 73 のいずれか
- 両方のブラウザで、[Start Video]ボタンをクリック
- 片方のブラウザで、[Connect]ボタンをクリック
    - → P2P通信が確立し、片方の映像がもう一方のブラウザに表示される
- 片方のブラウザで、[Add Screen]ボタンをクリック
    - スクリーン全体、ウィンドウを選択する
    - → キャプチャーが始まり、もう一方のブラウザに表示される
- [Remove Screen]ボタンをクリックすると、キャプチャーが終了する

---

## JavaScript API の視点

1つのPeerConnectionで、複数のVideo / Audioの通信が可能

<img width="1081" alt="multistream_js.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/7b2080cc-e180-1333-1d77-8f5215332735.png">

---

## プロトコルの視点

<img width="1081" alt="multistream_port.png" src="https://qiita-image-store.s3.amazonaws.com/0/19403/427a8ded-9962-569f-bf7a-1840d2c731b2.png">

---

## コードの抜粋

```js:画面共有開始側
let screenStream = await navigator.mediaDevices.getDisplayMedia({video: true});
let screenSender = peer.addTrack(screenStream.getVideoTracks()[0], screenStream);

// --- peer.onnegotiationneeded() が発火 --
peer.onnegotiationneeded = async () =>  {
  let offer = await peer.createOffer();
  await peer.setLocalDescription(offer);

  sendSDP(offer); // 相手に送る
}
```

```js:受け側
// WebSocketで offer を受け取った場合の処理
async function setOffer(sessionDescription) {
  try{
    await peerConnection.setRemoteDescription(sessionDescription);
    makeAnswer();
  } catch(err){
    console.error('setRemoteDescription(offer) ERROR: ', err);
  }
}

async function makeAnswer() {
  try{
    let answer = await peerConnection.createAnswer();
    await peerConnection.setLocalDescription(answer);
    sendSdp(peerConnection.localDescription);
  } catch(err){
    console.error(err);
  } 
}
```

---

# 注意： onnegotiationneeded() のタイミングの違い

- videoTrack, audioTrack を連続して addTrack()した場合
    - Chrome ... 2回発生する
    - Firefox, Safari ... 1回だけ発生する


---

# 追記：Unified Plan と Peer.addTranceiver()

---

# Peer.addTransceiver() による片方向通信

Chromeでは、Unified Planと共に RTCPeerConnection.addTransceiver() がサポート

- これまで
    - 映像、音声、データチャネルのいずれか1つ以上が無いと、有効なOfferが生成できなかった
    - → 片方向配信の場合、必ず配信側がOfferを出す必用があった
- PeerConnection.addTransceiver() の登場
    - メディアが無くても、RTCRtpTransceiver が作れる
    - RTCRtpTransceiver.direction = 'recvonly' で受信のみが指定可能
    - その状態で、createOffer()できる
- onnnegotiationneeded()も発火
    - addTransceiver()
    - directrion 変更


```js:コード抜粋
  let videoTransceiver = peer.addTransceiver('video');
  videoTransceiver.direction = 'recvonly';

  let audioTransceiver = peer.addTransceiver('audio');
  audioTransceiver.direction = 'recvonly';

  let offer = await peer.createOffer();
```

デモ

---

# 双方向の場合

![sendrcev.png](https://qiita-image-store.s3.amazonaws.com/0/19403/dac43f79-efeb-0961-c1ad-9f36481d4647.png)

---

# 片方向の場合

![recvonly.png](https://qiita-image-store.s3.amazonaws.com/0/19403/1ca8ec84-f01d-c9d8-f77c-a3e012a8113b.png)

---

# まとめ

- WebRTC は、仕様の実装に向けて進んでいる
- マルチストリームもUnfied Planで統一
    - 参考：[Safari 12.1 で VP8 と Unified Plan が入る](https://medium.com/@voluntas/safari-12-1-%E3%81%A7-vp8-%E3%81%A8-unified-plan-%E3%81%8C%E5%85%A5%E3%82%8B-47c3aa0cb9eb)
        - "Safari 12.1 からSafari はWebRTC 対応でブラウザはなく推奨ブラウザです、覚えておいてください。"
- マルチストリームも相互接続性が上がり、いよいよ使える


# End