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