@octane-xplat/camera 0.13.0 → 0.15.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/README.md +89 -3
- package/package.json +35 -11
- package/platforms/android/AndroidManifest.xml +2 -0
- package/platforms/android/include.gradle +1 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +13 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
- package/platforms/ios/Info.plist +2 -0
- package/platforms/macos/src/CameraOwnership.swift +22 -0
- package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
- package/platforms/macos/src/XplatCameraHost.swift +639 -0
- package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
- package/src/CameraView.android.tsrx +27 -3
- package/src/CameraView.ios.tsrx +32 -4
- package/src/CameraView.macos.tsrx +95 -0
- package/src/CameraView.web.tsrx +31 -2
- package/src/CameraView.windows.tsrx +126 -0
- package/src/camera-permission.mobile.ts +18 -4
- package/src/camera-permission.windows.ts +76 -0
- package/src/camera-session.macos.ts +75 -0
- package/src/camera-session.windows.ts +126 -0
- package/src/host-movie.web.ts +295 -0
- package/src/index.ts +32 -0
- package/src/index.web.ts +32 -0
- package/src/layout-child.windows.ts +79 -0
- package/src/movie-metadata.web.ts +84 -0
- package/src/movie-storage.web.ts +164 -0
- package/src/preview-tap.ts +34 -0
- package/src/props.ts +14 -1
- package/src/session-backend.android.ts +439 -0
- package/src/session-backend.ios.ts +1258 -0
- package/src/session-backend.linux.ts +887 -0
- package/src/session-backend.macos.ts +707 -0
- package/src/session-backend.ts +59 -0
- package/src/session-backend.web.ts +891 -0
- package/src/session-backend.windows.ts +1599 -0
- package/src/session-core.ts +723 -0
- package/src/session.ts +37 -0
- package/src/synthetic-session.ts +377 -0
- package/src/types.ts +357 -0
- package/types/index.d.ts +43 -0
- package/types/props.d.ts +14 -1
- package/types/session-core.d.ts +139 -0
- package/types/synthetic-session.d.ts +51 -0
- package/types/types.d.ts +290 -0
- package/dist/native/CameraView.android.js +0 -166
- package/dist/native/CameraView.ios.js +0 -131
- package/dist/native/camera-permission.mobile.js +0 -45
- package/dist/native/camera-session.ios.js +0 -45
- package/dist/native/cx.mobile.js +0 -9
- package/dist/native/index.js +0 -2
- package/dist/native/layout-child.js +0 -53
- package/dist/native/octane-client-build.json +0 -8
- package/dist/web/CameraView.web.tsrx.js +0 -317
- package/dist/web/index.js +0 -2
- package/dist/web/layout-child.web.js +0 -29
- package/dist/web/octane-client-build.json +0 -8
- package/src/CameraView.windows.ts +0 -5
- package/src/cx.mobile.ts +0 -6
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
|
+
}
|
package/types/index.d.ts
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
import type { UniversalComponent } from 'octane/universal'
|
|
2
2
|
import type { CameraViewProps } from './props.js'
|
|
3
|
+
import type { CameraSession, CameraSessionConfig } from './types.js'
|
|
4
|
+
import type { SyntheticCameraScript, SyntheticCameraStorage } from './synthetic-session.js'
|
|
3
5
|
|
|
4
6
|
export declare const CameraView: UniversalComponent<CameraViewProps>
|
|
5
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'
|
package/types/props.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Octane } from 'octane/jsx-runtime';
|
|
2
|
+
import type { CameraPreviewPoint, CameraSession } from './types';
|
|
2
3
|
export type CameraAccessibilityRole = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
|
|
3
4
|
export interface CameraViewHandle {
|
|
4
5
|
/** The native preview surface (`HTMLVideoElement` on web). */
|
|
@@ -30,7 +31,11 @@ export interface CameraViewProps {
|
|
|
30
31
|
checked?: boolean;
|
|
31
32
|
};
|
|
32
33
|
accessibilityLiveRegion?: 'none' | 'polite' | 'assertive';
|
|
33
|
-
/**
|
|
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`. */
|
|
34
39
|
facing?: 'front' | 'back';
|
|
35
40
|
/** Default true. False releases the preview session. */
|
|
36
41
|
active?: boolean;
|
|
@@ -39,10 +44,18 @@ export interface CameraViewProps {
|
|
|
39
44
|
onError?: (error: {
|
|
40
45
|
message?: string;
|
|
41
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;
|
|
42
54
|
/** Native preview surface for platform-specific controls. */
|
|
43
55
|
ref?: Octane.Ref<CameraViewHandle>;
|
|
44
56
|
/** Platform-specific properties applied to the preview surface. */
|
|
45
57
|
ios?: Record<string, any>;
|
|
46
58
|
android?: Record<string, any>;
|
|
47
59
|
web?: Record<string, any>;
|
|
60
|
+
windows?: Record<string, any>;
|
|
48
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;
|