@octane-xplat/camera 0.13.0 → 0.14.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.
Files changed (81) hide show
  1. package/README.md +63 -3
  2. package/dist/linux/CameraView.web.tsrx.js +337 -0
  3. package/dist/linux/host-movie.web.js +100 -0
  4. package/dist/linux/index.js +5 -0
  5. package/dist/linux/layout-child.web.js +29 -0
  6. package/dist/linux/movie-metadata.web.js +65 -0
  7. package/dist/linux/octane-client-build.json +8 -0
  8. package/dist/linux/session-backend.linux.js +479 -0
  9. package/dist/linux/session-core.js +368 -0
  10. package/dist/linux/session.js +29 -0
  11. package/dist/linux/synthetic-session.js +280 -0
  12. package/dist/linux/types.js +19 -0
  13. package/dist/native/CameraView.android.js +25 -1
  14. package/dist/native/CameraView.ios.js +24 -1
  15. package/dist/native/CameraView.windows.js +173 -0
  16. package/dist/native/camera-permission.windows.js +43 -0
  17. package/dist/native/camera-session.windows.js +78 -0
  18. package/dist/native/cx.windows.js +9 -0
  19. package/dist/native/index.js +4 -1
  20. package/dist/native/layout-child.windows.js +53 -0
  21. package/dist/native/octane-client-build.json +1 -1
  22. package/dist/native/session-backend.android.js +234 -0
  23. package/dist/native/session-backend.ios.js +682 -0
  24. package/dist/native/session-backend.js +55 -0
  25. package/dist/native/session-backend.windows.js +905 -0
  26. package/dist/native/session-core.js +368 -0
  27. package/dist/native/session.js +29 -0
  28. package/dist/native/synthetic-session.js +280 -0
  29. package/dist/native/types.js +19 -0
  30. package/dist/web/CameraView.web.tsrx.js +27 -7
  31. package/dist/web/host-movie.web.js +100 -0
  32. package/dist/web/index.js +4 -1
  33. package/dist/web/movie-metadata.web.js +65 -0
  34. package/dist/web/movie-storage.web.js +119 -0
  35. package/dist/web/octane-client-build.json +1 -1
  36. package/dist/web/session-backend.web.js +490 -0
  37. package/dist/web/session-core.js +368 -0
  38. package/dist/web/session.js +29 -0
  39. package/dist/web/synthetic-session.js +280 -0
  40. package/dist/web/types.js +19 -0
  41. package/package.json +22 -4
  42. package/platforms/android/AndroidManifest.xml +2 -0
  43. package/platforms/android/include.gradle +1 -0
  44. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +4 -0
  45. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +363 -0
  46. package/platforms/ios/Info.plist +2 -0
  47. package/platforms/macos/src/XplatCameraHost.swift +513 -0
  48. package/src/CameraView.android.tsrx +21 -1
  49. package/src/CameraView.ios.tsrx +28 -2
  50. package/src/CameraView.macos.tsrx +93 -0
  51. package/src/CameraView.web.tsrx +20 -2
  52. package/src/CameraView.windows.tsrx +124 -0
  53. package/src/camera-permission.windows.ts +76 -0
  54. package/src/camera-session.macos.ts +75 -0
  55. package/src/camera-session.windows.ts +123 -0
  56. package/src/cx.macos.ts +5 -0
  57. package/src/cx.windows.ts +6 -0
  58. package/src/host-movie.web.ts +180 -0
  59. package/src/index.ts +31 -0
  60. package/src/index.web.ts +31 -0
  61. package/src/layout-child.windows.ts +79 -0
  62. package/src/movie-metadata.web.ts +84 -0
  63. package/src/movie-storage.web.ts +164 -0
  64. package/src/props.ts +7 -1
  65. package/src/session-backend.android.ts +397 -0
  66. package/src/session-backend.ios.ts +1092 -0
  67. package/src/session-backend.linux.ts +869 -0
  68. package/src/session-backend.macos.ts +624 -0
  69. package/src/session-backend.ts +57 -0
  70. package/src/session-backend.web.ts +874 -0
  71. package/src/session-backend.windows.ts +1517 -0
  72. package/src/session-core.ts +659 -0
  73. package/src/session.ts +37 -0
  74. package/src/synthetic-session.ts +367 -0
  75. package/src/types.ts +329 -0
  76. package/types/index.d.ts +43 -0
  77. package/types/props.d.ts +7 -1
  78. package/types/session-core.d.ts +129 -0
  79. package/types/synthetic-session.d.ts +48 -0
  80. package/types/types.d.ts +263 -0
  81. package/src/CameraView.windows.ts +0 -5
package/README.md CHANGED
@@ -5,7 +5,9 @@ pnpm add @octane-xplat/camera
5
5
  ```
6
6
 
7
7
  Live camera preview for Octane xplat apps: `getUserMedia` video on web,
8
- `AVCaptureSession` on iOS, CameraX on Android.
8
+ `AVCaptureSession` on iOS, CameraX on Android,
9
+ `AVCaptureSession` + `AVCaptureVideoPreviewLayer` on macOS (AppKit), and
10
+ `MediaCapture` + `CaptureElement` on the Windows (WinUI 3) target.
9
11
 
10
12
  ```tsx
11
13
  import { CameraView } from '@octane-xplat/camera'
@@ -26,8 +28,11 @@ This is the preview surface only — for one-shot still capture and library
26
28
  picking use [`@octane-xplat/media`](../media/README.md). Permissions are
27
29
  requested when the session starts; `onError` is how a denial shows up.
28
30
  `ref` hands you the platform surface (`HTMLVideoElement` on web) for
29
- controls the shared props don't cover, and the `ios`/`android`/`web`
30
- escape props apply after the shared props.
31
+ controls the shared props don't cover, and the `ios`/`android`/`web`/
32
+ `windows` escape props apply after the shared props. On macOS the leaf is a
33
+ compiled Swift `XplatCameraHost` that owns the capture session; the app bundle
34
+ needs `NSCameraUsageDescription` (and `NSMicrophoneUsageDescription` for
35
+ audio) declared through `xplat.targets.macos.package.infoPlist`.
31
36
 
32
37
  ```tsx
33
38
  // Preview.web.tsx — browser properties belong in this platform file.
@@ -44,6 +49,61 @@ export function Preview() {
44
49
  }
45
50
  ```
46
51
 
52
+ ## Movie recording
53
+
54
+ `createCameraSession` creates a shared camera owner: the same session can
55
+ drive a `CameraView` preview and one movie-recording attempt at a time.
56
+ Pass the session to `CameraView` instead of `facing`; the preview attaches
57
+ the camera the session owns. Recording requires an attached, ready preview.
58
+
59
+ ```tsx
60
+ import { CameraView, createCameraSession } from '@octane-xplat/camera'
61
+
62
+ const session = createCameraSession({ camera: 'default' })
63
+
64
+ export function Recorder() {
65
+ return <CameraView session={session} />
66
+ }
67
+ ```
68
+
69
+ A take settles after the movie is finalized, metadata is verified, and local
70
+ storage is committed. Native output lives in app-private files — including the
71
+ packaged Linux WebKitGTK host, which writes under the app's data directory;
72
+ Web uses persistent origin storage. Reopen it with `session.openOutput(output)`.
73
+
74
+ ```ts
75
+ const take = session.startRecording({ maximumDurationMs: 30_000 })
76
+ // … later, on a stop press or a maximum-duration limit:
77
+ const outcome = await take.stop()
78
+ if (outcome.kind === 'clip') {
79
+ const clip = outcome.clip
80
+ console.log(clip.durationMs, clip.width, clip.height, clip.output)
81
+ }
82
+ ```
83
+
84
+ One preview and one recording per session; a second `startRecording` throws
85
+ `CameraCaptureError` with `kind: 'busy'`. Check `session.capabilities()` for
86
+ `supported`/`available` before presenting capture UI, and `session.snapshot()`
87
+ or `session.subscribe` for state. iOS, Android, Web, macOS, the Windows WinUI
88
+ target, and the packaged Linux WebKitGTK host have recording adapters; Web
89
+ requires persistent origin storage.
90
+ macOS writes app-private MOV files through `AVCaptureMovieFileOutput` and
91
+ reports finalized track metadata; on macOS 14+ the four cardinal orientations
92
+ are lockable. Inside a qualified `xplat` WKWebView host the Web backend stores
93
+ output in the app's private storage root instead of IndexedDB. Windows records
94
+ through `MediaCapture` into app-private MP4 files and is pending real-host
95
+ qualification. On Linux the GJS/WebKitGTK host owns media permissions and
96
+ writes MP4 clips under the app's data directory.
97
+ See [Record and reopen a camera movie](../../docs/app/movie-capture.md) for
98
+ explicit permission actions, audio, local output, and qualification limits.
99
+
100
+ ```ts
101
+ console.log(await session.permissions())
102
+ // Call from an app button:
103
+ await session.requestPermission('camera')
104
+ console.log(await session.capabilities())
105
+ ```
106
+
47
107
  Guide: [Building screens](../../docs/app/primitives.md) (leaf components);
48
108
  component index: [`docs/app/components.md`](../../docs/app/components.md).
49
109
  Exercised by [`CameraDemo`](../demos/src/CameraDemo.tsrx).
@@ -0,0 +1,337 @@
1
+ import { attachSessionPreview } from "./session.js";
2
+ import { applyEscapeProps, layoutChildProps } from "./layout-child.web.js";
3
+ import { __s, bag15, bag5, child, clone, delegateEvents, evt2e, evt2u, hookSlots, ifBlock, markDangerouslySetInnerHTMLChildren, queueRefAttach, queueRefDetach, setStyleProperty, snapshotSpread, template, useEffect, useImperativeHandle, useRef } from "octane";
4
+ import { bindSignalHostPropSources, enableSignalBindings, replaceRef, setAttributeIfChanged } from "octane/internal/client";
5
+ //#region src/CameraView.web.tsrx
6
+ enableSignalBindings(1, true);
7
+ delegateEvents(["playing"]);
8
+ var _t$0 = /* @__PURE__ */ template("<video autoplay=\"\" playsinline=\"\" style=\"display: block; width: 100%; height: 100%; object-fit: cover;\"></video>");
9
+ var _t$1 = /* @__PURE__ */ template("<div><!></div>");
10
+ var _hs$ = /* @__PURE__ */ hookSlots(5);
11
+ var __event = (__eventArg, props, readyFired) => {
12
+ if (props.session || readyFired.current) return;
13
+ readyFired.current = true;
14
+ props.onReady?.();
15
+ };
16
+ var _rm$1 = [
17
+ "r",
18
+ "b",
19
+ "a"
20
+ ];
21
+ function __then$0(__props, __s, __extra) {
22
+ const facing = __extra[0];
23
+ const hostRef = __extra[1];
24
+ const props = __extra[2];
25
+ const readyFired = __extra[3];
26
+ const video = __extra[4];
27
+ let _b = __s.slots[0];
28
+ if (_b === void 0) {
29
+ let _m1, _m4;
30
+ const _root = clone(_t$0, void 0, "||");
31
+ queueRefAttach(__s, _m1 = (element) => {
32
+ hostRef.current = element;
33
+ video.current = element;
34
+ if (!element) return;
35
+ applyEscapeProps(element, props);
36
+ }, _root);
37
+ (__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
38
+ _m4 = evt2e(_root, "$oplaying", __event, props, readyFired);
39
+ _b = bag5(__s, _root, _root, _m1, void 0, __s, _m4);
40
+ __s.refFields = _rm$1;
41
+ } else {
42
+ {
43
+ const _r = (element) => {
44
+ hostRef.current = element;
45
+ video.current = element;
46
+ if (!element) return;
47
+ applyEscapeProps(element, props);
48
+ };
49
+ if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
50
+ }
51
+ evt2u(_b.e, __event, props, readyFired);
52
+ }
53
+ _b.c = setAttributeIfChanged(true, _b.c, _b.a, "muted");
54
+ {
55
+ const _v = facing === "front" ? "scaleX(-1)" : void 0;
56
+ if (_b.d !== _v) {
57
+ setStyleProperty(_b.a, "transform", _v, "display: block; width: 100%; height: 100%; object-fit: cover;", _b.d);
58
+ _b.d = _v;
59
+ }
60
+ }
61
+ }
62
+ var _rm$3 = [
63
+ "s",
64
+ "n",
65
+ "m"
66
+ ];
67
+ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
68
+ const hostRef = useRef(null, _hs$ + 0);
69
+ const video = useRef(null, _hs$ + 1);
70
+ const facing = props.facing ?? "back";
71
+ const active = props.active !== false;
72
+ const readyFired = useRef(false, _hs$ + 2);
73
+ useEffect(() => {
74
+ const element = video.current;
75
+ if (!element || !active) return;
76
+ if (props.session) {
77
+ if (props.facing !== void 0) {
78
+ props.onError?.({ message: "camera session conflicts with facing" });
79
+ return;
80
+ }
81
+ try {
82
+ return attachSessionPreview(props.session, element, {
83
+ onReady: props.onReady,
84
+ onError: (issue) => props.onError?.({ message: issue.message })
85
+ });
86
+ } catch (error) {
87
+ props.onError?.({ message: error?.message ?? "camera session error" });
88
+ return;
89
+ }
90
+ }
91
+ const mediaDevices = navigator.mediaDevices;
92
+ if (!mediaDevices?.getUserMedia) {
93
+ props.onError?.({ message: "camera unsupported" });
94
+ return;
95
+ }
96
+ let stream = null;
97
+ let disposed = false;
98
+ readyFired.current = false;
99
+ mediaDevices.getUserMedia({
100
+ video: { facingMode: facing === "front" ? "user" : "environment" },
101
+ audio: false
102
+ }).then((nextStream) => {
103
+ if (disposed) {
104
+ for (const track of nextStream.getTracks()) track.stop();
105
+ return;
106
+ }
107
+ stream = nextStream;
108
+ element.srcObject = nextStream;
109
+ element.play()?.catch(() => {});
110
+ }).catch((error) => {
111
+ props.onError?.({ message: error?.name === "NotAllowedError" ? "camera permission denied" : String(error?.name ?? error ?? "camera error") });
112
+ });
113
+ return () => {
114
+ disposed = true;
115
+ element.srcObject = null;
116
+ if (stream) for (const track of stream.getTracks()) track.stop();
117
+ };
118
+ }, [
119
+ active,
120
+ facing,
121
+ props.session
122
+ ], _hs$ + 3);
123
+ useImperativeHandle(props.ref, () => ({ native: hostRef.current }), void 0, _hs$ + 4);
124
+ let _b = __s.slots[0];
125
+ if (_b === void 0) {
126
+ let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m13, _m14;
127
+ const _root = clone(_t$1);
128
+ _m0 = props.id;
129
+ _m1 = [
130
+ "vx-layers",
131
+ "vx-cameraview",
132
+ props.className
133
+ ];
134
+ _m2 = snapshotSpread(layoutChildProps(props, {
135
+ position: "relative",
136
+ overflow: "hidden",
137
+ backgroundColor: "#000",
138
+ ...props.style
139
+ }));
140
+ _m3 = props.accessibilityRole;
141
+ _m4 = props.accessibilityLabel;
142
+ _m5 = props.accessible === false ? true : void 0;
143
+ _m6 = props.accessibilityHint;
144
+ _m7 = props.accessibilityValue;
145
+ _m8 = props.accessibilityState?.disabled;
146
+ _m9 = props.accessibilityState?.selected;
147
+ _m10 = props.accessibilityState?.checked;
148
+ _m11 = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
149
+ _m13 = bindSignalHostPropSources(__s, void 0, _root, [
150
+ [
151
+ false,
152
+ "id",
153
+ _m0
154
+ ],
155
+ [
156
+ false,
157
+ "className",
158
+ _m1
159
+ ],
160
+ [true, _m2],
161
+ [
162
+ false,
163
+ "role",
164
+ _m3
165
+ ],
166
+ [
167
+ false,
168
+ "aria-label",
169
+ _m4
170
+ ],
171
+ [
172
+ false,
173
+ "aria-hidden",
174
+ _m5
175
+ ],
176
+ [
177
+ false,
178
+ "aria-description",
179
+ _m6
180
+ ],
181
+ [
182
+ false,
183
+ "aria-valuetext",
184
+ _m7
185
+ ],
186
+ [
187
+ false,
188
+ "aria-disabled",
189
+ _m8
190
+ ],
191
+ [
192
+ false,
193
+ "aria-selected",
194
+ _m9
195
+ ],
196
+ [
197
+ false,
198
+ "aria-checked",
199
+ _m10
200
+ ],
201
+ [
202
+ false,
203
+ "aria-live",
204
+ _m11
205
+ ]
206
+ ], "b:d3c40afd", true);
207
+ markDangerouslySetInnerHTMLChildren(_root);
208
+ _m14 = child(_root);
209
+ _b = bag15(__s, _root, _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _root, _m13, _m14);
210
+ __s.refFields = _rm$3;
211
+ } else {
212
+ {
213
+ const _v = props.id;
214
+ if (_b.a !== _v) _b.a = _v;
215
+ }
216
+ {
217
+ const _v = [
218
+ "vx-layers",
219
+ "vx-cameraview",
220
+ props.className
221
+ ];
222
+ if (_b.b !== _v) _b.b = _v;
223
+ }
224
+ _b.c = snapshotSpread(layoutChildProps(props, {
225
+ position: "relative",
226
+ overflow: "hidden",
227
+ backgroundColor: "#000",
228
+ ...props.style
229
+ }));
230
+ {
231
+ const _v = props.accessibilityRole;
232
+ if (_b.d !== _v) _b.d = _v;
233
+ }
234
+ {
235
+ const _v = props.accessibilityLabel;
236
+ if (_b.e !== _v) _b.e = _v;
237
+ }
238
+ {
239
+ const _v = props.accessible === false ? true : void 0;
240
+ if (_b.f !== _v) _b.f = _v;
241
+ }
242
+ {
243
+ const _v = props.accessibilityHint;
244
+ if (_b.g !== _v) _b.g = _v;
245
+ }
246
+ {
247
+ const _v = props.accessibilityValue;
248
+ if (_b.h !== _v) _b.h = _v;
249
+ }
250
+ {
251
+ const _v = props.accessibilityState?.disabled;
252
+ if (_b.i !== _v) _b.i = _v;
253
+ }
254
+ {
255
+ const _v = props.accessibilityState?.selected;
256
+ if (_b.j !== _v) _b.j = _v;
257
+ }
258
+ {
259
+ const _v = props.accessibilityState?.checked;
260
+ if (_b.k !== _v) _b.k = _v;
261
+ }
262
+ {
263
+ const _v = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
264
+ if (_b.l !== _v) _b.l = _v;
265
+ }
266
+ _b.n = bindSignalHostPropSources(__s, _b.n, _b.m, [
267
+ [
268
+ false,
269
+ "id",
270
+ _b.a
271
+ ],
272
+ [
273
+ false,
274
+ "className",
275
+ _b.b
276
+ ],
277
+ [true, _b.c],
278
+ [
279
+ false,
280
+ "role",
281
+ _b.d
282
+ ],
283
+ [
284
+ false,
285
+ "aria-label",
286
+ _b.e
287
+ ],
288
+ [
289
+ false,
290
+ "aria-hidden",
291
+ _b.f
292
+ ],
293
+ [
294
+ false,
295
+ "aria-description",
296
+ _b.g
297
+ ],
298
+ [
299
+ false,
300
+ "aria-valuetext",
301
+ _b.h
302
+ ],
303
+ [
304
+ false,
305
+ "aria-disabled",
306
+ _b.i
307
+ ],
308
+ [
309
+ false,
310
+ "aria-selected",
311
+ _b.j
312
+ ],
313
+ [
314
+ false,
315
+ "aria-checked",
316
+ _b.k
317
+ ],
318
+ [
319
+ false,
320
+ "aria-live",
321
+ _b.l
322
+ ]
323
+ ], "b:d3c40afd", true);
324
+ }
325
+ {
326
+ const __ifCondition = active;
327
+ ifBlock(__s, 1, _b.m, __ifCondition, __then$0, null, _b.o, __ifCondition ? [
328
+ facing,
329
+ hostRef,
330
+ props,
331
+ readyFired,
332
+ video
333
+ ] : void 0, 1);
334
+ }
335
+ });
336
+ //#endregion
337
+ export { CameraView };
@@ -0,0 +1,100 @@
1
+ import { CameraCaptureError } from "./types.js";
2
+ import { createHostClient } from "@octane-xplat/platform/host";
3
+ import { createWebKitTransport } from "@octane-xplat/platform/host/web";
4
+ //#region src/host-movie.web.ts
5
+ var requiredMethods = [
6
+ "permissionStatus",
7
+ "requestPermission",
8
+ "movieDirectory",
9
+ "reserveMoviePath",
10
+ "writeMovieFile",
11
+ "movieFileInfo",
12
+ "readMovieFile",
13
+ "deleteMovieFile"
14
+ ];
15
+ var hostProbe;
16
+ /** The camera-capable desktop host client, or null outside a qualified host. */
17
+ function cameraHost() {
18
+ hostProbe ??= (async () => {
19
+ try {
20
+ const transport = createWebKitTransport();
21
+ if (!transport) return null;
22
+ const client = createHostClient(transport);
23
+ const methods = (await client.capabilities().catch(() => null))?.camera;
24
+ if (Array.isArray(methods) && requiredMethods.every((name) => methods.includes(name))) return client;
25
+ client.dispose();
26
+ return null;
27
+ } catch {
28
+ return null;
29
+ }
30
+ })();
31
+ return hostProbe;
32
+ }
33
+ var error = (kind, message) => new CameraCaptureError({
34
+ kind,
35
+ operation: "storage",
36
+ message
37
+ });
38
+ var blobToBase64 = async (bytes) => {
39
+ const data = new Uint8Array(await bytes.arrayBuffer());
40
+ let binary = "";
41
+ const chunk = 32768;
42
+ for (let offset = 0; offset < data.length; offset += chunk) binary += String.fromCharCode(...data.subarray(offset, offset + chunk));
43
+ return btoa(binary);
44
+ };
45
+ var base64ToBlob = (base64, type) => {
46
+ const binary = atob(base64);
47
+ const data = new Uint8Array(binary.length);
48
+ for (let index = 0; index < binary.length; index++) data[index] = binary.charCodeAt(index);
49
+ return new Blob([data], { type });
50
+ };
51
+ var mimeForFileUrl = (fileUrl) => fileUrl.endsWith(".webm") ? "video/webm" : fileUrl.endsWith(".mov") ? "video/quicktime" : "video/mp4";
52
+ /** Reserve a host-side movie file. `destinationFileUrl` is validated
53
+ * host-side: it must be a new file inside the app-private storage root. */
54
+ var reserveHostMovie = async (host, options) => {
55
+ const { reservation } = await host.call("camera", "reserveMoviePath", {
56
+ fileUrl: options.destinationFileUrl,
57
+ container: options.container
58
+ });
59
+ if (!reservation?.fileUrl) throw error("destinationUnavailable", options.destinationFileUrl ? "destinationFileUrl must be a new file inside the app-private recordings directory" : "The host could not allocate a movie recording file");
60
+ return reservation;
61
+ };
62
+ /** Commit finalized movie bytes to host-side app-private storage. */
63
+ var commitHostMovie = async (host, fileUrl, bytes) => {
64
+ const base64 = await blobToBase64(bytes);
65
+ const { file } = await host.call("camera", "writeMovieFile", {
66
+ fileUrl,
67
+ base64
68
+ });
69
+ if (!file?.uri) throw error("storageFailed", "The host could not write the recorded movie");
70
+ return {
71
+ kind: "nativeFile",
72
+ resourceId: file.uri,
73
+ fileUrl: file.uri
74
+ };
75
+ };
76
+ /** Drop an uncommitted host reservation after a failed attempt. */
77
+ var removeHostReservation = async (host, fileUrl) => {
78
+ await host.call("camera", "deleteMovieFile", { fileUrl }).catch(() => {});
79
+ };
80
+ /** Reopen a host-stored movie as a temporary playback URL. The durable
81
+ * reference remains `fileUrl`; the returned url lives until `release()`. */
82
+ var openHostMovie = async (host, output) => {
83
+ if (output.kind !== "nativeFile" || !output.fileUrl) throw new CameraCaptureError({
84
+ kind: "invalidArgument",
85
+ operation: "openOutput",
86
+ message: "Only file-backed output can be opened through the desktop host"
87
+ });
88
+ const { info } = await host.call("camera", "movieFileInfo", { fileUrl: output.fileUrl });
89
+ if (!info?.exists) throw error("unavailable", `Movie file is missing at ${output.fileUrl}`);
90
+ const { base64 } = await host.call("camera", "readMovieFile", { fileUrl: output.fileUrl });
91
+ if (!base64) throw error("unavailable", `Movie file could not be read at ${output.fileUrl}`);
92
+ const url = URL.createObjectURL(base64ToBlob(base64, mimeForFileUrl(output.fileUrl)));
93
+ return {
94
+ url,
95
+ fileUrl: output.fileUrl,
96
+ release: () => URL.revokeObjectURL(url)
97
+ };
98
+ };
99
+ //#endregion
100
+ export { cameraHost, commitHostMovie, openHostMovie, removeHostReservation, reserveHostMovie };
@@ -0,0 +1,5 @@
1
+ import { CameraCaptureError } from "./types.js";
2
+ import { createCameraSession } from "./session.js";
3
+ import { CameraView } from "./CameraView.web.tsrx.js";
4
+ import { createMemoryCameraStorage, createSyntheticCameraSession } from "./synthetic-session.js";
5
+ export { CameraCaptureError, CameraView, createCameraSession, createMemoryCameraStorage, createSyntheticCameraSession };
@@ -0,0 +1,29 @@
1
+ //#region src/layout-child.web.ts
2
+ function layoutChildProps(props, baseStyle = props.style) {
3
+ const style = { ...baseStyle };
4
+ if (props.row !== void 0) style.gridRow = `${props.row + 1} / span ${props.rowSpan ?? 1}`;
5
+ else if (props.rowSpan !== void 0) style.gridRow = `span ${props.rowSpan}`;
6
+ if (props.col !== void 0) style.gridColumn = `${props.col + 1} / span ${props.colSpan ?? 1}`;
7
+ else if (props.colSpan !== void 0) style.gridColumn = `span ${props.colSpan}`;
8
+ if (props.left !== void 0) style.left = props.left;
9
+ if (props.top !== void 0) style.top = props.top;
10
+ if (props.flexGrow !== void 0) style.flexGrow = props.flexGrow;
11
+ if (props.flexShrink !== void 0) style.flexShrink = props.flexShrink;
12
+ if (props.alignSelf !== void 0) style.alignSelf = props.alignSelf;
13
+ if (props.order !== void 0) style.order = props.order;
14
+ return { style };
15
+ }
16
+ function applyEscapeProps(element, props) {
17
+ if (!element || !props.web) return;
18
+ for (const [key, value] of Object.entries(props.web)) if (key.includes("-")) {
19
+ if (value == null || value === false) element.removeAttribute(key);
20
+ else element.setAttribute(key, value === true ? "" : String(value));
21
+ } else if (key in element) try {
22
+ element[key] = value;
23
+ } catch {
24
+ element.setAttribute(key, String(value));
25
+ }
26
+ else if (value != null) element.setAttribute(key, String(value));
27
+ }
28
+ //#endregion
29
+ export { applyEscapeProps, layoutChildProps };
@@ -0,0 +1,65 @@
1
+ import { CameraCaptureError } from "./types.js";
2
+ import { BlobSource, Input, MP4, WEBM } from "mediabunny";
3
+ //#region src/movie-metadata.web.ts
4
+ /** Read packet end timestamps and track metadata, never the wall-clock stop time. */
5
+ async function inspectMovie(bytes, audioRequested) {
6
+ if (!bytes.size) throw new CameraCaptureError({
7
+ kind: "noMedia",
8
+ operation: "finalize",
9
+ message: "The recorder produced no movie bytes"
10
+ });
11
+ const input = new Input({
12
+ source: new BlobSource(bytes),
13
+ formats: [WEBM, MP4]
14
+ });
15
+ const read = async () => {
16
+ const video = await input.getPrimaryVideoTrack();
17
+ const audio = await input.getPrimaryAudioTrack();
18
+ if (!video) throw new CameraCaptureError({
19
+ kind: "noMedia",
20
+ operation: "finalize",
21
+ message: "The movie contains no video track"
22
+ });
23
+ const videoDuration = await video.computeDuration();
24
+ const hasAudio = audio !== null && await audio.computeDuration() > 0;
25
+ const durationMs = await input.computeDuration() * 1e3;
26
+ const width = await video.getDisplayWidth();
27
+ const height = await video.getDisplayHeight();
28
+ if (!Number.isFinite(durationMs) || durationMs <= 0 || videoDuration <= 0 || width <= 0 || height <= 0) throw new CameraCaptureError({
29
+ kind: "noMedia",
30
+ operation: "finalize",
31
+ message: "The movie has no usable video media"
32
+ });
33
+ if (audioRequested && !hasAudio) throw new CameraCaptureError({
34
+ kind: "captureFailed",
35
+ operation: "finalize",
36
+ message: "Requested audio is absent from the finalized movie"
37
+ });
38
+ return {
39
+ durationMs,
40
+ width,
41
+ height,
42
+ hasAudio,
43
+ mimeType: await input.getMimeType(),
44
+ orientation: "unspecified"
45
+ };
46
+ };
47
+ let timer;
48
+ try {
49
+ return await Promise.race([read(), new Promise((_resolve, reject) => {
50
+ timer = setTimeout(() => {
51
+ input.dispose();
52
+ reject(new CameraCaptureError({
53
+ kind: "finalizationFailed",
54
+ operation: "metadata",
55
+ message: "Movie metadata verification timed out"
56
+ }));
57
+ }, 3e4);
58
+ })]);
59
+ } finally {
60
+ clearTimeout(timer);
61
+ input.dispose();
62
+ }
63
+ }
64
+ //#endregion
65
+ export { inspectMovie };
@@ -0,0 +1,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "13c3f733-39bd-4532-b458-4284c848f839",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }