7839

雑草魂エンジニアブログ

【JS】MediaStream API - ブラウザでカメラ/マイクへアクセスする

ブラウザのWEBアプリケーションから、パソコンやスマホのカメラ/マイクにアクセスして、映像/音声を取得したいことがあると思う。

映像や音声データを取得することで様々なアプリが作成可能になる。

例えば、

  • WebRTCアプリ
  • QRコードの読み込みアプリ
  • 音声データの録音アプリ

(高度の解析などはブラウザでの処理には重くなりすぎるので、デバイスのスペックに依存してくるので注意が必要である。)

今回、WebRTCアプリを作るために私はMediaStreamを使ってみた。

MediaStream APIとは

MediaStream APIとは、ローカルのWebカメラ、マイク等から得られるストリームを操作するためのAPIである。ストリームは、映像と音声などのトラックから構成されている。

developer.mozilla.org

英語で読みたい方はこっちからどうぞ。

www.w3.org

MediaStreamの使い方

基本的には、下記の流れで使うと思う。

  1. ローカルのカメラやマイクのデバイス一覧情報を取得する
  2. MediaStream オブジェクトを作成する
  3. MediaStream オブジェクトを使う
  4. MediaStreamの設定を変更する(カメラやマイクのミュート設定)

私は、今回4の設定変更の部分で、カメラやマイクのミュート設定を実施したのであるが、時間を浪費してしまったw

1. ローカルのカメラやマイクのデバイス一覧情報を取得する

  • navigator.mediaDevices.enumerateDevices() で一覧を取得できる
  • MediaDeviceInfoオブジェクトの配列で満たされたPromise が返される

developer.mozilla.org

let audioLists = []
let videoLists = []

function getLocalDevices () {
  navigator.mediaDevices.enumerateDevices()
    .then(deviceInfos => {
       const audios = [{ text: '指定なし', value: '' }]
       const videos = [{ text: '指定なし', value: '' }]
       for (let i = 0; i < deviceInfos.length; i++) {
         const deviceInfo = deviceInfos[i]
         if (deviceInfo.kind === 'audioinput') {
           audios.push({
             text: deviceInfo.label || `Microphone ${audios.length + 1}`,
             value: deviceInfo.deviceId
           })
         } else if (deviceInfo.kind === 'videoinput') {
           videos.push({
             text: deviceInfo.label || `Camera  ${videos.length + 1}`,
             value: deviceInfo.deviceId
           })
         }
       }
       audioLists = audios
       videoLists = videos
     })
}

audios

2. MediaStream オブジェクトを作成する

  • navigator.mediaDevices.getUserMedia() でオブジェクトの生成ができる
  • このメソッドが実行されると、ブラウザ上でユーザーにメディア入力を使用する許可を求める
  • MediaStream オブジェクトがPromiseで返却される

  • でデバイス一覧を取得後、ユーザーに選択させる場合は別途選択させるUIが必要であるが、今回は割愛する。

developer.mozilla.org

let selectedAudio = "" //ユーザーに選択させたIDを代入
let selectedVideo = "" //ユーザーに選択させたIDを代入
let localstream = null

function connectLocalCamera () {
  const constraints = {
    audio: selectedAudio ? { deviceId: { exact: selectedAudio } } : true,
    video: selectedVideo ? { deviceId: { exact: selectedVideo } } : true
  }

  navigator.mediaDevices.getUserMedia(constraints)
    .then(stream => {
      localStream = stream
    }).catch(error => {
      console.error('mediaDevice.getUserMedia() error:', error)
    })

}

ユーザーがデバイスを選択しなかった場合は、ブラウザに既定デバイスとして設定されているデバイスが使われることになる。 また、上記では、audioとvideoの両方を使うことにしているが、audioのみでいい場合は、constrainsの変数を以下のようにする。

const constraints = { audio: true, video: false }

その他、映像の縦横サイズや解像度など詳細も設定可能である。

3. MediaStream オブジェクトを使う

MediaStream オブジェクトを<video>タグで表示する場合は、以下のように設定することで表示できる。

document.getElementById('my-video').srcObject = stream

その他は、canvasで処理をしたり、WebRTCにオブジェクトを渡すことができる。アプリケーションで実現したい処理をここで記載することで、様々な機能を実現可能だ。

4. MediaStreamの設定を変更する(カメラやマイクのミュート設定)

カメラやマイクのミュート設定について紹介する。 私が時間を浪費したのは、カメラをオフにしてもPCのカメラのライトがオフにならずに、色々と試行錯誤のすえ、やっと行き着いた方法は以下の通りである。

let isMuteAudio = true
let isMuteVideo = true
let localStream = null  //MediaStream オブジェクト作成済み

function muteAudio () {
  if (!localStream) return
  localStream.getAudioTracks()[0].enabled = !isMuteAudio
  isMuteAudio = !isMuteAudio
},

function muteVideo () {
  if (!localStream) return
  if (isMuteVideo) {
    // Mute
    localStream.getVideoTracks()[0].stop()
    localStream.removeTrack(localStream.getVideoTracks()[0])
    document.getElementById('my-video').srcObject = null
  } else {
     // Re-connect
     connectLocalCamera()
  }
  isMuteVideo = !isMuteVideo
},

オブジェクトの映像トラックをミュートするだけであれば、以下のコードで可能である。ただし、これではカメラのライトが消えない。なぜなら、オブジェクトの変数を変更しただけあり、カメラとの接続が切れていないからである。

 localStream.getVideoTracks()[0].enabled = false

そこで、stop()メソッドを使ってカメラの操作を停止することにした。

localStream.getVideoTracks()[0].stop()

developer.mozilla.org

これで大丈夫だろうと思った。

しかし、ダメだったwww

参照させている srcObject も書き換えてあげないといけなかった。

document.getElementById('my-video').srcObject = null

これで、無事にカメラのミュートができた。

ちなみに、以下のコードを追加しているのは、stop() メソッドで停止させても、映像のトラックが残っていたので、意図的に削除することにした。

localStream.removeTrack(localStream.getVideoTracks()[0])

まとめ

  • MediaStreamはかなり便利
  • 使い方次第で色々と応用できそうであった

是非、みなさんも使ってみてください!!!

参考コード

最後に、全文をVueのコードでご紹介。

<script>
export default {
  data () {
    return {
      videos: [],
      audios: [],
      selectedAudio: '',
      selectedVideo: '',
      localStream: null,
      isMuteAudio: false,
      isMuteVideo: false
    }
  },

  async mounted () {
    await this.prepareAudioVideoDevice()
    await this.connectLocalCamera()
  },

  destroyed() {
    this.disconnectLocalCamera()
  },

  methods: {
    prepareAudioVideoDevice () {
      navigator.mediaDevices.enumerateDevices()
        .then(deviceInfos => {
          // MediaDeviceInfo
          // - deviceId (デバイスID)
          // - kind 3type(audioinput, videoinput, audiooutput)
          // - label (名称)※ 取得できる場合と、できない場合がある
          // - groupId
          const audios = [{ text: '指定なし', value: '' }]
          const videos = [{ text: '指定なし', value: '' }]
          for (let i = 0; i < deviceInfos.length; i++) {
            const deviceInfo = deviceInfos[i]
            if (deviceInfo.kind === 'audioinput') {
              audios.push({
                text: deviceInfo.label || `Microphone ${audios.length + 1}`,
                value: deviceInfo.deviceId
              })
            } else if (deviceInfo.kind === 'videoinput') {
              videos.push({
                text: deviceInfo.label || `Camera  ${videos.length + 1}`,
                value: deviceInfo.deviceId
              })
            }
          }
          this.audios = audios
          this.videos = videos
        })
    },

    connectLocalCamera () {
      const constraints = {
        audio: this.selectedAudio ? { deviceId: { exact: this.selectedAudio } } : true,
        video: this.selectedVideo ? { deviceId: { exact: this.selectedVideo } } : true,
      }

      if (this.localStream) {
        this.localStream = null
      }

      navigator.mediaDevices.getUserMedia(constraints)
        .then(stream => {
          document.getElementById('my-video').srcObject = stream
          this.localStream = stream
        }).catch(error => {
          console.error('mediaDevice.getUserMedia() error:', error)
        })
    },

    muteAudio () {
      if (!this.localStream) return
      this.localStream.getAudioTracks()[0].enabled = !this.isMuteAudio
      this.isMuteAudio = !this.isMuteAudio
    },

    muteVideo () {
      if (!this.localStream) return
      if (!this.isMuteVideo) {
        // Mute
        this.localStream.getVideoTracks()[0].stop()
        this.localStream.removeTrack(this.localStream.getVideoTracks()[0])
        document.getElementById('my-video').srcObject = null
      } else {
        // Re-connect
        this.connectLocalCamera()
      }
      this.isMuteVideo = !this.isMuteVideo
    },

    disconnectLocalCamera () {
      if (!!this.localStream) {
        this.localStream.getTracks().forEach(track => track.stop())
        document.getElementById('my-video').srcObject = null
        this.localStream = null
      }
    },

  }
}
</script>

<template>
  <div class="video-container">
    <div class="video-area">
      <video
        id="my-video"
        autoplay
        playsinline />
    </div>
    <div class="btns">
      <button :class="isMuteAudio ? 'can' : 'disabled'" @click="muteAudio()">audio</button>
      <button :class="isMuteVideo ? 'can' : 'disabled'" @click="muteVideo()">video</button>
    </div>
  </div>
</template>

<style lang="scss" scoped>
.video {
  &-container {
    button {
      width: 100px;
      height: 50px;
      &.can {
        background-color: red;
      }
    }
  }
}
</style>