UNPKG

vue-camera-kit

Version:

A versatile Vue 3 camera component for capturing photos and videos with advanced features

417 lines (416 loc) 13.8 kB
import { unref as T, getCurrentScope as ae, onScopeDispose as oe, ref as v, computed as S, watch as re, getCurrentInstance as se, onMounted as E, defineComponent as D, createElementBlock as y, openBlock as w, createCommentVNode as C, normalizeClass as M, normalizeStyle as G, useCssVars as ne, onBeforeUnmount as ie, createBlock as P, Fragment as le, createElementVNode as g } from "vue"; function ue(a) { return ae() ? (oe(a), !0) : !1; } function z(a) { return typeof a == "function" ? a() : T(a); } const U = typeof window < "u" && typeof document < "u"; typeof WorkerGlobalScope < "u" && globalThis instanceof WorkerGlobalScope; const ce = Object.prototype.toString, de = (a) => ce.call(a) === "[object Object]", pe = () => { }; function ve(a) { let o; function n() { return o || (o = a()), o; } return n.reset = async () => { const e = o; o = void 0, e && await e; }, n; } function me(a) { var o; const n = z(a); return (o = n == null ? void 0 : n.$el) != null ? o : n; } const fe = U ? window : void 0, j = U ? window.navigator : void 0; function I(...a) { let o, n, e, i; if (typeof a[0] == "string" || Array.isArray(a[0]) ? ([n, e, i] = a, o = fe) : [o, n, e, i] = a, !o) return pe; Array.isArray(n) || (n = [n]), Array.isArray(e) || (e = [e]); const r = [], u = () => { r.forEach((l) => l()), r.length = 0; }, c = (l, f, h, k) => (l.addEventListener(f, h, k), () => l.removeEventListener(f, h, k)), d = re( () => [me(o), z(i)], ([l, f]) => { if (u(), !l) return; const h = de(f) ? { ...f } : f; r.push( ...n.flatMap((k) => e.map((p) => c(l, k, p, h))) ); }, { immediate: !0, flush: "post" } ), m = () => { d(), u(); }; return ue(m), m; } function he() { const a = v(!1), o = se(); return o && E(() => { a.value = !0; }, o), a; } function W(a) { const o = he(); return S(() => (o.value, !!a())); } function ge(a, o = {}) { const { controls: n = !1, navigator: e = j } = o, i = W(() => e && "permissions" in e); let r; const u = { name: a }, c = v(), d = () => { r && (c.value = r.state); }, m = ve(async () => { if (i.value) { if (!r) try { r = await e.permissions.query(u), I(r, "change", d), d(); } catch { c.value = "prompt"; } return r; } }); return m(), n ? { state: c, isSupported: i, query: m } : c; } function we(a = {}) { const { navigator: o = j, requestPermissions: n = !1, constraints: e = { audio: !0, video: !0 }, onUpdated: i } = a, r = v([]), u = S(() => r.value.filter((p) => p.kind === "videoinput")), c = S(() => r.value.filter((p) => p.kind === "audioinput")), d = S(() => r.value.filter((p) => p.kind === "audiooutput")), m = W(() => o && o.mediaDevices && o.mediaDevices.enumerateDevices), l = v(!1); let f; async function h() { m.value && (r.value = await o.mediaDevices.enumerateDevices(), i == null || i(r.value), f && (f.getTracks().forEach((p) => p.stop()), f = null)); } async function k() { if (!m.value) return !1; if (l.value) return !0; const { state: p, query: B } = ge("camera", { controls: !0 }); return await B(), p.value !== "granted" && (f = await o.mediaDevices.getUserMedia(e), h()), l.value = !0, l.value; } return m.value && (n && k(), I(o.mediaDevices, "devicechange", h), h()), { devices: r, ensurePermissions: k, permissionGranted: l, videoInputs: u, audioInputs: c, audioOutputs: d, isSupported: m }; } const ye = { class: "camera-overlay" }, ke = ["src", "alt"], be = /* @__PURE__ */ D({ __name: "CameraOverlay", props: { showGrid: { type: Boolean, default: !1 }, gridType: { default: "rule-of-thirds" }, aspectRatio: { default: "original" }, watermark: {}, watermarkAlt: {}, watermarkPosition: { default: "bottom-right" }, watermarkSize: { default: 20 } }, setup(a) { const o = a, n = S(() => ({ "top-left": { top: "10px", left: "10px" }, "top-right": { top: "10px", right: "10px" }, "bottom-left": { bottom: "10px", left: "10px" }, "bottom-right": { bottom: "10px", right: "10px" }, center: { top: "50%", left: "50%", transform: "translate(-50%, -50%)" } })[o.watermarkPosition || "bottom-right"]); return (e, i) => (w(), y("div", ye, [ e.showGrid ? (w(), y("div", { key: 0, class: M(["grid-overlay", [e.gridType, e.aspectRatio]]) }, null, 2)) : C("", !0), e.watermark ? (w(), y("img", { key: 1, src: e.watermark, alt: e.watermarkAlt || "Watermark", class: "watermark", style: G([ n.value, { width: `${e.watermarkSize}%`, maxWidth: "150px", opacity: 0.7 } ]) }, null, 12, ke)) : C("", !0) ])); } }), x = (a, o) => { const n = a.__vccOpts || a; for (const [e, i] of o) n[e] = i; return n; }, L = /* @__PURE__ */ x(be, [["__scopeId", "data-v-49bfc96b"]]), Ce = ["width", "height"], Se = { class: "camera-controls" }, Re = ["disabled"], Be = ["disabled"], Me = { key: 1, class: "preview-modal" }, Ae = { class: "preview-content" }, De = ["src"], _e = ["src"], Oe = /* @__PURE__ */ D({ __name: "Camera", props: { showOverlay: { type: Boolean, default: !0 }, showGridButton: { type: Boolean, default: !0 }, showAspectRatioButton: { type: Boolean, default: !0 }, width: { default: 640 }, height: { default: 480 }, facingMode: { default: "environment" }, photoQuality: { default: 0.92 }, videoConstraints: { default: () => ({}) }, showPreviewByDefault: { type: Boolean, default: !0 }, showGrid: { type: Boolean, default: !1 }, gridType: { default: "rule-of-thirds" }, aspectRatio: { default: "original" }, watermark: {}, watermarkAlt: {}, watermarkPosition: { default: "bottom-right" }, watermarkSize: { default: 20 } }, emits: ["photo-captured", "video-started", "video-stopped", "error"], setup(a, { expose: o, emit: n }) { ne((t) => ({ a1462450: t.width + "px" })); const e = a, i = n, r = v(null), u = v(null), c = v(null), d = v(!1), m = v([]), l = v(""), f = v("photo"), h = v(!1); we({ requestPermissions: !0, onUpdated() { _(); } }); const k = v([]), p = v(e.showGrid), B = v(e.aspectRatio), R = v(e.facingMode), q = S(() => !!(navigator.mediaDevices && navigator.mediaDevices.getUserMedia)), V = D({ props: { message: { type: String, required: !0 } }, template: ` <div class="camera-error"> <p>{{ message }}</p> </div> ` }), _ = async () => { try { const t = { video: { facingMode: R.value, width: { ideal: e.width }, height: { ideal: e.height }, ...e.videoConstraints }, audio: !0 }; u.value && u.value.getTracks().forEach((b) => b.stop()); const s = await navigator.mediaDevices.getUserMedia(t); u.value = s, r.value && (r.value.srcObject = s, r.value.setAttribute("playsinline", ""), r.value.setAttribute("webkit-playsinline", ""), await r.value.play()); } catch (t) { console.error("Camera initialization error:", t), i("error", t); } }, $ = async () => { try { if (k.value.length <= 1) return; R.value = R.value === "user" ? "environment" : "user", u.value && u.value.getTracks().forEach((s) => s.stop()); const t = { video: { facingMode: R.value, width: { ideal: e.width }, height: { ideal: e.height }, ...e.videoConstraints }, audio: !0 }; u.value = await navigator.mediaDevices.getUserMedia(t), r.value && (r.value.srcObject = u.value, await r.value.play()); } catch (t) { console.error("Camera switch error:", t), i("error", t); } }, F = () => { p.value = !p.value; }, A = [ "original", "aspect-1-1", "aspect-16-9", "aspect-4-3", "aspect-3-2" ], N = () => { const s = (A.indexOf(B.value) + 1) % A.length; B.value = A[s]; }, Q = () => { if (!r.value) return; const t = document.createElement("canvas"); t.width = r.value.videoWidth, t.height = r.value.videoHeight; const s = t.getContext("2d"); if (!s) return; s.drawImage(r.value, 0, 0); const b = t.toDataURL("image/jpeg", e.photoQuality); t.toBlob( (O) => { O && (l.value = b, f.value = "photo", h.value = e.showPreviewByDefault, i("photo-captured", { dataUrl: b, blob: O })); }, "image/jpeg", e.photoQuality ); }, H = () => { d.value ? K() : X(); }, X = async () => { if (u.value) try { const t = { mimeType: J(), videoBitsPerSecond: 25e5 // 2.5 Mbps }; m.value = [], c.value = new MediaRecorder(u.value, t), c.value.ondataavailable = (s) => { s.data.size > 0 && m.value.push(s.data); }, c.value.onstop = () => { var b; const s = new Blob(m.value, { type: ((b = c.value) == null ? void 0 : b.mimeType) || "video/webm" }); i("video-stopped", { blob: s }), d.value = !1; }, c.value.start(), d.value = !0, i("video-started"); } catch (t) { console.error("Recording error:", t), i("error", t); } }, J = () => { const t = [ "video/webm;codecs=h264", "video/webm", "video/mp4", "video/mp4;codecs=h264", "video/webm;codecs=vp8,opus" ]; for (const s of t) if (MediaRecorder.isTypeSupported(s)) return s; return "video/webm"; }, K = () => { c.value && d.value && (c.value.stop(), d.value = !1); }, Y = () => { h.value = !1; }, Z = () => { h.value = !1, l.value = "", f.value === "video" && (m.value = []); }; E(() => { _(); }), ie(() => { c.value && d.value && c.value.stop(), u.value && u.value.getTracks().forEach((t) => t.stop()); }); const ee = () => { }, te = S(() => ({ transform: R.value === "user" ? "scaleX(-1)" : "none", width: "100%", height: "auto", maxWidth: "100%", objectFit: "cover" })); return o({}), (t, s) => (w(), y("div", { class: M(["vue-camera-kit", { "is-recording": d.value }]) }, [ q.value ? (w(), y(le, { key: 0 }, [ g("video", { ref_key: "videoRef", ref: r, width: t.width, height: t.height, autoplay: "", playsinline: "", "webkit-playsinline": "", muted: "", style: G(te.value), onLoadedmetadata: ee }, null, 44, Ce), t.showOverlay ? (w(), P(L, { key: 0, "show-grid": p.value, "grid-type": t.gridType, "aspect-ratio": t.aspectRatio, watermark: t.watermark, "watermark-alt": t.watermarkAlt, "watermark-position": t.watermarkPosition, "watermark-size": t.watermarkSize }, null, 8, ["show-grid", "grid-type", "aspect-ratio", "watermark", "watermark-alt", "watermark-position", "watermark-size"])) : C("", !0), g("div", Se, [ k.value.length > 1 ? (w(), y("button", { key: 0, class: "control-btn switch-camera", onClick: $, disabled: d.value }, s[0] || (s[0] = [ g("span", { class: "icon" }, "🔄", -1) ]), 8, Re)) : C("", !0), t.showGridButton ? (w(), y("button", { key: 1, class: M(["control-btn toggle-grid", { active: p.value }]), onClick: F }, s[1] || (s[1] = [ g("span", { class: "icon" }, "⊞", -1) ]), 2)) : C("", !0), t.showAspectRatioButton ? (w(), y("button", { key: 2, class: "control-btn aspect-ratio", onClick: N }, s[2] || (s[2] = [ g("span", { class: "icon" }, "⊡", -1) ]))) : C("", !0), g("button", { class: "control-btn capture-photo", onClick: Q, disabled: d.value }, s[3] || (s[3] = [ g("span", { class: "icon" }, "📸", -1) ]), 8, Be), g("button", { class: M(["control-btn record-video", { "is-recording": d.value }]), onClick: H }, s[4] || (s[4] = [ g("span", { class: "icon" }, "🎥", -1) ]), 2) ]), h.value ? (w(), y("div", Me, [ g("div", Ae, [ f.value === "photo" ? (w(), y("img", { key: 0, src: l.value, alt: "Captured photo" }, null, 8, De)) : (w(), y("video", { key: 1, src: l.value, controls: "" }, null, 8, _e)), g("div", { class: "preview-controls" }, [ g("button", { onClick: Y }, "Accept"), g("button", { onClick: Z }, "Retake") ]) ]) ])) : C("", !0) ], 64)) : (w(), P(T(V), { key: 1, message: "Camera access is not supported in this browser" })) ], 2)); } }), Pe = /* @__PURE__ */ x(Oe, [["__scopeId", "data-v-14158381"]]), Ee = { install: (a) => { a.component("Camera", Pe), a.component("CameraOverlay", L); } }; export { Pe as Camera, L as CameraOverlay, Ee as default };