// Webcam access. getUserMedia requires a SECURE CONTEXT (https:// or http://localhost) — // opening the file directly with file:// gives no camera. We detect that and surface a clear, // friendly reason so the teacher knows to run it from a local server. export class CameraError extends Error { constructor(type, message) { super(message); this.name = "CameraError"; this.type = type; // "insecure" | "unsupported" | "denied" | "notfound" | "unknown" } } export async function startCamera(videoEl) { if (!window.isSecureContext) { throw new CameraError( "insecure", "The camera only works over a secure connection. Please open the game from " + "http://localhost (a local server) or an https:// address — not by double-clicking the file." ); } if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { throw new CameraError( "unsupported", "This browser does not support camera access. Try the latest Chrome or Edge." ); } let stream; try { stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: "user", width: { ideal: 1280 }, height: { ideal: 720 } }, audio: false, }); } catch (err) { if (err && (err.name === "NotAllowedError" || err.name === "SecurityError")) { throw new CameraError( "denied", "Camera permission was blocked. Please allow the camera and try again." ); } if (err && (err.name === "NotFoundError" || err.name === "DevicesNotFoundError")) { throw new CameraError("notfound", "No camera was found on this device."); } throw new CameraError("unknown", "Could not start the camera: " + (err && err.message)); } videoEl.srcObject = stream; // Autoplay-safe: muted + playsinline are set in the HTML. await videoEl.play(); // Wait until we actually have frame dimensions before anyone maps coordinates. if (!videoEl.videoWidth) { await new Promise((resolve) => { videoEl.addEventListener("loadedmetadata", resolve, { once: true }); }); } return stream; }