【JS】MediaStream API - ブラウザでカメラ/マイクへアクセスする
ブラウザのWEBアプリケーションから、パソコンやスマホのカメラ/マイクにアクセスして、映像/音声を取得したいことがあると思う。
映像や音声データを取得することで様々なアプリが作成可能になる。
例えば、
- WebRTCアプリ
- QRコードの読み込みアプリ
- 音声データの録音アプリ
(高度の解析などはブラウザでの処理には重くなりすぎるので、デバイスのスペックに依存してくるので注意が必要である。)
今回、WebRTCアプリを作るために私はMediaStreamを使ってみた。
MediaStream APIとは
MediaStream APIとは、ローカルのWebカメラ、マイク等から得られるストリームを操作するためのAPIである。ストリームは、映像と音声などのトラックから構成されている。
英語で読みたい方はこっちからどうぞ。
MediaStreamの使い方
基本的には、下記の流れで使うと思う。
- ローカルのカメラやマイクのデバイス一覧情報を取得する
- MediaStream オブジェクトを作成する
- MediaStream オブジェクトを使う
- MediaStreamの設定を変更する(カメラやマイクのミュート設定)
私は、今回4の設定変更の部分で、カメラやマイクのミュート設定を実施したのであるが、時間を浪費してしまったw
1. ローカルのカメラやマイクのデバイス一覧情報を取得する
navigator.mediaDevices.enumerateDevices()
で一覧を取得できる- MediaDeviceInfoオブジェクトの配列で満たされたPromise が返される
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が必要であるが、今回は割愛する。
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()
これで大丈夫だろうと思った。
しかし、ダメだった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>