Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 65 additions & 25 deletions src/components/common/deviceButton.tsx
Original file line number Diff line number Diff line change
@@ -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<MediaDeviceInfo[]>([]);
const [device, setDevice] = useState<MediaDeviceInfo | null>(null);

Expand All @@ -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 (
<div className="dropdown">
<button className="btn btn-dark btn-sm btn-outline-light dropdown-toggle w-100" id="deviceButton" data-bs-toggle="dropdown" aria-expanded="false">
{(device === null) ? "Select a Device": `Device: ${device.label.split("(")[0]}`}
{(device === null) ? "Select a Device" : `Device: ${device.label.split("(")[0] || "Default"}`}
</button>
<ul className="dropdown-menu" aria-labelledby="deviceButton">
{devices.map(device =>
{devices.map(device =>
<li key={device.deviceId}>
<a onClick={(e) => handleClick(e, device)} className="dropdown-item" href="#">
{device.label.split("(")[0]}
{device.label.split("(")[0] || `Camera ${device.deviceId.slice(0, 4)}`}
</a>
</li>
)}
Expand Down
68 changes: 37 additions & 31 deletions src/components/common/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }: {
Expand Down Expand Up @@ -110,24 +119,21 @@ const Video = ({ piecesModelRef, canvasRef, videoRef, sidebarRef, playing,
useEffect(() => {
updateWidthHeight();

let streamPromise: any = null;
let streamPromise: Promise<MediaStream | null> = 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(() => {
Expand Down Expand Up @@ -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 = () => {
Expand Down