Webアプリケーションで音声出力先を切り替える

# はじめに

音声出力を扱うWebアプリケーションを作っていると、音声出力先を切り替えたくなることはありませんか？
この記事では、Google Chrome限定で、その機能を実現する方法をご紹介します。

# 前提条件

- 音声出力を扱うWebアプリケーションとして、SkyWayのp2p-videochatサンプルを利用します
- Google Chromeしか機能が実装されていないため、それ以外のブラウザでは動作しません
  - Chrome M67 Stableで動作確認済み

# デモ

こちらで動作を確認できます。

https://yusuke84.github.io/webrtc-device-selection-sample/

# 実装方法

## 音声出力デバイスの情報を収集する

[MediaDevices.enumerateDevices()](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/enumerateDevices)を利用します。

```js
navigator.mediaDevices.enumerateDevices()
    .then(function (devices) { // success
    }).catch(function (error) { // error
    return;
});
```

実行すると使用できる入出力メディアデバイスの情報を持つMediaDeviceInfoオブジェクトが配列で返されます。
私の環境で実行すると以下のような配列が返ってきます。どんなディスプレイを使っているとか、カメラを使っているとかもろわかりですねｗ

![deviceInfos.png](https://qiita-image-store.s3.amazonaws.com/0/6651/c6f07721-1a97-4db9-c207-a7108af9a8f8.png)

**kind** に着目してください。**audiooutput**となっているデバイスが音声出力デバイスです。この情報を利用します。

## ユーザにデバイスを選択してもらう

SkyWayの公式サンプルコードには、すでに音声と映像の入力ソースを選択するコードが入っています。今回はそこに音声出力先を切り替えるコードも追記します。

```js
const audioSelect = $('#audioSource');
const videoSelect = $('#videoSource');
const audioDeviceSelect = $('#audioDevice');  // 追記
selectors = [audioSelect, videoSelect, audioDeviceSelect];  // 修正
navigator.mediaDevices.enumerateDevices()
.then(deviceInfos => {
    const values = selectors.map(select => select.val() || '');
    selectors.forEach(select => {
    const children = select.children(':first');
    while (children.length) {
        select.remove(children);
    }
    });

    for (let i = 0; i !== deviceInfos.length; ++i) {
    const deviceInfo = deviceInfos[i];
    const option = $('<option>').val(deviceInfo.deviceId);

    if (deviceInfo.kind === 'audioinput') {
        option.text(deviceInfo.label ||
        'Microphone ' + (audioSelect.children().length + 1));
        audioSelect.append(option);
    } else if (deviceInfo.kind === 'videoinput') {
        option.text(deviceInfo.label ||
        'Camera ' + (videoSelect.children().length + 1));
        videoSelect.append(option);
    } else if (deviceInfo.kind === 'audiooutput') { // ここから追記
        option.text(deviceInfo.label ||
        'Output device ' + (audioDeviceSelect.children().length + 1));
        audioDeviceSelect.append(option);             
    } // ここまで追記
    }

    selectors.forEach((select, selectorIndex) => {
    if (Array.prototype.slice.call(select.children()).some(n => {
        return n.value === values[selectorIndex];
    })) {
        select.val(values[selectorIndex]);
    }
    });
    
    videoSelect.on('change', step1);
    audioSelect.on('change', step1);
    audioDeviceSelect.on('change', step1); // 追記
});
```

修正する場所と追記する場所はコメントの通りです。

## 音声出力先を切り替える

[HTMLMediaElement.setSinkId()](https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId)を利用します。

```js
const audio = document.createElement('audio');
audio.setSinkId(deviceId);
```

MediaDevices.enumerateDevices()で取得した音声出力デバイスの**deviceId**を引数として与えることで、音声出力デバイスを切り替えることができます。
今回はこのように利用しています。

```js
if(audioDevice){
    $('#their-video').get(0).setSinkId(audioDevice);
}
```

deviceIdがnullの場合はエラーが出るため、if文で予めチェックしています。

# 実行してみる

このように切り替えることができるようになります。もちろん、ビデオチャット中に動的に切り替える事もできます。

![audiooutputselect.png](https://qiita-image-store.s3.amazonaws.com/0/6651/ea0abdbd-b512-2f33-8440-e0f66af29858.png)

# 余談 MediaDevices.enumerateDevices() の実行タイミング

Chrome、Firefoxともに、MediaDevices.getUserMedia()でユーザがカメラとマイクの利用を許可しなければ、**labe**の値が取得できません。
今回取り上げた音声出力デバイスの情報については、マイクの利用を許可しなければ以下のように、空になります。

![audiodeviceisnull.png](https://qiita-image-store.s3.amazonaws.com/0/6651/1bbf518d-3e77-c3c6-3b8c-451c3d7fec1e.png)

labelの値はUIに出力したいですよね。そんな時は、MediaDevices.getUserMedia() → ユーザのアクション → MediaDevices.enumerateDevices()という順番で実行することで、解決できます。Firefoxは前からこのような挙動でしたが、Chromeは最近（同僚に調べてもらったところ恐らくM66から）このような挙動になっているようなので、ご注意下さい。それ以前はユーザの許可無しでlabel情報が取得できていました。

尚、SkyWayの公式サンプルは、ユーザが許可する前にMediaDevices.enumerateDevices()を実行するため、初回アクセス時はlabel情報が取得できません。今回のデモでは暫定的にその部分を修正しています。