@octane-xplat/camera 0.0.0-nightly.g2e22171

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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/package.json +104 -0
  4. package/platforms/android/AndroidManifest.xml +6 -0
  5. package/platforms/android/include.gradle +14 -0
  6. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +84 -0
  7. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
  8. package/platforms/ios/Info.plist +10 -0
  9. package/platforms/macos/src/CameraOwnership.swift +22 -0
  10. package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
  11. package/platforms/macos/src/XplatCameraHost.swift +639 -0
  12. package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
  13. package/src/CameraView.android.tsrx +150 -0
  14. package/src/CameraView.ios.tsrx +102 -0
  15. package/src/CameraView.macos.tsrx +95 -0
  16. package/src/CameraView.mobile.ts +6 -0
  17. package/src/CameraView.web.tsrx +132 -0
  18. package/src/CameraView.windows.tsrx +126 -0
  19. package/src/camera-permission.mobile.ts +91 -0
  20. package/src/camera-permission.windows.ts +76 -0
  21. package/src/camera-session.ios.ts +65 -0
  22. package/src/camera-session.macos.ts +75 -0
  23. package/src/camera-session.windows.ts +126 -0
  24. package/src/host-movie.web.ts +295 -0
  25. package/src/index.ts +36 -0
  26. package/src/index.web.ts +34 -0
  27. package/src/layout-child.ts +81 -0
  28. package/src/layout-child.web.ts +66 -0
  29. package/src/layout-child.windows.ts +79 -0
  30. package/src/movie-metadata.web.ts +84 -0
  31. package/src/movie-storage.web.ts +164 -0
  32. package/src/preview-tap.ts +34 -0
  33. package/src/props.ts +77 -0
  34. package/src/session-backend.android.ts +439 -0
  35. package/src/session-backend.ios.ts +1258 -0
  36. package/src/session-backend.linux.ts +887 -0
  37. package/src/session-backend.macos.ts +707 -0
  38. package/src/session-backend.ts +59 -0
  39. package/src/session-backend.web.ts +891 -0
  40. package/src/session-backend.windows.ts +1599 -0
  41. package/src/session-core.ts +723 -0
  42. package/src/session.ts +37 -0
  43. package/src/synthetic-session.ts +377 -0
  44. package/src/types.ts +357 -0
  45. package/types/index.d.ts +48 -0
  46. package/types/props.d.ts +61 -0
  47. package/types/session-core.d.ts +139 -0
  48. package/types/synthetic-session.d.ts +51 -0
  49. package/types/types.d.ts +290 -0
package/src/types.ts ADDED
@@ -0,0 +1,357 @@
1
+ /** Shared camera-session and movie-recording contract.
2
+ *
3
+ * A `CameraSession` owns one camera acquisition shared between a
4
+ * `CameraView` preview and at most one movie-recording attempt. Recording
5
+ * settles with an immutable `MovieOutcome` only after the local movie is
6
+ * finalized and its metadata is known — a returned clip is always
7
+ * reopenable through `openOutput`.
8
+ *
9
+ * This module is platform-pure: adapters implement
10
+ * `CameraSessionBackend` per host. Adapters that do not support capture
11
+ * yet report `supported: false` and reject `startRecording` with
12
+ * `unsupportedPlatform` instead of fabricating behavior.
13
+ */
14
+
15
+ /** Permission resource a `CameraSession` can inspect or request. */
16
+ export type CameraPermissionKind = 'camera' | 'microphone'
17
+
18
+ /** Portable authorization state. Checking never prompts. `blocked` and
19
+ * `unknown` exist so hosts that cannot distinguish user denial from
20
+ * policy refusal do not fabricate `restricted`. */
21
+ export type CameraPermissionStatus =
22
+ | 'notDetermined'
23
+ | 'granted'
24
+ | 'denied'
25
+ | 'restricted'
26
+ | 'blocked'
27
+ | 'unknown'
28
+ | 'unavailable'
29
+
30
+ /** Portable error categories. `unsupported*` covers missing host features,
31
+ * devices, or capture configurations — including adapters that are not
32
+ * implemented yet during staged delivery. */
33
+ export type CameraCaptureErrorKind =
34
+ | 'permissionDenied'
35
+ | 'permissionRestricted'
36
+ | 'permissionBlocked'
37
+ | 'unsupportedPlatform'
38
+ | 'unsupportedConfiguration'
39
+ | 'unavailable'
40
+ | 'busy'
41
+ | 'invalidState'
42
+ | 'invalidArgument'
43
+ | 'destinationUnavailable'
44
+ | 'insufficientStorage'
45
+ | 'noMedia'
46
+ | 'captureFailed'
47
+ | 'finalizationFailed'
48
+ | 'storageFailed'
49
+ | 'configurationMissing'
50
+
51
+ /** Structured diagnostic carried by failures and non-fatal warnings. */
52
+ export interface CaptureIssue {
53
+ kind: CameraCaptureErrorKind
54
+ /** Operation being attempted, e.g. 'startRecording', 'finalize'. */
55
+ operation: string
56
+ message: string
57
+ permission?: CameraPermissionKind
58
+ /** Host-specific detail for logging; never required for control flow. */
59
+ cause?: unknown
60
+ }
61
+
62
+ /** Error thrown or settled by the session API. Satisfies `CaptureIssue`
63
+ * so `error.kind` drives control flow without unwrapping. */
64
+ export class CameraCaptureError extends Error implements CaptureIssue {
65
+ kind: CameraCaptureErrorKind
66
+ operation: string
67
+ permission?: CameraPermissionKind
68
+ cause?: unknown
69
+
70
+ constructor(issue: Omit<CaptureIssue, 'cause'> & { cause?: unknown }) {
71
+ super(issue.message)
72
+ this.name = 'CameraCaptureError'
73
+ this.kind = issue.kind
74
+ this.operation = issue.operation
75
+ this.permission = issue.permission
76
+ this.cause = issue.cause
77
+ }
78
+ }
79
+
80
+ export type CameraFacing = 'front' | 'back'
81
+
82
+ /** A normalized position on the live preview as the user sees it:
83
+ * `{x: 0, y: 0}` is the top-left corner and `{x: 1, y: 1}` is
84
+ * bottom-right, independent of sensor orientation and front-camera
85
+ * mirroring. The `CameraView` `onTap` event and
86
+ * `CameraSession.focusAtPoint` share this space. */
87
+ export interface CameraPreviewPoint {
88
+ x: number
89
+ y: number
90
+ }
91
+
92
+ export type CameraOrientation =
93
+ | 'portrait'
94
+ | 'portraitUpsideDown'
95
+ | 'landscapeLeft'
96
+ | 'landscapeRight'
97
+ | 'unspecified'
98
+
99
+ /** Camera selection for `CameraSessionConfig.camera`. Desktop and browser
100
+ * hosts expose device identities instead of front/back semantics. */
101
+ export type CameraSelection = 'default' | 'front' | 'back' | { deviceId: string }
102
+
103
+ export interface CameraSessionConfig {
104
+ /** Lens or device identity. Default `'default'`: rear when available. */
105
+ camera?: CameraSelection
106
+ /** Include an audio track; requires microphone permission. Default
107
+ * false — silent capture is a deliberate app choice, never a
108
+ * fallback from denied microphone access. */
109
+ audio?: boolean
110
+ /** Capture profile. `'standard'` targets 720p at 30 fps and may select
111
+ * a device-supported alternative; the started attempt discloses the
112
+ * actual configuration. */
113
+ profile?: 'standard'
114
+ }
115
+
116
+ /** Resolution/frame-rate combination advertised by capability discovery.
117
+ * Entries are supported combinations, not independent lists. */
118
+ export interface CameraProfileCapability {
119
+ profile: 'standard'
120
+ width: number
121
+ height: number
122
+ frameRate: number
123
+ }
124
+
125
+ export interface CameraDeviceInfo {
126
+ id: string
127
+ label: string
128
+ facing?: CameraFacing
129
+ }
130
+
131
+ /** What the session's host can do right now. `supported: false` means the
132
+ * adapter or host cannot record (reason explains); `available` reflects
133
+ * current hardware/permission state and changes over time. */
134
+ export interface CameraSessionCapabilities {
135
+ supported: boolean
136
+ /** Why capture is unsupported or currently unavailable. */
137
+ reason?: string
138
+ available: boolean
139
+ cameras: CameraDeviceInfo[]
140
+ profiles: CameraProfileCapability[]
141
+ /** The session can add an audio track when permission allows. */
142
+ audio: boolean
143
+ /** `focusAtPoint` can move autofocus metering on the active camera —
144
+ * false on fixed-focus devices (common for front cameras) and on
145
+ * platforms without point-of-interest metering. */
146
+ focusPointOfInterest: boolean
147
+ /** `focusAtPoint` can move auto-exposure metering on the active
148
+ * camera. */
149
+ exposurePointOfInterest: boolean
150
+ /** Fixed orientations the source can lock for a take. */
151
+ orientations: CameraOrientation[]
152
+ /** `native` enforcement can still overshoot at media boundaries;
153
+ * `bestEffort` limits are scheduled and may overshoot more. */
154
+ durationLimit: 'native' | 'bestEffort'
155
+ /** Where `elapsedMs` comes from while recording. */
156
+ elapsedTime: 'nativeMedia' | 'estimated'
157
+ output: {
158
+ mimeType: string
159
+ container: string
160
+ /** `appPrivateFile` output survives dispose and app restart;
161
+ * `originLocal` is browser storage subject to site-data clearing. */
162
+ storage: 'appPrivateFile' | 'originLocal'
163
+ /** Accepts an app-supplied `destinationFileUrl` on start. */
164
+ destinationFileUrl: boolean
165
+ }
166
+ }
167
+
168
+ export type CameraRecorderState = 'idle' | 'starting' | 'recording' | 'finalizing' | 'disposed'
169
+
170
+ /** Frozen capture configuration reported once capture actually begins. */
171
+ export interface EffectiveCaptureConfig {
172
+ cameraId: string
173
+ facing?: CameraFacing
174
+ audio: boolean
175
+ width: number
176
+ height: number
177
+ frameRate: number
178
+ orientation: CameraOrientation
179
+ mimeType: string
180
+ container: string
181
+ }
182
+
183
+ export interface CameraSessionSnapshot {
184
+ state: CameraRecorderState
185
+ /** Non-prompting last-known authorization, refreshed by `permissions()`
186
+ * and `requestPermission()`. */
187
+ permission: Record<CameraPermissionKind, CameraPermissionStatus>
188
+ /** Camera hardware/driver availability, separate from recorder state. */
189
+ cameraAvailable: boolean
190
+ /** An active preview view is attached to this session. */
191
+ previewAttached: boolean
192
+ interrupted: boolean
193
+ attempt?: {
194
+ id: string
195
+ state: 'starting' | 'recording' | 'finalizing'
196
+ elapsedMs: number
197
+ configuration?: EffectiveCaptureConfig
198
+ }
199
+ }
200
+
201
+ /** What ended a capture attempt. `interrupted` covers system suspension
202
+ * and preview teardown; `previewReleased` distinguishes an app-driven
203
+ * deactivation from an OS interruption. */
204
+ export type MovieEndReason =
205
+ | 'stopped'
206
+ | 'maximumDuration'
207
+ | 'interrupted'
208
+ | 'backgrounded'
209
+ | 'cameraUnavailable'
210
+ | 'previewReleased'
211
+ | 'disposed'
212
+ | 'captureError'
213
+
214
+ /** Portable completed-output reference. `resourceId` reopens the clip
215
+ * through `openOutput`; `fileUrl` additionally guarantees file-backed
216
+ * native output. `synthetic` marks test-seam clips. */
217
+ export type MovieOutput =
218
+ | {
219
+ kind: 'nativeFile'
220
+ resourceId: string
221
+ fileUrl: string
222
+ synthetic?: boolean
223
+ }
224
+ | {
225
+ kind: 'browserStorage'
226
+ resourceId: string
227
+ retention: 'persistent' | 'bestEffort'
228
+ synthetic?: boolean
229
+ }
230
+
231
+ export interface MovieClip {
232
+ output: MovieOutput
233
+ mimeType: string
234
+ /** Authoritative duration read from the finalized movie. */
235
+ durationMs: number
236
+ /** Presentation dimensions after the orientation transform. */
237
+ width: number
238
+ height: number
239
+ orientation: CameraOrientation
240
+ /** Whether the finalized movie actually contains an audio track.
241
+ * Missing requested audio is a failure, not silent capture. */
242
+ hasAudio: boolean
243
+ }
244
+
245
+ /** One immutable terminal result per admitted attempt. `partial` marks a
246
+ * usable clip ended early by an external condition — still a success;
247
+ * app review decides whether to keep it. */
248
+ export type MovieOutcome =
249
+ | {
250
+ kind: 'clip'
251
+ clip: MovieClip
252
+ endReason: MovieEndReason
253
+ partial: boolean
254
+ warning?: CaptureIssue
255
+ }
256
+ | {
257
+ kind: 'failed'
258
+ endReason: MovieEndReason
259
+ stage: 'preparation' | 'capture' | 'finalization' | 'storage'
260
+ error: CaptureIssue
261
+ }
262
+
263
+ /** Ordered lifecycle observation, correlated to an attempt when relevant. */
264
+ export type CameraSessionEvent =
265
+ | { type: 'stateChanged'; snapshot: CameraSessionSnapshot }
266
+ | { type: 'started'; attemptId: string; configuration: EffectiveCaptureConfig }
267
+ | { type: 'finished'; attemptId: string; outcome: MovieOutcome }
268
+ | {
269
+ type: 'interruption'
270
+ phase: 'began' | 'ended'
271
+ reason?: CameraInterruptionReason
272
+ }
273
+ | { type: 'availability'; available: boolean }
274
+
275
+ export type CameraInterruptionReason =
276
+ | 'background'
277
+ | 'audioInUse'
278
+ | 'cameraInUse'
279
+ | 'multitasking'
280
+ | 'systemPressure'
281
+ | 'unknown'
282
+
283
+ export interface StartRecordingOptions {
284
+ /** Positive finite milliseconds. Enforcement precision comes from
285
+ * `capabilities().durationLimit`; the final duration is read from
286
+ * the movie and may overshoot. */
287
+ maximumDurationMs?: number
288
+ /** Fixed orientation for the take. Must appear in
289
+ * `capabilities().orientations`; omit for the source default. */
290
+ orientation?: CameraOrientation
291
+ /** App-supplied writable local `file://` URL (native hosts only,
292
+ * where `capabilities().output.destinationFileUrl` is true).
293
+ * Existing files are never overwritten. */
294
+ destinationFileUrl?: string
295
+ }
296
+
297
+ /** One admitted capture attempt. `completion` is available immediately and
298
+ * settles exactly once with the terminal outcome; repeated `stop()`
299
+ * joins the same finalization. */
300
+ export interface MovieTake {
301
+ readonly id: string
302
+ readonly state: 'starting' | 'recording' | 'finalizing' | 'settled'
303
+ /** Elapsed capture time in milliseconds; excludes preparation and
304
+ * finalization. Source identified by `capabilities().elapsedTime`. */
305
+ readonly elapsedMs: number
306
+ readonly completion: Promise<MovieOutcome>
307
+ /** Requests stop and joins finalization. Returns `completion`. */
308
+ stop(): Promise<MovieOutcome>
309
+ }
310
+
311
+ /** Reopened access to a stored clip. `release()` frees temporary access;
312
+ * it never deletes the clip. */
313
+ export interface MovieOutputHandle {
314
+ /** Playable/readable location for the clip. */
315
+ readonly url: string
316
+ readonly fileUrl?: string
317
+ release(): void
318
+ }
319
+
320
+ export interface CameraSession {
321
+ /** Non-prompting permission inspection; may return `unknown` when the
322
+ * host cannot observe authorization. */
323
+ permissions(): Promise<Record<CameraPermissionKind, CameraPermissionStatus>>
324
+ /** Runs the system authorization flow for one kind. Calls are
325
+ * serialized; a status other than `notDetermined` returns without
326
+ * prompting. */
327
+ requestPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus>
328
+ capabilities(): Promise<CameraSessionCapabilities>
329
+ snapshot(): CameraSessionSnapshot
330
+ /** Ordered lifecycle events. A caller never loses a result by
331
+ * subscribing late — `completion` is authoritative. Listener
332
+ * exceptions do not change capture. */
333
+ subscribe(listener: (event: CameraSessionEvent) => void): () => void
334
+ /** Tap-to-focus: meters one autofocus/auto-exposure pass at a
335
+ * normalized preview-space point (see `CameraPreviewPoint`), pairing
336
+ * with the `CameraView` `onTap` event. No-ops while no preview is
337
+ * attached, after `dispose`, and on hosts without point-of-interest
338
+ * metering — `capabilities()` reports `focusPointOfInterest` and
339
+ * `exposurePointOfInterest` separately. Out-of-range coordinates
340
+ * clamp to the preview edge; non-finite coordinates throw
341
+ * `invalidArgument`. Metering is per-device state: a camera swap
342
+ * through `configure` returns focus/exposure to the new device's
343
+ * default. */
344
+ focusAtPoint(point: CameraPreviewPoint): void
345
+ /** Replaces configuration while idle. Validates against current
346
+ * capabilities and rejects with `invalidState` during an attempt. */
347
+ configure(config: Partial<CameraSessionConfig>): Promise<void>
348
+ /** Admits one capture attempt. Throws `CameraCaptureError` before
349
+ * admission for invalid arguments, busy, invalid state, or
350
+ * unavailable camera — no attempt is created. */
351
+ startRecording(options?: StartRecordingOptions): MovieTake
352
+ /** Reopens a stored output for playback or reading. */
353
+ openOutput(output: MovieOutput): Promise<MovieOutputHandle>
354
+ /** Requests stop on any active attempt, awaits finalization, then
355
+ * releases camera ownership. */
356
+ dispose(): Promise<void>
357
+ }
@@ -0,0 +1,48 @@
1
+ import type { UniversalComponent } from 'octane/universal'
2
+ import type { CameraViewProps } from './props.js'
3
+ import type { CameraSession, CameraSessionConfig } from './types.js'
4
+ import type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session.js'
5
+
6
+ export declare const CameraView: UniversalComponent<CameraViewProps>
7
+ export type { CameraViewHandle, CameraViewProps, CameraAccessibilityRole } from './props.js'
8
+
9
+ export declare function createCameraSession(config?: CameraSessionConfig): CameraSession
10
+ export declare function createSyntheticCameraSession(
11
+ config?: CameraSessionConfig,
12
+ script?: Partial<SyntheticCameraScript>,
13
+ storage?: SyntheticCameraStorage,
14
+ ): CameraSession
15
+
16
+ export declare function createMemoryCameraStorage(): SyntheticCameraStorage & {
17
+ files: Map<string, Uint8Array>
18
+ }
19
+
20
+ export { CameraCaptureError } from './types.js'
21
+ export type {
22
+ CameraCaptureErrorKind,
23
+ CameraDeviceInfo,
24
+ CameraFacing,
25
+ CameraInterruptionReason,
26
+ CameraOrientation,
27
+ CameraPermissionKind,
28
+ CameraPermissionStatus,
29
+ CameraProfileCapability,
30
+ CameraRecorderState,
31
+ CameraSelection,
32
+ CameraSession,
33
+ CameraSessionCapabilities,
34
+ CameraSessionConfig,
35
+ CameraSessionEvent,
36
+ CameraSessionSnapshot,
37
+ CaptureIssue,
38
+ EffectiveCaptureConfig,
39
+ MovieClip,
40
+ MovieEndReason,
41
+ MovieOutcome,
42
+ MovieOutput,
43
+ MovieOutputHandle,
44
+ MovieTake,
45
+ StartRecordingOptions,
46
+ } from './types.js'
47
+
48
+ export type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session.js'
@@ -0,0 +1,61 @@
1
+ import type { Octane } from 'octane/jsx-runtime';
2
+ import type { CameraPreviewPoint, CameraSession } from './types';
3
+ export type CameraAccessibilityRole = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
4
+ export interface CameraViewHandle {
5
+ /** The native preview surface (`HTMLVideoElement` on web). */
6
+ native: any;
7
+ }
8
+ export interface CameraViewProps {
9
+ className?: any;
10
+ style?: any;
11
+ id?: string;
12
+ row?: number;
13
+ col?: number;
14
+ rowSpan?: number;
15
+ colSpan?: number;
16
+ dock?: 'left' | 'top' | 'right' | 'bottom';
17
+ left?: number;
18
+ top?: number;
19
+ flexGrow?: number;
20
+ flexShrink?: number;
21
+ alignSelf?: string;
22
+ order?: number;
23
+ accessible?: boolean;
24
+ accessibilityLabel?: string;
25
+ accessibilityRole?: CameraAccessibilityRole;
26
+ accessibilityHint?: string;
27
+ accessibilityValue?: string;
28
+ accessibilityState?: {
29
+ disabled?: boolean;
30
+ selected?: boolean;
31
+ checked?: boolean;
32
+ };
33
+ accessibilityLiveRegion?: 'none' | 'polite' | 'assertive';
34
+ /** Shared camera owner enabling movie recording. When set, the preview
35
+ * attaches to the session's acquisition instead of owning one, and
36
+ * `facing` must be omitted — camera selection lives on the session. */
37
+ session?: CameraSession;
38
+ /** Lens — defaults to the rear camera. Invalid together with `session`. */
39
+ facing?: 'front' | 'back';
40
+ /** Default true. False releases the preview session. */
41
+ active?: boolean;
42
+ /** Fires after the preview surface starts. */
43
+ onReady?: () => void;
44
+ onError?: (error: {
45
+ message?: string;
46
+ }) => void;
47
+ /** Notification-only tap event — fires with the normalized
48
+ * preview-space position (`CameraPreviewPoint`) and never consumes
49
+ * the gesture, so recognizers layered above the preview (double-tap,
50
+ * pan) still run. Pair with `session.focusAtPoint` for tap-to-focus.
51
+ * Delivered on iOS, Android, and web; desktop runtimes do not dispatch
52
+ * it yet. */
53
+ onTap?: (point: CameraPreviewPoint) => void;
54
+ /** Native preview surface for platform-specific controls. */
55
+ ref?: Octane.Ref<CameraViewHandle>;
56
+ /** Platform-specific properties applied to the preview surface. */
57
+ ios?: Record<string, any>;
58
+ android?: Record<string, any>;
59
+ web?: Record<string, any>;
60
+ windows?: Record<string, any>;
61
+ }
@@ -0,0 +1,139 @@
1
+ import type { CameraInterruptionReason, CameraPermissionKind, CameraPermissionStatus, CameraPreviewPoint, CameraSession, CameraSessionCapabilities, CameraSessionConfig, CameraSessionEvent, CameraSessionSnapshot, CaptureIssue, EffectiveCaptureConfig, MovieEndReason, MovieOutput, MovieOutputHandle, MovieTake, StartRecordingOptions } from './types';
2
+ /** Result an adapter reports when a capture attempt settles. Media
3
+ * metadata is already read from the finalized movie by the adapter. */
4
+ export type BackendSettle = {
5
+ kind: 'clip';
6
+ output: MovieOutput;
7
+ mimeType: string;
8
+ durationMs: number;
9
+ width: number;
10
+ height: number;
11
+ orientation: EffectiveCaptureConfig['orientation'];
12
+ hasAudio: boolean;
13
+ /** Native duration-limit enforcement ended the take. */
14
+ limitReached?: boolean;
15
+ /** Adapter-driven endings when no earlier app/lifecycle cause exists. */
16
+ endReason?: MovieEndReason;
17
+ warning?: CaptureIssue;
18
+ } | {
19
+ kind: 'failed';
20
+ stage: 'preparation' | 'capture' | 'finalization' | 'storage';
21
+ issue: CaptureIssue;
22
+ };
23
+ /** Per-attempt callbacks handed to `startCapture`. */
24
+ export interface BackendCaptureSink {
25
+ /** Native capture actually began; exactly once per settled attempt. */
26
+ started(config: EffectiveCaptureConfig): void;
27
+ /** Capture has ended; metadata verification and storage still own the attempt. */
28
+ finishing(reason?: MovieEndReason): void;
29
+ /** Terminal result; exactly once per admitted attempt. */
30
+ settled(result: BackendSettle): void;
31
+ }
32
+ export interface BackendCaptureRequest {
33
+ audio: boolean;
34
+ orientation: EffectiveCaptureConfig['orientation'] | undefined;
35
+ maximumDurationMs: number | undefined;
36
+ /** Resolved writable path for native adapters. */
37
+ destinationPath: string | undefined;
38
+ }
39
+ /** Adapter-owned preview attach. `listener` reports preview lifecycle;
40
+ * returns the detach function. */
41
+ export interface BackendPreviewListener {
42
+ onReady?: () => void;
43
+ onError?: (issue: CaptureIssue) => void;
44
+ }
45
+ /** Platform adapter surface driven by the shared session core. Adapters
46
+ * own permission policy, capability discovery, capture, and local output
47
+ * access; the core owns states, ordering, causes, and outcomes. */
48
+ export interface CameraSessionBackend {
49
+ readonly platform: string;
50
+ /** False when the adapter cannot capture on this host; the core
51
+ * rejects `startRecording` with `unsupportedPlatform`. */
52
+ readonly supported: boolean;
53
+ checkPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus>;
54
+ requestPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus>;
55
+ getCapabilities(): Promise<CameraSessionCapabilities>;
56
+ /** An active, ready preview is attached and the camera source is
57
+ * running — the admission requirement for `startRecording`. */
58
+ previewReady(): boolean;
59
+ /** Attach the shared-source preview to a platform view. */
60
+ attachPreview(host: unknown, listener?: BackendPreviewListener): () => void;
61
+ /** Replace configuration while idle; throws CameraCaptureError. */
62
+ configure?(config: CameraSessionConfig): void;
63
+ /** Meter one autofocus/auto-exposure pass at a normalized preview-space
64
+ * point — the tap-to-focus primitive. Absent means the adapter cannot
65
+ * meter at a point; the core turns it into a no-op. */
66
+ focusAtPoint?(point: CameraPreviewPoint): void;
67
+ /** Synchronous platform-specific admission checks; creates no attempt on failure. */
68
+ validateStart?(options: StartRecordingOptions): void;
69
+ /** Begin capture for an admitted attempt. Async failures settle via
70
+ * `sink.settled`; a synchronous throw settles as a preparation failure. */
71
+ startCapture(request: BackendCaptureRequest, sink: BackendCaptureSink): void;
72
+ /** Request capture stop; settlement still arrives via the sink. */
73
+ stopCapture(): void;
74
+ /** Elapsed capture time in ms for the in-flight attempt. */
75
+ elapsedMs(): number;
76
+ openOutput(output: MovieOutput): Promise<MovieOutputHandle>;
77
+ /** Release camera ownership. The core calls this only after any
78
+ * in-flight attempt has settled. */
79
+ release(): Promise<void>;
80
+ /** Reattach/reconfigure bookkeeping when the preview view is detached
81
+ * while an attempt is settling — the adapter keeps hardware until
82
+ * settlement, then stops. */
83
+ onPreviewDetached?(): void;
84
+ /** Lifecycle observations the adapter emits at any time. */
85
+ setObserver(observer: BackendObserver | undefined): void;
86
+ }
87
+ export interface BackendObserver {
88
+ interruption(phase: 'began' | 'ended', reason?: CameraInterruptionReason): void;
89
+ availability(available: boolean): void;
90
+ /** Preview became ready/unready (session running state changed). */
91
+ previewReady(ready: boolean): void;
92
+ }
93
+ /** Collapse '.' and '..' segments without touching the filesystem — the
94
+ * adapters' app-private containment checks compare against this canonical
95
+ * form, so a '..' inside the requested path can never keep the root
96
+ * prefix while escaping it. */
97
+ export declare const canonicalizePath: (raw: string, separator?: string) => string;
98
+ /** The concrete session object wrapping a platform backend. Internal to
99
+ * the package; `CameraSession` is the public shape. */
100
+ export declare class SharedCameraSession implements CameraSession {
101
+ private backend;
102
+ private config;
103
+ private listeners;
104
+ private state;
105
+ private permission;
106
+ private cameraAvailable;
107
+ private previewAttached;
108
+ private interrupted;
109
+ private disposeRequested;
110
+ private disposed;
111
+ private lastCapabilities;
112
+ private requestChains;
113
+ private attempt;
114
+ constructor(backend: CameraSessionBackend, config: CameraSessionConfig);
115
+ /** Package-internal: attach a preview host to this session. */
116
+ attach(host: unknown, listener?: BackendPreviewListener): () => void;
117
+ get attached(): boolean;
118
+ private detachInternal;
119
+ private emit;
120
+ private setState;
121
+ snapshot(): CameraSessionSnapshot;
122
+ subscribe(listener: (event: CameraSessionEvent) => void): () => void;
123
+ permissions(): Promise<{
124
+ camera: CameraPermissionStatus;
125
+ microphone: CameraPermissionStatus;
126
+ }>;
127
+ requestPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus>;
128
+ capabilities(): Promise<CameraSessionCapabilities>;
129
+ focusAtPoint(point: CameraPreviewPoint): void;
130
+ configure(next: Partial<CameraSessionConfig>): Promise<void>;
131
+ private validateCameraSelection;
132
+ private requestStop;
133
+ private handleInterruption;
134
+ private handleAvailability;
135
+ startRecording(options?: StartRecordingOptions): MovieTake;
136
+ openOutput(output: MovieOutput): Promise<MovieOutputHandle>;
137
+ dispose(): Promise<void>;
138
+ private finishDispose;
139
+ }
@@ -0,0 +1,51 @@
1
+ import type { CameraInterruptionReason, CameraPermissionStatus, CameraPreviewPoint, CameraSession, CameraSessionConfig } from './types';
2
+ /** Development/testing seam — a deterministic camera source that drives
3
+ * the shared session state machine without hardware. NOT a production
4
+ * recording source: outcomes carry `synthetic: true` on their output so
5
+ * seam clips stay distinguishable from real capture.
6
+ *
7
+ * `script` is read live at each transition — mutate fields between
8
+ * operations to script permissions, delayed starts, interruptions,
9
+ * partial completions, and failures.
10
+ */
11
+ export interface SyntheticCameraScript {
12
+ permission: {
13
+ camera: CameraPermissionStatus;
14
+ microphone: CameraPermissionStatus;
15
+ };
16
+ /** Camera hardware availability observation. */
17
+ available: boolean;
18
+ /** Delay before `started` reports; models slow native activation. */
19
+ startDelayMs: number;
20
+ /** Finalized clip duration reported by the seam. */
21
+ clipDurationMs: number;
22
+ /** Produce a usable clip on stop; false settles `noMedia`. */
23
+ produceMedia: boolean;
24
+ /** When set, the attempt settles failed at this stage. */
25
+ failAt?: 'start' | 'capture' | 'finalize' | 'storage';
26
+ /** Emit an interruption after this many ms of recording; the take
27
+ * ends with the mapped end reason and a partial clip. */
28
+ interruption?: {
29
+ afterMs: number;
30
+ reason: CameraInterruptionReason;
31
+ endsAfterMs?: number;
32
+ };
33
+ /** When set, each `focusAtPoint` call pushes its normalized point
34
+ * here so a spec can observe metering requests. */
35
+ focusRequests?: CameraPreviewPoint[];
36
+ }
37
+ export interface SyntheticCameraStorage {
38
+ write(path: string, bytes: Uint8Array): void | Promise<void>;
39
+ read(path: string): Uint8Array | undefined | Promise<Uint8Array | undefined>;
40
+ delete?(path: string): void | Promise<void>;
41
+ exists?(path: string): boolean | Promise<boolean>;
42
+ }
43
+ /** In-memory storage for the seam — a `fileUrl`-shaped map so outcomes
44
+ * exercise the same reopen path without touching a real filesystem. */
45
+ export declare function createMemoryCameraStorage(): SyntheticCameraStorage & {
46
+ files: Map<string, Uint8Array>;
47
+ };
48
+ /** Create a `CameraSession` backed by the deterministic synthetic source.
49
+ * Marked for development and testing — never present this as live
50
+ * capture to users. */
51
+ export declare function createSyntheticCameraSession(config?: CameraSessionConfig, script?: Partial<SyntheticCameraScript>, storage?: SyntheticCameraStorage): CameraSession;