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
JavaScript
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
};