@ai-matrx/capture 0.4.3 → 0.5.1
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 +65 -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.d.cts
CHANGED
|
@@ -40,6 +40,14 @@ interface CaptureCloudPort {
|
|
|
40
40
|
/** Opens the host's cloud media library (tiled gallery). */
|
|
41
41
|
onOpenLibrary: () => void;
|
|
42
42
|
}
|
|
43
|
+
/** One media item in the cloud library sheet. The host supplies the
|
|
44
|
+
* thumbnail NODE (its own resolver/auth) — the package never fetches. */
|
|
45
|
+
interface CaptureCloudLibraryItem {
|
|
46
|
+
id: string;
|
|
47
|
+
fileName: string;
|
|
48
|
+
kind: "image" | "video";
|
|
49
|
+
thumbnail: React.ReactNode;
|
|
50
|
+
}
|
|
43
51
|
/**
|
|
44
52
|
* The engine port — everything the chrome needs from the host's camera
|
|
45
53
|
* runtime. The host owns lease acquisition/release, capture and recording;
|
|
@@ -709,17 +717,270 @@ interface TrackControls {
|
|
|
709
717
|
declare function useTrackControls(stream: MediaStream | null): TrackControls;
|
|
710
718
|
|
|
711
719
|
interface DefaultEngineOptions {
|
|
712
|
-
/** Receives every captured photo as a JPEG File
|
|
720
|
+
/** Receives every captured photo as a JPEG File (full sensor, aspect crop
|
|
721
|
+
* already applied). */
|
|
713
722
|
onPhoto: (file: File) => void;
|
|
714
723
|
/** Receives the finished recording. */
|
|
715
724
|
onVideo: (file: File, durationMs: number) => void;
|
|
716
725
|
/** Receives files chosen through the Upload lane. */
|
|
717
726
|
onFiles: (files: FileList) => void;
|
|
718
|
-
/** Record microphone audio with video
|
|
727
|
+
/** Record microphone audio with video. Default true. With audio on, the
|
|
728
|
+
* engine folds the mic permission into the CAMERA's browser prompt (one
|
|
729
|
+
* combined prompt) whenever a mic prompt would actually appear. */
|
|
719
730
|
withAudio?: boolean;
|
|
731
|
+
/** The chrome's current mode. In "video" the engine warm-holds the mic so
|
|
732
|
+
* iOS Safari prompts at most once per medium per session. Omitting it
|
|
733
|
+
* skips the warm hold (the record path still acquires the mic). */
|
|
734
|
+
mode?: CaptureCameraMode;
|
|
720
735
|
facingMode?: "environment" | "user";
|
|
721
736
|
photoQuality?: number;
|
|
737
|
+
/** Filename prefix for captures (default "capture"). */
|
|
738
|
+
fileNamePrefix?: string;
|
|
739
|
+
/** Loud failure sink (a toast, a logger). Default: console.error. A tapped
|
|
740
|
+
* shutter that produced nothing must say so — never a silent drop. */
|
|
741
|
+
onError?: (message: string, err: unknown) => void;
|
|
742
|
+
}
|
|
743
|
+
/** The engine plus the extras the default path owns (white shutter flash). */
|
|
744
|
+
interface DefaultCaptureEngine extends CaptureCameraEngine {
|
|
745
|
+
/** White shutter-flash flag (120 ms), for the host's flash overlay. */
|
|
746
|
+
flash: boolean;
|
|
747
|
+
/** Take a photo with a non-default filename prefix (e.g. delineators). */
|
|
748
|
+
capturePhotoWith: (opts: {
|
|
749
|
+
fileNamePrefix: string;
|
|
750
|
+
aspect?: CaptureAspect;
|
|
751
|
+
}) => void;
|
|
752
|
+
}
|
|
753
|
+
declare function useDefaultCaptureEngine(options: DefaultEngineOptions): DefaultCaptureEngine;
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* src/engine/crop.ts — the ONE aspect-crop implementation.
|
|
757
|
+
*
|
|
758
|
+
* Center-crops a captured JPEG to the requested output aspect. The crop is
|
|
759
|
+
* applied to the FULL sensor frame (honest pixels, not a preview grab) —
|
|
760
|
+
* §2 policy 6 of the chrome: the shutter always captures full-sensor and the
|
|
761
|
+
* selected aspect is a REAL crop of that frame. Ported from the matrx-frontend
|
|
762
|
+
* host adapter (C22 retrofit, 2026-08-30) so every host shares one geometry.
|
|
763
|
+
*/
|
|
764
|
+
|
|
765
|
+
declare const PHOTO_JPEG_QUALITY = 0.92;
|
|
766
|
+
declare function cropBlobToAspect(blob: Blob, aspect: CaptureAspect, quality?: number): Promise<Blob>;
|
|
767
|
+
/**
|
|
768
|
+
* Cycle to the next camera in a device list — the flip algorithm the host
|
|
769
|
+
* adapter and the default engine share. Returns null when there is nothing
|
|
770
|
+
* to flip to (fewer than two cameras).
|
|
771
|
+
*/
|
|
772
|
+
declare function nextCameraDevice<T extends {
|
|
773
|
+
deviceId: string;
|
|
774
|
+
}>(cameras: readonly T[], currentDeviceId: string | null | undefined): T | null;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* src/engine/permissions.ts — permission pre-checks + error classification
|
|
778
|
+
* for the default engine (and for hosts, so they stop re-implementing the
|
|
779
|
+
* NotAllowedError branch — the C22 rule: quirk branches live HERE).
|
|
780
|
+
*/
|
|
781
|
+
type MediaPermissionState = "granted" | "denied" | "prompt" | "unknown";
|
|
782
|
+
/**
|
|
783
|
+
* Pre-check the camera permission WITHOUT prompting. A known-"denied" state
|
|
784
|
+
* lets a surface render its how-to-re-enable explainer instead of hammering
|
|
785
|
+
* getUserMedia with calls that instantly reject (the proven matrx-frontend
|
|
786
|
+
* pre-check contract). "unknown" (Safari) means: just try — the gUM outcome
|
|
787
|
+
* is the truth there.
|
|
788
|
+
*/
|
|
789
|
+
declare function queryCameraPermission(): Promise<MediaPermissionState>;
|
|
790
|
+
/** Same pre-check for the microphone — drives the combined-prompt decision. */
|
|
791
|
+
declare function queryMicPermission(): Promise<MediaPermissionState>;
|
|
792
|
+
/**
|
|
793
|
+
* Pure decision for the combined camera+mic prompt: only worth it when a mic
|
|
794
|
+
* prompt would actually appear ("prompt"/"unknown"). "granted" needs no
|
|
795
|
+
* prompt; "denied" would reject the WHOLE combined call and take the camera
|
|
796
|
+
* with it.
|
|
797
|
+
*/
|
|
798
|
+
declare function shouldCombineMicPrompt(micPermissionState: MediaPermissionState): boolean;
|
|
799
|
+
/** True when a getUserMedia rejection means the USER/OS denied permission
|
|
800
|
+
* (a missing device is not a denial). */
|
|
801
|
+
declare function isMediaDenialError(err: unknown): boolean;
|
|
802
|
+
/**
|
|
803
|
+
* Classify a camera-acquisition failure into the engine's `blocked` reason.
|
|
804
|
+
* The ONE place the NotAllowedError/SecurityError branch lives — hosts adapt
|
|
805
|
+
* their runtime errors through this instead of re-implementing it.
|
|
806
|
+
*/
|
|
807
|
+
declare function classifyCameraBlockReason(err: unknown): "permission-denied" | "not-supported";
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* src/engine/warm-mic.ts — the package's warm microphone manager.
|
|
811
|
+
*
|
|
812
|
+
* Ported VERBATIM (2026-08-30, the C22/C23 retrofit) from matrx-frontend's
|
|
813
|
+
* proven `features/audio/micStream.ts` — the module that encodes the real
|
|
814
|
+
* iOS Safari device pain the default engine previously did not have:
|
|
815
|
+
*
|
|
816
|
+
* WHY THIS EXISTS
|
|
817
|
+
* ---------------
|
|
818
|
+
* Every recording surface used to call `navigator.mediaDevices.getUserMedia`
|
|
819
|
+
* itself and fully `track.stop()` the stream when it finished. On mobile
|
|
820
|
+
* (notably iOS Safari) that produces a permission/"in use" prompt on *every*
|
|
821
|
+
* single recording, because the previous grant is torn down before the next
|
|
822
|
+
* acquisition. Tapping record three times = three prompts.
|
|
823
|
+
*
|
|
824
|
+
* This manager keeps ONE stream warm and hands it to every caller. After the
|
|
825
|
+
* last holder releases it, the stream is kept alive for a SHORT keepalive
|
|
826
|
+
* window; if another recording starts within that window, the same live
|
|
827
|
+
* stream (and the same OS grant) is reused — no second prompt. Only after the
|
|
828
|
+
* window elapses with no holders is the stream actually stopped (mic light
|
|
829
|
+
* off), so we don't hold the mic open forever.
|
|
830
|
+
*
|
|
831
|
+
* KEEPALIVE IS DELIBERATELY SHORT (a few seconds). Minutes keep the browser's
|
|
832
|
+
* recording indicator lit long after the user stopped — alarming and abnormal
|
|
833
|
+
* ("why is my mic still on?"). A few seconds still coalesces a quick
|
|
834
|
+
* stop→re-record (the iOS re-prompt case) while clearing the mic light almost
|
|
835
|
+
* immediately once the user is actually done. On desktop, re-acquiring after
|
|
836
|
+
* release does NOT re-prompt (the origin's grant persists), so a short window
|
|
837
|
+
* costs nothing there.
|
|
838
|
+
*
|
|
839
|
+
* THE FOUR iOS-SAFARI RECOVERY BRANCHES (`MicInterruptionReason`), each of
|
|
840
|
+
* which used to be a silent audio drop:
|
|
841
|
+
* 1. `ended` — the OS killed the track (lock screen, incoming
|
|
842
|
+
* call, app switch, device unplug). The warm grant
|
|
843
|
+
* is gone; the dead stream is dropped so the next
|
|
844
|
+
* acquire re-acquires (and WILL re-prompt).
|
|
845
|
+
* 2. `muted` — transient interruption (iOS mutes during a
|
|
846
|
+
* call). The grant survives; no re-prompt.
|
|
847
|
+
* 3. `unmuted` — the transient interruption ended.
|
|
848
|
+
* 4. `permission-revoked` — the user/OS pulled the permission; the warm
|
|
849
|
+
* stream is hard-stopped NOW.
|
|
850
|
+
* All four are reported loudly on the interruption channel so a recording
|
|
851
|
+
* surface can react instead of just dropping audio.
|
|
852
|
+
*
|
|
853
|
+
* Reference-counted: concurrent holders (an analyser tap + a recorder) are
|
|
854
|
+
* fine; the stream is only eligible for release when the count hits zero.
|
|
855
|
+
* Callers MUST NOT call `track.stop()` on the returned stream — call
|
|
856
|
+
* `releaseWarmMic()` instead.
|
|
857
|
+
*
|
|
858
|
+
* State lives on `globalThis` under a `Symbol.for` slot (the media-cache
|
|
859
|
+
* pattern) — a dual ESM/CJS module graph must still see ONE mic manager.
|
|
860
|
+
*/
|
|
861
|
+
type Listener = (state: MicStreamState) => void;
|
|
862
|
+
type MicInterruptionReason = "ended" | "muted" | "unmuted" | "permission-revoked";
|
|
863
|
+
type InterruptionListener = (reason: MicInterruptionReason) => void;
|
|
864
|
+
type MicStreamState = "idle" | "acquiring" | "active" | "keepalive" | "error";
|
|
865
|
+
/**
|
|
866
|
+
* Subscribe to mic interruptions (track end / mute / permission loss).
|
|
867
|
+
* Returns an unsubscribe fn. Surfaces are expected to make these LOUD — an
|
|
868
|
+
* interruption during a recording is a real event the user must see, not a
|
|
869
|
+
* silent drop.
|
|
870
|
+
*/
|
|
871
|
+
declare function subscribeWarmMicInterruption(listener: InterruptionListener): () => void;
|
|
872
|
+
/**
|
|
873
|
+
* Mic-permission revocation → stop the warm stream NOW and emit a loud
|
|
874
|
+
* interruption. In matrx-frontend the ONE canonical permission watcher
|
|
875
|
+
* (audioDevices) calls this; the package, having no host device layer, wires
|
|
876
|
+
* its own minimal Permissions API watcher lazily (`watchMicPermission`) —
|
|
877
|
+
* best-effort only, since Safari's Permissions API may not answer for
|
|
878
|
+
* "microphone" (there, `ended` is the branch that fires instead).
|
|
879
|
+
*/
|
|
880
|
+
declare function notifyMicPermissionRevoked(): void;
|
|
881
|
+
/**
|
|
882
|
+
* Set the preferred microphone. Applied as an `{ideal}` deviceId constraint
|
|
883
|
+
* on the NEXT acquire — it does NOT switch a live stream (call
|
|
884
|
+
* `hardStopWarmMic()` then re-acquire to switch mid-session). Pass null to
|
|
885
|
+
* fall back to the system default.
|
|
886
|
+
*/
|
|
887
|
+
declare function setPreferredMicDeviceId(id: string | null): void;
|
|
888
|
+
/**
|
|
889
|
+
* The default audio constraints this manager acquires with — exported so the
|
|
890
|
+
* default engine's COMBINED `getUserMedia({video, audio})` prompt (one
|
|
891
|
+
* browser prompt for both permissions instead of camera-then-mic) requests
|
|
892
|
+
* the mic with exactly the same processing + preferred-device shape the
|
|
893
|
+
* singleton itself would use.
|
|
894
|
+
*/
|
|
895
|
+
declare function buildWarmMicConstraints(): MediaTrackConstraints;
|
|
896
|
+
/**
|
|
897
|
+
* Adopt an audio stream acquired ELSEWHERE (the default engine's combined
|
|
898
|
+
* camera+mic prompt) as this manager's warm stream, so the grant that
|
|
899
|
+
* combined prompt just earned is banked exactly like one of our own: the
|
|
900
|
+
* next `acquireWarmMic()` inside the keepalive window reuses it with no
|
|
901
|
+
* second prompt. If a warm stream already exists (or one is being acquired),
|
|
902
|
+
* the incoming tracks are stopped immediately — never two live mic streams.
|
|
903
|
+
* With zero holders the adopted stream is parked in the keepalive window, so
|
|
904
|
+
* the mic light clears on the normal schedule.
|
|
905
|
+
*/
|
|
906
|
+
declare function adoptWarmAudioStream(stream: MediaStream): void;
|
|
907
|
+
/**
|
|
908
|
+
* Acquire the shared warm mic stream, incrementing the holder count. The
|
|
909
|
+
* returned stream is shared — DO NOT stop its tracks. Call `releaseWarmMic()`
|
|
910
|
+
* when done. If a warm stream exists (active or in its keepalive window) it
|
|
911
|
+
* is reused with no new permission prompt.
|
|
912
|
+
*/
|
|
913
|
+
declare function acquireWarmMic(constraints?: MediaTrackConstraints): Promise<MediaStream>;
|
|
914
|
+
/**
|
|
915
|
+
* Release a previously-acquired hold. When the last holder releases, the
|
|
916
|
+
* stream is kept warm for the keepalive window, then actually stopped.
|
|
917
|
+
*/
|
|
918
|
+
declare function releaseWarmMic(): void;
|
|
919
|
+
/**
|
|
920
|
+
* Immediately stop the warm stream regardless of keepalive. Use only when
|
|
921
|
+
* the mic must be released NOW; normal teardown uses `releaseWarmMic()`.
|
|
922
|
+
*/
|
|
923
|
+
declare function hardStopWarmMic(): void;
|
|
924
|
+
declare function getWarmMicState(): MicStreamState;
|
|
925
|
+
declare function subscribeWarmMic(listener: Listener): () => void;
|
|
926
|
+
/** Diagnostics snapshot for debug panels. */
|
|
927
|
+
declare function warmMicDebug(): {
|
|
928
|
+
state: MicStreamState;
|
|
929
|
+
refCount: number;
|
|
930
|
+
live: boolean;
|
|
931
|
+
keepAliveMs: number;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* CloudLibrarySheet — the recents-thumb destination: where the iPhone opens
|
|
936
|
+
* the camera roll, we open the user's CLOUD media in a full-screen tiled
|
|
937
|
+
* gallery — dark chrome to match the camera, square tiles, newest first.
|
|
938
|
+
*
|
|
939
|
+
* Absorbed from the matrx-frontend host (C22/C23 retrofit, 2026-08-30):
|
|
940
|
+
* the 206-line host sheet was pure chrome over three injectable facts — the
|
|
941
|
+
* item list, the thumbnail renderer, and where a tile navigates. The chrome
|
|
942
|
+
* now ships here; the host injects only data and identity:
|
|
943
|
+
*
|
|
944
|
+
* <CloudLibrarySheet
|
|
945
|
+
* open={open} onClose={close}
|
|
946
|
+
* loading={treeLoading}
|
|
947
|
+
* items={files.map(f => ({ id, fileName, kind, thumbnail: <MyThumb/> }))}
|
|
948
|
+
* onOpenItem={(id) => router.push(`/files/f/${id}`)}
|
|
949
|
+
* onUpload={openPicker} // v3 hosts — upload lives INSIDE the library
|
|
950
|
+
* />
|
|
951
|
+
*
|
|
952
|
+
* Every tile OPENS the real file (no dead ends), and an empty gallery whose
|
|
953
|
+
* only affordance is "close" is a dead end too — with `onUpload` supplied,
|
|
954
|
+
* the empty state offers the one thing a user can do from here.
|
|
955
|
+
*/
|
|
956
|
+
|
|
957
|
+
interface CloudLibrarySheetProps {
|
|
958
|
+
open: boolean;
|
|
959
|
+
onClose: () => void;
|
|
960
|
+
/** Newest-first media items. The host supplies the thumbnail node — the
|
|
961
|
+
* sheet never fetches (law 1: no network in this package). */
|
|
962
|
+
items: readonly CaptureCloudLibraryItem[];
|
|
963
|
+
/** True while the host's library data is still loading. */
|
|
964
|
+
loading: boolean;
|
|
965
|
+
/** Opening a tile (host navigation) — a busy overlay renders while true. */
|
|
966
|
+
busy?: boolean;
|
|
967
|
+
/** A tile was tapped. The host opens the REAL file route (no dead ends). */
|
|
968
|
+
onOpenItem: (id: string) => void;
|
|
969
|
+
/**
|
|
970
|
+
* Pick files from the device. Supplied by v3 hosts, where this drawer is
|
|
971
|
+
* the ONLY door to existing media (Arman, 2026-08-30: "we don't need both
|
|
972
|
+
* upload and cloud because we can modify the drawer that has the cloud
|
|
973
|
+
* images to just show an option for uploading").
|
|
974
|
+
*
|
|
975
|
+
* Two controls that both mean "media I already have" is a choice the user
|
|
976
|
+
* should never have been asked to make: whether a file happens to be in
|
|
977
|
+
* the cloud yet is our bookkeeping, not their mental model. Omitted by v2
|
|
978
|
+
* hosts, which still carry a separate UPLOAD lane in the mode row.
|
|
979
|
+
*/
|
|
980
|
+
onUpload?: (() => void) | undefined;
|
|
981
|
+
/** Sheet title. Default "Your media". */
|
|
982
|
+
title?: string;
|
|
722
983
|
}
|
|
723
|
-
declare function
|
|
984
|
+
declare function CloudLibrarySheet({ open, onClose, items, loading, busy, onOpenItem, onUpload, title, }: CloudLibrarySheetProps): React__default.JSX.Element | null;
|
|
724
985
|
|
|
725
|
-
export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CountdownOverlay, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, MediaViewer, type MediaViewerProps, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, getMediaUrl, invalidateMedia, primeMedia, useDefaultCaptureEngine, useMediaUrl, useTrackControls };
|
|
986
|
+
export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudLibraryItem, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CloudLibrarySheet, type CloudLibrarySheetProps, CountdownOverlay, type DefaultCaptureEngine, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, type MediaPermissionState, MediaViewer, type MediaViewerProps, type MicInterruptionReason, type MicStreamState, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, PHOTO_JPEG_QUALITY, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, acquireWarmMic, adoptWarmAudioStream, buildWarmMicConstraints, classifyCameraBlockReason, cropBlobToAspect, getMediaUrl, getWarmMicState, hardStopWarmMic, invalidateMedia, isMediaDenialError, nextCameraDevice, notifyMicPermissionRevoked, primeMedia, queryCameraPermission, queryMicPermission, releaseWarmMic, setPreferredMicDeviceId, shouldCombineMicPrompt, subscribeWarmMic, subscribeWarmMicInterruption, useDefaultCaptureEngine, useMediaUrl, useTrackControls, warmMicDebug };
|
package/dist/react.d.ts
CHANGED
|
@@ -40,6 +40,14 @@ interface CaptureCloudPort {
|
|
|
40
40
|
/** Opens the host's cloud media library (tiled gallery). */
|
|
41
41
|
onOpenLibrary: () => void;
|
|
42
42
|
}
|
|
43
|
+
/** One media item in the cloud library sheet. The host supplies the
|
|
44
|
+
* thumbnail NODE (its own resolver/auth) — the package never fetches. */
|
|
45
|
+
interface CaptureCloudLibraryItem {
|
|
46
|
+
id: string;
|
|
47
|
+
fileName: string;
|
|
48
|
+
kind: "image" | "video";
|
|
49
|
+
thumbnail: React.ReactNode;
|
|
50
|
+
}
|
|
43
51
|
/**
|
|
44
52
|
* The engine port — everything the chrome needs from the host's camera
|
|
45
53
|
* runtime. The host owns lease acquisition/release, capture and recording;
|
|
@@ -709,17 +717,270 @@ interface TrackControls {
|
|
|
709
717
|
declare function useTrackControls(stream: MediaStream | null): TrackControls;
|
|
710
718
|
|
|
711
719
|
interface DefaultEngineOptions {
|
|
712
|
-
/** Receives every captured photo as a JPEG File
|
|
720
|
+
/** Receives every captured photo as a JPEG File (full sensor, aspect crop
|
|
721
|
+
* already applied). */
|
|
713
722
|
onPhoto: (file: File) => void;
|
|
714
723
|
/** Receives the finished recording. */
|
|
715
724
|
onVideo: (file: File, durationMs: number) => void;
|
|
716
725
|
/** Receives files chosen through the Upload lane. */
|
|
717
726
|
onFiles: (files: FileList) => void;
|
|
718
|
-
/** Record microphone audio with video
|
|
727
|
+
/** Record microphone audio with video. Default true. With audio on, the
|
|
728
|
+
* engine folds the mic permission into the CAMERA's browser prompt (one
|
|
729
|
+
* combined prompt) whenever a mic prompt would actually appear. */
|
|
719
730
|
withAudio?: boolean;
|
|
731
|
+
/** The chrome's current mode. In "video" the engine warm-holds the mic so
|
|
732
|
+
* iOS Safari prompts at most once per medium per session. Omitting it
|
|
733
|
+
* skips the warm hold (the record path still acquires the mic). */
|
|
734
|
+
mode?: CaptureCameraMode;
|
|
720
735
|
facingMode?: "environment" | "user";
|
|
721
736
|
photoQuality?: number;
|
|
737
|
+
/** Filename prefix for captures (default "capture"). */
|
|
738
|
+
fileNamePrefix?: string;
|
|
739
|
+
/** Loud failure sink (a toast, a logger). Default: console.error. A tapped
|
|
740
|
+
* shutter that produced nothing must say so — never a silent drop. */
|
|
741
|
+
onError?: (message: string, err: unknown) => void;
|
|
742
|
+
}
|
|
743
|
+
/** The engine plus the extras the default path owns (white shutter flash). */
|
|
744
|
+
interface DefaultCaptureEngine extends CaptureCameraEngine {
|
|
745
|
+
/** White shutter-flash flag (120 ms), for the host's flash overlay. */
|
|
746
|
+
flash: boolean;
|
|
747
|
+
/** Take a photo with a non-default filename prefix (e.g. delineators). */
|
|
748
|
+
capturePhotoWith: (opts: {
|
|
749
|
+
fileNamePrefix: string;
|
|
750
|
+
aspect?: CaptureAspect;
|
|
751
|
+
}) => void;
|
|
752
|
+
}
|
|
753
|
+
declare function useDefaultCaptureEngine(options: DefaultEngineOptions): DefaultCaptureEngine;
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* src/engine/crop.ts — the ONE aspect-crop implementation.
|
|
757
|
+
*
|
|
758
|
+
* Center-crops a captured JPEG to the requested output aspect. The crop is
|
|
759
|
+
* applied to the FULL sensor frame (honest pixels, not a preview grab) —
|
|
760
|
+
* §2 policy 6 of the chrome: the shutter always captures full-sensor and the
|
|
761
|
+
* selected aspect is a REAL crop of that frame. Ported from the matrx-frontend
|
|
762
|
+
* host adapter (C22 retrofit, 2026-08-30) so every host shares one geometry.
|
|
763
|
+
*/
|
|
764
|
+
|
|
765
|
+
declare const PHOTO_JPEG_QUALITY = 0.92;
|
|
766
|
+
declare function cropBlobToAspect(blob: Blob, aspect: CaptureAspect, quality?: number): Promise<Blob>;
|
|
767
|
+
/**
|
|
768
|
+
* Cycle to the next camera in a device list — the flip algorithm the host
|
|
769
|
+
* adapter and the default engine share. Returns null when there is nothing
|
|
770
|
+
* to flip to (fewer than two cameras).
|
|
771
|
+
*/
|
|
772
|
+
declare function nextCameraDevice<T extends {
|
|
773
|
+
deviceId: string;
|
|
774
|
+
}>(cameras: readonly T[], currentDeviceId: string | null | undefined): T | null;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* src/engine/permissions.ts — permission pre-checks + error classification
|
|
778
|
+
* for the default engine (and for hosts, so they stop re-implementing the
|
|
779
|
+
* NotAllowedError branch — the C22 rule: quirk branches live HERE).
|
|
780
|
+
*/
|
|
781
|
+
type MediaPermissionState = "granted" | "denied" | "prompt" | "unknown";
|
|
782
|
+
/**
|
|
783
|
+
* Pre-check the camera permission WITHOUT prompting. A known-"denied" state
|
|
784
|
+
* lets a surface render its how-to-re-enable explainer instead of hammering
|
|
785
|
+
* getUserMedia with calls that instantly reject (the proven matrx-frontend
|
|
786
|
+
* pre-check contract). "unknown" (Safari) means: just try — the gUM outcome
|
|
787
|
+
* is the truth there.
|
|
788
|
+
*/
|
|
789
|
+
declare function queryCameraPermission(): Promise<MediaPermissionState>;
|
|
790
|
+
/** Same pre-check for the microphone — drives the combined-prompt decision. */
|
|
791
|
+
declare function queryMicPermission(): Promise<MediaPermissionState>;
|
|
792
|
+
/**
|
|
793
|
+
* Pure decision for the combined camera+mic prompt: only worth it when a mic
|
|
794
|
+
* prompt would actually appear ("prompt"/"unknown"). "granted" needs no
|
|
795
|
+
* prompt; "denied" would reject the WHOLE combined call and take the camera
|
|
796
|
+
* with it.
|
|
797
|
+
*/
|
|
798
|
+
declare function shouldCombineMicPrompt(micPermissionState: MediaPermissionState): boolean;
|
|
799
|
+
/** True when a getUserMedia rejection means the USER/OS denied permission
|
|
800
|
+
* (a missing device is not a denial). */
|
|
801
|
+
declare function isMediaDenialError(err: unknown): boolean;
|
|
802
|
+
/**
|
|
803
|
+
* Classify a camera-acquisition failure into the engine's `blocked` reason.
|
|
804
|
+
* The ONE place the NotAllowedError/SecurityError branch lives — hosts adapt
|
|
805
|
+
* their runtime errors through this instead of re-implementing it.
|
|
806
|
+
*/
|
|
807
|
+
declare function classifyCameraBlockReason(err: unknown): "permission-denied" | "not-supported";
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* src/engine/warm-mic.ts — the package's warm microphone manager.
|
|
811
|
+
*
|
|
812
|
+
* Ported VERBATIM (2026-08-30, the C22/C23 retrofit) from matrx-frontend's
|
|
813
|
+
* proven `features/audio/micStream.ts` — the module that encodes the real
|
|
814
|
+
* iOS Safari device pain the default engine previously did not have:
|
|
815
|
+
*
|
|
816
|
+
* WHY THIS EXISTS
|
|
817
|
+
* ---------------
|
|
818
|
+
* Every recording surface used to call `navigator.mediaDevices.getUserMedia`
|
|
819
|
+
* itself and fully `track.stop()` the stream when it finished. On mobile
|
|
820
|
+
* (notably iOS Safari) that produces a permission/"in use" prompt on *every*
|
|
821
|
+
* single recording, because the previous grant is torn down before the next
|
|
822
|
+
* acquisition. Tapping record three times = three prompts.
|
|
823
|
+
*
|
|
824
|
+
* This manager keeps ONE stream warm and hands it to every caller. After the
|
|
825
|
+
* last holder releases it, the stream is kept alive for a SHORT keepalive
|
|
826
|
+
* window; if another recording starts within that window, the same live
|
|
827
|
+
* stream (and the same OS grant) is reused — no second prompt. Only after the
|
|
828
|
+
* window elapses with no holders is the stream actually stopped (mic light
|
|
829
|
+
* off), so we don't hold the mic open forever.
|
|
830
|
+
*
|
|
831
|
+
* KEEPALIVE IS DELIBERATELY SHORT (a few seconds). Minutes keep the browser's
|
|
832
|
+
* recording indicator lit long after the user stopped — alarming and abnormal
|
|
833
|
+
* ("why is my mic still on?"). A few seconds still coalesces a quick
|
|
834
|
+
* stop→re-record (the iOS re-prompt case) while clearing the mic light almost
|
|
835
|
+
* immediately once the user is actually done. On desktop, re-acquiring after
|
|
836
|
+
* release does NOT re-prompt (the origin's grant persists), so a short window
|
|
837
|
+
* costs nothing there.
|
|
838
|
+
*
|
|
839
|
+
* THE FOUR iOS-SAFARI RECOVERY BRANCHES (`MicInterruptionReason`), each of
|
|
840
|
+
* which used to be a silent audio drop:
|
|
841
|
+
* 1. `ended` — the OS killed the track (lock screen, incoming
|
|
842
|
+
* call, app switch, device unplug). The warm grant
|
|
843
|
+
* is gone; the dead stream is dropped so the next
|
|
844
|
+
* acquire re-acquires (and WILL re-prompt).
|
|
845
|
+
* 2. `muted` — transient interruption (iOS mutes during a
|
|
846
|
+
* call). The grant survives; no re-prompt.
|
|
847
|
+
* 3. `unmuted` — the transient interruption ended.
|
|
848
|
+
* 4. `permission-revoked` — the user/OS pulled the permission; the warm
|
|
849
|
+
* stream is hard-stopped NOW.
|
|
850
|
+
* All four are reported loudly on the interruption channel so a recording
|
|
851
|
+
* surface can react instead of just dropping audio.
|
|
852
|
+
*
|
|
853
|
+
* Reference-counted: concurrent holders (an analyser tap + a recorder) are
|
|
854
|
+
* fine; the stream is only eligible for release when the count hits zero.
|
|
855
|
+
* Callers MUST NOT call `track.stop()` on the returned stream — call
|
|
856
|
+
* `releaseWarmMic()` instead.
|
|
857
|
+
*
|
|
858
|
+
* State lives on `globalThis` under a `Symbol.for` slot (the media-cache
|
|
859
|
+
* pattern) — a dual ESM/CJS module graph must still see ONE mic manager.
|
|
860
|
+
*/
|
|
861
|
+
type Listener = (state: MicStreamState) => void;
|
|
862
|
+
type MicInterruptionReason = "ended" | "muted" | "unmuted" | "permission-revoked";
|
|
863
|
+
type InterruptionListener = (reason: MicInterruptionReason) => void;
|
|
864
|
+
type MicStreamState = "idle" | "acquiring" | "active" | "keepalive" | "error";
|
|
865
|
+
/**
|
|
866
|
+
* Subscribe to mic interruptions (track end / mute / permission loss).
|
|
867
|
+
* Returns an unsubscribe fn. Surfaces are expected to make these LOUD — an
|
|
868
|
+
* interruption during a recording is a real event the user must see, not a
|
|
869
|
+
* silent drop.
|
|
870
|
+
*/
|
|
871
|
+
declare function subscribeWarmMicInterruption(listener: InterruptionListener): () => void;
|
|
872
|
+
/**
|
|
873
|
+
* Mic-permission revocation → stop the warm stream NOW and emit a loud
|
|
874
|
+
* interruption. In matrx-frontend the ONE canonical permission watcher
|
|
875
|
+
* (audioDevices) calls this; the package, having no host device layer, wires
|
|
876
|
+
* its own minimal Permissions API watcher lazily (`watchMicPermission`) —
|
|
877
|
+
* best-effort only, since Safari's Permissions API may not answer for
|
|
878
|
+
* "microphone" (there, `ended` is the branch that fires instead).
|
|
879
|
+
*/
|
|
880
|
+
declare function notifyMicPermissionRevoked(): void;
|
|
881
|
+
/**
|
|
882
|
+
* Set the preferred microphone. Applied as an `{ideal}` deviceId constraint
|
|
883
|
+
* on the NEXT acquire — it does NOT switch a live stream (call
|
|
884
|
+
* `hardStopWarmMic()` then re-acquire to switch mid-session). Pass null to
|
|
885
|
+
* fall back to the system default.
|
|
886
|
+
*/
|
|
887
|
+
declare function setPreferredMicDeviceId(id: string | null): void;
|
|
888
|
+
/**
|
|
889
|
+
* The default audio constraints this manager acquires with — exported so the
|
|
890
|
+
* default engine's COMBINED `getUserMedia({video, audio})` prompt (one
|
|
891
|
+
* browser prompt for both permissions instead of camera-then-mic) requests
|
|
892
|
+
* the mic with exactly the same processing + preferred-device shape the
|
|
893
|
+
* singleton itself would use.
|
|
894
|
+
*/
|
|
895
|
+
declare function buildWarmMicConstraints(): MediaTrackConstraints;
|
|
896
|
+
/**
|
|
897
|
+
* Adopt an audio stream acquired ELSEWHERE (the default engine's combined
|
|
898
|
+
* camera+mic prompt) as this manager's warm stream, so the grant that
|
|
899
|
+
* combined prompt just earned is banked exactly like one of our own: the
|
|
900
|
+
* next `acquireWarmMic()` inside the keepalive window reuses it with no
|
|
901
|
+
* second prompt. If a warm stream already exists (or one is being acquired),
|
|
902
|
+
* the incoming tracks are stopped immediately — never two live mic streams.
|
|
903
|
+
* With zero holders the adopted stream is parked in the keepalive window, so
|
|
904
|
+
* the mic light clears on the normal schedule.
|
|
905
|
+
*/
|
|
906
|
+
declare function adoptWarmAudioStream(stream: MediaStream): void;
|
|
907
|
+
/**
|
|
908
|
+
* Acquire the shared warm mic stream, incrementing the holder count. The
|
|
909
|
+
* returned stream is shared — DO NOT stop its tracks. Call `releaseWarmMic()`
|
|
910
|
+
* when done. If a warm stream exists (active or in its keepalive window) it
|
|
911
|
+
* is reused with no new permission prompt.
|
|
912
|
+
*/
|
|
913
|
+
declare function acquireWarmMic(constraints?: MediaTrackConstraints): Promise<MediaStream>;
|
|
914
|
+
/**
|
|
915
|
+
* Release a previously-acquired hold. When the last holder releases, the
|
|
916
|
+
* stream is kept warm for the keepalive window, then actually stopped.
|
|
917
|
+
*/
|
|
918
|
+
declare function releaseWarmMic(): void;
|
|
919
|
+
/**
|
|
920
|
+
* Immediately stop the warm stream regardless of keepalive. Use only when
|
|
921
|
+
* the mic must be released NOW; normal teardown uses `releaseWarmMic()`.
|
|
922
|
+
*/
|
|
923
|
+
declare function hardStopWarmMic(): void;
|
|
924
|
+
declare function getWarmMicState(): MicStreamState;
|
|
925
|
+
declare function subscribeWarmMic(listener: Listener): () => void;
|
|
926
|
+
/** Diagnostics snapshot for debug panels. */
|
|
927
|
+
declare function warmMicDebug(): {
|
|
928
|
+
state: MicStreamState;
|
|
929
|
+
refCount: number;
|
|
930
|
+
live: boolean;
|
|
931
|
+
keepAliveMs: number;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
/**
|
|
935
|
+
* CloudLibrarySheet — the recents-thumb destination: where the iPhone opens
|
|
936
|
+
* the camera roll, we open the user's CLOUD media in a full-screen tiled
|
|
937
|
+
* gallery — dark chrome to match the camera, square tiles, newest first.
|
|
938
|
+
*
|
|
939
|
+
* Absorbed from the matrx-frontend host (C22/C23 retrofit, 2026-08-30):
|
|
940
|
+
* the 206-line host sheet was pure chrome over three injectable facts — the
|
|
941
|
+
* item list, the thumbnail renderer, and where a tile navigates. The chrome
|
|
942
|
+
* now ships here; the host injects only data and identity:
|
|
943
|
+
*
|
|
944
|
+
* <CloudLibrarySheet
|
|
945
|
+
* open={open} onClose={close}
|
|
946
|
+
* loading={treeLoading}
|
|
947
|
+
* items={files.map(f => ({ id, fileName, kind, thumbnail: <MyThumb/> }))}
|
|
948
|
+
* onOpenItem={(id) => router.push(`/files/f/${id}`)}
|
|
949
|
+
* onUpload={openPicker} // v3 hosts — upload lives INSIDE the library
|
|
950
|
+
* />
|
|
951
|
+
*
|
|
952
|
+
* Every tile OPENS the real file (no dead ends), and an empty gallery whose
|
|
953
|
+
* only affordance is "close" is a dead end too — with `onUpload` supplied,
|
|
954
|
+
* the empty state offers the one thing a user can do from here.
|
|
955
|
+
*/
|
|
956
|
+
|
|
957
|
+
interface CloudLibrarySheetProps {
|
|
958
|
+
open: boolean;
|
|
959
|
+
onClose: () => void;
|
|
960
|
+
/** Newest-first media items. The host supplies the thumbnail node — the
|
|
961
|
+
* sheet never fetches (law 1: no network in this package). */
|
|
962
|
+
items: readonly CaptureCloudLibraryItem[];
|
|
963
|
+
/** True while the host's library data is still loading. */
|
|
964
|
+
loading: boolean;
|
|
965
|
+
/** Opening a tile (host navigation) — a busy overlay renders while true. */
|
|
966
|
+
busy?: boolean;
|
|
967
|
+
/** A tile was tapped. The host opens the REAL file route (no dead ends). */
|
|
968
|
+
onOpenItem: (id: string) => void;
|
|
969
|
+
/**
|
|
970
|
+
* Pick files from the device. Supplied by v3 hosts, where this drawer is
|
|
971
|
+
* the ONLY door to existing media (Arman, 2026-08-30: "we don't need both
|
|
972
|
+
* upload and cloud because we can modify the drawer that has the cloud
|
|
973
|
+
* images to just show an option for uploading").
|
|
974
|
+
*
|
|
975
|
+
* Two controls that both mean "media I already have" is a choice the user
|
|
976
|
+
* should never have been asked to make: whether a file happens to be in
|
|
977
|
+
* the cloud yet is our bookkeeping, not their mental model. Omitted by v2
|
|
978
|
+
* hosts, which still carry a separate UPLOAD lane in the mode row.
|
|
979
|
+
*/
|
|
980
|
+
onUpload?: (() => void) | undefined;
|
|
981
|
+
/** Sheet title. Default "Your media". */
|
|
982
|
+
title?: string;
|
|
722
983
|
}
|
|
723
|
-
declare function
|
|
984
|
+
declare function CloudLibrarySheet({ open, onClose, items, loading, busy, onOpenItem, onUpload, title, }: CloudLibrarySheetProps): React__default.JSX.Element | null;
|
|
724
985
|
|
|
725
|
-
export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CountdownOverlay, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, MediaViewer, type MediaViewerProps, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, getMediaUrl, invalidateMedia, primeMedia, useDefaultCaptureEngine, useMediaUrl, useTrackControls };
|
|
986
|
+
export { CameraCapture, type CameraCaptureProps, CameraCaptureV3, type CameraCaptureV3Props, CameraFeed, type CaptureAspect, type CaptureCameraEngine, type CaptureCameraMode, type CaptureCameraSlots, type CaptureCameraV3Slots, type CaptureCloudLibraryItem, type CaptureCloudPort, CaptureExpandingField, type CaptureExpandingFieldProps, CaptureFilmstrip, type CaptureFilmstripProps, type CaptureMediaItem, type CaptureMediaSession, type CaptureOptionTile, CaptureRail, type CaptureRailAction, type CaptureRailProps, CaptureSheet, type CaptureSheetAction, type CaptureSheetProps, type CaptureTimerSetting, CloudLibrarySheet, type CloudLibrarySheetProps, CountdownOverlay, type DefaultCaptureEngine, type DefaultEngineOptions, GridOverlay, HoldShutter, type HoldShutterProps, ImageEditSheet, type ImageEditSheetProps, type MediaPermissionState, MediaViewer, type MediaViewerProps, type MicInterruptionReason, type MicStreamState, ModeSelector, type ModeSelectorProps, OptionsGridPanel, type OptionsGridPanelProps, PHOTO_JPEG_QUALITY, type ResolvedMedia, ShutterButton, type ShutterButtonProps, type TrackControls, ZoomRow, type ZoomRowProps, acquireWarmMic, adoptWarmAudioStream, buildWarmMicConstraints, classifyCameraBlockReason, cropBlobToAspect, getMediaUrl, getWarmMicState, hardStopWarmMic, invalidateMedia, isMediaDenialError, nextCameraDevice, notifyMicPermissionRevoked, primeMedia, queryCameraPermission, queryMicPermission, releaseWarmMic, setPreferredMicDeviceId, shouldCombineMicPrompt, subscribeWarmMic, subscribeWarmMicInterruption, useDefaultCaptureEngine, useMediaUrl, useTrackControls, warmMicDebug };
|