📹

【React】端末に接続されている機器を取得する

2022/11/14に公開

概要

WebRTCを実現する上で、デバイスの操作は避けては通れません。
Amazon ChimeZoom APIなどのライブラリ・コンポーネントはデバイス操作をラップして提供してくれていたりしますが、自前で行う場合は普段使わないようなAPIを用います。

今回は自分の学習メモも兼ねて、その辺りをまとめたいと思います。

端末の取得

enumerateDevicesで取得する

Reactにおいては、navigator.mediaDevicesではじまるMediaDevicesというAPIにアクセスすることで大体が実現できます。
端末一覧を取得するにはenumerateDevices()を用います。
https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/enumerateDevices

レスポンスはPromise<MediaDeviceInfo[]>で、MediaDeviceInfoは下記のような構造です。

{
  deviceId: 'xxxxx',
  groupId: 'yyyyyy',
  kind: 'audiooutput',
  label: 'ほげほげ'
}

UI上で表示されるのはlabelの値ですが、内部的に端末のスイッチを行う際はdeviceIdを用います。
またkindのプロパティがあることから、 マイク・スピーカー・カメラが合わせて配列として返ってきます。
なので用途にもよりますが、表示する際にはkindfilterをかけて使用するのが一般的です。

例.useSpeakerDeviceList

例えば自分は以下のようにカスタムフックを作成して各ロジック上で使用しています。

import {useState, useEffect, useCallback} from 'react';

// スピーカー機器のリストを取得するカスタムフック
const useSpeakerDeviceList = () => {
  const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
  // 機器の更新処理
  const refreshDevices = useCallback(async () => {
    const latestDevices = (
      await navigator.mediaDevices.enumerateDevices()
    ).filter((d) => d.kind === "audiooutput");
    setDevices(latestDevices);
  }, []);

  useEffect(() => {
    // 初回(=componentDidMount)で最新の機器一覧を取得
    refreshDevices();
    // 以降は機器の着脱が行われるタイミングで更新処理を実施
    navigator.mediaDevices.addEventListener("devicechange", refreshDevices);
    return () => {
      navigator.mediaDevices.removeEventListener(
        "devicechange",
        refreshDevices
      );
    };
  }, []);

  return devices;
};

ポイントはMediaDevicesに対してイベントリスナーを設定して、機器の着脱を検知して常に最新の配列を返すようにしている点です。
他の機器(カメラ・マイク)についてはfilterをかける際の条件を変えればOKです。

端末の使用

端末の一覧が取得できたので、後はそれを使用する方法について記載します。
この辺りから機器の種類によって実装方法が分かれてきます。

カメラ(マイク)

カメラの出力は<video>タグなので、基本はそのrefを操作していくイメージです。
MediaDevicesにはgetUserMediaというメソッドがあり、カメラとマイクに関してはこれで設定ができます。

https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia

※マイクに関しては、音の大小をUIとして表示したい場合のみこのような書き方をする必要があるので割愛します。

SampleVideo.tsx
import { useEffect, useRef } from "react";

const SampleVideo = (selectedVideo: MediaDeviceInfo) => {
  // <video>のrefを取得しておく
  const videoRef = useRef<HTMLVideoElement>(null);
  useEffect(() => {
    if (videoRef) {
      navigator.mediaDevices
        .getUserMedia({
	  // ここでvideoに選択しているデバイスの情報をそのまま渡す
          video: selectedDevice,
        })
        .then((stream) => {
          if (videoRef?.current) {
            videoRef.current.srcObject = stream;
            videoRef.current.play();
          }
        })
        .catch((e) => {
          // エラー処理
        });
    }
  }, [videoRef, selectedDevice]);

  return (
    <div>
      <Video ref={videoRef} id="video"></Video>
    </div>
  );
}

スピーカー

スピーカーについてはUIに何か表示されるより、任意の音声を流すようなユースケースが多いと思うので、その方法について記載します。
Audioインスタンス(HTMLAudioElement)のコンストラクタに音声ファイルのパスを指定して、その後でsetSinkIdを実行します。

https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement/setSinkId

※一旦anyにキャストしている理由ですが、HTMLAudioElementHTMLMediaElementを継承しているためHTMLMediaElementの持つsetSinkIdは使えるはずですが、自分の環境の型定義(@types/react-dom@*17.0.11)だと型エラーになったためです。動作自体は行われるので一旦問題なしとしています。

const play = (selectedDevice: MediaDeviceInfo) => {
  let audio = new Audio(`https://example.com/sample.mp3`);
  await (audio as any)?.setSinkId(selectedDevice?.deviceId);
  audio.play();

  // 音が流れた後で何かしたい場合はイベントリスナーを設定する
  audio.addEventListener("ended", () => {
    // 何かしらの処理...
  });
};

まとめ

今回はReact + Typescriptの環境において、特定のライブラリに依存することなくカメラ・マイク・スピーカーを扱うTipsについて紹介しました。
微妙な書き方をしている箇所もあるため、もう少しブラッシュアップは必要そうですが、概ね今回のようなAPIを扱って実装していくかと思います。

今回の内容が役立ちましたら幸いです。

Discussion