Spaces:
Running
Running
| // 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; | |
| } | |