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

はじめに謝ります。

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

Chromeでスクリーンキャプチャー

現時点においてはまだ拡張機能を作成する必要があります。
サンプルGitHub

拡張機能のbackgroundのスクリプト
chrome.desktopCapture.chooseDesktopMedia(['screen', 'window', 'tab'], streamId => {
 // streamIdをcontent_script等に渡す
});
拡張機能の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

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

スクリーンキャプチャーする上での注意事項(Chrome, Electron共通)

constraintsにmandatoryを使用するため、mandatoryで設定できるものを新しい仕様でのスキーマで記述するとエラーになってしまいました。
例えば、以下のようにサイズ(解像度)を新しいスキーマで記述するといった場合です。

{
  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

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でのスクリーンキャプチャーもシンプルです。
テストページ

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使用率が上がりました。)

システムのオーディオをキャプチャー

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

Chromeでのシステムオーディオキャプチャー

サンプルGitHub

拡張機能のbackground
chrome.desktopCapture.chooseDesktopMedia(['screen', 'audio'], streamId => {
 // streamIdをcontent_script等に渡す
});
拡張機能の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']の組み合わせを渡すことになりますが理由は後述します。

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

*getUserMedia()に渡すconstraints

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

※とりあえずエラーが発生しない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が必要なのは、セキュリティー上、ユーザーにキャプチャーを行っていることを示すことが必要なためかと考えます。

chromeMediaSourceに設定できる値は、ググって調べた限りだとdesktop, screen, systemの3つみたいです。
desktopだとキャプチャーできます。
screenだとキャプチャーできません。
systemにするとキャプチャーできないうえにデスクトップに流れている音も止まってしまいます。
ですので、chromeMediaSourceに設定する値はdesktopのみとなります。

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

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

*'screen'でしかシステムのオーディオキャプチャーができない

例えば、chooseDesktopMedia()に渡すタイプ配列に、['screen','window', 'tab', 'audio']を渡して実行したとします。あなたの全画面(screen)アプリケーション ウィンドウ(window), Chrome タブ(tab)が選択できるダイアログが表示されます。
ここで 'アプリケーション ウィンドウ''Chrome タブ'から選択すると、システムオーディオのキャプチャーができませんでした。
あなたの全画面から選択した場合のみ、システムオーディオのキャプチャーができました。
ですので、chooseDesktopMedia()に渡すタイプ配列に'window''tab'を含めたところで、キャプチャーできないので意味がないものとなります。
これにより、システムオーディオのキャプチャー(のみ)を行いたい場合は、chooseDesktopMedia()に渡すタイプ配列は['screen', 'audio']となります。

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

Electronでのシステムオーディオキャプチャー

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

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の回答を見つけ、
回答には以下のコードが記述されてました。

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