@octane-xplat/camera 0.14.0 → 0.16.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 +26 -0
- package/package.json +22 -16
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +9 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +31 -0
- package/platforms/macos/src/CameraOwnership.swift +22 -0
- package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
- package/platforms/macos/src/XplatCameraHost.swift +139 -13
- package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
- package/src/CameraView.android.tsrx +8 -18
- package/src/CameraView.ios.tsrx +4 -17
- package/src/CameraView.macos.tsrx +4 -17
- package/src/CameraView.web.tsrx +11 -0
- package/src/CameraView.windows.tsrx +6 -18
- package/src/camera-permission.mobile.ts +18 -4
- package/src/camera-session.windows.ts +4 -1
- package/src/host-movie.web.ts +141 -26
- package/src/index.ts +1 -0
- package/src/index.web.ts +1 -0
- package/src/layout-child.ts +39 -18
- package/src/layout-child.windows.ts +5 -2
- package/src/preview-tap.ts +34 -0
- package/src/props.ts +8 -1
- package/src/session-backend.android.ts +42 -0
- package/src/session-backend.ios.ts +183 -17
- package/src/session-backend.linux.ts +22 -4
- package/src/session-backend.macos.ts +100 -17
- package/src/session-backend.ts +2 -0
- package/src/session-backend.web.ts +17 -0
- package/src/session-backend.windows.ts +88 -6
- package/src/session-core.ts +73 -9
- package/src/synthetic-session.ts +10 -0
- package/src/types.ts +28 -0
- package/types/props.d.ts +8 -1
- package/types/session-core.d.ts +11 -1
- package/types/synthetic-session.d.ts +4 -1
- package/types/types.d.ts +27 -0
- package/dist/linux/CameraView.web.tsrx.js +0 -337
- package/dist/linux/host-movie.web.js +0 -100
- package/dist/linux/index.js +0 -5
- package/dist/linux/layout-child.web.js +0 -29
- package/dist/linux/movie-metadata.web.js +0 -65
- package/dist/linux/octane-client-build.json +0 -8
- package/dist/linux/session-backend.linux.js +0 -479
- package/dist/linux/session-core.js +0 -368
- package/dist/linux/session.js +0 -29
- package/dist/linux/synthetic-session.js +0 -280
- package/dist/linux/types.js +0 -19
- package/dist/native/CameraView.android.js +0 -190
- package/dist/native/CameraView.ios.js +0 -154
- package/dist/native/CameraView.windows.js +0 -173
- package/dist/native/camera-permission.mobile.js +0 -45
- package/dist/native/camera-permission.windows.js +0 -43
- package/dist/native/camera-session.ios.js +0 -45
- package/dist/native/camera-session.windows.js +0 -78
- package/dist/native/cx.mobile.js +0 -9
- package/dist/native/cx.windows.js +0 -9
- package/dist/native/index.js +0 -5
- package/dist/native/layout-child.js +0 -53
- package/dist/native/layout-child.windows.js +0 -53
- package/dist/native/octane-client-build.json +0 -8
- package/dist/native/session-backend.android.js +0 -234
- package/dist/native/session-backend.ios.js +0 -682
- package/dist/native/session-backend.js +0 -55
- package/dist/native/session-backend.windows.js +0 -905
- package/dist/native/session-core.js +0 -368
- package/dist/native/session.js +0 -29
- package/dist/native/synthetic-session.js +0 -280
- package/dist/native/types.js +0 -19
- package/dist/web/CameraView.web.tsrx.js +0 -337
- package/dist/web/host-movie.web.js +0 -100
- package/dist/web/index.js +0 -5
- package/dist/web/layout-child.web.js +0 -29
- package/dist/web/movie-metadata.web.js +0 -65
- package/dist/web/movie-storage.web.js +0 -119
- package/dist/web/octane-client-build.json +0 -8
- package/dist/web/session-backend.web.js +0 -490
- package/dist/web/session-core.js +0 -368
- package/dist/web/session.js +0 -29
- package/dist/web/synthetic-session.js +0 -280
- package/dist/web/types.js +0 -19
- package/src/cx.macos.ts +0 -5
- package/src/cx.mobile.ts +0 -6
- package/src/cx.windows.ts +0 -6
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
import { CameraCaptureError } from "./types.js";
|
|
2
|
-
//#region src/session-core.ts
|
|
3
|
-
var attemptCounter = 0;
|
|
4
|
-
var issue = (kind, operation, message, extra) => new CameraCaptureError({
|
|
5
|
-
kind,
|
|
6
|
-
operation,
|
|
7
|
-
message,
|
|
8
|
-
...extra
|
|
9
|
-
});
|
|
10
|
-
/** The concrete session object wrapping a platform backend. Internal to
|
|
11
|
-
* the package; `CameraSession` is the public shape. */
|
|
12
|
-
var SharedCameraSession = class {
|
|
13
|
-
backend;
|
|
14
|
-
config;
|
|
15
|
-
listeners = /* @__PURE__ */ new Set();
|
|
16
|
-
state = "idle";
|
|
17
|
-
permission = {
|
|
18
|
-
camera: "unknown",
|
|
19
|
-
microphone: "unknown"
|
|
20
|
-
};
|
|
21
|
-
cameraAvailable = true;
|
|
22
|
-
previewAttached = false;
|
|
23
|
-
interrupted = false;
|
|
24
|
-
disposeRequested = false;
|
|
25
|
-
disposed = false;
|
|
26
|
-
lastCapabilities;
|
|
27
|
-
requestChains = /* @__PURE__ */ new Map();
|
|
28
|
-
attempt;
|
|
29
|
-
constructor(backend, config) {
|
|
30
|
-
this.backend = backend;
|
|
31
|
-
this.config = { ...config };
|
|
32
|
-
backend.setObserver({
|
|
33
|
-
interruption: (phase, reason) => this.handleInterruption(phase, reason),
|
|
34
|
-
availability: (available) => this.handleAvailability(available),
|
|
35
|
-
previewReady: () => {
|
|
36
|
-
this.emit({
|
|
37
|
-
type: "stateChanged",
|
|
38
|
-
snapshot: this.snapshot()
|
|
39
|
-
});
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
/** Package-internal: attach a preview host to this session. */
|
|
44
|
-
attach(host, listener) {
|
|
45
|
-
if (this.disposed || this.disposeRequested) throw issue("invalidState", "attachPreview", "CameraSession is disposed");
|
|
46
|
-
if (this.previewAttached) throw issue("invalidState", "attachPreview", "CameraSession already has a preview");
|
|
47
|
-
const detach = this.backend.attachPreview(host, {
|
|
48
|
-
onReady: listener?.onReady,
|
|
49
|
-
onError: (captureIssue) => {
|
|
50
|
-
listener?.onError?.(captureIssue);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
this.previewAttached = true;
|
|
54
|
-
this.emit({
|
|
55
|
-
type: "stateChanged",
|
|
56
|
-
snapshot: this.snapshot()
|
|
57
|
-
});
|
|
58
|
-
return () => {
|
|
59
|
-
this.detachInternal(detach);
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
get attached() {
|
|
63
|
-
return this.previewAttached;
|
|
64
|
-
}
|
|
65
|
-
detachInternal(detach) {
|
|
66
|
-
if (!this.previewAttached) return;
|
|
67
|
-
this.previewAttached = false;
|
|
68
|
-
const attempt = this.attempt;
|
|
69
|
-
if (attempt && (attempt.state === "starting" || attempt.state === "recording")) this.requestStop("previewReleased");
|
|
70
|
-
detach();
|
|
71
|
-
this.backend.onPreviewDetached?.();
|
|
72
|
-
this.emit({
|
|
73
|
-
type: "stateChanged",
|
|
74
|
-
snapshot: this.snapshot()
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
emit(event) {
|
|
78
|
-
for (const listener of this.listeners) try {
|
|
79
|
-
listener(event);
|
|
80
|
-
} catch {}
|
|
81
|
-
}
|
|
82
|
-
setState(state) {
|
|
83
|
-
if (this.state === state) return;
|
|
84
|
-
this.state = state;
|
|
85
|
-
this.emit({
|
|
86
|
-
type: "stateChanged",
|
|
87
|
-
snapshot: this.snapshot()
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
snapshot() {
|
|
91
|
-
const attempt = this.attempt;
|
|
92
|
-
return {
|
|
93
|
-
state: this.state,
|
|
94
|
-
permission: { ...this.permission },
|
|
95
|
-
cameraAvailable: this.cameraAvailable,
|
|
96
|
-
previewAttached: this.previewAttached,
|
|
97
|
-
interrupted: this.interrupted,
|
|
98
|
-
attempt: attempt && attempt.state !== "settled" ? {
|
|
99
|
-
id: attempt.id,
|
|
100
|
-
state: attempt.state,
|
|
101
|
-
elapsedMs: this.backend.elapsedMs(),
|
|
102
|
-
configuration: attempt.configuration
|
|
103
|
-
} : void 0
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
subscribe(listener) {
|
|
107
|
-
this.listeners.add(listener);
|
|
108
|
-
return () => {
|
|
109
|
-
this.listeners.delete(listener);
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
async permissions() {
|
|
113
|
-
const [camera, microphone] = await Promise.all([this.backend.checkPermission("camera"), this.backend.checkPermission("microphone")]);
|
|
114
|
-
this.permission = {
|
|
115
|
-
camera,
|
|
116
|
-
microphone
|
|
117
|
-
};
|
|
118
|
-
this.emit({
|
|
119
|
-
type: "stateChanged",
|
|
120
|
-
snapshot: this.snapshot()
|
|
121
|
-
});
|
|
122
|
-
return { ...this.permission };
|
|
123
|
-
}
|
|
124
|
-
requestPermission(kind) {
|
|
125
|
-
const chain = (this.requestChains.get(kind) ?? Promise.resolve("unknown")).then(async () => {
|
|
126
|
-
if (this.disposed) return this.permission[kind];
|
|
127
|
-
const status = await this.backend.requestPermission(kind);
|
|
128
|
-
this.permission = {
|
|
129
|
-
...this.permission,
|
|
130
|
-
[kind]: status
|
|
131
|
-
};
|
|
132
|
-
this.emit({
|
|
133
|
-
type: "stateChanged",
|
|
134
|
-
snapshot: this.snapshot()
|
|
135
|
-
});
|
|
136
|
-
return status;
|
|
137
|
-
});
|
|
138
|
-
this.requestChains.set(kind, chain);
|
|
139
|
-
return chain;
|
|
140
|
-
}
|
|
141
|
-
async capabilities() {
|
|
142
|
-
const capabilities = await this.backend.getCapabilities();
|
|
143
|
-
this.lastCapabilities = capabilities;
|
|
144
|
-
return capabilities;
|
|
145
|
-
}
|
|
146
|
-
async configure(next) {
|
|
147
|
-
if (this.disposed || this.disposeRequested) throw issue("invalidState", "configure", "CameraSession is disposed");
|
|
148
|
-
if (this.state !== "idle") throw issue("invalidState", "configure", `CameraSession cannot configure while ${this.state}`);
|
|
149
|
-
const merged = {
|
|
150
|
-
...this.config,
|
|
151
|
-
...next
|
|
152
|
-
};
|
|
153
|
-
const capabilities = await this.capabilities();
|
|
154
|
-
this.validateCameraSelection(merged.camera, capabilities);
|
|
155
|
-
this.config = merged;
|
|
156
|
-
this.backend.configure?.(merged);
|
|
157
|
-
}
|
|
158
|
-
validateCameraSelection(camera, capabilities) {
|
|
159
|
-
if (camera && typeof camera === "object" && "deviceId" in camera) {
|
|
160
|
-
if (!capabilities.cameras.some((entry) => entry.id === camera.deviceId)) throw issue("unsupportedConfiguration", "configure", `Camera device '${camera.deviceId}' is not available`);
|
|
161
|
-
} else if (camera === "front" || camera === "back") {
|
|
162
|
-
if (!capabilities.cameras.some((entry) => entry.facing === camera) && capabilities.cameras.length > 0) throw issue("unsupportedConfiguration", "configure", `No ${camera}-facing camera is available`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
requestStop(cause) {
|
|
166
|
-
const attempt = this.attempt;
|
|
167
|
-
if (!attempt || attempt.state === "finalizing" || attempt.state === "settled") return;
|
|
168
|
-
attempt.stopRequested = true;
|
|
169
|
-
attempt.cause ??= cause;
|
|
170
|
-
if (attempt.state === "recording") {
|
|
171
|
-
this.setState("finalizing");
|
|
172
|
-
attempt.state = "finalizing";
|
|
173
|
-
try {
|
|
174
|
-
this.backend.stopCapture();
|
|
175
|
-
} catch {}
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
handleInterruption(phase, reason) {
|
|
179
|
-
this.interrupted = phase === "began";
|
|
180
|
-
this.emit({
|
|
181
|
-
type: "interruption",
|
|
182
|
-
phase,
|
|
183
|
-
reason
|
|
184
|
-
});
|
|
185
|
-
if (phase === "began") {
|
|
186
|
-
const cause = reason === "background" ? "backgrounded" : reason === "cameraInUse" ? "cameraUnavailable" : "interrupted";
|
|
187
|
-
this.requestStop(cause);
|
|
188
|
-
}
|
|
189
|
-
this.emit({
|
|
190
|
-
type: "stateChanged",
|
|
191
|
-
snapshot: this.snapshot()
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
handleAvailability(available) {
|
|
195
|
-
if (this.cameraAvailable === available) return;
|
|
196
|
-
this.cameraAvailable = available;
|
|
197
|
-
this.emit({
|
|
198
|
-
type: "availability",
|
|
199
|
-
available
|
|
200
|
-
});
|
|
201
|
-
if (!available) this.requestStop("cameraUnavailable");
|
|
202
|
-
this.emit({
|
|
203
|
-
type: "stateChanged",
|
|
204
|
-
snapshot: this.snapshot()
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
startRecording(options = {}) {
|
|
208
|
-
const operation = "startRecording";
|
|
209
|
-
if (this.disposed || this.disposeRequested) throw issue("invalidState", operation, "CameraSession is disposed");
|
|
210
|
-
if (!this.backend.supported) throw issue("unsupportedPlatform", operation, "Movie recording is unsupported on this platform");
|
|
211
|
-
if (this.state !== "idle" || this.attempt) throw issue("busy", operation, `CameraSession is ${this.state}`);
|
|
212
|
-
if (options.maximumDurationMs !== void 0) {
|
|
213
|
-
const value = options.maximumDurationMs;
|
|
214
|
-
if (!Number.isFinite(value) || value <= 0) throw issue("invalidArgument", operation, "maximumDurationMs must be a positive finite number of milliseconds");
|
|
215
|
-
}
|
|
216
|
-
if (options.destinationFileUrl !== void 0 && !options.destinationFileUrl.startsWith("file://")) throw issue("invalidArgument", operation, "destinationFileUrl must be a local file:// URL");
|
|
217
|
-
if (options.orientation !== void 0 && options.orientation !== "unspecified" && this.lastCapabilities && !this.lastCapabilities.orientations.includes(options.orientation)) throw issue("unsupportedConfiguration", "startRecording", `Orientation '${options.orientation}' is not advertised by this camera`);
|
|
218
|
-
if (this.interrupted || !this.cameraAvailable || !this.backend.previewReady()) throw issue("unavailable", operation, this.interrupted ? "Camera session is interrupted" : !this.cameraAvailable ? "Camera is unavailable" : "An active, ready preview is required before recording");
|
|
219
|
-
this.backend.validateStart?.(options);
|
|
220
|
-
attemptCounter += 1;
|
|
221
|
-
const id = `take-${Date.now()}-${attemptCounter}`;
|
|
222
|
-
let resolveCompletion;
|
|
223
|
-
const completion = new Promise((resolve) => {
|
|
224
|
-
resolveCompletion = resolve;
|
|
225
|
-
});
|
|
226
|
-
const attempt = {
|
|
227
|
-
id,
|
|
228
|
-
state: "starting",
|
|
229
|
-
stopRequested: false,
|
|
230
|
-
resolve: resolveCompletion,
|
|
231
|
-
completion
|
|
232
|
-
};
|
|
233
|
-
this.attempt = attempt;
|
|
234
|
-
this.setState("starting");
|
|
235
|
-
const capabilities = this.lastCapabilities;
|
|
236
|
-
const sink = {
|
|
237
|
-
started: (configuration) => {
|
|
238
|
-
const current = this.attempt;
|
|
239
|
-
if (!current || current.id !== id || current.state === "settled") return;
|
|
240
|
-
current.configuration = configuration;
|
|
241
|
-
current.startedAt = Date.now();
|
|
242
|
-
if (current.state === "starting") {
|
|
243
|
-
current.state = "recording";
|
|
244
|
-
this.setState("recording");
|
|
245
|
-
this.emit({
|
|
246
|
-
type: "started",
|
|
247
|
-
attemptId: id,
|
|
248
|
-
configuration
|
|
249
|
-
});
|
|
250
|
-
if (options.maximumDurationMs !== void 0 && capabilities?.durationLimit === "bestEffort") {
|
|
251
|
-
const limit = options.maximumDurationMs;
|
|
252
|
-
const checkLimit = () => {
|
|
253
|
-
if (this.attempt?.id !== id || current.state !== "recording") return;
|
|
254
|
-
const remaining = limit - this.backend.elapsedMs();
|
|
255
|
-
if (remaining > 0) current.limitTimer = setTimeout(checkLimit, Math.min(remaining, 2147483647));
|
|
256
|
-
else this.requestStop("maximumDuration");
|
|
257
|
-
};
|
|
258
|
-
current.limitTimer = setTimeout(checkLimit, Math.min(limit, 2147483647));
|
|
259
|
-
}
|
|
260
|
-
if (current.stopRequested) this.requestStop(current.cause ?? "stopped");
|
|
261
|
-
}
|
|
262
|
-
},
|
|
263
|
-
finishing: (reason) => {
|
|
264
|
-
const current = this.attempt;
|
|
265
|
-
if (!current || current.id !== id || current.state === "settled") return;
|
|
266
|
-
current.cause ??= reason;
|
|
267
|
-
current.state = "finalizing";
|
|
268
|
-
this.setState("finalizing");
|
|
269
|
-
},
|
|
270
|
-
settled: (result) => {
|
|
271
|
-
const current = this.attempt;
|
|
272
|
-
if (!current || current.id !== id || current.state === "settled") return;
|
|
273
|
-
if (current.limitTimer) clearTimeout(current.limitTimer);
|
|
274
|
-
current.state = "settled";
|
|
275
|
-
const cause = current.cause ?? (result.kind === "clip" ? result.endReason : void 0);
|
|
276
|
-
const outcome = result.kind === "clip" ? {
|
|
277
|
-
kind: "clip",
|
|
278
|
-
clip: {
|
|
279
|
-
output: result.output,
|
|
280
|
-
mimeType: result.mimeType,
|
|
281
|
-
durationMs: result.durationMs,
|
|
282
|
-
width: result.width,
|
|
283
|
-
height: result.height,
|
|
284
|
-
orientation: result.orientation,
|
|
285
|
-
hasAudio: result.hasAudio
|
|
286
|
-
},
|
|
287
|
-
endReason: cause ?? (result.limitReached ? "maximumDuration" : "stopped"),
|
|
288
|
-
partial: cause !== void 0 && cause !== "stopped" && cause !== "maximumDuration",
|
|
289
|
-
warning: result.warning
|
|
290
|
-
} : {
|
|
291
|
-
kind: "failed",
|
|
292
|
-
endReason: cause ?? "captureError",
|
|
293
|
-
stage: result.stage,
|
|
294
|
-
error: new CameraCaptureError(result.issue)
|
|
295
|
-
};
|
|
296
|
-
this.attempt = void 0;
|
|
297
|
-
current.resolve(outcome);
|
|
298
|
-
this.emit({
|
|
299
|
-
type: "finished",
|
|
300
|
-
attemptId: id,
|
|
301
|
-
outcome
|
|
302
|
-
});
|
|
303
|
-
if (this.disposeRequested) this.finishDispose();
|
|
304
|
-
else this.setState("idle");
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
const request = {
|
|
308
|
-
audio: this.config.audio === true,
|
|
309
|
-
orientation: options.orientation,
|
|
310
|
-
maximumDurationMs: options.maximumDurationMs,
|
|
311
|
-
destinationPath: options.destinationFileUrl?.startsWith("file://") ? decodeURIComponent(options.destinationFileUrl.slice(7)) : void 0
|
|
312
|
-
};
|
|
313
|
-
try {
|
|
314
|
-
this.backend.startCapture(request, sink);
|
|
315
|
-
} catch (cause) {
|
|
316
|
-
sink.settled({
|
|
317
|
-
kind: "failed",
|
|
318
|
-
stage: "preparation",
|
|
319
|
-
issue: cause instanceof CameraCaptureError ? cause : {
|
|
320
|
-
kind: "captureFailed",
|
|
321
|
-
operation: "startRecording",
|
|
322
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
323
|
-
cause
|
|
324
|
-
}
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
const backend = this.backend;
|
|
328
|
-
return {
|
|
329
|
-
id,
|
|
330
|
-
get state() {
|
|
331
|
-
return attempt.state;
|
|
332
|
-
},
|
|
333
|
-
get elapsedMs() {
|
|
334
|
-
return attempt.state === "recording" || attempt.state === "finalizing" ? backend.elapsedMs() : 0;
|
|
335
|
-
},
|
|
336
|
-
completion,
|
|
337
|
-
stop: () => {
|
|
338
|
-
if (this.attempt?.id === id) this.requestStop("stopped");
|
|
339
|
-
return completion;
|
|
340
|
-
}
|
|
341
|
-
};
|
|
342
|
-
}
|
|
343
|
-
openOutput(output) {
|
|
344
|
-
return this.backend.openOutput(output);
|
|
345
|
-
}
|
|
346
|
-
async dispose() {
|
|
347
|
-
if (this.disposed) return;
|
|
348
|
-
this.disposeRequested = true;
|
|
349
|
-
const attempt = this.attempt;
|
|
350
|
-
if (attempt && attempt.state !== "settled") {
|
|
351
|
-
this.requestStop("disposed");
|
|
352
|
-
await attempt.completion;
|
|
353
|
-
}
|
|
354
|
-
await this.finishDispose();
|
|
355
|
-
}
|
|
356
|
-
async finishDispose() {
|
|
357
|
-
if (this.disposed) return;
|
|
358
|
-
this.disposed = true;
|
|
359
|
-
this.backend.setObserver(void 0);
|
|
360
|
-
try {
|
|
361
|
-
await this.backend.release();
|
|
362
|
-
} finally {
|
|
363
|
-
this.setState("disposed");
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
//#endregion
|
|
368
|
-
export { SharedCameraSession };
|
package/dist/native/session.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { CameraCaptureError } from "./types.js";
|
|
2
|
-
import { SharedCameraSession } from "./session-core.js";
|
|
3
|
-
import { createSessionBackend } from "./src/session-backend";
|
|
4
|
-
//#region src/session.ts
|
|
5
|
-
/** Create a shared camera owner: one acquisition shared between a
|
|
6
|
-
* `CameraView` preview (`session` prop) and at most one movie-recording
|
|
7
|
-
* attempt. Creating a session never prompts for permission or acquires
|
|
8
|
-
* hardware — both happen when a session-backed preview activates or a
|
|
9
|
-
* permission request runs.
|
|
10
|
-
*
|
|
11
|
-
* Recording support is delivered per platform; `capabilities()` reports
|
|
12
|
-
* honest `supported`/`available` state on every host. */
|
|
13
|
-
function createCameraSession(config = {}) {
|
|
14
|
-
return new SharedCameraSession(createSessionBackend(config), config);
|
|
15
|
-
}
|
|
16
|
-
/** Package-internal: attach `host` as the session's preview surface.
|
|
17
|
-
* Used by `CameraView` when its `session` prop is set; detaching the
|
|
18
|
-
* returned function requests stop on an in-flight attempt and releases
|
|
19
|
-
* camera ownership once finalization settles. */
|
|
20
|
-
function attachSessionPreview(session, host, listener) {
|
|
21
|
-
if (!(session instanceof SharedCameraSession)) throw new CameraCaptureError({
|
|
22
|
-
kind: "invalidArgument",
|
|
23
|
-
operation: "attachPreview",
|
|
24
|
-
message: "session was not created by createCameraSession"
|
|
25
|
-
});
|
|
26
|
-
return session.attach(host, listener);
|
|
27
|
-
}
|
|
28
|
-
//#endregion
|
|
29
|
-
export { attachSessionPreview, createCameraSession };
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { SharedCameraSession } from "./session-core.js";
|
|
2
|
-
//#region src/synthetic-session.ts
|
|
3
|
-
/** In-memory storage for the seam — a `fileUrl`-shaped map so outcomes
|
|
4
|
-
* exercise the same reopen path without touching a real filesystem. */
|
|
5
|
-
function createMemoryCameraStorage() {
|
|
6
|
-
const files = /* @__PURE__ */ new Map();
|
|
7
|
-
return {
|
|
8
|
-
files,
|
|
9
|
-
write: (path, bytes) => {
|
|
10
|
-
files.set(path, bytes);
|
|
11
|
-
},
|
|
12
|
-
read: (path) => files.get(path),
|
|
13
|
-
delete: (path) => {
|
|
14
|
-
files.delete(path);
|
|
15
|
-
},
|
|
16
|
-
exists: (path) => files.has(path)
|
|
17
|
-
};
|
|
18
|
-
}
|
|
19
|
-
var defaultScript = () => ({
|
|
20
|
-
permission: {
|
|
21
|
-
camera: "granted",
|
|
22
|
-
microphone: "granted"
|
|
23
|
-
},
|
|
24
|
-
available: true,
|
|
25
|
-
startDelayMs: 20,
|
|
26
|
-
clipDurationMs: 3e3,
|
|
27
|
-
produceMedia: true
|
|
28
|
-
});
|
|
29
|
-
var capabilities = {
|
|
30
|
-
supported: true,
|
|
31
|
-
available: true,
|
|
32
|
-
cameras: [{
|
|
33
|
-
id: "synthetic-back",
|
|
34
|
-
label: "Synthetic back camera",
|
|
35
|
-
facing: "back"
|
|
36
|
-
}, {
|
|
37
|
-
id: "synthetic-front",
|
|
38
|
-
label: "Synthetic front camera",
|
|
39
|
-
facing: "front"
|
|
40
|
-
}],
|
|
41
|
-
profiles: [{
|
|
42
|
-
profile: "standard",
|
|
43
|
-
width: 1280,
|
|
44
|
-
height: 720,
|
|
45
|
-
frameRate: 30
|
|
46
|
-
}],
|
|
47
|
-
audio: true,
|
|
48
|
-
orientations: [
|
|
49
|
-
"portrait",
|
|
50
|
-
"portraitUpsideDown",
|
|
51
|
-
"landscapeLeft",
|
|
52
|
-
"landscapeRight"
|
|
53
|
-
],
|
|
54
|
-
durationLimit: "native",
|
|
55
|
-
elapsedTime: "nativeMedia",
|
|
56
|
-
output: {
|
|
57
|
-
mimeType: "video/quicktime",
|
|
58
|
-
container: "mov",
|
|
59
|
-
storage: "appPrivateFile",
|
|
60
|
-
destinationFileUrl: true
|
|
61
|
-
}
|
|
62
|
-
};
|
|
63
|
-
/** Create a `CameraSession` backed by the deterministic synthetic source.
|
|
64
|
-
* Marked for development and testing — never present this as live
|
|
65
|
-
* capture to users. */
|
|
66
|
-
function createSyntheticCameraSession(config = {}, script = {}, storage = createMemoryCameraStorage()) {
|
|
67
|
-
const controls = {
|
|
68
|
-
...defaultScript(),
|
|
69
|
-
...script
|
|
70
|
-
};
|
|
71
|
-
return new SharedCameraSession(createSyntheticBackend(controls, storage), config);
|
|
72
|
-
}
|
|
73
|
-
function createSyntheticBackend(script, storage) {
|
|
74
|
-
let observer;
|
|
75
|
-
let attached = false;
|
|
76
|
-
let sink;
|
|
77
|
-
let startedAt;
|
|
78
|
-
let timers = [];
|
|
79
|
-
let pendingRequest;
|
|
80
|
-
const later = (ms, fn) => {
|
|
81
|
-
timers.push(setTimeout(fn, ms));
|
|
82
|
-
};
|
|
83
|
-
const clearTimers = () => {
|
|
84
|
-
for (const timer of timers) clearTimeout(timer);
|
|
85
|
-
timers = [];
|
|
86
|
-
};
|
|
87
|
-
const settleClip = async (request, limitReached) => {
|
|
88
|
-
const path = request.destinationPath ?? `/synthetic/clip-${Date.now()}-${Math.random().toString(36).slice(2)}.mov`;
|
|
89
|
-
if (!script.produceMedia) {
|
|
90
|
-
sink?.settled({
|
|
91
|
-
kind: "failed",
|
|
92
|
-
stage: "capture",
|
|
93
|
-
issue: {
|
|
94
|
-
kind: "noMedia",
|
|
95
|
-
operation: "finalize",
|
|
96
|
-
message: "Synthetic source produced no media"
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (script.failAt === "finalize") {
|
|
102
|
-
sink?.settled({
|
|
103
|
-
kind: "failed",
|
|
104
|
-
stage: "finalization",
|
|
105
|
-
issue: {
|
|
106
|
-
kind: "finalizationFailed",
|
|
107
|
-
operation: "finalize",
|
|
108
|
-
message: "Synthetic finalization failure"
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
if (script.failAt === "storage") {
|
|
114
|
-
sink?.settled({
|
|
115
|
-
kind: "failed",
|
|
116
|
-
stage: "storage",
|
|
117
|
-
issue: {
|
|
118
|
-
kind: "storageFailed",
|
|
119
|
-
operation: "store",
|
|
120
|
-
message: "Synthetic storage failure"
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
try {
|
|
126
|
-
await storage.write(path, new Uint8Array([
|
|
127
|
-
120,
|
|
128
|
-
112,
|
|
129
|
-
108,
|
|
130
|
-
97,
|
|
131
|
-
116
|
|
132
|
-
]));
|
|
133
|
-
} catch (cause) {
|
|
134
|
-
sink?.settled({
|
|
135
|
-
kind: "failed",
|
|
136
|
-
stage: "storage",
|
|
137
|
-
issue: {
|
|
138
|
-
kind: "storageFailed",
|
|
139
|
-
operation: "store",
|
|
140
|
-
message: cause instanceof Error ? cause.message : String(cause),
|
|
141
|
-
cause
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const settle = {
|
|
147
|
-
kind: "clip",
|
|
148
|
-
output: {
|
|
149
|
-
kind: "nativeFile",
|
|
150
|
-
resourceId: `synthetic:${path}`,
|
|
151
|
-
fileUrl: `file://${path}`,
|
|
152
|
-
synthetic: true
|
|
153
|
-
},
|
|
154
|
-
mimeType: "video/quicktime",
|
|
155
|
-
durationMs: script.clipDurationMs,
|
|
156
|
-
width: 1280,
|
|
157
|
-
height: 720,
|
|
158
|
-
orientation: request.orientation ?? "portrait",
|
|
159
|
-
hasAudio: request.audio,
|
|
160
|
-
limitReached
|
|
161
|
-
};
|
|
162
|
-
sink?.settled(settle);
|
|
163
|
-
};
|
|
164
|
-
return {
|
|
165
|
-
platform: "synthetic",
|
|
166
|
-
supported: true,
|
|
167
|
-
checkPermission: async (kind) => script.permission[kind],
|
|
168
|
-
requestPermission: async (kind) => script.permission[kind],
|
|
169
|
-
getCapabilities: async () => ({
|
|
170
|
-
...capabilities,
|
|
171
|
-
available: script.available,
|
|
172
|
-
reason: script.available ? void 0 : "Synthetic camera is unavailable"
|
|
173
|
-
}),
|
|
174
|
-
previewReady: () => attached && script.available,
|
|
175
|
-
attachPreview: (_host, listener) => {
|
|
176
|
-
attached = true;
|
|
177
|
-
queueMicrotask(() => {
|
|
178
|
-
listener?.onReady?.();
|
|
179
|
-
observer?.previewReady(true);
|
|
180
|
-
});
|
|
181
|
-
return () => {
|
|
182
|
-
attached = false;
|
|
183
|
-
observer?.previewReady(false);
|
|
184
|
-
};
|
|
185
|
-
},
|
|
186
|
-
startCapture: (request, attemptSink) => {
|
|
187
|
-
sink = attemptSink;
|
|
188
|
-
pendingRequest = request;
|
|
189
|
-
if (!script.available) {
|
|
190
|
-
attemptSink.settled({
|
|
191
|
-
kind: "failed",
|
|
192
|
-
stage: "preparation",
|
|
193
|
-
issue: {
|
|
194
|
-
kind: "unavailable",
|
|
195
|
-
operation: "startRecording",
|
|
196
|
-
message: "Synthetic camera is unavailable"
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
if (script.failAt === "start") {
|
|
202
|
-
later(script.startDelayMs, () => {
|
|
203
|
-
attemptSink.settled({
|
|
204
|
-
kind: "failed",
|
|
205
|
-
stage: "preparation",
|
|
206
|
-
issue: {
|
|
207
|
-
kind: "captureFailed",
|
|
208
|
-
operation: "startRecording",
|
|
209
|
-
message: "Synthetic start failure"
|
|
210
|
-
}
|
|
211
|
-
});
|
|
212
|
-
});
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
later(script.startDelayMs, () => {
|
|
216
|
-
startedAt = Date.now();
|
|
217
|
-
attemptSink.started({
|
|
218
|
-
cameraId: "synthetic-back",
|
|
219
|
-
facing: "back",
|
|
220
|
-
audio: request.audio,
|
|
221
|
-
width: 1280,
|
|
222
|
-
height: 720,
|
|
223
|
-
frameRate: 30,
|
|
224
|
-
orientation: request.orientation ?? "portrait",
|
|
225
|
-
mimeType: "video/quicktime",
|
|
226
|
-
container: "mov"
|
|
227
|
-
});
|
|
228
|
-
if (script.interruption) later(script.interruption.afterMs, () => {
|
|
229
|
-
observer?.interruption("began", script.interruption?.reason);
|
|
230
|
-
if (script.interruption?.endsAfterMs !== void 0) later(script.interruption.endsAfterMs, () => {
|
|
231
|
-
observer?.interruption("ended", script.interruption?.reason);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
if (request.maximumDurationMs !== void 0) later(request.maximumDurationMs, () => {
|
|
235
|
-
settleClip(request, true);
|
|
236
|
-
});
|
|
237
|
-
if (script.failAt === "capture") later(50, () => {
|
|
238
|
-
attemptSink.settled({
|
|
239
|
-
kind: "failed",
|
|
240
|
-
stage: "capture",
|
|
241
|
-
issue: {
|
|
242
|
-
kind: "captureFailed",
|
|
243
|
-
operation: "record",
|
|
244
|
-
message: "Synthetic capture failure"
|
|
245
|
-
}
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
});
|
|
249
|
-
},
|
|
250
|
-
stopCapture: () => {
|
|
251
|
-
const request = pendingRequest;
|
|
252
|
-
later(5, () => {
|
|
253
|
-
if (request) settleClip(request, false);
|
|
254
|
-
});
|
|
255
|
-
},
|
|
256
|
-
elapsedMs: () => startedAt === void 0 ? 0 : Date.now() - startedAt,
|
|
257
|
-
openOutput: async (output) => {
|
|
258
|
-
const path = output.kind === "nativeFile" ? output.fileUrl.slice(7) : "";
|
|
259
|
-
if (!(storage.exists ? await storage.exists(path) : !!await storage.read(path))) throw new Error(`Synthetic clip is missing at ${output.resourceId}`);
|
|
260
|
-
return {
|
|
261
|
-
url: output.kind === "nativeFile" ? output.fileUrl : output.resourceId,
|
|
262
|
-
fileUrl: output.kind === "nativeFile" ? output.fileUrl : void 0,
|
|
263
|
-
release: () => {}
|
|
264
|
-
};
|
|
265
|
-
},
|
|
266
|
-
release: async () => {
|
|
267
|
-
attached = false;
|
|
268
|
-
clearTimers();
|
|
269
|
-
sink = void 0;
|
|
270
|
-
startedAt = void 0;
|
|
271
|
-
pendingRequest = void 0;
|
|
272
|
-
},
|
|
273
|
-
onPreviewDetached: () => {},
|
|
274
|
-
setObserver: (next) => {
|
|
275
|
-
observer = next;
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
}
|
|
279
|
-
//#endregion
|
|
280
|
-
export { createMemoryCameraStorage, createSyntheticCameraSession };
|
package/dist/native/types.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
//#region src/types.ts
|
|
2
|
-
/** Error thrown or settled by the session API. Satisfies `CaptureIssue`
|
|
3
|
-
* so `error.kind` drives control flow without unwrapping. */
|
|
4
|
-
var CameraCaptureError = class extends Error {
|
|
5
|
-
kind;
|
|
6
|
-
operation;
|
|
7
|
-
permission;
|
|
8
|
-
cause;
|
|
9
|
-
constructor(issue) {
|
|
10
|
-
super(issue.message);
|
|
11
|
-
this.name = "CameraCaptureError";
|
|
12
|
-
this.kind = issue.kind;
|
|
13
|
-
this.operation = issue.operation;
|
|
14
|
-
this.permission = issue.permission;
|
|
15
|
-
this.cause = issue.cause;
|
|
16
|
-
}
|
|
17
|
-
};
|
|
18
|
-
//#endregion
|
|
19
|
-
export { CameraCaptureError };
|