状況
ReactとTypeScriptを用いてカメラアプリを開発中に、以下のフローでユーザーのカメラデバイスにアクセスしようとしたところ、FireFox上ではアクセスできたのに対して、Chrome上ではアクセスできなかった現象が発生した(カメラへのアクセスを問うアラートダイアログすら表示されなかった)
- MediaDevices: enumerateDevices() メソッドで利用可能なメディア入出力機器の一覧と、それぞれのデバイスの情報を取得。
- 取得したメディア入出力機器のうち、videoinputタイプのものを取得
- メディア入出力機器それぞれに付与されているdeviceIDを用いて、MediaDevices: getUserMedia() メソッドで、メディア入出力機器(今回の場合はカメラ機器)の入力ストリームを取得
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などがきちんと取得できている
ChromeでのenumerateDevices()メソッドの返り値の中身。deviceIDなどの値が空になっている
解決方法
結論から述べると、enumerateDevices()メソッドを使う前に、getUserMedia()メソッドを使って、メディア入力(今回はカメラ)を使用する許可をユーザーに求める処理を追加する。
原因から分析するに、ChromeではgetUserMedia()メソッドを使って、ユーザーにメディア入力を使用する許可を承認してもらわない限りは、enumerateDevices()メソッドでメディア入出力機器の一覧やそれぞれのデバイス情報を取得できないようだ(FireFoxとのセキュリティポリシの違いが起因の仕様なのか?)
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]);
(省略)
};