エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

MediaStream APIで画面キャプチャとマイクからの音声を同時に収録する

f:id:iwata1990:20191212185413j:plain
熱海のMOA美術館で撮影した景色

この記事は エムスリー Advent Calendar 2019 の 16 日目の記事です。

こんにちは、 エムスリーエンジニアリンググループ、プロダクトマネージャーの岩田です。 今回はMediaStream APIで画面キャプチャとマイクからの音声を同時に録画する方法についてご紹介します。

この知見は以前本ブログでご紹介させて頂いた↓のサービス開発の中で得たものになります。 www.m3tech.blog

なお以下の記載はすべてGoogle Chromeを前提としています。Safariなど一部のブラウザでは動作しない場合があるのでご留意ください。

MediaStream APIとは

MediaStream APIとは、ローカルのWebカメラ、マイク等から得られるストリームを操作するためのAPIです。 APIの仕様はW3Cのサイト上で公開されています。

Media Capture and Streams

MediaStreamでデバイスを検索する

画面キャプチャは直感的に理解できると思いますが、音声についてはPCから出る音と外部マイクから拾える音の二種類があり、 かつそれぞれでAPIが微妙に異なるので一旦整理します。

取得できる音声 取得できる映像
getUserMedia ①マイクからの音声 ②外部カメラからの映像
getDisplayMedia ③PCからの音声 ④PCの画面キャプチャ

今回は画面キャプチャとマイクからの音声の同時収録が目的なので、

  • getUserMediaで①マイクからの音声
  • getDisplayMediaで④PC画面キャプチャ

を取得します。

マイクからの音声ストリームを取得する

マイクからの音声ストリームの取得自体はそれほど難しくありません。 サンプルコードは↓。

const audioStream = await navigator.mediaDevices.getUserMedia({
     video: false,
     audio: true
});

ただしこうしたローカルデバイスへのアクセスはユーザーからの許可を取得する必要があります。
ここが意外と難しかったので、以下に記載します。

ユーザーから許可を取得する

W3Cの仕様上、音声ストリームを取得する際は必ず以下のようなポップアップをブラウザー側が表示します。

f:id:iwata1990:20191203225158p:plain
Chromeの場合 ※一部マスキング

ここでユーザーが「許可」を選択してくれればそのまま音声ストリームの取得が開始されますが、「許可しない」「ブロック」を選択した場合、当該サイトではデフォルトで音声ストリームの取得をブロックしてしまいます。 そうした場合はユーザーへ音声ストリームの許可を促す必要がありますが、この手順は結構複雑です。
以下にChromeの場合の手順を記載します。*1

1: アドレスバーにある南京錠アイコンをクリックする

f:id:iwata1990:20191212181725p:plain
南京錠アイコン ※一部マスキング

2: 設定画面の「マイク」のタブを「許可」に変更する

f:id:iwata1990:20191212181835p:plain
設定画面 ※一部マスキング

ユーザーがここに一発でたどり着くのはなかなか至難の業であるため、サービス側で手順を案内するのが必須ではないでしょうか。 以下のコードで音声ストリームのパーミッションを確認できるので、許可されてない場合はマニュアルページへ遷移する、などの導線は確保できます。

resultgrantedであれば、音声ストリームの取得が許可されています。

const result = await navigator.permissions.query({ name: 'microphone' })

それでも音声が取得されない - 意図したマイクをChromeが認識してないケース

上記でユーザーが許可しており、かつストリームの取得処理がエラー無く動作しても、肝心の音声データが何も入ってないというケースが稀にあります。
その原因の1つとして、意図したマイクをChromeが認識してないケースが挙げられます。

具体的には、以下の設定を確認する必要があります。

1: アドレスバーへchrome://settings/contentと入力し、設定画面へ遷移する

f:id:iwata1990:20191212182845p:plain

2: 上記画面で「マイク」を選択

3: 画面上部にあるプルダウンのうち、使用したいマイクを選択する

f:id:iwata1990:20191212183004p:plain

USBマイクなどを使用した後、これを外してもChromeのここの設定が外部マイクを見たまま変わってないということがあります。
そうした場合でもストリーム取得ではエラーが出ないため、ここは要注意です。

画面キャプチャストリームを取得する

ここも取得自体はそれほど難しくありません。

const videoStream = await navigator.mediaDevices.getDisplayMedia({
      video: true,
      audio: false
});

ただし音声と同様に、ユーザーからの許可を取得する必要があります。
かつ許可の取得方法も音声とは異なります。

ちなみにここでaudiotrueにすると、マイクからの音声ではなく、PC上で再生された音声が収録されます。 今回の目的はマイクからの音声を収録することなので、別途マイクからの音声ストリームを取得する必要があります。 ややこしいですね。。。

録画範囲の確認ダイアログが表示される

音声とは異なり、画面キャプチャの場合は毎回以下のような確認ダイアログが表示されます。
録画する画面キャプチャの範囲を、ユーザーは以下の3パターンから選択できます。

f:id:iwata1990:20191212190251p:plain
録画範囲の確認ダイアログ ※一部マスキング

  1. 全画面: ディスプレイに表示されているキャプチャがそのまま録画されます。たとえChrome以外のアプリケーションでも同様です。
  2. アプリケーションウィンドウ: アプリケーション単位(Chrome、 Keynote、など)で録画されます。選択した以外のアプリケーションでのキャプチャは録画されません。
  3. Chromeタブ: 選択したChromeタブのみが録画されます。他のタブやアプリケーションは録画されません。

ここでユーザーが「共有」をクリックした瞬間から録画は開始されます。

音声とキャプチャを同時に録る

複数のストリームを扱う際はMediaRecorderが有用です。 以下、サンプルコードです。

const videoStream = await navigator.mediaDevices.getDisplayMedia({
  video: true,
  audio: false
});
const audioStream = await navigator.mediaDevices.getUserMedia({
  video: false,
  audio: true
});
this.combinedStream = new MediaStream([...videoStream.getTracks(), ...audioStream.getTracks()])
this.mediaRecorder = new MediaRecorder(this.combinedStream, { mimeType: 'video/webm;codecs=h264' })
this.mediaRecorder.addEventListener('dataavailable', (event) => {
  if (event.data && event.data.size > 0) {
      // do something
  }
});
this.mediaRecorder.start();

画面キャプチャと音声それぞれのストリームのトラックを1つのMediaStreamとして統合し、MediaRecorderに渡してあげると、同時録画が開始されます。

まとめ

以上、MediaStream APIで画面キャプチャと音声を同時に録画する際の実装方法と、個人的に嵌ったポイントについて記載しました。
ニッチな部分ではありますが、皆さまのお役に立てれば幸いです。

We are hiring

エムスリーではテクノロジーを活用して医療業界を変えるプロダクト作りに取り組んでいます。 エンジニア、プロダクトマネージャー、プロダクトデザイナーを絶賛募集中です。 ご興味があればぜひカジュアル面談をしましょう。

jobs.m3.com

*1:バージョン: 78.0.3904.97