fruitlearn / js /camera.js
Nguyen Van Manh
a
4cfb14f
Raw
History Blame Contribute Delete
2.1 kB
// 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;
}