@octane-xplat/camera 0.14.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.
Files changed (81) hide show
  1. package/README.md +26 -0
  2. package/package.json +22 -16
  3. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +9 -0
  4. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +31 -0
  5. package/platforms/macos/src/CameraOwnership.swift +22 -0
  6. package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
  7. package/platforms/macos/src/XplatCameraHost.swift +139 -13
  8. package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
  9. package/src/CameraView.android.tsrx +6 -2
  10. package/src/CameraView.ios.tsrx +4 -2
  11. package/src/CameraView.macos.tsrx +4 -2
  12. package/src/CameraView.web.tsrx +11 -0
  13. package/src/CameraView.windows.tsrx +4 -2
  14. package/src/camera-permission.mobile.ts +18 -4
  15. package/src/camera-session.windows.ts +4 -1
  16. package/src/host-movie.web.ts +141 -26
  17. package/src/index.ts +1 -0
  18. package/src/index.web.ts +1 -0
  19. package/src/preview-tap.ts +34 -0
  20. package/src/props.ts +8 -1
  21. package/src/session-backend.android.ts +42 -0
  22. package/src/session-backend.ios.ts +183 -17
  23. package/src/session-backend.linux.ts +22 -4
  24. package/src/session-backend.macos.ts +100 -17
  25. package/src/session-backend.ts +2 -0
  26. package/src/session-backend.web.ts +17 -0
  27. package/src/session-backend.windows.ts +88 -6
  28. package/src/session-core.ts +73 -9
  29. package/src/synthetic-session.ts +10 -0
  30. package/src/types.ts +28 -0
  31. package/types/props.d.ts +8 -1
  32. package/types/session-core.d.ts +11 -1
  33. package/types/synthetic-session.d.ts +4 -1
  34. package/types/types.d.ts +27 -0
  35. package/dist/linux/CameraView.web.tsrx.js +0 -337
  36. package/dist/linux/host-movie.web.js +0 -100
  37. package/dist/linux/index.js +0 -5
  38. package/dist/linux/layout-child.web.js +0 -29
  39. package/dist/linux/movie-metadata.web.js +0 -65
  40. package/dist/linux/octane-client-build.json +0 -8
  41. package/dist/linux/session-backend.linux.js +0 -479
  42. package/dist/linux/session-core.js +0 -368
  43. package/dist/linux/session.js +0 -29
  44. package/dist/linux/synthetic-session.js +0 -280
  45. package/dist/linux/types.js +0 -19
  46. package/dist/native/CameraView.android.js +0 -190
  47. package/dist/native/CameraView.ios.js +0 -154
  48. package/dist/native/CameraView.windows.js +0 -173
  49. package/dist/native/camera-permission.mobile.js +0 -45
  50. package/dist/native/camera-permission.windows.js +0 -43
  51. package/dist/native/camera-session.ios.js +0 -45
  52. package/dist/native/camera-session.windows.js +0 -78
  53. package/dist/native/cx.mobile.js +0 -9
  54. package/dist/native/cx.windows.js +0 -9
  55. package/dist/native/index.js +0 -5
  56. package/dist/native/layout-child.js +0 -53
  57. package/dist/native/layout-child.windows.js +0 -53
  58. package/dist/native/octane-client-build.json +0 -8
  59. package/dist/native/session-backend.android.js +0 -234
  60. package/dist/native/session-backend.ios.js +0 -682
  61. package/dist/native/session-backend.js +0 -55
  62. package/dist/native/session-backend.windows.js +0 -905
  63. package/dist/native/session-core.js +0 -368
  64. package/dist/native/session.js +0 -29
  65. package/dist/native/synthetic-session.js +0 -280
  66. package/dist/native/types.js +0 -19
  67. package/dist/web/CameraView.web.tsrx.js +0 -337
  68. package/dist/web/host-movie.web.js +0 -100
  69. package/dist/web/index.js +0 -5
  70. package/dist/web/layout-child.web.js +0 -29
  71. package/dist/web/movie-metadata.web.js +0 -65
  72. package/dist/web/movie-storage.web.js +0 -119
  73. package/dist/web/octane-client-build.json +0 -8
  74. package/dist/web/session-backend.web.js +0 -490
  75. package/dist/web/session-core.js +0 -368
  76. package/dist/web/session.js +0 -29
  77. package/dist/web/synthetic-session.js +0 -280
  78. package/dist/web/types.js +0 -19
  79. package/src/cx.macos.ts +0 -5
  80. package/src/cx.mobile.ts +0 -6
  81. package/src/cx.windows.ts +0 -6
@@ -1,78 +0,0 @@
1
- //#region src/camera-session.windows.ts
2
- /** Standalone (non-session) WinUI preview for `CameraView` without a
3
- * `session` prop. Owns a private `MediaCapture` bound to a
4
- * `CaptureElement`; the returned function stops preview and closes the
5
- * device. Session-backed previews share their capture through
6
- * `session-backend.windows.ts` instead. */
7
- var windows = () => globalThis;
8
- var toPromise = (op) => {
9
- const { NSWinRT } = windows();
10
- if (NSWinRT?.toPromise && op != null) return NSWinRT.toPromise(op);
11
- if (op?.done) return new Promise((resolve, reject) => {
12
- try {
13
- op.done(resolve, reject);
14
- } catch (cause) {
15
- reject(cause);
16
- }
17
- });
18
- return Promise.resolve(op);
19
- };
20
- var vectorToArray = (items) => {
21
- const result = [];
22
- const size = Number(items?.Size ?? items?.Count ?? 0);
23
- for (let index = 0; index < size; index += 1) result.push(items.GetAt(index));
24
- return result;
25
- };
26
- /** Attach a MediaCapture preview to the view's `CaptureElement`.
27
- * `facing` maps to enclosure panels; desktop cameras without a panel fall
28
- * back to the OS default — front/back is a preference, never a fabrication. */
29
- async function startCameraPreview(view, facing, onReady) {
30
- const { Windows } = windows();
31
- const enumeration = Windows?.Devices?.Enumeration;
32
- const captureApi = Windows?.Media?.Capture;
33
- if (!enumeration || !captureApi) throw new Error("camera preview unsupported");
34
- const devices = vectorToArray(await toPromise(enumeration.DeviceInformation.FindAllAsync(enumeration.DeviceClass.VideoCapture)));
35
- if (!devices.length) throw new Error("camera unavailable");
36
- const wantedPanel = facing === "front" ? 1 : 2;
37
- const device = devices.find((entry) => Number(entry?.EnclosureLocation?.Panel) === wantedPanel) ?? devices[0];
38
- const settings = new captureApi.MediaCaptureInitializationSettings();
39
- settings.VideoDeviceId = String(device?.Id ?? device?.id ?? "");
40
- settings.StreamingCaptureMode = 0;
41
- const capture = new captureApi.MediaCapture();
42
- try {
43
- await toPromise(capture.InitializeAsync(settings));
44
- } catch (cause) {
45
- try {
46
- capture.Close();
47
- } catch {
48
- try {
49
- capture.Dispose();
50
- } catch {}
51
- }
52
- throw cause instanceof Error ? cause : /* @__PURE__ */ new Error("camera unavailable");
53
- }
54
- const element = view?.nativeViewProtected ?? view;
55
- element.Source = capture;
56
- try {
57
- if (Number(device?.EnclosureLocation?.Panel) === 1 && typeof capture.SetPreviewMirroring === "function") capture.SetPreviewMirroring(true);
58
- } catch {}
59
- await toPromise(capture.StartPreviewAsync());
60
- onReady?.();
61
- return async () => {
62
- try {
63
- element.Source = null;
64
- } catch {}
65
- try {
66
- await toPromise(capture.StopPreviewAsync());
67
- } catch {}
68
- try {
69
- capture.Close();
70
- } catch {
71
- try {
72
- capture.Dispose();
73
- } catch {}
74
- }
75
- };
76
- }
77
- //#endregion
78
- export { startCameraPreview };
@@ -1,9 +0,0 @@
1
- //#region src/cx.mobile.ts
2
- /** Imperative `view.className` writes still need a pre-joined string —
3
- * NativeScript's className setter splits on ' ' only. Normalize arrays here
4
- * so they stay space separated. */
5
- function cx(...parts) {
6
- return parts.flat(Infinity).filter(Boolean).join(" ");
7
- }
8
- //#endregion
9
- export { cx };
@@ -1,9 +0,0 @@
1
- //#region src/cx.windows.ts
2
- /** Imperative `view.className` writes still need a pre-joined string —
3
- * NativeScript's className setter splits on ' ' only. Normalize arrays here
4
- * so they stay space separated. */
5
- function cx(...parts) {
6
- return parts.flat(Infinity).filter(Boolean).join(" ");
7
- }
8
- //#endregion
9
- export { cx };
@@ -1,5 +0,0 @@
1
- import { CameraCaptureError } from "./types.js";
2
- import { createCameraSession } from "./session.js";
3
- import { createMemoryCameraStorage, createSyntheticCameraSession } from "./synthetic-session.js";
4
- import { CameraView } from "./src/CameraView";
5
- export { CameraCaptureError, CameraView, createCameraSession, createMemoryCameraStorage, createSyntheticCameraSession };
@@ -1,53 +0,0 @@
1
- import { isIOS } from "@nativescript/core";
2
- //#region src/layout-child.ts
3
- /** NativeScript reads layout fields from a child view during parent layout. */
4
- function layoutChildProps(props) {
5
- const result = {};
6
- for (const key of [
7
- "row",
8
- "col",
9
- "rowSpan",
10
- "colSpan",
11
- "dock",
12
- "left",
13
- "top",
14
- "flexGrow",
15
- "flexShrink",
16
- "alignSelf",
17
- "order"
18
- ]) if (props[key] !== void 0) result[key] = props[key];
19
- return result;
20
- }
21
- function applyEscapeProps(view, props) {
22
- if (!view) return;
23
- const bag = isIOS ? props.ios : props.android;
24
- if (bag) Object.assign(view, bag);
25
- }
26
- var nativeRoles = {
27
- button: "button",
28
- link: "link",
29
- search: "search",
30
- image: "image",
31
- heading: "header",
32
- adjustable: "adjustable",
33
- summary: "summary",
34
- text: "text",
35
- none: "none",
36
- progressbar: "progressBar",
37
- checkbox: "checkbox",
38
- switch: "switch",
39
- radio: "radioButton",
40
- spinbutton: "spinButton",
41
- tab: "button"
42
- };
43
- function nativeAccessibilityRole(role) {
44
- return role ? nativeRoles[role] : void 0;
45
- }
46
- function nativeAccessibilityState(state) {
47
- if (state?.disabled) return "disabled";
48
- if (state?.selected) return "selected";
49
- if (state?.checked === true) return "checked";
50
- if (state?.checked === false) return "unchecked";
51
- }
52
- //#endregion
53
- export { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState };
@@ -1,53 +0,0 @@
1
- //#region src/layout-child.windows.ts
2
- /** Windows twins of the layout-child helpers — the unsuffixed module
3
- * resolves the platform escape bag through `isIOS`, which has no Windows
4
- * branch. Pure helpers are re-declared here to keep divergence at the file
5
- * boundary. */
6
- function layoutChildProps(props) {
7
- const result = {};
8
- for (const key of [
9
- "row",
10
- "col",
11
- "rowSpan",
12
- "colSpan",
13
- "dock",
14
- "left",
15
- "top",
16
- "flexGrow",
17
- "flexShrink",
18
- "alignSelf",
19
- "order"
20
- ]) if (props[key] !== void 0) result[key] = props[key];
21
- return result;
22
- }
23
- function applyEscapeProps(view, props) {
24
- if (view && props.windows) Object.assign(view, props.windows);
25
- }
26
- var nativeRoles = {
27
- button: "button",
28
- link: "link",
29
- search: "search",
30
- image: "image",
31
- heading: "header",
32
- adjustable: "adjustable",
33
- summary: "summary",
34
- text: "text",
35
- none: "none",
36
- progressbar: "progressBar",
37
- checkbox: "checkbox",
38
- switch: "switch",
39
- radio: "radioButton",
40
- spinbutton: "spinButton",
41
- tab: "button"
42
- };
43
- function nativeAccessibilityRole(role) {
44
- return role ? nativeRoles[role] : void 0;
45
- }
46
- function nativeAccessibilityState(state) {
47
- if (state?.disabled) return "disabled";
48
- if (state?.selected) return "selected";
49
- if (state?.checked === true) return "checked";
50
- if (state?.checked === false) return "unchecked";
51
- }
52
- //#endregion
53
- export { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState };
@@ -1,8 +0,0 @@
1
- {
2
- "version": 1,
3
- "buildId": "1f1a64b1-cefc-4981-aaa1-ec37fe3f7c71",
4
- "mode": "production",
5
- "capabilities": {
6
- "independentHydration": false
7
- }
8
- }
@@ -1,234 +0,0 @@
1
- import { CameraCaptureError } from "./types.js";
2
- import { Application } from "@nativescript/core";
3
- //#region src/session-backend.android.ts
4
- var requestCode = 7820;
5
- var permissionChain = Promise.resolve();
6
- var owner;
7
- var nativeBridge = () => globalThis.com?.octanexplat?.camera?.CameraSessionBridge;
8
- var permissionName = (kind) => kind === "camera" ? "android.permission.CAMERA" : "android.permission.RECORD_AUDIO";
9
- var error = (kind, operation, message, cause) => new CameraCaptureError({
10
- kind,
11
- operation,
12
- message,
13
- cause
14
- });
15
- var context = () => Application.android.context;
16
- var preferences = () => context().getSharedPreferences("octane-camera-permissions", 0);
17
- async function checkPermission(kind) {
18
- const activity = Application.android.foregroundActivity ?? Application.android.startActivity;
19
- if (!activity) return "unavailable";
20
- const permission = permissionName(kind);
21
- if (activity.checkSelfPermission(permission) === 0) return "granted";
22
- return preferences().getBoolean(kind, false) ? activity.shouldShowRequestPermissionRationale(permission) ? "denied" : "blocked" : "notDetermined";
23
- }
24
- function requestPermission(kind) {
25
- const result = permissionChain.then(async () => {
26
- const status = await checkPermission(kind);
27
- if (status !== "notDetermined") return status;
28
- const activity = Application.android.foregroundActivity ?? Application.android.startActivity;
29
- if (!activity) return "unavailable";
30
- return new Promise((resolve) => {
31
- const code = requestCode++;
32
- const finish = (value) => {
33
- clearTimeout(timer);
34
- Application.android.off(Application.android.activityRequestPermissionsEvent, onResult);
35
- resolve(value);
36
- };
37
- const onResult = (args) => {
38
- if (args.requestCode !== code) return;
39
- if (!args.grantResults?.length) {
40
- finish("unknown");
41
- return;
42
- }
43
- preferences().edit().putBoolean(kind, true).apply();
44
- finish(args.grantResults[0] === 0 ? "granted" : activity.shouldShowRequestPermissionRationale(permissionName(kind)) ? "denied" : "blocked");
45
- };
46
- const timer = setTimeout(() => finish("unknown"), 12e4);
47
- Application.android.on(Application.android.activityRequestPermissionsEvent, onResult);
48
- try {
49
- activity.requestPermissions([permissionName(kind)], code);
50
- } catch {
51
- finish("unavailable");
52
- }
53
- });
54
- });
55
- permissionChain = result.catch(() => {});
56
- return result;
57
- }
58
- function createSessionBackend(config) {
59
- let currentConfig = { ...config };
60
- let observer;
61
- let bridge;
62
- let host;
63
- let listener;
64
- let sink;
65
- let released = false;
66
- const lease = {};
67
- const selected = () => typeof currentConfig.camera === "object" ? `id:${currentConfig.camera.deviceId}` : currentConfig.camera ?? "default";
68
- const releaseLease = () => {
69
- if (owner === lease) owner = void 0;
70
- };
71
- const onSuspend = () => bridge?.background();
72
- const onResume = () => bridge?.foreground();
73
- const ensureBridge = () => {
74
- if (bridge) return;
75
- const Bridge = nativeBridge();
76
- if (!Bridge) throw error("unsupportedPlatform", "preview", "CameraX video bridge is missing from the native build");
77
- bridge = new Bridge(context(), new Bridge.Listener({ event(type, payload) {
78
- if (type === "ready") {
79
- observer?.previewReady(payload === "true");
80
- if (payload === "true") listener?.onReady?.();
81
- } else if (type === "availability") observer?.availability(payload === "true");
82
- else if (type === "interruption") observer?.interruption("began", payload);
83
- else if (type === "recovered") observer?.interruption("ended", payload);
84
- else if (type === "previewError") {
85
- observer?.availability(false);
86
- listener?.onError?.(error("unavailable", "preview", payload));
87
- if (!host) releaseLease();
88
- } else if (type === "finishing") sink?.finishing(payload ? payload : void 0);
89
- else if (type === "started") sink?.started(JSON.parse(payload));
90
- else if (type === "finished" || type === "failed") {
91
- const target = sink;
92
- sink = void 0;
93
- if (!host) releaseLease();
94
- const result = JSON.parse(payload);
95
- target?.settled(type === "finished" ? result : {
96
- kind: "failed",
97
- stage: result.stage,
98
- issue: error(result.kind, result.stage, result.message)
99
- });
100
- }
101
- } }));
102
- Application.on(Application.suspendEvent, onSuspend);
103
- Application.on(Application.resumeEvent, onResume);
104
- };
105
- return {
106
- platform: "android",
107
- get supported() {
108
- return !!nativeBridge();
109
- },
110
- checkPermission,
111
- requestPermission: async (kind) => {
112
- const status = await requestPermission(kind);
113
- if (kind === "camera" && status === "granted" && host && !sink && !released && !bridge?.isReady()) try {
114
- ensureBridge();
115
- bridge.attach(host, selected());
116
- } catch (cause) {
117
- observer?.availability(false);
118
- listener?.onError?.(error("unavailable", "preview", "Could not activate CameraX after permission approval", cause));
119
- }
120
- return status;
121
- },
122
- getCapabilities: async () => {
123
- const value = bridge ? JSON.parse(bridge.capabilities()) : {
124
- cameras: [],
125
- profiles: []
126
- };
127
- const supported = !!nativeBridge();
128
- return {
129
- supported,
130
- available: !!bridge?.isReady(),
131
- reason: !supported ? "CameraX video bridge is missing from the native build" : !bridge?.isReady() ? "An active permitted preview is required" : void 0,
132
- cameras: value.cameras,
133
- profiles: value.profiles,
134
- audio: context().getPackageManager().hasSystemFeature("android.hardware.microphone"),
135
- orientations: [],
136
- durationLimit: "native",
137
- elapsedTime: "nativeMedia",
138
- output: {
139
- mimeType: "video/mp4",
140
- container: "mp4",
141
- storage: "appPrivateFile",
142
- destinationFileUrl: true
143
- }
144
- };
145
- },
146
- previewReady: () => !!bridge?.isReady(),
147
- validateStart: (options) => {
148
- if (options.maximumDurationMs !== void 0 && options.maximumDurationMs > 9223372036854) throw error("unsupportedConfiguration", "startRecording", "The duration exceeds CameraX media-time range");
149
- if (options.orientation && options.orientation !== "unspecified") throw error("unsupportedConfiguration", "startRecording", "Fixed cardinal orientation has not been qualified on this Android source");
150
- },
151
- attachPreview: (target, callbacks) => {
152
- if (released || sink) throw error("invalidState", "attachPreview", "The camera is disposed or finalizing");
153
- if (owner && owner !== lease) throw error("busy", "attachPreview", "Another camera session is still finalizing");
154
- owner = lease;
155
- host = target;
156
- listener = callbacks;
157
- try {
158
- ensureBridge();
159
- } catch (cause) {
160
- host = void 0;
161
- listener = void 0;
162
- releaseLease();
163
- throw error("unavailable", "attachPreview", "Could not construct the CameraX native bridge", cause);
164
- }
165
- checkPermission("camera").then((status) => {
166
- if (released || !host) return;
167
- if (status !== "granted") {
168
- listener?.onError?.(error("permissionDenied", "preview", "Request camera permission before activating this preview"));
169
- return;
170
- }
171
- try {
172
- bridge.attach(host, selected());
173
- } catch (cause) {
174
- listener?.onError?.(error("unavailable", "preview", "Could not bind shared CameraX preview", cause));
175
- }
176
- });
177
- return () => {
178
- host = void 0;
179
- listener = void 0;
180
- bridge.detach();
181
- if (!sink) releaseLease();
182
- };
183
- },
184
- configure: (next) => {
185
- const sourceChanged = JSON.stringify(next.camera) !== JSON.stringify(currentConfig.camera) || next.profile !== currentConfig.profile;
186
- currentConfig = { ...next };
187
- if (host && sourceChanged) {
188
- bridge.detach();
189
- bridge.attach(host, selected());
190
- }
191
- },
192
- startCapture: (request, target) => {
193
- sink = target;
194
- try {
195
- bridge.start(request.audio, request.destinationPath ?? "", request.maximumDurationMs === void 0 ? 0 : Math.max(1, Math.ceil(request.maximumDurationMs)));
196
- } catch (cause) {
197
- sink = void 0;
198
- target.settled({
199
- kind: "failed",
200
- stage: "preparation",
201
- issue: error("captureFailed", "startRecording", "CameraX could not begin recording", cause)
202
- });
203
- }
204
- },
205
- stopCapture: () => bridge?.stop(),
206
- elapsedMs: () => Number(bridge?.elapsedMs() ?? 0),
207
- openOutput: async (output) => {
208
- if (output.kind !== "nativeFile" || output.synthetic) throw error("invalidArgument", "openOutput", "Expected a native Android movie reference");
209
- try {
210
- const url = nativeBridge().openOutput(output.resourceId, output.fileUrl);
211
- return {
212
- url,
213
- fileUrl: url,
214
- release: () => {}
215
- };
216
- } catch (cause) {
217
- throw error("destinationUnavailable", "openOutput", "The stored Android movie cannot be reopened", cause);
218
- }
219
- },
220
- release: async () => {
221
- released = true;
222
- Application.off(Application.suspendEvent, onSuspend);
223
- Application.off(Application.resumeEvent, onResume);
224
- bridge?.release();
225
- host = void 0;
226
- releaseLease();
227
- },
228
- setObserver: (value) => {
229
- observer = value;
230
- }
231
- };
232
- }
233
- //#endregion
234
- export { createSessionBackend };