【経験談】Chrome上でカメラデバイスにアクセスできなかった原因

# 状況
ReactとTypeScriptを用いて[カメラアプリ](https://mizbe-photo-app-2024.web.app/)を開発中に、以下のフローでユーザーのカメラデバイスにアクセスしようとしたところ、FireFox上ではアクセスできたのに対して、Chrome上ではアクセスできなかった現象が発生した（カメラへのアクセスを問うアラートダイアログすら表示されなかった）

1. [MediaDevices: enumerateDevices()　メソッド](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/enumerateDevices)で利用可能なメディア入出力機器の一覧と、それぞれのデバイスの情報を取得。
1. 取得したメディア入出力機器のうち、videoinputタイプのものを取得
1. メディア入出力機器それぞれに付与されているdeviceIDを用いて、[MediaDevices: getUserMedia() メソッド](https://developer.mozilla.org/ja/docs/Web/API/MediaDevices/getUserMedia)で、メディア入出力機器（今回の場合はカメラ機器）の入力ストリームを取得

```typescript
const CameraApp: React.FC = () => {
  const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
  const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
  const [stream, setStream] = useState<MediaStream | null>(null);
  const [photoDataUrl, setPhotoDataUrl] = useState<string | null>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const getDevices = async () => {
      try {
        const mediaDevices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = mediaDevices.filter(device => device.kind === 'videoinput');
        setDevices(videoDevices);
        if (videoDevices.length > 0) {
          setSelectedDeviceId(videoDevices[0].deviceId);
        }
      } catch (error) {
        console.error('Error getting devices:', error);
      }
    };
    getDevices();
  }, []);

  useEffect(() => {
    const getStream = async () => {
      if (selectedDeviceId) {
        try {
          if (stream) {
            stream.getTracks().forEach(track => track.stop());
          }
          const newStream = await navigator.mediaDevices.getUserMedia({
            video: {
              deviceId: { exact: selectedDeviceId },
              width: { ideal: 720 },
              height: { ideal: 1080 },
            },
          });
          setStream(newStream);
          if (videoRef.current) {
            videoRef.current.srcObject = newStream;
          }
        } catch (error) {
          console.error('Error getting stream:', error);
        }
      }
    };
    getStream();
  }, [selectedDeviceId]);

  （省略）

   };
```

# 原因
enumerateDevices()メソッドの返り値をconsole.logで出力させたところ、FireFoxではdeviceIDなどのメディア入出力機器の情報が取得できていたのに対して、Chromeでは中身が空の状態で取得されていた。
つまりChromeでは、deviceIDが取得できていない状態でカメラデバイスにアクセスしようとしていたため、カメラへのアクセスを問うアラートダイアログすら表示されず、アクセスに失敗していたことになる。


FireFoxでのenumerateDevices()メソッドの返り値の中身。deviceIDなどがきちんと取得できている
![スクリーンショット 2024-09-13 194916.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3642100/bddb3f97-7ae6-65bf-db9d-58d61c2fd222.png)

ChromeでのenumerateDevices()メソッドの返り値の中身。deviceIDなどの値が空になっている
![スクリーンショット 2024-09-13 194927.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3642100/f91ff35c-c5a1-59aa-5192-94f025b698b3.png)

# 解決方法
結論から述べると、enumerateDevices()メソッドを使う前に、getUserMedia()メソッドを使って、メディア入力（今回はカメラ）を使用する許可をユーザーに求める処理を追加する。
原因から分析するに、ChromeではgetUserMedia()メソッドを使って、ユーザーにメディア入力を使用する許可を承認してもらわない限りは、enumerateDevices()メソッドでメディア入出力機器の一覧やそれぞれのデバイス情報を取得できないようだ（FireFoxとのセキュリティポリシの違いが起因の仕様なのか？）

```typescript
const CameraApp: React.FC = () => {
  const [devices, setDevices] = useState<MediaDeviceInfo[]>([]);
  const [selectedDeviceId, setSelectedDeviceId] = useState<string | null>(null);
  const [stream, setStream] = useState<MediaStream | null>(null);
  const [photoDataUrl, setPhotoDataUrl] = useState<string | null>(null);
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  useEffect(() => {
    const getDevices = async () => {
        /*
       enumerateDevices()メソッドを使ってメディア入出力機器の一覧を取得する前に
       ここでgetUserMedia()メソッドを使って、メディア入力を使用する許可をユーザーに求める
       */
      await navigator.mediaDevices.getUserMedia({ video: true });
    
      try {
        const mediaDevices = await navigator.mediaDevices.enumerateDevices();
        const videoDevices = mediaDevices.filter(device => device.kind === 'videoinput');
        setDevices(videoDevices);
        if (videoDevices.length > 0) {
          setSelectedDeviceId(videoDevices[0].deviceId);
        }
      } catch (error) {
        console.error('Error getting devices:', error);
      }
    };
    getDevices();
  }, []);

  useEffect(() => {
    const getStream = async () => {
      if (selectedDeviceId) {
        try {
          if (stream) {
            stream.getTracks().forEach(track => track.stop());
          }
          const newStream = await navigator.mediaDevices.getUserMedia({
            video: {
              deviceId: { exact: selectedDeviceId },
              width: { ideal: 720 },
              height: { ideal: 1080 },
            },
          });
          setStream(newStream);
          if (videoRef.current) {
            videoRef.current.srcObject = newStream;
          }
        } catch (error) {
          console.error('Error getting stream:', error);
        }
      }
    };
    getStream();
  }, [selectedDeviceId]);

  （省略）

   };
```