【備忘録】[getUserMedia] ChromeとElectron(とFirefox)でスクリーンキャプチャーおよびChromeとElectronで(システムまたはタブ)オーディオのキャプチャー

## はじめに謝ります。
WebRTCとはちょっと外れた内容となっています。
ですが、WebRTCに深くかかわるところですので、WebRTCアドベントカレンダーの10日目として投稿させていただきました。
申し訳ございません。

## Chromeでスクリーンキャプチャー
(**2018/10/26追記**)ようやく、ChromeにScreen Capture API(navigator.getDisplayMedia)の実装が開始されました。これにより、拡張機能なしにスクリーンキャプチャーができるようになります。
ただし、Chrome70においてはまだデフォルトで有効となっていません。Chrome70で試したい場合は、Chrome://flagsの[Experimental Web Platform features](chrome://flags/#enable-experimental-web-platform-features)をEnabledに設定することで使用することができます。

```js:getDisplayMedia()
navigator.getDisplayMedia({video:true}).then(stream => video.srcObject = stream);
// Chrome70ではまだaudioがサポートされてませんが、将来的にaudioもサポートされるみたい。
```

現時点において(デフォルトの設定において)はまだ拡張機能を作成する必要があります
[サンプルGitHub](https://github.com/turbographics2000/Chrome_ScreenCapture_Test)

```js:拡張機能のbackgroundのスクリプト
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window', 'tab'], streamId => {
 // streamIdをcontent_script等に渡す
});
```

```js:拡張機能のcontent_scriptまたはページのスクリプト
// 渡されたstreamIdを使用してgetUserMedia()を実行しストリームを取得
navigator.mediaDevices.getUserMedia({
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
      // 必要に応じてサイズ(解像度)の制約も記述
      // minWidth: 1280,
      // maxWidth: 1920,
      // minHeight: 720,
      // maxHeight: 1080
    }
  }
}).then(stream => {
  video.srcObject = stream;
}).catch(err => {
  // error
});
```
## Electronでスクリーンキャプチャー
Chromeのスクリーンキャプチャーの経験をもとに、Electronでこれと同等なAPIはどれかと探すと、desktopCapturerが見つかります。
[サンプルGitHub](https://github.com/turbographics2000/Electron_ScreenCapture_Test)

```js:rendererプロセス
const desktopCapturer = require('electron').desktopCapturer;
let mediaSources = null;

desktopCapturer.getSources({types: [ 'screen', 'window']}, (err, sources) => {
  mediaSources = sources;
  /* 含まれる情報
  sources.forEach(souce => {
    source.id // id
    source.name // 画面名(ウィンドウタイトル)
    source.thumbnail // サムネイル
  });
  */
});

navigator.mediaDevices.getUserMedia({
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: mediaSources[1].id,
      // 必要に応じてサイズ(解像度)の制約も記述
      // minWidth: 1280,
      // maxWidth: 1920,
      // minHeight: 720,
      // maxHeight: 1080
    }
  }
}).then(stream => {
  video.srcObject = stream;
}).catch(err => {
  // error
});
```

## Chrome(の拡張機能)のchrome.desktopCapture.chooseDesktopMedia()と、ElectronのdesktopCapturer.getSources()違い
Chromeのchrome.desktopCapture.chooseDesktopMedia()は実行するとまず、スクリーンまたはウインドウをユーザーが選択するためのダイアログが表示されます。
ユーザーがどの画面をキャプチャーするかを選択(単一選択)すると、第2引数のコールバックに選択した画面の(たった一つの)streamIdのみが渡されます。
それに対し、ElectronのdesktopCapturer.getSources()は、実行しても選択ダイアログが表示されることはなく、その代わりに第2引数のコールバックには、キャプチャー可能なスクリーン及びウィンドウが列挙された配列が渡されます。
渡される情報には、(streamIdに当たる)idのほかにname(ウィンドウタイトル)やthumbnail(サムネイル画像)が含まれています。
つまり、Chromeの場合は、追加コードを書なくても選択ダイアログが表示されるという手軽さはありますが、選択ダイアログのデザインを変更することはできません。逆にElectronでは、コードを書く手間が増えますが、渡された情報をもとにダイアログ形式に限らず自由にデザインした選択UIを作成/表示することが可能となります。

サンプル
![ssdialogsample.png](https://qiita-image-store.s3.amazonaws.com/0/17554/d362476d-9284-c03e-748b-85710ad70d35.png)

## スクリーンキャプチャーする上での注意事項(Chrome, Electron共通)
constraintsにmandatoryを使用するため、mandatoryで設定できるものを新しい仕様でのスキーマで記述するとエラーになってしまいました。
例えば、以下のようにサイズ(解像度)を新しいスキーマで記述するといった場合です。

```js
{
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    },
    width: { min: 1280, max: 1920 },
    height: { min: 720, max: 1080 }
}   
```
この場合、mandatoryでの古いスキーマである、minWidth / maxWidth / minHeight / maxHeightで記述しなければなりません。

## Electronでプライマリモニターのスクリーンキャプチャーの場合はもっとシンプルに
Electronでプライマリモニターのスクリーンキャプチャー限定となりますが、もっとシンプルなコード(constraints)で済みます。
[サンプルGitHub](https://github.com/turbographics2000/Electron_Simple_ScreenCapture_Test)

```js
navigator.mediaDevices.getUserMedia({
  video: {
    mandatory: {
      chromeMediaSource: 'desktop'
      // 必要に応じてサイズ(解像度)の制約も記述
      // minWidth: 1280,
      // maxWidth: 1920,
      // minWidth: 720,
      // maxWidth: 1080
    }
  }
}).then(stream => {
  video.srcObject = stream;
}).catch(err => {
 // error
});
```
desktopCapturer.getSources()する必要なくこれだけでキャプチャーできます。

## ちなみにFirefoxでのスクリーンキャプチャー
Firefoxでのスクリーンキャプチャーもシンプルです。
[テストページ](https://turbographics2000.github.io/Firefox_ScreenCapture_Test/)

```js
navigator.mediaDevices.getUserMedia({
  video: {
    // 'screen':全画面 'window':ウィンドウ　'application':アプリケーションのいずれかを設定
    mediaSource: 'screen', 
    // 同様にサイズ(解像度)等の制約を追加可能
    // width: {min: 1280, max: 1920},
    // height {min: 720, max: 1080}
  }
}).then(stream => {
  video.srcObject = stream;
}).catch(err => {
 // error
});
```
ChromeやElectronと違い、Firefoxの場合、`'screen'`や`'window'`の設定が排他的になるところです。
(`'application'`は試してみてもうまく動作しませんでした。あとやたらとCPU使用率が上がりました。)

## Screen Capture API
このように、ブラウザー(プラットフォーム)ごとに実装方法がバラバラなのが現状ですが、現在
[Screen Capture API](https://w3c.github.io/mediacapture-screen-share/)
が策定中です。
これは、getUserMedia()とは別にgetDisplayMedia()という関数を用意して、スクリーンキャプチャーを行うというAPIとなっており、これが実装されれば、拡張機能なしにかつ統一された実装方法でスクリーンキャプチャーが行えるようになると思われます。

## システムのオーディオをキャプチャー
getUserMedia()は、キャプチャーデバイスからストリームを取得することを基本としていますが、(ブラウザーの独自実装により)システムの音(現在デスクトップ上で流れている音)もキャプチャーすることが可能です。

## Chromeでの(システムまたはタブの)オーディオキャプチャー
<font color="red">2018/2/16更新'tab'においてもオーディオキャプチャーできることを追記</font>
[サンプルGitHub](https://github.com/turbographics2000/Chrome_SystemAudioCapture_Test)

```js:拡張機能のbackground
chrome.desktopCapture.chooseDesktopMedia(['screen', 'tab', 'audio'], streamId => {
 // streamIdをcontent_script等に渡す
});
```

```js:拡張機能のcontent_scriptまたはページのスクリプト
navigator.mediaDevices.getUserMedia({
  audio:{
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  },
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  }
}).then(stream => {
    stream.getVideoTracks().forEach(track => stream.removeTrack(track)); // videoトラック削除
    video.srcObject = stream;
}).catch(err => {
 // error
});
```

## Chromeでオーディオキャプチャーをする上での注意事項

### *chooseDesktopMedia()に渡すタイプの配列
chooseDesktopMedia()の第1引数に渡すタイプの配列ですが、`'audio'`だけではエラーが発生してしまいます。
そのほかのタイプと合わせて渡す必要があります。
結局`['screen', 'audio']`の組み合わせを渡すことになりますが理由は後述します。

```js:※とりあえずエラーが発生しない組み合わせを〇にしています
['audio'] // ✖
['screen', 'audio'] // 〇
['window', 'audio'] // 〇
['screen', 'window', 'tab', 'audio'] // 〇
```

### *getUserMedia()に渡すconstraints
getUserMedia()に渡すconstraintsにも注意する必要があります。
こちらもaudioだけではだめで、videoも設定したconstraintsを渡さなければエラーとなりました。

```js:※とりあえずエラーが発生しないconstraintsを〇にしています
// ✖
{
  audio: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  }
}    

// ✖
{
  audio: {
    mandatory: {
      chromeMediaSource: 'desktop',
      // audioにchromeMediaSourceId必要ある？と思って外してみたらエラーとなった。
      // audioにもchromeMediaSourceIdは必要みたい。
      // chromeMediaSourceId: streamId 
    }
  },
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  }
}

// ✖
{
  audio: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  },
  video: true
}

// 〇  
{
  audio: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  },
  video: {
    mandatory: {
      chromeMediaSource: 'desktop',
      chromeMediaSourceId: streamId
    }
  }
}
```

~~constraintsにvideoが必要なのは、セキュリティー上、ユーザーにキャプチャーを行っていることを示すことが必要なためかと考えます。~~
<font color="red">2018/02/16追記　将来、オーディオのみでキャプチャーできるようになるようです。</font>[crbug](https://bugs.chromium.org/p/chromium/issues/detail?id=326740)

chromeMediaSourceに設定できる値は、ググって調べた限りだと`desktop`, `screen`, `system`の３つみたいです。
`desktop`だとキャプチャーできます。
`screen`だとキャプチャーできません。
`system`にするとキャプチャーできないうえにデスクトップに流れている音も止まってしまいます。
　　　　('system'は何らかの状態になっているのだと思われますがどういった状態なのか想像ができませんでした)

ですので、chromeMediaSourceに設定する値は`desktop`のみとなります。

もうすぐオーディオのみでのキャプチャーができるようになるようですが、現在はまだconstraintsにvideoも含めなければオーディオのキャプチャーが行えませんので、取得されるストリームにはvideoトラックも含まれてしまいます。
これは仕方がありませんので、ストリームをaudioトラックのみにしたい場合は、getUserMedia()実行後に取得したストリームからvideoトラックを削除するという方法をとります。

```js
navigator.mediaDevices.getUserMedia({
  // ...
}).then(stream => {
  // ストリームからvideoトラックを削除
  stream.getVideoTracks().forEach(track => stream.removeTrack(track));
  // ...
});
```


### *システムのオーディオをキャプチャーする場合は'screen'、Chromeの特定のタブのオーディオをキャプチャーする場合は'tab'を設定。
例えば、chooseDesktopMedia()に渡すタイプ配列に、`['screen','window', 'tab', 'audio']`を渡して実行したとします。`あなたの全画面(screen)`、`アプリケーション ウィンドウ(window)`, `Chrome タブ(tab)`が選択できるダイアログが表示されます。

* `あなたの全画面(screen)`から選択した場合、システムのオーディオのキャプチャーができました。
* `Chrome タブ(tab)`から選択した場合、そのタブから出力される音のみキャプチャーすることができました。

ですので、chooseDesktopMedia()に渡すタイプ配列に`'window'`を含めてもキャプチャーできないので、意味がないものとなります。
これにより、システムまたはタブのオーディオをキャプチャーを行いたい場合は、chooseDesktopMedia()に渡すタイプ配列は`['screen', 'tab', 'audio']`となります。

以上の注意事項を考慮したコードが`Chromeでのオーディオキャプチャー`項目冒頭のコードとなります。

## Electronでのシステムオーディオキャプチャー
Electronはもっとシンプルに、chooseDesktopMediaに相当するdesktopCapturer.getSources()する必要なく、以下のコードでシステムオーディオキャプチャーをすることができます。
[サンプルGitHub](https://github.com/turbographics2000/Electron_SystemAudioCapture_Test)

```js
navigator.mediaDevices.getUserMedia({
  audio: {
    mandatory: {
      chromeMediaSource: 'desktop'
    }
  },
  video: {
    mandatory: {
      chromeMediaSource: 'desktop'
    }
  }
)).then(stream => {
  stream.getVideoTracks().forEach(track => stream.removeTrack(track));
  video.srcObject = stream;
}).catch(err => {
  // error
});
```

## Electronでシステムオーディオキャプチャーをする上での注意事項

### *getUserMedia()に渡すconstraints
ほぼ、Chromeの注意事項に準じます。
Chromeと同様に、constraintsにvideoを含めなければシステムオーディオのキャプチャーが行えません。
また、chromeMediaSourceの値も、Chromeと同様`desktop`でなければうまくキャプチャーできません。
これまたChromeと同様、取得されるストリームにはvideoトラックが含まれますので、audioトラックのみ必要な場合は、getUserMeida()実行後に、videoトラックを削除する方法をとります。
ただし、Chromeでは必要だったchromeMediaSourceIdは不要です。

## ちなみにFirefoxでシステムオーディオのキャプチャーは？
まず先に結果を述べると、私の環境がWin10だからというのもあるかもしれませんが、ダメでした。
ググってみると[stackoverflowの回答](https://stackoverflow.com/a/34239609)を見つけ、
回答には以下のコードが記述されてました。

```js
// about:configでmedia.getusermedia.audiocapture.enabledをtrueに変更後
navigator.mozGetUserMedia({
    audio: {
        mediaSource: 'audioCapture'
    },
    video: false, // Just being explicit, we only want audio for now
}, function(stream) {
    // Do what you want with this MediaStream.
}, function(error) {
    // Handle error
});
```
投稿日が2年前で、コードも古い仕様のものですので、そもそも動作するかも微妙なところです。
このコードを最新の仕様であるnavigator.mediaDevices.getUserMedia()に置き換えてもダメでした。



## まとめ
簡単にまとめると

* スクリーンキャプチャー
    * [共通] サイズ(解像度)等の制約はmandatory配下に書く
* システムオーディオキャプチャー
    * [Chrome] chooseDesktopMedia()第一引数には`['screen', 'audio']`を渡す
    * [Chrome] constraintsのchromeMediaSourceIdはaudioにも必要
    * [Electron] constraintsのchromeMediaSourceIdは不要 
    * [共通] constraints.audio.chromeMediaSourceは`'desktop'`を設定
    * [共通] オーディオのみのストリームにしたい場合はgetUserMedi()実行後に、removeTrack()で対処
    
ということになります。

以上がChromeおよびElectronでのスクリーンキャプチャーおよびシステムオーディオのキャプチャーを行う方法となります。
何も知らないところから実装しようとすると、ネット上にもそれほど情報がないため、意外と手間取ります。
手間取ることないように備忘録を兼ねて記事にまとめました。