UNPKG

use-simple-camera

Version:

Production-ready React Hooks for Camera, Video Recording, QR/Barcode Scanning, Motion Detection, and Audio Analysis. Zero dependencies, fully typed, and easy to use.

572 lines (571 loc) 19.3 kB
import { useState as S, useRef as P, useEffect as F, useCallback as d } from "react"; const ue = (a) => { const [n, s] = S(0), x = P(null), l = P(null), w = P(null); return F(() => { if (!a) { s(0); return; } if (!a.getAudioTracks()[0]) return; const i = window.AudioContext || window.webkitAudioContext; if (!i) { console.warn("AudioContext not supported"); return; } const p = new i(); x.current = p; const h = p.createMediaStreamSource(a), R = p.createAnalyser(); R.fftSize = 256, h.connect(R), l.current = R; const v = new Uint8Array(R.frequencyBinCount), k = () => { if (!l.current) return; l.current.getByteFrequencyData(v); let f = 0; for (let o = 0; o < v.length; o++) f += v[o]; const u = f / v.length, r = Math.min(100, Math.round(u / 255 * 100 * 2.5)); s(r), w.current = requestAnimationFrame(k); }; return k(), () => { w.current && cancelAnimationFrame(w.current), x.current && x.current.close().catch(console.error); }; }, [a]), { volume: n }; }, de = (a, n) => { const { onDetect: s, formats: x = ["qr_code"] } = n, [l, w] = S(!1), [e, i] = S(!1), p = P(null); return F(() => { window.BarcodeDetector ? window.BarcodeDetector.getSupportedFormats().then((h) => { const R = x.some((v) => h.includes(v)); w(R); }).catch(() => w(!1)) : w(!1); }, [x]), F(() => { if (!a || !l) return; const h = document.createElement("video"); h.srcObject = a, h.muted = !0, h.play().catch(console.error); const R = new window.BarcodeDetector({ formats: x }), v = async () => { if (h.videoWidth !== 0) try { const k = await R.detect(h); k.length > 0 && k.forEach((f) => s(f.rawValue)); } catch (k) { console.warn("Barcode detection failed", k); } }; return i(!0), p.current = setInterval(v, 500), () => { p.current && clearInterval(p.current), h.pause(), h.srcObject = null; }; }, [a, l]), { isSupported: l, isScanning: e }; }, le = (a) => { var O, W; const [n, s] = S(null), [x, l] = S(null), [w, e] = S(1), [i, p] = S(!1), [h, R] = S(0), [v, k] = S(0), [f, u] = S("none"), [r, o] = S(0); F(() => { var _, N; if (!a) { s(null), l(null); return; } const g = a.getVideoTracks()[0]; if (!g) return; const L = ((_ = g.getCapabilities) == null ? void 0 : _.call(g)) || {}, T = ((N = g.getSettings) == null ? void 0 : N.call(g)) || {}; s(L), l(T), T.zoom && e(T.zoom); const I = T; I.pan && R(I.pan), I.tilt && k(I.tilt), I.focusMode && u(I.focusMode), I.focusDistance && o(I.focusDistance); }, [a]); const c = d( async (g) => { if (!a) return; const L = a.getVideoTracks()[0]; if (L) try { await L.applyConstraints({ advanced: [g] }); const T = L.getSettings(); l(T); } catch (T) { console.error("Failed to apply constraints:", T); } }, [a] ), C = d( async (g) => { await c({ zoom: g }), e(g); }, [c] ), m = d( async (g) => { await c({ torch: g }), p(g); }, [c] ), E = d( async (g) => { await c({ pan: g }), R(g); }, [c] ), D = d( async (g) => { await c({ tilt: g }), k(g); }, [c] ), q = d( async (g) => { await c({ focusMode: g }), u(g); }, [c] ), B = d( async (g) => { await c({ focusDistance: g }), o(g); }, [c] ); return { // Current Values zoom: w, minZoom: ((O = n == null ? void 0 : n.zoom) == null ? void 0 : O.min) || 1, maxZoom: ((W = n == null ? void 0 : n.zoom) == null ? void 0 : W.max) || 1, flash: i, hasFlash: !!(n != null && n.torch), pan: h, tilt: v, focusMode: f, focusDistance: r, // Setters setZoom: C, setFlash: m, setPan: E, setTilt: D, setFocusMode: q, setFocusDistance: B, // Support flags supports: { zoom: !!(n != null && n.zoom), flash: !!(n != null && n.torch), pan: !!(n != null && n.pan), tilt: !!(n != null && n.tilt), focusMode: !!(n != null && n.focusMode), focusDistance: !!(n != null && n.focusDistance) } }; }, fe = () => { const [a, n] = S([]), [s, x] = S([]), [l, w] = S([]), e = d(async () => { try { const i = await navigator.mediaDevices.enumerateDevices(); n(i), x(i.filter((p) => p.kind === "videoinput")), w(i.filter((p) => p.kind === "audioinput")); } catch (i) { console.error("Error enumerating devices:", i); } }, []); return F(() => (e(), navigator.mediaDevices.addEventListener("devicechange", e), () => { navigator.mediaDevices.removeEventListener( "devicechange", e ); }), [e]), { devices: a, videoDevices: s, audioDevices: l, enumerateDevices: e }; }, me = (a, n = {}) => { const { sensitivity: s = 0.2, intervalMs: x = 100, onMotion: l } = n, [w, e] = S(!1), i = P(null), p = P(null), h = P(null); return F(() => { if (!a || !a.getVideoTracks()[0]) return; const v = document.createElement("video"); v.srcObject = a, v.muted = !0, v.play().catch(console.error); const k = document.createElement("canvas"); k.width = 100, k.height = 100; const f = k.getContext("2d", { willReadFrequently: !0 }); p.current = k; const u = () => { if (!f || v.videoWidth === 0) return; f.drawImage(v, 0, 0, 100, 100); const r = f.getImageData(0, 0, 100, 100).data; if (h.current) { let o = 0; for (let E = 0; E < r.length; E += 4) { const D = Math.abs(r[E] - h.current[E]), q = Math.abs(r[E + 1] - h.current[E + 1]), B = Math.abs(r[E + 2] - h.current[E + 2]); D + q + B > 100 && o++; } const c = 100 * 100; o / c > s ? (e(!0), l == null || l()) : e(!1); } h.current = r; }; return i.current = setInterval(u, x), () => { i.current && clearInterval(i.current), v.pause(), v.srcObject = null; }; }, [a, s, x]), { motionDetected: w }; }, ge = () => { const [a, n] = S("portrait"), [s, x] = S(0); return F(() => { var w; const l = () => { var p; if (!((p = window.screen) != null && p.orientation)) return; const e = window.screen.orientation.type, i = window.screen.orientation.angle; x(i), e.includes("portrait") ? n("portrait") : n("landscape"); }; if (l(), (w = window.screen) != null && w.orientation) return window.screen.orientation.addEventListener("change", l), () => { window.screen.orientation.removeEventListener( "change", l ); }; }, []), { orientation: a, angle: s }; }, we = (a) => { const [n, s] = S(!1), [x, l] = S(!1), [w, e] = S([]), i = P(null), p = P(null), h = d( (r) => { if (!a) throw new Error("No stream available to record"); if (!n) try { let o = a; const c = (r == null ? void 0 : r.mode) || "both"; if (c === "video-only") { const D = a.getVideoTracks(); D.length > 0 && (o = new MediaStream(D)); } else if (c === "audio-only") { const D = a.getAudioTracks(); D.length > 0 && (o = new MediaStream(D)); } const C = (r == null ? void 0 : r.mimeType) || (c === "audio-only" ? "audio/webm" : "video/webm"), m = new MediaRecorder(o, { mimeType: C }); i.current = m; const E = []; m.ondataavailable = (D) => { D.data.size > 0 && E.push(D.data); }, m.onstop = () => { const D = new Blob(E, { type: C }); s(!1), l(!1), e(E), p.current && clearTimeout(p.current), r != null && r.onComplete && r.onComplete(D); }, m.start(1e3), s(!0), l(!1), r != null && r.timeLimitMs && (p.current = setTimeout(() => { R(); }, r.timeLimitMs)); } catch (o) { console.error("Failed to start recording", o); } }, [a, n] ), R = d(() => { i.current && i.current.state !== "inactive" && i.current.stop(); }, []), v = d(() => { i.current && i.current.state === "recording" && (i.current.pause(), l(!0)); }, []), k = d(() => { i.current && i.current.state === "paused" && (i.current.resume(), l(!1)); }, []), f = d(async () => { if (!a) return null; const r = a.getVideoTracks()[0]; if (!r) return null; const o = new window.ImageCapture(r); try { return await o.takePhoto(); } catch { return null; } }, [a]), u = d(() => new Blob(w, { type: "video/webm" }), [w]); return { isRecording: n, isPaused: x, recordedBlob: w.length > 0 ? u() : null, startRecording: h, stopRecording: R, pauseRecording: v, resumeRecording: k, takeSnapshot: f, clearRecordings: () => e([]) }; }, he = (a = {}) => { const n = a.dbName || "CameraStore", s = a.storeName || "media", x = a.defaultRetentionMs || 7 * 24 * 60 * 60 * 1e3, [l, w] = S(!1), e = d(() => new Promise((f, u) => { const r = indexedDB.open(n, 2); r.onerror = () => u(r.error), r.onsuccess = () => f(r.result), r.onupgradeneeded = (o) => { const c = o.target.result; if (!c.objectStoreNames.contains(s)) c.createObjectStore(s, { keyPath: "id" }).createIndex("expiresAt", "expiresAt", { unique: !1 }); else { const m = o.target.transaction.objectStore(s); m.indexNames.contains("expiresAt") || m.createIndex("expiresAt", "expiresAt", { unique: !1 }); } }; }), [n, s]), i = d(async () => { try { const r = (await e()).transaction(s, "readwrite").objectStore(s), o = r.index("expiresAt"), c = Date.now(), C = IDBKeyRange.upperBound(c), m = o.openCursor(C); m.onsuccess = (E) => { const D = E.target.result; D && (console.log( `[useStorage] Auto-deleting expired item: ${D.primaryKey}` ), r.delete(D.primaryKey), D.continue()); }; } catch (f) { console.warn("Failed to prune expired items", f); } }, [e, s]); F(() => { i(); }, [i]); const p = d( async (f, u, r) => { try { const o = await e(); return new Promise((c, C) => { const E = o.transaction(s, "readwrite").objectStore(s), D = (r == null ? void 0 : r.retentionMs) || x, q = Date.now() + D, B = { id: u, blob: f, date: (/* @__PURE__ */ new Date()).toISOString(), type: f.type, expiresAt: q }, O = E.put(B); O.onsuccess = () => c(), O.onerror = () => C(O.error); }); } catch (o) { throw console.error("Save failed", o), o; } }, [e, s, x] ), h = d( async (f) => { try { const u = await e(); return new Promise((r, o) => { const m = u.transaction(s, "readonly").objectStore(s).get(f); m.onsuccess = () => r(m.result ? m.result.blob : null), m.onerror = () => o(m.error); }); } catch { return null; } }, [e, s] ), R = d( async (f) => { const u = await e(); return new Promise((r, o) => { const m = u.transaction(s, "readwrite").objectStore(s).delete(f); m.onsuccess = () => r(), m.onerror = () => o(m.error); }); }, [e, s] ), v = d( async (f) => { const u = await h(f); if (!u) throw new Error("File not found"); const r = URL.createObjectURL(u), o = document.createElement("a"); o.href = r, o.download = f, document.body.appendChild(o), o.click(), document.body.removeChild(o), URL.revokeObjectURL(r); }, [h] ), k = d( async (f, u) => (w(!0), new Promise((r, o) => { const c = new XMLHttpRequest(); c.open(u.method || "PUT", u.url), u.withCredentials && (c.withCredentials = !0), u.timeout && (c.timeout = u.timeout), u.headers && Object.entries(u.headers).forEach( ([C, m]) => c.setRequestHeader(C, m) ), c.upload.onprogress = (C) => { if (C.lengthComputable && u.onProgress) { const m = Math.round(C.loaded / C.total * 100); u.onProgress(m); } }, c.onload = () => { w(!1), c.status >= 200 && c.status < 300 ? r() : o(new Error(`Upload failed: ${c.statusText}`)); }, c.onerror = () => { w(!1), o(new Error("Network Error")); }, c.ontimeout = () => { w(!1), o(new Error("Upload Timed Out")); }, c.send(f); })), [] ); return { saveToLocal: p, getFromLocal: h, deleteFromLocal: R, downloadFromLocal: v, uploadToRemote: k, isUploading: l }; }, ye = (a = {}) => { const { autoStart: n = !1, defaultConstraints: s = { video: !0, audio: !0 }, mock: x = !1, autoRetry: l = !1, debug: w = !1 } = a, [e, i] = S(null), [p, h] = S(null), [R, v] = S(!1), [k, f] = S(null), [u, r] = S(null), o = P(0), c = d( (...t) => { w && console.log("[useSimpleCamera]", ...t); }, [w] ), { videoDevices: C, audioDevices: m } = fe(), E = le(e), D = we(e), q = ue(e), B = ge(), O = he(), [W, g] = S(void 0), [L, T] = S(void 0), I = me(e, { onMotion: W }), _ = de(e, { onDetect: (t) => L == null ? void 0 : L(t) }), N = d( (t) => { let y = "UNKNOWN_ERROR"; t.name === "NotAllowedError" || t.name === "PermissionDeniedError" ? y = "PERMISSION_DENIED" : t.name === "NotFoundError" || t.name === "DevicesNotFoundError" ? y = "NO_DEVICE_FOUND" : t.name === "OverconstrainedError" && (y = "CONSTRAINT_ERROR"); const b = { type: y, message: t.message || "An unexpected error occurred", originalError: t }; return c("Error encountered:", b), h(b), v(!1), b; }, [c] ), X = d(async () => { try { (await navigator.mediaDevices.getUserMedia({ video: !0, audio: !0 })).getTracks().forEach((y) => y.stop()), v(!0); } catch (t) { N(t); } }, [N]), J = (t) => { switch (t) { case "SD": return { width: 640, height: 480 }; case "HD": return { width: 1280, height: 720 }; case "FHD": return { width: 1920, height: 1080 }; case "4K": return { width: 3840, height: 2160 }; case "Instagram": return { aspectRatio: 1 / 1 }; default: return {}; } }, Q = () => { const t = document.createElement("canvas"); t.width = 640, t.height = 480; const y = t.getContext("2d"); y && (y.fillStyle = "black", y.fillRect(0, 0, 640, 480), y.fillStyle = "white", y.font = "48px sans-serif", y.fillText("MOCK CAMERA", 150, 240)); const b = t.captureStream(30), j = new AudioContext().createMediaStreamDestination(); return b.addTrack(j.stream.getAudioTracks()[0]), b; }, U = d( async (t = s) => { if (h(null), c("Starting camera...", t), x) { c("Mock mode enabled. Generating synthetic stream."), i(Q()), v(!0); return; } try { let y; if ("preset" in t) { const { preset: A, deviceId: j } = t; f(A), y = { video: { ...J(A), deviceId: j ? { exact: j } : void 0 }, audio: !0 }; } else y = t; const b = await navigator.mediaDevices.getUserMedia(y); if (i(b), v(!0), o.current = 0, "wakeLock" in navigator) try { const A = await navigator.wakeLock.request("screen"); r(A); } catch (A) { console.warn("Wake Lock failed", A); } } catch (y) { const b = N(y); if (l && o.current < 3 && b.type !== "PERMISSION_DENIED") { const A = (o.current + 1) * 1e3; c( `Auto-retrying in ${A}ms... (Attempt ${o.current + 1}/3)` ), o.current += 1, setTimeout(() => U(t), A); } } }, [s, x, l, N, c] ), Y = d(async () => { try { const t = await navigator.mediaDevices.getDisplayMedia({ video: !0, audio: !0 }); i(t), v(!0); } catch (t) { N(t); } }, [N]), Z = d(() => { e && (e.getTracks().forEach((t) => t.stop()), i(null), u && (u.release().catch(console.error), r(null))); }, [e, u]), ee = d( (t) => { e == null || e.getVideoTracks().forEach((y) => y.enabled = t); }, [e] ), te = d( (t) => { e == null || e.getAudioTracks().forEach((y) => y.enabled = t); }, [e] ), re = d(async () => { if (!e) return; const b = e.getVideoTracks()[0].getSettings().facingMode; Z(), await U({ video: { facingMode: { exact: b === "user" ? "environment" : "user" } }, audio: !0 }); }, [e, Z, U]), ne = d(async (t) => { try { document.pictureInPictureElement ? await document.exitPictureInPicture() : t && await t.requestPictureInPicture(); } catch (y) { console.error("PiP failed", y); } }, []), oe = d( async (t) => { if (!e) throw new Error("No stream"); const y = e.getVideoTracks()[0], b = "ImageCapture" in window ? new window.ImageCapture(y) : null, A = (t == null ? void 0 : t.mirror) || !1, j = (t == null ? void 0 : t.format) || "image/png", ce = (t == null ? void 0 : t.quality) || 0.92, V = (t == null ? void 0 : t.filter) || "none"; let H; const $ = (M) => { const K = document.createElement("canvas"), G = "videoWidth" in M ? M.videoWidth : M.width, ae = "videoHeight" in M ? M.videoHeight : M.height; K.width = G, K.height = ae; const z = K.getContext("2d"); if (!z) throw new Error("No Canvas Context"); return A && (z.translate(G, 0), z.scale(-1, 1)), V !== "none" && (z.filter = V === "grayscale" ? "grayscale(100%)" : V === "sepia" ? "sepia(100%)" : V === "contrast" ? "contrast(150%)" : V === "blur" ? "blur(5px)" : "none"), z.drawImage(M, 0, 0), new Promise( (se) => K.toBlob((ie) => se(ie), j, ce) ); }; if (b) { const M = await b.grabFrame(); H = await $(M), M.close(); } else { const M = document.createElement("video"); M.srcObject = e, M.muted = !0, await M.play(), H = await $(M), M.pause(), M.srcObject = null; } return H ? URL.createObjectURL(H) : ""; }, [e] ); return F(() => () => { e && e.getTracks().forEach((t) => t.stop()); }, []), F(() => { n && !e && U(); }, [n, U]), { stream: e, error: p, permissionGranted: R, videoDevices: C, audioDevices: m, activePreset: k, startCamera: U, stopCamera: Z, acquirePermissions: X, startScreenShare: Y, toggleVideo: ee, toggleAudio: te, toggleFacingMode: re, togglePiP: ne, captureImage: oe, controls: E, recorder: D, audioLevel: q, motionDetection: I, barcodeScanner: _, orientation: B, // Expose Storage Utilities storage: O, setMotionCallback: g, setBarcodeCallback: T, isCameraActive: !!e }; }; export { ue as useAudioLevel, de as useBarcodeScanner, le as useCameraControls, fe as useMediaDevices, me as useMotionDetection, ge as useOrientation, we as useRecorder, ye as useSimpleCamera, he as useStorage };