WebRTCをやりたくて(Query media devices)

前回に引き続き今回は”Query media devices”です。サンプルは2つ有ります。

実行環境は以下の通り。

  • 機器    : NoteBook
  • OS     : Ubuntu 20.04.2 LTS
  • ブラウザー : FireFox 88.0.1 (64 ビット)

Choose camera, microphone and speaker

最初のサンプルは、”Choose camera, microphone and speaker”です。このHTMLを実行するとNoteBookの、”Audio input”、”Audio output”、”Video source”を見つけてHPのプルダウンリストに登録し映像の再生を行います。例によってサンプルのHTMLを簡略したのですが、今回はあまり簡略出来ませんでした。

samp01.html

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <title>Select audio and video sources</title>

        <style>
            div.select {
                display: inline-block;
                margin: 0 0 1em 0;
            }

            p.small {
                font-size: 0.7em;
            }

            label {
                width: 12em;
                display: inline-block;
            }

            video {
              background: #222;
              margin: 0 0 20px 0;
              max-width: 60em;
              --width: 100%;
              width: var(--width);
              height: calc(var(--width) * 0.75);
            }
        </style>

    </head>

    <body>
        <div id="container">
            <h1>WebRTC samples Select sources &amp; outputs</h1>
            <div class="select">
                <label for="audioSource">Audio input source: </label><select id="audioSource"></select>
            </div>

            <div class="select">
                <label for="audioOutput">Audio output destination: </label><select id="audioOutput"></select>
            </div>

            <div class="select">
                <label for="videoSource">Video source: </label><select id="videoSource"></select>
            </div>
            <video id="video" playsinline autoplay></video>
        </div>

        <script>
            'use strict';

            const videoElement = document.querySelector('video');
            const audioInputSelect = document.querySelector('select#audioSource');
            const audioOutputSelect = document.querySelector('select#audioOutput');
            const videoSelect = document.querySelector('select#videoSource');
            const selectors = [audioInputSelect, audioOutputSelect, videoSelect];

            audioOutputSelect.disabled = !('sinkId' in HTMLMediaElement.prototype);

            function gotDevices(deviceInfos) {
              // Handles being called several times to update labels. Preserve values.
              const values = selectors.map(select => select.value);
              selectors.forEach(select => {
                while (select.firstChild) {
                  select.removeChild(select.firstChild);
                }
              });
              for (let i = 0; i !== deviceInfos.length; ++i) {
                const deviceInfo = deviceInfos[i];
                const option = document.createElement('option');
                option.value = deviceInfo.deviceId;
                if (deviceInfo.kind === 'audioinput') {
                  option.text = deviceInfo.label || `microphone ${audioInputSelect.length + 1}`;
                  audioInputSelect.appendChild(option);
                } else if (deviceInfo.kind === 'audiooutput') {
                  option.text = deviceInfo.label || `speaker ${audioOutputSelect.length + 1}`;
                  audioOutputSelect.appendChild(option);
                } else if (deviceInfo.kind === 'videoinput') {
                  option.text = deviceInfo.label || `camera ${videoSelect.length + 1}`;
                  videoSelect.appendChild(option);
                } else {
                  console.log('Some other kind of source/device: ', deviceInfo);
                }
              }
              selectors.forEach((select, selectorIndex) => {
                if (Array.prototype.slice.call(select.childNodes).some(n => n.value === values[selectorIndex])) {
                  select.value = values[selectorIndex];
                }
              });
            }

            navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError);

            // Attach audio output device to video element using device/sink ID.
            function attachSinkId(element, sinkId) {
              if (typeof element.sinkId !== 'undefined') {
                element.setSinkId(sinkId)
                    .then(() => {
                      console.log(`Success, audio output device attached: ${sinkId}`);
                    })
                    .catch(error => {
                      let errorMessage = error;
                      if (error.name === 'SecurityError') {
                        errorMessage = `You need to use HTTPS for selecting audio output device: ${error}`;
                      }
                      console.error(errorMessage);
                      // Jump back to first output device in the list as it's the default.
                      audioOutputSelect.selectedIndex = 0;
                    });
              } else {
                console.warn('Browser does not support output device selection.');
              }
            }

            function changeAudioDestination() {
              const audioDestination = audioOutputSelect.value;
              attachSinkId(videoElement, audioDestination);
            }

            function gotStream(stream) {
              window.stream = stream; // make stream available to console
              videoElement.srcObject = stream;
              // Refresh button list in case labels have become available
              return navigator.mediaDevices.enumerateDevices();
            }

            function handleError(error) {
              console.log('navigator.MediaDevices.getUserMedia error: ', error.message, error.name);
            }

            function start() {
              if (window.stream) {
                window.stream.getTracks().forEach(track => {
                  track.stop();
                });
              }
              const audioSource = audioInputSelect.value;
              const videoSource = videoSelect.value;
              const constraints = {
                audio: {deviceId: audioSource ? {exact: audioSource} : undefined},
                video: {deviceId: videoSource ? {exact: videoSource} : undefined}
              };
              navigator.mediaDevices.getUserMedia(constraints).then(gotStream).then(gotDevices).catch(handleError);
            }

            audioInputSelect.onchange = start;
            audioOutputSelect.onchange = changeAudioDestination;

            videoSelect.onchange = start;

            start();

        </script>
    </body>
</html>

実行するとこんな画面になります。

最初Javascript部が難しくプログラムの動作が理解出来ませんでした。使用可能な機器をHPのプルダウンリストに追加して、最初の機器を使って映像を表示するプログラムでした。キー関数は94行目の、”navigator.mediaDevices.enumerateDevices()”です。

”navigator.mediaDevices.enumerateDevices()”はPromiseを返す関数ですが、成功するとデバイス情報のアレイが帰って来ます。この中には、オーディオ入力デバイス(マイク)、 ビデオ入力デバイス(カメラ)、 オーディオ出力デバイス(スピーカー)等の情報が入っています。これを踏まえて簡単にHTMLの動作を追って見ると、

  • 94行: ここで、”navigator.mediaDevices.enumerateDevices()”を実行しています。
    • 問題無く実行されれば、”gotDevices”へ、エラーが出れば、”handleError”へ
  • 62行: gotDevices(deviceInfos) 引数、”deviceInfos”がデバイスの情報アレイです。
    • 64−69行: ”audioSource”、”audioOutput”、”videoSource”で指定されたプルダウン要素の読込
    • 70−86行: ”deviceInfos”の情報を元に、各プルダウンリストに登録。
    • 87−91行: リストの一番最初の項目を選択。
  • 153行: 次に、ここが実行されます。
  • 133行: プルダウンリストを元に引数を決定して、navigator.mediaDevices.getUserMedia()を実行
    • 正常終了すれば、”gotStream”。”gotStream”が正常終了すれば、”gotDevices”を実行。
    • エラーがでると、”handleError”を実行
  • 後はこれの繰り返し。

このHTMLですが、プルダウンリストへの登録にこの様な方法が有るのかと本来の”navigator.mediaDevices.enumerateDevices()”の使い方よりJavascriptの書き方として大いに参考になりました。

Choose media source and audio output

次は、”Choose media source and audio output”です。でもこれ注記に、”This demo must be run from localhost or over HTTPS Chrome 49 or later, Firefox is not supported yet.”と有りました。今回は、ブラウザが Firefoxなのでスキップします。