diff --git a/src/components/common/deviceButton.tsx b/src/components/common/deviceButton.tsx index b26cd9d..7bf4235 100644 --- a/src/components/common/deviceButton.tsx +++ b/src/components/common/deviceButton.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; import { MEDIA_CONSTRAINTS } from "../../utils/constants"; -const DeviceButton = ({ videoRef }: {videoRef: any }) => { +const DeviceButton = ({ videoRef }: { videoRef: any }) => { const [devices, setDevices] = useState([]); const [device, setDevice] = useState(null); @@ -12,43 +12,83 @@ const DeviceButton = ({ videoRef }: {videoRef: any }) => { return; } + // Stop current stream tracks before requesting a new one + if (videoRef.current && videoRef.current.srcObject) { + const currentStream = videoRef.current.srcObject as MediaStream; + currentStream.getTracks().forEach(track => track.stop()); + videoRef.current.srcObject = null; + } + setDevice(newDevice); - const constraints: any = {...MEDIA_CONSTRAINTS} - constraints["video"]["deviceId"] = newDevice.deviceId - const stream = await navigator.mediaDevices.getUserMedia(constraints); - videoRef.current.srcObject = stream; - } + const constraints: any = JSON.parse(JSON.stringify(MEDIA_CONSTRAINTS)); + constraints["video"]["deviceId"] = { exact: newDevice.deviceId }; + // Remove facingMode when deviceId is specified to avoid conflicts + delete constraints["video"]["facingMode"]; - useEffect(() => { - const newDevices: MediaDeviceInfo[] = []; - navigator.mediaDevices - .enumerateDevices() - .then((devices) => { - devices.forEach((device: MediaDeviceInfo) => { - if (device.kind != "videoinput") { - return; + try { + const stream = await navigator.mediaDevices.getUserMedia(constraints); + if (videoRef.current) { + videoRef.current.srcObject = stream; + } + } catch (err) { + console.error("Error switching camera:", err); + // Fallback to default constraints if specific device fails + try { + const fallbackStream = await navigator.mediaDevices.getUserMedia(MEDIA_CONSTRAINTS); + if (videoRef.current) { + videoRef.current.srcObject = fallbackStream; } - newDevices.push(device); - }); - }) - .catch((err) => { - console.error(`${err.name}: ${err.message}`); - }); + } catch (fallbackErr) { + console.error("Fallback camera failed:", fallbackErr); + } + } + }; + + useEffect(() => { + const updateDevices = () => { + navigator.mediaDevices + .enumerateDevices() + .then((devices) => { + const videoDevices = devices.filter(d => d.kind === "videoinput"); + setDevices(videoDevices); + + // Try to sync the dropdown state with the currently active device + if (videoRef.current && videoRef.current.srcObject) { + const currentTrack = (videoRef.current.srcObject as MediaStream).getVideoTracks()[0]; + if (currentTrack) { + const settings = currentTrack.getSettings(); + const activeDevice = videoDevices.find(d => d.deviceId === settings.deviceId); + if (activeDevice) { + setDevice(activeDevice); + } + } + } + }) + .catch((err) => { + console.error(`${err.name}: ${err.message}`); + }); + }; + + updateDevices(); - setDevices(newDevices); - }, []) + // Re-run when devices change (e.g. plugging in a USB camera) + navigator.mediaDevices.addEventListener('devicechange', updateDevices); + return () => { + navigator.mediaDevices.removeEventListener('devicechange', updateDevices); + }; + }, []); return (
    - {devices.map(device => + {devices.map(device =>
  • handleClick(e, device)} className="dropdown-item" href="#"> - {device.label.split("(")[0]} + {device.label.split("(")[0] || `Camera ${device.deviceId.slice(0, 4)}`}
  • )} diff --git a/src/components/common/video.tsx b/src/components/common/video.tsx index a924bda..43afa4b 100644 --- a/src/components/common/video.tsx +++ b/src/components/common/video.tsx @@ -10,6 +10,15 @@ import { CornersPayload, Game, Mode, MovesPair, SetBoolean, SetStringArray } fro import { gameSelect, makeBoard } from "../../slices/gameSlice"; import { getMovesPairs } from "../../utils/moves"; +type ZoomCapabilities = MediaTrackCapabilities & { + zoom?: { + min: number; + }; +}; + +type ZoomConstraints = MediaTrackConstraints & { + zoom?: number; +}; const Video = ({ piecesModelRef, canvasRef, videoRef, sidebarRef, playing, setPlaying, playingRef, setText, mode, cornersRef }: { @@ -110,24 +119,21 @@ const Video = ({ piecesModelRef, canvasRef, videoRef, sidebarRef, playing, useEffect(() => { updateWidthHeight(); - let streamPromise: any = null; + let streamPromise: Promise = Promise.resolve(null); if (mode !== "upload") { - streamPromise = awaitSetupWebcam() + streamPromise = awaitSetupWebcam(); } findPieces(piecesModelRef, videoRef, canvasRef, playingRef, setText, dispatch, cornersRef, boardRef, movesPairsRef, lastMoveRef, moveTextRef, mode); - const stopWebcam = async () => { - const stream = await streamPromise; - if (stream !== null) { - stream.getTracks().forEach((track: any) => track.stop()); - } - } - return () => { - stopWebcam(); - } + streamPromise.then((stream) => { + if (stream !== null) { + stream.getTracks().forEach(track => track.stop()); + } + }); + }; }, []); useEffect(() => { @@ -172,36 +178,36 @@ const Video = ({ piecesModelRef, canvasRef, videoRef, sidebarRef, playing, if (mode === "upload") { return; } - window.setTimeout(() => { - if (!(videoRef.current)) { + + const applySettings = () => { + if (!(videoRef.current) || !(videoRef.current.srcObject)) { return; } - const tracks = videoRef.current.srcObject.getVideoTracks(); - if (tracks.length == 0) { + const stream = videoRef.current.srcObject as MediaStream; + const tracks = stream.getVideoTracks(); + if (tracks.length === 0) { return; } try { - const capabilities = tracks[0].getCapabilities(); - console.log("Capabilties", capabilities); - - if (capabilities.zoom) { - tracks[0].applyConstraints({ - zoom: capabilities.zoom.min, - }) + const track = tracks[0]; + if (typeof track.getCapabilities === 'function') { + const capabilities = track.getCapabilities() as ZoomCapabilities; + if (capabilities.zoom) { + const constraints: ZoomConstraints = { + zoom: capabilities.zoom.min, + }; + track.applyConstraints(constraints).catch(e => console.debug("Apply constraints failed", e)); + } } - } catch (_) { - console.log("Cannot update track capabilities") + } catch (e) { + console.debug("Capabilities check failed", e); } + }; - try { - const settings = tracks[0].getSettings(); - console.log("Settings", settings); - } catch (_) { - console.log("Cannot log track settings") - } - }, 2000); + applySettings(); + window.setTimeout(applySettings, 500); }; const onCanPlay = () => {