panda's tech note

メディアデバイスの取り扱い

WebRTCではカメラやマイク,スピーカーだけでなく,ブラウザによってはスクリーン共有や外部のキャプチャボードの入力もできます。ここでは,これらのメディアデバイスを取り扱います。これらのメディアデバイスにアクセスするためのWebブラウザのAPIとして,navigator.mediaDevices を使用します。

カメラ・マイク・キャプチャボード

カメラやマイク,キャプチャボード,スピーカーなど,ビデオおよびオーディオの入出力デバイスは navigator.mediaDevices.enumerateDevices() により取得できます。ただし,いずれかの入力デバイスへのアクセスが許可されていないとラベルが取得できないので,navigator.mediaDevices.getUserMedia() で適当な入力デバイスを有効にしてから入出力デバイス情報を取得する必要があります。詳しくは後述のサンプルで説明します。

なお,キャプチャボードの対応はブラウザによってかなり異なるようで,手元のBlackmagic Intensity ShuttleをMacBook Proに繋げて試したところ,Firefoxでのみオーディオ・ビデオ共に取得できました。Chromeではオーディオ入力のみ取得できました。Safariではオーディオ・ビデオ入力ともに取得できませんでした。最近使っていないのでもしかしたらドライバが古い等,別の原因があるかもしれません。

スクリーン共有

スクリーンは navigator.mediaDevices.getDisplayMedia() により取得できます。2020年4月5日の時点では,Safariでは全画面共有のみ,Chromeでは全画面共有以外に共有するウィンドウを選択することもできます。Chromeでは navigator.mediaDevices.getDisplayMedia() を実行した際に,共有対象の選択画面がユーザに表示されます。なお,Safariでは navigator.mediaDevices.getDisplayMedia() は gesture handler(クリックなどに対応したイベントハンドラ)から呼び出す必要があるため,window.onload から呼び出すことはできません。

サンプル

以下のソースコードは,ページを開いたときに一時的にマイクを有効にし,入出力デバイス情報をセレクトボックスのオプションに追加します。Start camera ボタンを押下すると,AudioおよびVideoセレクトボックスで指定したデバイスを開き,画面中央のビデオ画面に表示します。Start screen sharing ボタンを押下すると,対象の画面を中央に表示します(このデモでは通信はしていないので,誰とも共有はされません)。Stop ボタンは,現在のストリーム(カメラ・マイクまたはスクリーン共有)を停止します。

このサンプルはここで試すことができます。

<!doctype html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="Author" content="Hirochika Asai" />
<script type="text/javascript">

let localVideo;
let localStream;

// ページ読み込み時に呼び出す関数
window.onload = function() {
    localVideo = document.getElementById('localVideo');

    // マイクを有効化(ラベルを取得するため)
    option = {video: false, audio: {echoCancellation: false}};
    navigator.mediaDevices.getUserMedia(option).then(function(stream) {
        // 入出力デバイスの取得
        navigator.mediaDevices.enumerateDevices().then(function(devices) {
            devices.forEach(function(device) {
                switch ( device.kind ) {
                case 'audioinput':
                    addOption('audioOptions', device.deviceId, device.label);
                    break;
                case 'videoinput':
                    addOption('videoOptions', device.deviceId, device.label);
                    break;
                }
            });
        }).catch(function(err) {
            console.error(err);
        });
        // 停止
        stream.getTracks().forEach(function(track) {
            track.stop();
        });
    }).catch(function (err) {
       console.error(err);
   });
}

// <select> への <option> の追加
function addOption(target, key, value) {
    let sel = document.getElementById(target);
    let opt = document.createElement('option');
    opt.appendChild(document.createTextNode(value));
    opt.value = key;
    sel.appendChild(opt);
}

// カメラ・マイクの有効化
function startCamera() {
    // 再生中のストリームを停止
    stop();

    // 選択されているオーディオ・ビデオオプションの取得
    let audio = document.getElementById('audioOptions');
    let audioDeviceId;
    let video = document.getElementById('videoOptions');
    let videoDeviceId;
    for ( let i in audio.options ) {
        if ( audio.options[i].selected ) {
            audioDeviceId = audio.options[i].value;
        }
    }
    for ( let i in video.options ) {
        if ( video.options[i].selected ) {
            videoDeviceId = video.options[i].value;
        }
    }

    // カメラとマイクをストリームに繋ぐ
    option = {video: {deviceId: videoDeviceId},
    audio: {deviceId: audioDeviceId,
            echoCancellation: true}};
    navigator.mediaDevices.getUserMedia(option).then(function(stream) {
        localStream = stream;
        localVideo.srcObject = stream;
        localVideo.volume = 0;
        localVideo.play();
    }).catch(function (err) {
        console.error(err);
    });
}

// スクリーン共有
function startScreenSharing() {
    // 再生中のストリームを停止
    stop();
    option = {video: true, audio: false};
    navigator.mediaDevices.getDisplayMedia(option).then(function(stream) {
        localStream = stream;
        localVideo.srcObject = stream;
        localVideo.volume = 0;
        localVideo.play();
    }).catch(function (err) {
        console.error(err);
    });
}

// ビデオストリームを停止
function stop() {
    if ( localStream ) {
        localVideo.pause();
        localVideo.srcObject = null;
        localStream.getTracks().forEach(function(track) {
            track.stop();
        });
        localStream = null;
    }
}

</script>
<style>
 .video {
   width: 640px;
   height: 360px;
   border: 1px solid #000000;
 }
</style>
<title>WebRTC Media Demo &mdash; ja.tech.jar.jp</title>
</head>
<body>
    <h1>WebRTC Media Demo</h1>

        <p>
            <video id="localVideo" class="video" autoplay></video>
        </p>
        <p>
            <button type="button" onclick="startCamera();">Start camera</button>
            Audio: <select id="audioOptions">
            </select>
            Video: <select id="videoOptions">
            </select>
            <br />
            <button type="button" onclick="startScreenSharing();">Start screen sharing</button><br />
            <button type="button" onclick="stop();">Stop</button>
        </p>

</body>
</html>

サンプルソースコードの解説

HTMLは,カメラまたはスクリーン共有プレビュー用の640x360のビデオ領域(ID: localVideo)と各種操作用のボタンを用意しています。見れば分かると思うので説明は省略します。

スクリプトを順に説明します。

まず,

let localVideo;
let localStream;

は,それぞれHTMLのID localVideo エレメント用の変数とカメラまたはスクリーン共有ストリームを記憶するための変数です。

ページ読み込み時に実行されるイベントハンドラを以下の通り実装します。

window.onload = function() {
    localVideo = document.getElementById('localVideo');

    // マイクを有効化(ラベルを取得するため)
    option = {video: false, audio: {echoCancellation: false}};
    navigator.mediaDevices.getUserMedia(option).then(function(stream) {
        // 入出力デバイスの取得
        navigator.mediaDevices.enumerateDevices().then(function(devices) {
            devices.forEach(function(device) {
                switch ( device.kind ) {
                case 'audioinput':
                    addOption('audioOptions', device.deviceId, device.label);
                    break;
                case 'videoinput':
                    addOption('videoOptions', device.deviceId, device.label);
                    break;
                }
            });
        }).catch(function(err) {
            console.error(err);
        });
        // 停止
        stream.getTracks().forEach(function(track) {
            track.stop();
        });
    }).catch(function (err) {
       console.error(err);
   });
}

ID localVideo のエレメントは何度も操作するため,ここで取得し,localVideo 変数に保存しておきます。次に, navigator.mediaDevices.enumerateDevices() で入出力デバイスを取得するのですが,前述の通り,少なくとも1つ以上のデバイスが許可されていないとデバイスのラベル情報(デバイス名)が取得できないため,まずマイクのみを有効化します。ここではオプションに echoCancellation: false を指定してエコーキャンセル機能を無効化しています(大きな意味はないです)。マイクの有効化に成功したら,navigator.mediaDevices.enumerateDevices() で全ての入出力デバイスを取得し,そのうち kind が音声入力(audioinput)であるものとビデオ入力(videoinput)であるものをそれぞれ,HTMLのセレクトボックスのオプション(deviceIDをvalue,labelを表示名にしています)として追加します。これら以外の kind としては audiooutputvideooutput がありますが,今回は使用しません(デフォルトのものを使用します)。この入力デバイスの取得後,

        stream.getTracks().forEach(function(track) {
            track.stop();
        });

で,一時的に開いたマイクを停止しています。

上述のHTMLのセレクトボックスへのオプションの追加は以下の関数で定義しています。

function addOption(target, key, value) {
    let sel = document.getElementById(target);
    let opt = document.createElement('option');
    opt.appendChild(document.createTextNode(value));
    opt.value = key;
    sel.appendChild(opt);
}

次は Start camera ボタンを押下したときに呼び出す関数である startCamera() 関数の説明をします。まず,stop() 関数を呼び出し,既存のストリームがあれば停止します。

    // 再生中のストリームを停止
    stop();

その後,以下のようにAudioおよびVideoで選択されているdeviceId(オプションのvalue)を取得します。

    // 選択されているオーディオ・ビデオオプションの取得
    let audio = document.getElementById('audioOptions');
    let audioDeviceId;
    let video = document.getElementById('videoOptions');
    let videoDeviceId;
    for ( let i in audio.options ) {
        if ( audio.options[i].selected ) {
            audioDeviceId = audio.options[i].value;
        }
    }
    for ( let i in video.options ) {
        if ( video.options[i].selected ) {
            videoDeviceId = video.options[i].value;
        }
    }

選択されたデバイスのdeviceIdをオプションとして,navigator.mediaDevices.getUserMedia() を呼び出すことでカメラとマイクのストリームを生成します。生成したストリームは localStream 変数に保存し,画面中央のビデオ画面に設定します。マイクはエコーバックを防ぐためにボリュームを0にしています。

    // カメラとマイクをストリームに繋ぐ
    option = {video: {deviceId: videoDeviceId},
    audio: {deviceId: audioDeviceId,
            echoCancellation: true}};
    navigator.mediaDevices.getUserMedia(option).then(function(stream) {
        localStream = stream;
        localVideo.srcObject = stream;
        localVideo.volume = 0;
        localVideo.play();
    }).catch(function (err) {
        console.error(err);
    });

次に,Start screen sharing ボタンを押下したときに呼び出す関数である startScreenSharing() 関数の説明をします。 startCamera() 関数と同様に

    // 再生中のストリームを停止
    stop();

により,既存のストリームを停止します。その後,navigator.mediaDevices.getDisplayMedia() 関数をビデオのみ有効なオプションで呼び出し,スクリーン共有のストリームを生成します。生成したストリームは localStream 変数に保存し,画面中央のビデオ画面に設定し,再生します。

    option = {video: true, audio: false};
    navigator.mediaDevices.getDisplayMedia(option).then(function(stream) {
        localStream = stream;
        localVideo.srcObject = stream;
        localVideo.volume = 0;
        localVideo.play();
    }).catch(function (err) {
        console.error(err);
    });
}

ここまででストリーム生成のときにも呼んでいた stop() 関数は以下の通り定義します。

function stop() {
    if ( localStream ) {
        localVideo.pause();
        localVideo.srcObject = null;
        localStream.getTracks().forEach(function(track) {
            track.stop();
        });
        localStream = null;
    }
}

localVideo.srcObject を空にし,現在のストリーム localStream のすべてのトラック(ビデオ・オーディオ)を停止しています。