@ai-matrx/capture 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/react.cjs +674 -98
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +265 -4
- package/dist/react.d.ts +265 -4
- package/dist/react.js +675 -105
- package/dist/react.js.map +1 -1
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -72,6 +72,23 @@ function ImagesIcon(props) {
|
|
|
72
72
|
function Loader2Icon(props) {
|
|
73
73
|
return /* @__PURE__ */ jsx("svg", { ...svgProps(props), children: /* @__PURE__ */ jsx("path", { d: "M21 12a9 9 0 1 1-6.219-8.56" }) });
|
|
74
74
|
}
|
|
75
|
+
function UploadIcon(props) {
|
|
76
|
+
return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
|
|
77
|
+
/* @__PURE__ */ jsx("path", { d: "M12 3v12" }),
|
|
78
|
+
/* @__PURE__ */ jsx("path", { d: "m17 8-5-5-5 5" }),
|
|
79
|
+
/* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" })
|
|
80
|
+
] });
|
|
81
|
+
}
|
|
82
|
+
function ImageOffIcon(props) {
|
|
83
|
+
return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
|
|
84
|
+
/* @__PURE__ */ jsx("line", { x1: "2", x2: "22", y1: "2", y2: "22" }),
|
|
85
|
+
/* @__PURE__ */ jsx("path", { d: "M10.41 10.41a2 2 0 1 1-2.83-2.83" }),
|
|
86
|
+
/* @__PURE__ */ jsx("line", { x1: "13.5", x2: "6", y1: "13.5", y2: "21" }),
|
|
87
|
+
/* @__PURE__ */ jsx("line", { x1: "18", x2: "21", y1: "12", y2: "15" }),
|
|
88
|
+
/* @__PURE__ */ jsx("path", { d: "M3.59 3.59A1.99 1.99 0 0 0 3 5v14a2 2 0 0 0 2 2h14c.55 0 1.052-.22 1.41-.59" }),
|
|
89
|
+
/* @__PURE__ */ jsx("path", { d: "M21 15V5a2 2 0 0 0-2-2H9" })
|
|
90
|
+
] });
|
|
91
|
+
}
|
|
75
92
|
function LockIcon(props) {
|
|
76
93
|
return /* @__PURE__ */ jsxs("svg", { ...svgProps(props), children: [
|
|
77
94
|
/* @__PURE__ */ jsx("rect", { width: "18", height: "11", x: "3", y: "11", rx: "2", ry: "2" }),
|
|
@@ -2536,13 +2553,350 @@ function CameraFeed({
|
|
|
2536
2553
|
}
|
|
2537
2554
|
|
|
2538
2555
|
// src/engine/useDefaultEngine.ts
|
|
2539
|
-
import {
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2556
|
+
import { useCallback as useCallback8, useEffect as useEffect10, useMemo as useMemo3, useRef as useRef7, useState as useState10 } from "react";
|
|
2557
|
+
|
|
2558
|
+
// src/engine/crop.ts
|
|
2559
|
+
var PHOTO_JPEG_QUALITY = 0.92;
|
|
2560
|
+
async function cropBlobToAspect(blob, aspect, quality = PHOTO_JPEG_QUALITY) {
|
|
2561
|
+
if (aspect === "full") return blob;
|
|
2562
|
+
const [wRatio, hRatio] = aspect === "1:1" ? [1, 1] : aspect === "4:3" ? [4, 3] : [16, 9];
|
|
2563
|
+
const bitmap = await createImageBitmap(blob);
|
|
2564
|
+
try {
|
|
2565
|
+
const srcW = bitmap.width;
|
|
2566
|
+
const srcH = bitmap.height;
|
|
2567
|
+
const target = srcW >= srcH ? wRatio / hRatio : hRatio / wRatio;
|
|
2568
|
+
let cropW = srcW;
|
|
2569
|
+
let cropH = Math.round(srcW / target);
|
|
2570
|
+
if (cropH > srcH) {
|
|
2571
|
+
cropH = srcH;
|
|
2572
|
+
cropW = Math.round(srcH * target);
|
|
2573
|
+
}
|
|
2574
|
+
if (cropW === srcW && cropH === srcH) return blob;
|
|
2575
|
+
const canvas = document.createElement("canvas");
|
|
2576
|
+
canvas.width = cropW;
|
|
2577
|
+
canvas.height = cropH;
|
|
2578
|
+
const ctx = canvas.getContext("2d");
|
|
2579
|
+
if (!ctx) return blob;
|
|
2580
|
+
ctx.drawImage(
|
|
2581
|
+
bitmap,
|
|
2582
|
+
Math.round((srcW - cropW) / 2),
|
|
2583
|
+
Math.round((srcH - cropH) / 2),
|
|
2584
|
+
cropW,
|
|
2585
|
+
cropH,
|
|
2586
|
+
0,
|
|
2587
|
+
0,
|
|
2588
|
+
cropW,
|
|
2589
|
+
cropH
|
|
2590
|
+
);
|
|
2591
|
+
return await new Promise(
|
|
2592
|
+
(resolve) => canvas.toBlob((out) => resolve(out ?? blob), "image/jpeg", quality)
|
|
2593
|
+
);
|
|
2594
|
+
} finally {
|
|
2595
|
+
bitmap.close();
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function nextCameraDevice(cameras, currentDeviceId) {
|
|
2599
|
+
if (cameras.length < 2) return null;
|
|
2600
|
+
const currentIdx = currentDeviceId ? cameras.findIndex((c) => c.deviceId === currentDeviceId) : -1;
|
|
2601
|
+
return cameras[(Math.max(currentIdx, 0) + 1) % cameras.length] ?? null;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// src/engine/permissions.ts
|
|
2605
|
+
async function queryPermission(name) {
|
|
2606
|
+
if (typeof navigator === "undefined" || !navigator.permissions) {
|
|
2607
|
+
return "unknown";
|
|
2608
|
+
}
|
|
2609
|
+
try {
|
|
2610
|
+
const status = await navigator.permissions.query({
|
|
2611
|
+
name
|
|
2612
|
+
});
|
|
2613
|
+
if (status.state === "granted") return "granted";
|
|
2614
|
+
if (status.state === "denied") return "denied";
|
|
2615
|
+
return "prompt";
|
|
2616
|
+
} catch {
|
|
2617
|
+
return "unknown";
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
function queryCameraPermission() {
|
|
2621
|
+
return queryPermission("camera");
|
|
2622
|
+
}
|
|
2623
|
+
function queryMicPermission() {
|
|
2624
|
+
return queryPermission("microphone");
|
|
2625
|
+
}
|
|
2626
|
+
function shouldCombineMicPrompt(micPermissionState) {
|
|
2627
|
+
return micPermissionState === "prompt" || micPermissionState === "unknown";
|
|
2628
|
+
}
|
|
2629
|
+
function isMediaDenialError(err) {
|
|
2630
|
+
const name = err && typeof err === "object" && "name" in err ? String(err.name) : "";
|
|
2631
|
+
return name === "NotAllowedError" || name === "SecurityError";
|
|
2632
|
+
}
|
|
2633
|
+
function classifyCameraBlockReason(err) {
|
|
2634
|
+
return isMediaDenialError(err) ? "permission-denied" : "not-supported";
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
// src/engine/warm-mic.ts
|
|
2638
|
+
var DEFAULT_KEEPALIVE_MS = 6e3;
|
|
2639
|
+
var STATE_SLOT2 = /* @__PURE__ */ Symbol.for("ai-matrx.capture.warm-mic-state");
|
|
2640
|
+
function getManager() {
|
|
2641
|
+
const holder = globalThis;
|
|
2642
|
+
let m = holder[STATE_SLOT2];
|
|
2643
|
+
if (!m) {
|
|
2644
|
+
m = {
|
|
2645
|
+
stream: null,
|
|
2646
|
+
inFlight: null,
|
|
2647
|
+
refCount: 0,
|
|
2648
|
+
releaseTimer: null,
|
|
2649
|
+
state: "idle",
|
|
2650
|
+
keepAliveMs: DEFAULT_KEEPALIVE_MS,
|
|
2651
|
+
preferredInputDeviceId: null,
|
|
2652
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
2653
|
+
interruptionListeners: /* @__PURE__ */ new Set(),
|
|
2654
|
+
pageLifecycleWatched: false,
|
|
2655
|
+
permissionWatched: false
|
|
2656
|
+
};
|
|
2657
|
+
holder[STATE_SLOT2] = m;
|
|
2658
|
+
}
|
|
2659
|
+
return m;
|
|
2660
|
+
}
|
|
2661
|
+
function emitInterruption(reason) {
|
|
2662
|
+
for (const l of getManager().interruptionListeners) {
|
|
2663
|
+
try {
|
|
2664
|
+
l(reason);
|
|
2665
|
+
} catch {
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
2669
|
+
function subscribeWarmMicInterruption(listener) {
|
|
2670
|
+
const m = getManager();
|
|
2671
|
+
m.interruptionListeners.add(listener);
|
|
2672
|
+
return () => {
|
|
2673
|
+
m.interruptionListeners.delete(listener);
|
|
2674
|
+
};
|
|
2675
|
+
}
|
|
2676
|
+
function setState(next) {
|
|
2677
|
+
const m = getManager();
|
|
2678
|
+
m.state = next;
|
|
2679
|
+
for (const l of m.listeners) {
|
|
2680
|
+
try {
|
|
2681
|
+
l(next);
|
|
2682
|
+
} catch {
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
function clearReleaseTimer() {
|
|
2687
|
+
const m = getManager();
|
|
2688
|
+
if (m.releaseTimer) {
|
|
2689
|
+
clearTimeout(m.releaseTimer);
|
|
2690
|
+
m.releaseTimer = null;
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
function attachTrackHealth(stream) {
|
|
2694
|
+
const m = getManager();
|
|
2695
|
+
for (const track of stream.getAudioTracks()) {
|
|
2696
|
+
track.onended = () => {
|
|
2697
|
+
console.error(
|
|
2698
|
+
"[capture:warm-mic] mic track ENDED (OS interruption / device change). The warm grant is gone; the next recording will re-prompt."
|
|
2699
|
+
);
|
|
2700
|
+
if (m.stream === stream) {
|
|
2701
|
+
m.stream = null;
|
|
2702
|
+
setState("error");
|
|
2703
|
+
}
|
|
2704
|
+
emitInterruption("ended");
|
|
2705
|
+
};
|
|
2706
|
+
track.onmute = () => {
|
|
2707
|
+
console.warn(
|
|
2708
|
+
"[capture:warm-mic] mic track MUTED (transient interruption \u2014 e.g. a call). The grant survives; it should unmute when the interruption ends."
|
|
2709
|
+
);
|
|
2710
|
+
emitInterruption("muted");
|
|
2711
|
+
};
|
|
2712
|
+
track.onunmute = () => {
|
|
2713
|
+
emitInterruption("unmuted");
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function notifyMicPermissionRevoked() {
|
|
2718
|
+
console.error("[capture:warm-mic] microphone permission REVOKED.");
|
|
2719
|
+
hardStopWarmMic();
|
|
2720
|
+
emitInterruption("permission-revoked");
|
|
2721
|
+
}
|
|
2722
|
+
function watchMicPermission() {
|
|
2723
|
+
const m = getManager();
|
|
2724
|
+
if (m.permissionWatched) return;
|
|
2725
|
+
if (typeof navigator === "undefined" || !navigator.permissions) return;
|
|
2726
|
+
m.permissionWatched = true;
|
|
2727
|
+
void (async () => {
|
|
2728
|
+
try {
|
|
2729
|
+
const status = await navigator.permissions.query({
|
|
2730
|
+
name: "microphone"
|
|
2731
|
+
});
|
|
2732
|
+
status.onchange = () => {
|
|
2733
|
+
if (status.state === "denied") notifyMicPermissionRevoked();
|
|
2734
|
+
};
|
|
2735
|
+
} catch {
|
|
2736
|
+
}
|
|
2737
|
+
})();
|
|
2738
|
+
}
|
|
2739
|
+
function watchPageLifecycle() {
|
|
2740
|
+
const m = getManager();
|
|
2741
|
+
if (m.pageLifecycleWatched) return;
|
|
2742
|
+
if (typeof window === "undefined") return;
|
|
2743
|
+
m.pageLifecycleWatched = true;
|
|
2744
|
+
window.addEventListener("pagehide", () => {
|
|
2745
|
+
if (m.stream || m.refCount > 0) {
|
|
2746
|
+
if (m.refCount > 0) {
|
|
2747
|
+
console.error(
|
|
2748
|
+
`[capture:warm-mic] page hiding with ${m.refCount} unreleased mic holder(s) \u2014 forcing release. A surface leaked acquireWarmMic without a matching releaseWarmMic on unmount.`
|
|
2749
|
+
);
|
|
2750
|
+
}
|
|
2751
|
+
hardStopWarmMic();
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
}
|
|
2755
|
+
function setPreferredMicDeviceId(id) {
|
|
2756
|
+
getManager().preferredInputDeviceId = id;
|
|
2757
|
+
}
|
|
2758
|
+
function streamIsLive(stream) {
|
|
2759
|
+
if (!stream) return false;
|
|
2760
|
+
const tracks = stream.getAudioTracks();
|
|
2761
|
+
return tracks.length > 0 && tracks.every((t) => t.readyState === "live");
|
|
2762
|
+
}
|
|
2763
|
+
function buildWarmMicConstraints() {
|
|
2764
|
+
const m = getManager();
|
|
2765
|
+
const audio = {
|
|
2766
|
+
echoCancellation: true,
|
|
2767
|
+
noiseSuppression: true,
|
|
2768
|
+
autoGainControl: true
|
|
2769
|
+
};
|
|
2770
|
+
if (m.preferredInputDeviceId) {
|
|
2771
|
+
audio.deviceId = { ideal: m.preferredInputDeviceId };
|
|
2772
|
+
}
|
|
2773
|
+
return audio;
|
|
2774
|
+
}
|
|
2775
|
+
function adoptWarmAudioStream(stream) {
|
|
2776
|
+
const m = getManager();
|
|
2777
|
+
if (streamIsLive(m.stream) || m.inFlight) {
|
|
2778
|
+
for (const t of stream.getTracks()) {
|
|
2779
|
+
try {
|
|
2780
|
+
t.stop();
|
|
2781
|
+
} catch {
|
|
2782
|
+
}
|
|
2783
|
+
}
|
|
2784
|
+
return;
|
|
2785
|
+
}
|
|
2786
|
+
if (!streamIsLive(stream)) return;
|
|
2787
|
+
clearReleaseTimer();
|
|
2788
|
+
m.stream = stream;
|
|
2789
|
+
attachTrackHealth(stream);
|
|
2790
|
+
watchPageLifecycle();
|
|
2791
|
+
watchMicPermission();
|
|
2792
|
+
if (m.refCount > 0) {
|
|
2793
|
+
setState("active");
|
|
2794
|
+
} else {
|
|
2795
|
+
armKeepaliveStop();
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
async function acquireWarmMic(constraints) {
|
|
2799
|
+
const m = getManager();
|
|
2800
|
+
clearReleaseTimer();
|
|
2801
|
+
m.refCount += 1;
|
|
2802
|
+
if (streamIsLive(m.stream)) {
|
|
2803
|
+
setState("active");
|
|
2804
|
+
return m.stream;
|
|
2805
|
+
}
|
|
2806
|
+
m.stream = null;
|
|
2807
|
+
if (m.inFlight) {
|
|
2808
|
+
return m.inFlight;
|
|
2809
|
+
}
|
|
2810
|
+
setState("acquiring");
|
|
2811
|
+
const audio = constraints ?? buildWarmMicConstraints();
|
|
2812
|
+
if (m.preferredInputDeviceId && audio.deviceId == null) {
|
|
2813
|
+
audio.deviceId = { ideal: m.preferredInputDeviceId };
|
|
2814
|
+
}
|
|
2815
|
+
m.inFlight = (async () => {
|
|
2816
|
+
try {
|
|
2817
|
+
const stream = await navigator.mediaDevices.getUserMedia({ audio });
|
|
2818
|
+
m.stream = stream;
|
|
2819
|
+
attachTrackHealth(stream);
|
|
2820
|
+
watchPageLifecycle();
|
|
2821
|
+
watchMicPermission();
|
|
2822
|
+
if (m.refCount === 0) {
|
|
2823
|
+
armKeepaliveStop();
|
|
2824
|
+
return stream;
|
|
2825
|
+
}
|
|
2826
|
+
setState("active");
|
|
2827
|
+
return stream;
|
|
2828
|
+
} catch (err) {
|
|
2829
|
+
m.refCount = Math.max(0, m.refCount - 1);
|
|
2830
|
+
setState("error");
|
|
2831
|
+
throw err;
|
|
2832
|
+
} finally {
|
|
2833
|
+
m.inFlight = null;
|
|
2834
|
+
}
|
|
2835
|
+
})();
|
|
2836
|
+
return m.inFlight;
|
|
2837
|
+
}
|
|
2838
|
+
function releaseWarmMic() {
|
|
2839
|
+
const m = getManager();
|
|
2840
|
+
if (m.refCount === 0) {
|
|
2841
|
+
console.error(
|
|
2842
|
+
"[capture:warm-mic] releaseWarmMic() with zero holders \u2014 unbalanced release. Some caller released twice (or released a hold it never acquired)."
|
|
2843
|
+
);
|
|
2844
|
+
return;
|
|
2845
|
+
}
|
|
2846
|
+
m.refCount -= 1;
|
|
2847
|
+
if (m.refCount > 0) return;
|
|
2848
|
+
if (!m.stream) {
|
|
2849
|
+
setState(m.inFlight ? "keepalive" : "idle");
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
armKeepaliveStop();
|
|
2853
|
+
}
|
|
2854
|
+
function armKeepaliveStop() {
|
|
2855
|
+
const m = getManager();
|
|
2856
|
+
clearReleaseTimer();
|
|
2857
|
+
setState("keepalive");
|
|
2858
|
+
m.releaseTimer = setTimeout(() => {
|
|
2859
|
+
m.releaseTimer = null;
|
|
2860
|
+
if (m.refCount > 0) return;
|
|
2861
|
+
hardStopWarmMic();
|
|
2862
|
+
}, m.keepAliveMs);
|
|
2863
|
+
}
|
|
2864
|
+
function hardStopWarmMic() {
|
|
2865
|
+
const m = getManager();
|
|
2866
|
+
clearReleaseTimer();
|
|
2867
|
+
if (m.stream) {
|
|
2868
|
+
for (const t of m.stream.getTracks()) {
|
|
2869
|
+
try {
|
|
2870
|
+
t.stop();
|
|
2871
|
+
} catch {
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
m.stream = null;
|
|
2875
|
+
}
|
|
2876
|
+
m.refCount = 0;
|
|
2877
|
+
setState("idle");
|
|
2878
|
+
}
|
|
2879
|
+
function getWarmMicState() {
|
|
2880
|
+
return getManager().state;
|
|
2881
|
+
}
|
|
2882
|
+
function subscribeWarmMic(listener) {
|
|
2883
|
+
const m = getManager();
|
|
2884
|
+
m.listeners.add(listener);
|
|
2885
|
+
return () => {
|
|
2886
|
+
m.listeners.delete(listener);
|
|
2887
|
+
};
|
|
2888
|
+
}
|
|
2889
|
+
function warmMicDebug() {
|
|
2890
|
+
const m = getManager();
|
|
2891
|
+
return {
|
|
2892
|
+
state: m.state,
|
|
2893
|
+
refCount: m.refCount,
|
|
2894
|
+
live: streamIsLive(m.stream),
|
|
2895
|
+
keepAliveMs: m.keepAliveMs
|
|
2896
|
+
};
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
// src/engine/useDefaultEngine.ts
|
|
2546
2900
|
var RECORDING_MIME_LADDER = [
|
|
2547
2901
|
'video/mp4;codecs="avc1.42E01E,mp4a.40.2"',
|
|
2548
2902
|
"video/mp4",
|
|
@@ -2550,39 +2904,38 @@ var RECORDING_MIME_LADDER = [
|
|
|
2550
2904
|
'video/webm;codecs="vp8,opus"',
|
|
2551
2905
|
"video/webm"
|
|
2552
2906
|
];
|
|
2553
|
-
function cropRect(w, h, aspect) {
|
|
2554
|
-
if (aspect === "full") return { x: 0, y: 0, w, h };
|
|
2555
|
-
const [aw, ah] = aspect === "1:1" ? [1, 1] : aspect === "4:3" ? [3, 4] : [9, 16];
|
|
2556
|
-
const [rw, rh] = w >= h ? [ah, aw] : [aw, ah];
|
|
2557
|
-
const target = rw / rh;
|
|
2558
|
-
let cw = w;
|
|
2559
|
-
let ch = w / target;
|
|
2560
|
-
if (ch > h) {
|
|
2561
|
-
ch = h;
|
|
2562
|
-
cw = h * target;
|
|
2563
|
-
}
|
|
2564
|
-
return { x: (w - cw) / 2, y: (h - ch) / 2, w: cw, h: ch };
|
|
2565
|
-
}
|
|
2566
2907
|
function useDefaultCaptureEngine(options) {
|
|
2567
2908
|
const {
|
|
2568
2909
|
onPhoto,
|
|
2569
2910
|
onVideo,
|
|
2570
2911
|
onFiles,
|
|
2571
2912
|
withAudio = true,
|
|
2913
|
+
mode,
|
|
2572
2914
|
facingMode: initialFacing = "environment",
|
|
2573
|
-
photoQuality = 0.92
|
|
2915
|
+
photoQuality = 0.92,
|
|
2916
|
+
fileNamePrefix = "capture",
|
|
2917
|
+
onError
|
|
2574
2918
|
} = options;
|
|
2575
2919
|
const [stream, setStream] = useState10(null);
|
|
2576
2920
|
const [blocked, setBlocked] = useState10(null);
|
|
2577
|
-
const [
|
|
2578
|
-
const [
|
|
2921
|
+
const [deviceId, setDeviceId] = useState10(null);
|
|
2922
|
+
const [cameras, setCameras] = useState10([]);
|
|
2579
2923
|
const [recording, setRecording] = useState10(false);
|
|
2580
2924
|
const [recordElapsedSeconds, setRecordElapsedSeconds] = useState10(0);
|
|
2925
|
+
const [flash, setFlash] = useState10(false);
|
|
2926
|
+
const [acquireToken, setAcquireToken] = useState10(0);
|
|
2581
2927
|
const videoRef = useRef7(null);
|
|
2582
2928
|
const streamRef = useRef7(null);
|
|
2583
2929
|
const recorderRef = useRef7(null);
|
|
2584
|
-
const
|
|
2585
|
-
|
|
2930
|
+
const captureBusyRef = useRef7(false);
|
|
2931
|
+
const reacquiredOnceRef = useRef7(false);
|
|
2932
|
+
const callbacksRef = useRef7({ onPhoto, onVideo, onFiles, onError });
|
|
2933
|
+
callbacksRef.current = { onPhoto, onVideo, onFiles, onError };
|
|
2934
|
+
const reportError = useCallback8((message, err) => {
|
|
2935
|
+
const sink = callbacksRef.current.onError;
|
|
2936
|
+
if (sink) sink(message, err);
|
|
2937
|
+
else console.error(`[capture:engine] ${message}`, err);
|
|
2938
|
+
}, []);
|
|
2586
2939
|
useEffect10(() => {
|
|
2587
2940
|
let cancelled = false;
|
|
2588
2941
|
let acquired = null;
|
|
@@ -2590,110 +2943,176 @@ function useDefaultCaptureEngine(options) {
|
|
|
2590
2943
|
setBlocked({ reason: "not-supported" });
|
|
2591
2944
|
return;
|
|
2592
2945
|
}
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
},
|
|
2599
|
-
audio: false
|
|
2600
|
-
}).then((s) => {
|
|
2601
|
-
if (cancelled) {
|
|
2602
|
-
s.getTracks().forEach((t) => t.stop());
|
|
2946
|
+
void (async () => {
|
|
2947
|
+
const known = await queryCameraPermission();
|
|
2948
|
+
if (cancelled) return;
|
|
2949
|
+
if (known === "denied") {
|
|
2950
|
+
setBlocked({ reason: "permission-denied" });
|
|
2603
2951
|
return;
|
|
2604
2952
|
}
|
|
2605
|
-
|
|
2606
|
-
streamRef.current = s;
|
|
2607
|
-
setStream(s);
|
|
2608
|
-
setBlocked(null);
|
|
2609
|
-
return navigator.mediaDevices.enumerateDevices().then((devices) => {
|
|
2610
|
-
if (!cancelled)
|
|
2611
|
-
setMultipleCameras(
|
|
2612
|
-
devices.filter((d) => d.kind === "videoinput").length > 1
|
|
2613
|
-
);
|
|
2614
|
-
});
|
|
2615
|
-
}).catch((err) => {
|
|
2953
|
+
const includeMic = withAudio && shouldCombineMicPrompt(await queryMicPermission());
|
|
2616
2954
|
if (cancelled) return;
|
|
2617
|
-
const
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2955
|
+
const video = {
|
|
2956
|
+
width: { ideal: 4096 },
|
|
2957
|
+
height: { ideal: 4096 },
|
|
2958
|
+
...deviceId ? { deviceId: { exact: deviceId } } : { facingMode: initialFacing }
|
|
2959
|
+
};
|
|
2960
|
+
const acquire = async (mic) => {
|
|
2961
|
+
try {
|
|
2962
|
+
return await navigator.mediaDevices.getUserMedia(
|
|
2963
|
+
mic ? { video, audio: buildWarmMicConstraints() } : { video, audio: false }
|
|
2964
|
+
);
|
|
2965
|
+
} catch (err) {
|
|
2966
|
+
if (mic && classifyCameraBlockReason(err) === "permission-denied") {
|
|
2967
|
+
return navigator.mediaDevices.getUserMedia({ video, audio: false });
|
|
2968
|
+
}
|
|
2969
|
+
throw err;
|
|
2970
|
+
}
|
|
2971
|
+
};
|
|
2972
|
+
try {
|
|
2973
|
+
const s = await acquire(includeMic);
|
|
2974
|
+
if (cancelled) {
|
|
2975
|
+
s.getTracks().forEach((t) => t.stop());
|
|
2976
|
+
return;
|
|
2977
|
+
}
|
|
2978
|
+
const audioTracks = s.getAudioTracks();
|
|
2979
|
+
if (audioTracks.length > 0) {
|
|
2980
|
+
for (const t of audioTracks) s.removeTrack(t);
|
|
2981
|
+
adoptWarmAudioStream(new MediaStream(audioTracks));
|
|
2982
|
+
}
|
|
2983
|
+
acquired = s;
|
|
2984
|
+
streamRef.current = s;
|
|
2985
|
+
setStream(s);
|
|
2986
|
+
setBlocked(null);
|
|
2987
|
+
reacquiredOnceRef.current = false;
|
|
2988
|
+
for (const track of s.getVideoTracks()) {
|
|
2989
|
+
track.onended = () => {
|
|
2990
|
+
if (cancelled || streamRef.current !== s) return;
|
|
2991
|
+
console.error(
|
|
2992
|
+
"[capture:engine] camera track ENDED (OS interruption / device change) \u2014 reacquiring."
|
|
2993
|
+
);
|
|
2994
|
+
if (reacquiredOnceRef.current) {
|
|
2995
|
+
setBlocked({ reason: "not-supported" });
|
|
2996
|
+
return;
|
|
2997
|
+
}
|
|
2998
|
+
reacquiredOnceRef.current = true;
|
|
2999
|
+
setAcquireToken((n) => n + 1);
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
3003
|
+
if (!cancelled) {
|
|
3004
|
+
setCameras(devices.filter((d) => d.kind === "videoinput"));
|
|
3005
|
+
}
|
|
3006
|
+
} catch (err) {
|
|
3007
|
+
if (cancelled) return;
|
|
3008
|
+
setBlocked({ reason: classifyCameraBlockReason(err) });
|
|
3009
|
+
}
|
|
3010
|
+
})();
|
|
2622
3011
|
return () => {
|
|
2623
3012
|
cancelled = true;
|
|
2624
3013
|
acquired?.getTracks().forEach((t) => t.stop());
|
|
2625
3014
|
if (streamRef.current === acquired) streamRef.current = null;
|
|
2626
3015
|
setStream(null);
|
|
2627
3016
|
};
|
|
2628
|
-
}, [
|
|
3017
|
+
}, [deviceId, initialFacing, withAudio, acquireToken]);
|
|
3018
|
+
const cameraBlocked = blocked !== null;
|
|
3019
|
+
useEffect10(() => {
|
|
3020
|
+
if (mode !== "video" || cameraBlocked || !withAudio) return;
|
|
3021
|
+
let cancelled = false;
|
|
3022
|
+
let held = false;
|
|
3023
|
+
void (async () => {
|
|
3024
|
+
if (await queryMicPermission() === "denied") return;
|
|
3025
|
+
if (cancelled) return;
|
|
3026
|
+
acquireWarmMic().then(() => {
|
|
3027
|
+
if (cancelled) {
|
|
3028
|
+
releaseWarmMic();
|
|
3029
|
+
return;
|
|
3030
|
+
}
|
|
3031
|
+
held = true;
|
|
3032
|
+
}).catch(() => {
|
|
3033
|
+
});
|
|
3034
|
+
})();
|
|
3035
|
+
return () => {
|
|
3036
|
+
cancelled = true;
|
|
3037
|
+
if (held) releaseWarmMic();
|
|
3038
|
+
};
|
|
3039
|
+
}, [mode, cameraBlocked, withAudio]);
|
|
2629
3040
|
useEffect10(() => {
|
|
2630
3041
|
return () => {
|
|
2631
3042
|
const r = recorderRef.current;
|
|
2632
3043
|
if (r) {
|
|
2633
3044
|
clearInterval(r.timer);
|
|
2634
3045
|
if (r.recorder.state !== "inactive") r.recorder.stop();
|
|
2635
|
-
r.micTracks.forEach((t) => t.stop());
|
|
2636
3046
|
}
|
|
2637
3047
|
};
|
|
2638
3048
|
}, []);
|
|
2639
|
-
const
|
|
3049
|
+
const capturePhotoWith = useCallback8(
|
|
2640
3050
|
(opts) => {
|
|
2641
3051
|
const video = videoRef.current;
|
|
2642
3052
|
if (!video || video.videoWidth === 0) return;
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
(blob) => {
|
|
2666
|
-
if (!blob) return;
|
|
3053
|
+
if (captureBusyRef.current) return;
|
|
3054
|
+
captureBusyRef.current = true;
|
|
3055
|
+
void (async () => {
|
|
3056
|
+
try {
|
|
3057
|
+
const canvas = document.createElement("canvas");
|
|
3058
|
+
canvas.width = video.videoWidth;
|
|
3059
|
+
canvas.height = video.videoHeight;
|
|
3060
|
+
const ctx = canvas.getContext("2d");
|
|
3061
|
+
if (!ctx) throw new Error("2d canvas context unavailable");
|
|
3062
|
+
ctx.drawImage(video, 0, 0);
|
|
3063
|
+
const full = await new Promise(
|
|
3064
|
+
(resolve, reject) => canvas.toBlob(
|
|
3065
|
+
(b) => b ? resolve(b) : reject(new Error("toBlob returned null")),
|
|
3066
|
+
"image/jpeg",
|
|
3067
|
+
photoQuality
|
|
3068
|
+
)
|
|
3069
|
+
);
|
|
3070
|
+
const blob = await cropBlobToAspect(
|
|
3071
|
+
full,
|
|
3072
|
+
opts.aspect ?? "full",
|
|
3073
|
+
photoQuality
|
|
3074
|
+
);
|
|
2667
3075
|
callbacksRef.current.onPhoto(
|
|
2668
|
-
new File(
|
|
2669
|
-
|
|
2670
|
-
|
|
3076
|
+
new File(
|
|
3077
|
+
[blob],
|
|
3078
|
+
`${opts.fileNamePrefix}-${(/* @__PURE__ */ new Date()).toISOString()}.jpg`,
|
|
3079
|
+
{ type: "image/jpeg" }
|
|
3080
|
+
)
|
|
2671
3081
|
);
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
3082
|
+
setFlash(true);
|
|
3083
|
+
window.setTimeout(() => setFlash(false), 120);
|
|
3084
|
+
} catch (err) {
|
|
3085
|
+
reportError("The photo could not be captured \u2014 try again.", err);
|
|
3086
|
+
} finally {
|
|
3087
|
+
captureBusyRef.current = false;
|
|
3088
|
+
}
|
|
3089
|
+
})();
|
|
3090
|
+
},
|
|
3091
|
+
[photoQuality, reportError]
|
|
3092
|
+
);
|
|
3093
|
+
const onCapturePhoto = useCallback8(
|
|
3094
|
+
(opts) => {
|
|
3095
|
+
capturePhotoWith(
|
|
3096
|
+
opts?.aspect ? { fileNamePrefix, aspect: opts.aspect } : { fileNamePrefix }
|
|
2675
3097
|
);
|
|
2676
3098
|
},
|
|
2677
|
-
[
|
|
3099
|
+
[capturePhotoWith, fileNamePrefix]
|
|
2678
3100
|
);
|
|
2679
3101
|
const onStartRecording = useCallback8(() => {
|
|
2680
3102
|
const base = streamRef.current;
|
|
2681
3103
|
if (!base || recorderRef.current) return;
|
|
2682
3104
|
void (async () => {
|
|
2683
|
-
let
|
|
3105
|
+
let micClones = [];
|
|
3106
|
+
let micHeld = false;
|
|
2684
3107
|
if (withAudio) {
|
|
2685
3108
|
try {
|
|
2686
|
-
const mic = await
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
micTracks = mic.getAudioTracks();
|
|
3109
|
+
const mic = await acquireWarmMic();
|
|
3110
|
+
micHeld = true;
|
|
3111
|
+
micClones = mic.getAudioTracks().map((t) => t.clone());
|
|
2690
3112
|
} catch {
|
|
2691
3113
|
}
|
|
2692
3114
|
}
|
|
2693
|
-
const composed = new MediaStream([
|
|
2694
|
-
...base.getVideoTracks(),
|
|
2695
|
-
...micTracks
|
|
2696
|
-
]);
|
|
3115
|
+
const composed = new MediaStream([...base.getVideoTracks(), ...micClones]);
|
|
2697
3116
|
const mime = RECORDING_MIME_LADDER.find(
|
|
2698
3117
|
(m) => typeof MediaRecorder !== "undefined" && MediaRecorder.isTypeSupported(m)
|
|
2699
3118
|
);
|
|
@@ -2703,15 +3122,18 @@ function useDefaultCaptureEngine(options) {
|
|
|
2703
3122
|
composed,
|
|
2704
3123
|
mime ? { mimeType: mime } : void 0
|
|
2705
3124
|
);
|
|
2706
|
-
} catch {
|
|
2707
|
-
|
|
3125
|
+
} catch (err) {
|
|
3126
|
+
micClones.forEach((t) => t.stop());
|
|
3127
|
+
if (micHeld) releaseWarmMic();
|
|
3128
|
+
reportError("Could not start the video recording.", err);
|
|
2708
3129
|
return;
|
|
2709
3130
|
}
|
|
2710
3131
|
const entry = {
|
|
2711
3132
|
recorder,
|
|
2712
3133
|
chunks: [],
|
|
2713
3134
|
startedAt: performance.now(),
|
|
2714
|
-
|
|
3135
|
+
micClones,
|
|
3136
|
+
micHeld,
|
|
2715
3137
|
timer: setInterval(() => {
|
|
2716
3138
|
setRecordElapsedSeconds(
|
|
2717
3139
|
Math.floor((performance.now() - entry.startedAt) / 1e3)
|
|
@@ -2724,7 +3146,8 @@ function useDefaultCaptureEngine(options) {
|
|
|
2724
3146
|
};
|
|
2725
3147
|
recorder.onstop = () => {
|
|
2726
3148
|
clearInterval(entry.timer);
|
|
2727
|
-
entry.
|
|
3149
|
+
entry.micClones.forEach((t) => t.stop());
|
|
3150
|
+
if (entry.micHeld) releaseWarmMic();
|
|
2728
3151
|
recorderRef.current = null;
|
|
2729
3152
|
setRecording(false);
|
|
2730
3153
|
const durationMs = Math.round(performance.now() - entry.startedAt);
|
|
@@ -2732,7 +3155,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
2732
3155
|
const blob = new Blob(entry.chunks, { type });
|
|
2733
3156
|
const ext = type.includes("mp4") ? "mp4" : "webm";
|
|
2734
3157
|
callbacksRef.current.onVideo(
|
|
2735
|
-
new File([blob],
|
|
3158
|
+
new File([blob], `${fileNamePrefix}-video-${Date.now()}.${ext}`, {
|
|
2736
3159
|
type
|
|
2737
3160
|
}),
|
|
2738
3161
|
durationMs
|
|
@@ -2742,7 +3165,7 @@ function useDefaultCaptureEngine(options) {
|
|
|
2742
3165
|
setRecordElapsedSeconds(0);
|
|
2743
3166
|
setRecording(true);
|
|
2744
3167
|
})();
|
|
2745
|
-
}, [withAudio]);
|
|
3168
|
+
}, [withAudio, fileNamePrefix, reportError]);
|
|
2746
3169
|
const onStopRecording = useCallback8(() => {
|
|
2747
3170
|
const r = recorderRef.current;
|
|
2748
3171
|
if (r && r.recorder.state !== "inactive") r.recorder.stop();
|
|
@@ -2759,8 +3182,12 @@ function useDefaultCaptureEngine(options) {
|
|
|
2759
3182
|
input.click();
|
|
2760
3183
|
}, []);
|
|
2761
3184
|
const onFlipCamera = useCallback8(() => {
|
|
2762
|
-
|
|
2763
|
-
|
|
3185
|
+
if (recorderRef.current) return;
|
|
3186
|
+
const current = deviceId ?? streamRef.current?.getVideoTracks()[0]?.getSettings().deviceId ?? null;
|
|
3187
|
+
const next = nextCameraDevice(cameras, current);
|
|
3188
|
+
if (next) setDeviceId(next.deviceId);
|
|
3189
|
+
}, [cameras, deviceId]);
|
|
3190
|
+
const canFlip = cameras.length > 1 && !cameraBlocked && !recording;
|
|
2764
3191
|
return useMemo3(
|
|
2765
3192
|
() => ({
|
|
2766
3193
|
stream,
|
|
@@ -2772,7 +3199,9 @@ function useDefaultCaptureEngine(options) {
|
|
|
2772
3199
|
recording,
|
|
2773
3200
|
recordElapsedSeconds,
|
|
2774
3201
|
onUpload,
|
|
2775
|
-
onFlipCamera:
|
|
3202
|
+
onFlipCamera: canFlip ? onFlipCamera : null,
|
|
3203
|
+
flash,
|
|
3204
|
+
capturePhotoWith
|
|
2776
3205
|
}),
|
|
2777
3206
|
[
|
|
2778
3207
|
stream,
|
|
@@ -2784,10 +3213,131 @@ function useDefaultCaptureEngine(options) {
|
|
|
2784
3213
|
recordElapsedSeconds,
|
|
2785
3214
|
onUpload,
|
|
2786
3215
|
onFlipCamera,
|
|
2787
|
-
|
|
3216
|
+
canFlip,
|
|
3217
|
+
flash,
|
|
3218
|
+
capturePhotoWith
|
|
2788
3219
|
]
|
|
2789
3220
|
);
|
|
2790
3221
|
}
|
|
3222
|
+
|
|
3223
|
+
// src/components/CloudLibrarySheet.tsx
|
|
3224
|
+
import { useEffect as useEffect11, useState as useState11 } from "react";
|
|
3225
|
+
import { jsx as jsx18, jsxs as jsxs15 } from "react/jsx-runtime";
|
|
3226
|
+
var PAGE_SIZE = 60;
|
|
3227
|
+
function CloudLibrarySheet({
|
|
3228
|
+
open,
|
|
3229
|
+
onClose,
|
|
3230
|
+
items,
|
|
3231
|
+
loading,
|
|
3232
|
+
busy = false,
|
|
3233
|
+
onOpenItem,
|
|
3234
|
+
onUpload,
|
|
3235
|
+
title = "Your media"
|
|
3236
|
+
}) {
|
|
3237
|
+
const [visibleCount, setVisibleCount] = useState11(PAGE_SIZE);
|
|
3238
|
+
useEffect11(() => {
|
|
3239
|
+
if (open) setVisibleCount(PAGE_SIZE);
|
|
3240
|
+
}, [open]);
|
|
3241
|
+
if (!open) return null;
|
|
3242
|
+
const uploadThenClose = onUpload ? () => {
|
|
3243
|
+
onClose();
|
|
3244
|
+
onUpload();
|
|
3245
|
+
} : null;
|
|
3246
|
+
return /* @__PURE__ */ jsxs15("div", { className: "absolute inset-0 z-50 flex flex-col bg-black", children: [
|
|
3247
|
+
/* @__PURE__ */ jsxs15(
|
|
3248
|
+
"div",
|
|
3249
|
+
{
|
|
3250
|
+
className: "flex shrink-0 items-center justify-between bg-black/80 px-4",
|
|
3251
|
+
style: safeTop,
|
|
3252
|
+
children: [
|
|
3253
|
+
/* @__PURE__ */ jsx18("h2", { className: "py-4 text-[17px] font-semibold text-white", children: title }),
|
|
3254
|
+
/* @__PURE__ */ jsxs15("div", { className: "flex items-center gap-2", children: [
|
|
3255
|
+
uploadThenClose ? /* @__PURE__ */ jsxs15(
|
|
3256
|
+
"button",
|
|
3257
|
+
{
|
|
3258
|
+
type: "button",
|
|
3259
|
+
onClick: uploadThenClose,
|
|
3260
|
+
className: "flex h-10 touch-manipulation items-center gap-1.5 rounded-full bg-white/10 px-3.5 text-[13px] font-medium text-white transition-colors hover:bg-white/20",
|
|
3261
|
+
children: [
|
|
3262
|
+
/* @__PURE__ */ jsx18(UploadIcon, { className: "h-4 w-4" }),
|
|
3263
|
+
"Upload"
|
|
3264
|
+
]
|
|
3265
|
+
}
|
|
3266
|
+
) : null,
|
|
3267
|
+
/* @__PURE__ */ jsx18(
|
|
3268
|
+
"button",
|
|
3269
|
+
{
|
|
3270
|
+
type: "button",
|
|
3271
|
+
onClick: onClose,
|
|
3272
|
+
"aria-label": "Close library",
|
|
3273
|
+
className: "flex h-10 w-10 items-center justify-center rounded-full bg-white/10 text-white transition-colors hover:bg-white/20",
|
|
3274
|
+
children: /* @__PURE__ */ jsx18(XIcon, { className: "h-5 w-5" })
|
|
3275
|
+
}
|
|
3276
|
+
)
|
|
3277
|
+
] })
|
|
3278
|
+
]
|
|
3279
|
+
}
|
|
3280
|
+
),
|
|
3281
|
+
/* @__PURE__ */ jsxs15(
|
|
3282
|
+
"div",
|
|
3283
|
+
{
|
|
3284
|
+
className: "min-h-0 flex-1 overflow-y-auto overscroll-contain px-1",
|
|
3285
|
+
style: safeBottom,
|
|
3286
|
+
children: [
|
|
3287
|
+
loading ? /* @__PURE__ */ jsx18("div", { className: "flex h-full items-center justify-center", children: /* @__PURE__ */ jsx18(Loader2Icon, { className: "h-6 w-6 animate-spin text-white/60" }) }) : items.length === 0 ? /* @__PURE__ */ jsxs15("div", { className: "flex h-full flex-col items-center justify-center gap-3 text-white/60", children: [
|
|
3288
|
+
/* @__PURE__ */ jsx18(ImageOffIcon, { className: "h-8 w-8" }),
|
|
3289
|
+
/* @__PURE__ */ jsx18("p", { className: "text-sm", children: "No photos or videos in your cloud yet." }),
|
|
3290
|
+
uploadThenClose ? /* @__PURE__ */ jsxs15(
|
|
3291
|
+
"button",
|
|
3292
|
+
{
|
|
3293
|
+
type: "button",
|
|
3294
|
+
onClick: uploadThenClose,
|
|
3295
|
+
className: "flex h-11 touch-manipulation items-center gap-2 rounded-full bg-white/10 px-5 text-sm font-medium text-white transition-colors hover:bg-white/20",
|
|
3296
|
+
children: [
|
|
3297
|
+
/* @__PURE__ */ jsx18(UploadIcon, { className: "h-4 w-4" }),
|
|
3298
|
+
"Upload from this device"
|
|
3299
|
+
]
|
|
3300
|
+
}
|
|
3301
|
+
) : null
|
|
3302
|
+
] }) : /* @__PURE__ */ jsx18("div", { className: "grid grid-cols-3 gap-0.5 sm:grid-cols-5 md:grid-cols-7", children: items.slice(0, visibleCount).map((item) => /* @__PURE__ */ jsxs15(
|
|
3303
|
+
"button",
|
|
3304
|
+
{
|
|
3305
|
+
type: "button",
|
|
3306
|
+
onClick: () => onOpenItem(item.id),
|
|
3307
|
+
"aria-label": `Open ${item.fileName}`,
|
|
3308
|
+
className: "relative aspect-square overflow-hidden bg-white/5 transition-opacity active:opacity-70",
|
|
3309
|
+
children: [
|
|
3310
|
+
item.thumbnail,
|
|
3311
|
+
item.kind === "video" && /* @__PURE__ */ jsx18(
|
|
3312
|
+
PlayIcon,
|
|
3313
|
+
{
|
|
3314
|
+
className: "absolute bottom-1.5 left-1.5 h-4 w-4 text-white drop-shadow",
|
|
3315
|
+
fill: "currentColor"
|
|
3316
|
+
}
|
|
3317
|
+
)
|
|
3318
|
+
]
|
|
3319
|
+
},
|
|
3320
|
+
item.id
|
|
3321
|
+
)) }),
|
|
3322
|
+
!loading && items.length > visibleCount && /* @__PURE__ */ jsx18("div", { className: "flex justify-center py-4", children: /* @__PURE__ */ jsxs15(
|
|
3323
|
+
"button",
|
|
3324
|
+
{
|
|
3325
|
+
type: "button",
|
|
3326
|
+
onClick: () => setVisibleCount((n) => n + PAGE_SIZE),
|
|
3327
|
+
className: "h-11 touch-manipulation rounded-full bg-white/10 px-6 text-sm font-medium text-white",
|
|
3328
|
+
children: [
|
|
3329
|
+
"Show more (",
|
|
3330
|
+
items.length - visibleCount,
|
|
3331
|
+
" left)"
|
|
3332
|
+
]
|
|
3333
|
+
}
|
|
3334
|
+
) }),
|
|
3335
|
+
busy && /* @__PURE__ */ jsx18("div", { className: "pointer-events-none absolute inset-0 flex items-center justify-center bg-black/40", children: /* @__PURE__ */ jsx18(Loader2Icon, { className: "h-6 w-6 animate-spin text-white" }) })
|
|
3336
|
+
]
|
|
3337
|
+
}
|
|
3338
|
+
)
|
|
3339
|
+
] });
|
|
3340
|
+
}
|
|
2791
3341
|
export {
|
|
2792
3342
|
CameraCapture,
|
|
2793
3343
|
CameraCaptureV3,
|
|
@@ -2796,6 +3346,7 @@ export {
|
|
|
2796
3346
|
CaptureFilmstrip,
|
|
2797
3347
|
CaptureRail,
|
|
2798
3348
|
CaptureSheet,
|
|
3349
|
+
CloudLibrarySheet,
|
|
2799
3350
|
CountdownOverlay,
|
|
2800
3351
|
GridOverlay,
|
|
2801
3352
|
HoldShutter,
|
|
@@ -2803,13 +3354,32 @@ export {
|
|
|
2803
3354
|
MediaViewer,
|
|
2804
3355
|
ModeSelector,
|
|
2805
3356
|
OptionsGridPanel,
|
|
3357
|
+
PHOTO_JPEG_QUALITY,
|
|
2806
3358
|
ShutterButton,
|
|
2807
3359
|
ZoomRow,
|
|
3360
|
+
acquireWarmMic,
|
|
3361
|
+
adoptWarmAudioStream,
|
|
3362
|
+
buildWarmMicConstraints,
|
|
3363
|
+
classifyCameraBlockReason,
|
|
3364
|
+
cropBlobToAspect,
|
|
2808
3365
|
getMediaUrl,
|
|
3366
|
+
getWarmMicState,
|
|
3367
|
+
hardStopWarmMic,
|
|
2809
3368
|
invalidateMedia,
|
|
3369
|
+
isMediaDenialError,
|
|
3370
|
+
nextCameraDevice,
|
|
3371
|
+
notifyMicPermissionRevoked,
|
|
2810
3372
|
primeMedia,
|
|
3373
|
+
queryCameraPermission,
|
|
3374
|
+
queryMicPermission,
|
|
3375
|
+
releaseWarmMic,
|
|
3376
|
+
setPreferredMicDeviceId,
|
|
3377
|
+
shouldCombineMicPrompt,
|
|
3378
|
+
subscribeWarmMic,
|
|
3379
|
+
subscribeWarmMicInterruption,
|
|
2811
3380
|
useDefaultCaptureEngine,
|
|
2812
3381
|
useMediaUrl,
|
|
2813
|
-
useTrackControls
|
|
3382
|
+
useTrackControls,
|
|
3383
|
+
warmMicDebug
|
|
2814
3384
|
};
|
|
2815
3385
|
//# sourceMappingURL=react.js.map
|