@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.
Files changed (58) hide show
  1. package/README.md +89 -3
  2. package/package.json +35 -11
  3. package/platforms/android/AndroidManifest.xml +2 -0
  4. package/platforms/android/include.gradle +1 -0
  5. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +13 -0
  6. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
  7. package/platforms/ios/Info.plist +2 -0
  8. package/platforms/macos/src/CameraOwnership.swift +22 -0
  9. package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
  10. package/platforms/macos/src/XplatCameraHost.swift +639 -0
  11. package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
  12. package/src/CameraView.android.tsrx +27 -3
  13. package/src/CameraView.ios.tsrx +32 -4
  14. package/src/CameraView.macos.tsrx +95 -0
  15. package/src/CameraView.web.tsrx +31 -2
  16. package/src/CameraView.windows.tsrx +126 -0
  17. package/src/camera-permission.mobile.ts +18 -4
  18. package/src/camera-permission.windows.ts +76 -0
  19. package/src/camera-session.macos.ts +75 -0
  20. package/src/camera-session.windows.ts +126 -0
  21. package/src/host-movie.web.ts +295 -0
  22. package/src/index.ts +32 -0
  23. package/src/index.web.ts +32 -0
  24. package/src/layout-child.windows.ts +79 -0
  25. package/src/movie-metadata.web.ts +84 -0
  26. package/src/movie-storage.web.ts +164 -0
  27. package/src/preview-tap.ts +34 -0
  28. package/src/props.ts +14 -1
  29. package/src/session-backend.android.ts +439 -0
  30. package/src/session-backend.ios.ts +1258 -0
  31. package/src/session-backend.linux.ts +887 -0
  32. package/src/session-backend.macos.ts +707 -0
  33. package/src/session-backend.ts +59 -0
  34. package/src/session-backend.web.ts +891 -0
  35. package/src/session-backend.windows.ts +1599 -0
  36. package/src/session-core.ts +723 -0
  37. package/src/session.ts +37 -0
  38. package/src/synthetic-session.ts +377 -0
  39. package/src/types.ts +357 -0
  40. package/types/index.d.ts +43 -0
  41. package/types/props.d.ts +14 -1
  42. package/types/session-core.d.ts +139 -0
  43. package/types/synthetic-session.d.ts +51 -0
  44. package/types/types.d.ts +290 -0
  45. package/dist/native/CameraView.android.js +0 -166
  46. package/dist/native/CameraView.ios.js +0 -131
  47. package/dist/native/camera-permission.mobile.js +0 -45
  48. package/dist/native/camera-session.ios.js +0 -45
  49. package/dist/native/cx.mobile.js +0 -9
  50. package/dist/native/index.js +0 -2
  51. package/dist/native/layout-child.js +0 -53
  52. package/dist/native/octane-client-build.json +0 -8
  53. package/dist/web/CameraView.web.tsrx.js +0 -317
  54. package/dist/web/index.js +0 -2
  55. package/dist/web/layout-child.web.js +0 -29
  56. package/dist/web/octane-client-build.json +0 -8
  57. package/src/CameraView.windows.ts +0 -5
  58. package/src/cx.mobile.ts +0 -6
@@ -0,0 +1,290 @@
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
+ /** Permission resource a `CameraSession` can inspect or request. */
15
+ export type CameraPermissionKind = 'camera' | 'microphone';
16
+ /** Portable authorization state. Checking never prompts. `blocked` and
17
+ * `unknown` exist so hosts that cannot distinguish user denial from
18
+ * policy refusal do not fabricate `restricted`. */
19
+ export type CameraPermissionStatus = 'notDetermined' | 'granted' | 'denied' | 'restricted' | 'blocked' | 'unknown' | 'unavailable';
20
+ /** Portable error categories. `unsupported*` covers missing host features,
21
+ * devices, or capture configurations — including adapters that are not
22
+ * implemented yet during staged delivery. */
23
+ export type CameraCaptureErrorKind = 'permissionDenied' | 'permissionRestricted' | 'permissionBlocked' | 'unsupportedPlatform' | 'unsupportedConfiguration' | 'unavailable' | 'busy' | 'invalidState' | 'invalidArgument' | 'destinationUnavailable' | 'insufficientStorage' | 'noMedia' | 'captureFailed' | 'finalizationFailed' | 'storageFailed' | 'configurationMissing';
24
+ /** Structured diagnostic carried by failures and non-fatal warnings. */
25
+ export interface CaptureIssue {
26
+ kind: CameraCaptureErrorKind;
27
+ /** Operation being attempted, e.g. 'startRecording', 'finalize'. */
28
+ operation: string;
29
+ message: string;
30
+ permission?: CameraPermissionKind;
31
+ /** Host-specific detail for logging; never required for control flow. */
32
+ cause?: unknown;
33
+ }
34
+ /** Error thrown or settled by the session API. Satisfies `CaptureIssue`
35
+ * so `error.kind` drives control flow without unwrapping. */
36
+ export declare class CameraCaptureError extends Error implements CaptureIssue {
37
+ kind: CameraCaptureErrorKind;
38
+ operation: string;
39
+ permission?: CameraPermissionKind;
40
+ cause?: unknown;
41
+ constructor(issue: Omit<CaptureIssue, 'cause'> & {
42
+ cause?: unknown;
43
+ });
44
+ }
45
+ export type CameraFacing = 'front' | 'back';
46
+ /** A normalized position on the live preview as the user sees it:
47
+ * `{x: 0, y: 0}` is the top-left corner and `{x: 1, y: 1}` is
48
+ * bottom-right, independent of sensor orientation and front-camera
49
+ * mirroring. The `CameraView` `onTap` event and
50
+ * `CameraSession.focusAtPoint` share this space. */
51
+ export interface CameraPreviewPoint {
52
+ x: number;
53
+ y: number;
54
+ }
55
+ export type CameraOrientation = 'portrait' | 'portraitUpsideDown' | 'landscapeLeft' | 'landscapeRight' | 'unspecified';
56
+ /** Camera selection for `CameraSessionConfig.camera`. Desktop and browser
57
+ * hosts expose device identities instead of front/back semantics. */
58
+ export type CameraSelection = 'default' | 'front' | 'back' | {
59
+ deviceId: string;
60
+ };
61
+ export interface CameraSessionConfig {
62
+ /** Lens or device identity. Default `'default'`: rear when available. */
63
+ camera?: CameraSelection;
64
+ /** Include an audio track; requires microphone permission. Default
65
+ * false — silent capture is a deliberate app choice, never a
66
+ * fallback from denied microphone access. */
67
+ audio?: boolean;
68
+ /** Capture profile. `'standard'` targets 720p at 30 fps and may select
69
+ * a device-supported alternative; the started attempt discloses the
70
+ * actual configuration. */
71
+ profile?: 'standard';
72
+ }
73
+ /** Resolution/frame-rate combination advertised by capability discovery.
74
+ * Entries are supported combinations, not independent lists. */
75
+ export interface CameraProfileCapability {
76
+ profile: 'standard';
77
+ width: number;
78
+ height: number;
79
+ frameRate: number;
80
+ }
81
+ export interface CameraDeviceInfo {
82
+ id: string;
83
+ label: string;
84
+ facing?: CameraFacing;
85
+ }
86
+ /** What the session's host can do right now. `supported: false` means the
87
+ * adapter or host cannot record (reason explains); `available` reflects
88
+ * current hardware/permission state and changes over time. */
89
+ export interface CameraSessionCapabilities {
90
+ supported: boolean;
91
+ /** Why capture is unsupported or currently unavailable. */
92
+ reason?: string;
93
+ available: boolean;
94
+ cameras: CameraDeviceInfo[];
95
+ profiles: CameraProfileCapability[];
96
+ /** The session can add an audio track when permission allows. */
97
+ audio: boolean;
98
+ /** `focusAtPoint` can move autofocus metering on the active camera —
99
+ * false on fixed-focus devices (common for front cameras) and on
100
+ * platforms without point-of-interest metering. */
101
+ focusPointOfInterest: boolean;
102
+ /** `focusAtPoint` can move auto-exposure metering on the active
103
+ * camera. */
104
+ exposurePointOfInterest: boolean;
105
+ /** Fixed orientations the source can lock for a take. */
106
+ orientations: CameraOrientation[];
107
+ /** `native` enforcement can still overshoot at media boundaries;
108
+ * `bestEffort` limits are scheduled and may overshoot more. */
109
+ durationLimit: 'native' | 'bestEffort';
110
+ /** Where `elapsedMs` comes from while recording. */
111
+ elapsedTime: 'nativeMedia' | 'estimated';
112
+ output: {
113
+ mimeType: string;
114
+ container: string;
115
+ /** `appPrivateFile` output survives dispose and app restart;
116
+ * `originLocal` is browser storage subject to site-data clearing. */
117
+ storage: 'appPrivateFile' | 'originLocal';
118
+ /** Accepts an app-supplied `destinationFileUrl` on start. */
119
+ destinationFileUrl: boolean;
120
+ };
121
+ }
122
+ export type CameraRecorderState = 'idle' | 'starting' | 'recording' | 'finalizing' | 'disposed';
123
+ /** Frozen capture configuration reported once capture actually begins. */
124
+ export interface EffectiveCaptureConfig {
125
+ cameraId: string;
126
+ facing?: CameraFacing;
127
+ audio: boolean;
128
+ width: number;
129
+ height: number;
130
+ frameRate: number;
131
+ orientation: CameraOrientation;
132
+ mimeType: string;
133
+ container: string;
134
+ }
135
+ export interface CameraSessionSnapshot {
136
+ state: CameraRecorderState;
137
+ /** Non-prompting last-known authorization, refreshed by `permissions()`
138
+ * and `requestPermission()`. */
139
+ permission: Record<CameraPermissionKind, CameraPermissionStatus>;
140
+ /** Camera hardware/driver availability, separate from recorder state. */
141
+ cameraAvailable: boolean;
142
+ /** An active preview view is attached to this session. */
143
+ previewAttached: boolean;
144
+ interrupted: boolean;
145
+ attempt?: {
146
+ id: string;
147
+ state: 'starting' | 'recording' | 'finalizing';
148
+ elapsedMs: number;
149
+ configuration?: EffectiveCaptureConfig;
150
+ };
151
+ }
152
+ /** What ended a capture attempt. `interrupted` covers system suspension
153
+ * and preview teardown; `previewReleased` distinguishes an app-driven
154
+ * deactivation from an OS interruption. */
155
+ export type MovieEndReason = 'stopped' | 'maximumDuration' | 'interrupted' | 'backgrounded' | 'cameraUnavailable' | 'previewReleased' | 'disposed' | 'captureError';
156
+ /** Portable completed-output reference. `resourceId` reopens the clip
157
+ * through `openOutput`; `fileUrl` additionally guarantees file-backed
158
+ * native output. `synthetic` marks test-seam clips. */
159
+ export type MovieOutput = {
160
+ kind: 'nativeFile';
161
+ resourceId: string;
162
+ fileUrl: string;
163
+ synthetic?: boolean;
164
+ } | {
165
+ kind: 'browserStorage';
166
+ resourceId: string;
167
+ retention: 'persistent' | 'bestEffort';
168
+ synthetic?: boolean;
169
+ };
170
+ export interface MovieClip {
171
+ output: MovieOutput;
172
+ mimeType: string;
173
+ /** Authoritative duration read from the finalized movie. */
174
+ durationMs: number;
175
+ /** Presentation dimensions after the orientation transform. */
176
+ width: number;
177
+ height: number;
178
+ orientation: CameraOrientation;
179
+ /** Whether the finalized movie actually contains an audio track.
180
+ * Missing requested audio is a failure, not silent capture. */
181
+ hasAudio: boolean;
182
+ }
183
+ /** One immutable terminal result per admitted attempt. `partial` marks a
184
+ * usable clip ended early by an external condition — still a success;
185
+ * app review decides whether to keep it. */
186
+ export type MovieOutcome = {
187
+ kind: 'clip';
188
+ clip: MovieClip;
189
+ endReason: MovieEndReason;
190
+ partial: boolean;
191
+ warning?: CaptureIssue;
192
+ } | {
193
+ kind: 'failed';
194
+ endReason: MovieEndReason;
195
+ stage: 'preparation' | 'capture' | 'finalization' | 'storage';
196
+ error: CaptureIssue;
197
+ };
198
+ /** Ordered lifecycle observation, correlated to an attempt when relevant. */
199
+ export type CameraSessionEvent = {
200
+ type: 'stateChanged';
201
+ snapshot: CameraSessionSnapshot;
202
+ } | {
203
+ type: 'started';
204
+ attemptId: string;
205
+ configuration: EffectiveCaptureConfig;
206
+ } | {
207
+ type: 'finished';
208
+ attemptId: string;
209
+ outcome: MovieOutcome;
210
+ } | {
211
+ type: 'interruption';
212
+ phase: 'began' | 'ended';
213
+ reason?: CameraInterruptionReason;
214
+ } | {
215
+ type: 'availability';
216
+ available: boolean;
217
+ };
218
+ export type CameraInterruptionReason = 'background' | 'audioInUse' | 'cameraInUse' | 'multitasking' | 'systemPressure' | 'unknown';
219
+ export interface StartRecordingOptions {
220
+ /** Positive finite milliseconds. Enforcement precision comes from
221
+ * `capabilities().durationLimit`; the final duration is read from
222
+ * the movie and may overshoot. */
223
+ maximumDurationMs?: number;
224
+ /** Fixed orientation for the take. Must appear in
225
+ * `capabilities().orientations`; omit for the source default. */
226
+ orientation?: CameraOrientation;
227
+ /** App-supplied writable local `file://` URL (native hosts only,
228
+ * where `capabilities().output.destinationFileUrl` is true).
229
+ * Existing files are never overwritten. */
230
+ destinationFileUrl?: string;
231
+ }
232
+ /** One admitted capture attempt. `completion` is available immediately and
233
+ * settles exactly once with the terminal outcome; repeated `stop()`
234
+ * joins the same finalization. */
235
+ export interface MovieTake {
236
+ readonly id: string;
237
+ readonly state: 'starting' | 'recording' | 'finalizing' | 'settled';
238
+ /** Elapsed capture time in milliseconds; excludes preparation and
239
+ * finalization. Source identified by `capabilities().elapsedTime`. */
240
+ readonly elapsedMs: number;
241
+ readonly completion: Promise<MovieOutcome>;
242
+ /** Requests stop and joins finalization. Returns `completion`. */
243
+ stop(): Promise<MovieOutcome>;
244
+ }
245
+ /** Reopened access to a stored clip. `release()` frees temporary access;
246
+ * it never deletes the clip. */
247
+ export interface MovieOutputHandle {
248
+ /** Playable/readable location for the clip. */
249
+ readonly url: string;
250
+ readonly fileUrl?: string;
251
+ release(): void;
252
+ }
253
+ export interface CameraSession {
254
+ /** Non-prompting permission inspection; may return `unknown` when the
255
+ * host cannot observe authorization. */
256
+ permissions(): Promise<Record<CameraPermissionKind, CameraPermissionStatus>>;
257
+ /** Runs the system authorization flow for one kind. Calls are
258
+ * serialized; a status other than `notDetermined` returns without
259
+ * prompting. */
260
+ requestPermission(kind: CameraPermissionKind): Promise<CameraPermissionStatus>;
261
+ capabilities(): Promise<CameraSessionCapabilities>;
262
+ snapshot(): CameraSessionSnapshot;
263
+ /** Ordered lifecycle events. A caller never loses a result by
264
+ * subscribing late — `completion` is authoritative. Listener
265
+ * exceptions do not change capture. */
266
+ subscribe(listener: (event: CameraSessionEvent) => void): () => void;
267
+ /** Tap-to-focus: meters one autofocus/auto-exposure pass at a
268
+ * normalized preview-space point (see `CameraPreviewPoint`), pairing
269
+ * with the `CameraView` `onTap` event. No-ops while no preview is
270
+ * attached, after `dispose`, and on hosts without point-of-interest
271
+ * metering — `capabilities()` reports `focusPointOfInterest` and
272
+ * `exposurePointOfInterest` separately. Out-of-range coordinates
273
+ * clamp to the preview edge; non-finite coordinates throw
274
+ * `invalidArgument`. Metering is per-device state: a camera swap
275
+ * through `configure` returns focus/exposure to the new device's
276
+ * default. */
277
+ focusAtPoint(point: CameraPreviewPoint): void;
278
+ /** Replaces configuration while idle. Validates against current
279
+ * capabilities and rejects with `invalidState` during an attempt. */
280
+ configure(config: Partial<CameraSessionConfig>): Promise<void>;
281
+ /** Admits one capture attempt. Throws `CameraCaptureError` before
282
+ * admission for invalid arguments, busy, invalid state, or
283
+ * unavailable camera — no attempt is created. */
284
+ startRecording(options?: StartRecordingOptions): MovieTake;
285
+ /** Reopens a stored output for playback or reading. */
286
+ openOutput(output: MovieOutput): Promise<MovieOutputHandle>;
287
+ /** Requests stop on any active attempt, awaits finalization, then
288
+ * releases camera ownership. */
289
+ dispose(): Promise<void>;
290
+ }
@@ -1,166 +0,0 @@
1
- import { cameraHardwarePresent, ensureCameraPermission } from "./camera-permission.mobile.js";
2
- import { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState } from "./layout-child.js";
3
- import { cx } from "./cx.mobile.js";
4
- import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalProps, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
5
- import { Application, View } from "@nativescript/core";
6
- //#region src/CameraView.android.tsrx
7
- var _hs$ = /* @__PURE__ */ hookSlots(5);
8
- var _h$0 = /* @__PURE__ */ Symbol(_hs$);
9
- var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
10
- var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
11
- var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
12
- var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
13
- var __octaneUniversalPlan0 = universalPlan("nativescript", {
14
- "kind": "host",
15
- "type": "gridlayout",
16
- "propsSlot": 0,
17
- "children": [{
18
- "kind": "host",
19
- "type": "camerapreview",
20
- "props": {
21
- "row": 0,
22
- "col": 0
23
- },
24
- "bindings": [["ref", 1]]
25
- }]
26
- });
27
- var CameraPreview = class extends View {
28
- createNativeView() {
29
- const nativeClass = globalThis.com.octanexplat.camera.CameraPreviewView;
30
- return new nativeClass(this._context);
31
- }
32
- };
33
- registerElement("camerapreview", CameraPreview);
34
- var CameraView = defineUniversalComponent("nativescript", function CameraView(props) {
35
- const host = useRef(null, _h$0);
36
- const active = props.active !== false;
37
- const facing = props.facing ?? "back";
38
- useImperativeHandle(props.ref, () => ({ native: host.current.android }), [], _h$1);
39
- const bindHost = useCallback((view) => {
40
- host.current = view;
41
- }, [], _h$2);
42
- useEffect(() => {
43
- const nativeView = host.current?.android;
44
- if (!nativeView) return;
45
- let disposed = false;
46
- let provider;
47
- let preview;
48
- nativeView.setCameraActive(active);
49
- if (!active) return () => nativeView.setCameraActive(false);
50
- const fail = (message) => props.onError?.({ message });
51
- if (!cameraHardwarePresent()) {
52
- fail("camera unsupported");
53
- return () => nativeView.setCameraActive(false);
54
- }
55
- ensureCameraPermission().then((granted) => {
56
- if (disposed) return;
57
- if (!granted) {
58
- fail("camera permission denied");
59
- return;
60
- }
61
- try {
62
- const context = Application.android.context;
63
- const cameraX = globalThis.androidx.camera;
64
- const future = cameraX.lifecycle.ProcessCameraProvider.getInstance(context);
65
- const executor = globalThis.androidx.core.content.ContextCompat.getMainExecutor(context);
66
- future.addListener(new java.lang.Runnable({ run() {
67
- if (disposed) return;
68
- try {
69
- provider = future.get();
70
- const selectorClass = cameraX.core.CameraSelector;
71
- const lens = facing === "front" ? selectorClass.LENS_FACING_FRONT : selectorClass.LENS_FACING_BACK;
72
- const selector = new selectorClass.Builder().requireLensFacing(lens).build();
73
- preview = new cameraX.core.Preview.Builder().build();
74
- preview.setSurfaceProvider(nativeView.getSurfaceProvider());
75
- provider.bindToLifecycle(nativeView, selector, preview);
76
- props.onReady?.();
77
- } catch (error) {
78
- fail(error?.message ?? "camera error");
79
- }
80
- } }), executor);
81
- } catch (error) {
82
- fail(error?.message ?? "camera error");
83
- }
84
- });
85
- return () => {
86
- disposed = true;
87
- nativeView.setCameraActive(false);
88
- if (provider && preview) try {
89
- provider.unbind(preview);
90
- } catch {}
91
- };
92
- }, [active, facing], _h$3);
93
- useEffect(() => {
94
- const view = host.current;
95
- if (!view) return;
96
- applyEscapeProps(view.android, props);
97
- }, [props.ref, props.android], _h$4);
98
- return universalValue(__octaneUniversalPlan0, [universalProps([
99
- [
100
- "set",
101
- "id",
102
- props.id
103
- ],
104
- [
105
- "set",
106
- "rows",
107
- "*"
108
- ],
109
- [
110
- "set",
111
- "columns",
112
- "*"
113
- ],
114
- [
115
- "set",
116
- "className",
117
- cx("vx-layers", "vx-cameraview", props.className)
118
- ],
119
- [
120
- "set",
121
- "style",
122
- {
123
- backgroundColor: "#000",
124
- ...props.style
125
- }
126
- ],
127
- ["spread", layoutChildProps(props)],
128
- [
129
- "set",
130
- "accessible",
131
- props.accessible
132
- ],
133
- [
134
- "set",
135
- "accessibilityLabel",
136
- props.accessibilityLabel
137
- ],
138
- [
139
- "set",
140
- "accessibilityRole",
141
- nativeAccessibilityRole(props.accessibilityRole)
142
- ],
143
- [
144
- "set",
145
- "accessibilityHint",
146
- props.accessibilityHint
147
- ],
148
- [
149
- "set",
150
- "accessibilityValue",
151
- props.accessibilityValue
152
- ],
153
- [
154
- "set",
155
- "accessibilityState",
156
- nativeAccessibilityState(props.accessibilityState)
157
- ],
158
- [
159
- "set",
160
- "accessibilityLiveRegion",
161
- props.accessibilityLiveRegion
162
- ]
163
- ]), bindHost]);
164
- }, { "module": "@nativescript-community/octane" });
165
- //#endregion
166
- export { CameraView };
@@ -1,131 +0,0 @@
1
- import { cameraHardwarePresent, ensureCameraPermission } from "./camera-permission.mobile.js";
2
- import { startCameraPreview } from "./camera-session.ios.js";
3
- import { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState } from "./layout-child.js";
4
- import { cx } from "./cx.mobile.js";
5
- import { defineUniversalComponent, hookSlots, universalPlan, universalProps, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
6
- //#region src/CameraView.ios.tsrx
7
- var _hs$ = /* @__PURE__ */ hookSlots(5);
8
- var _h$0 = /* @__PURE__ */ Symbol(_hs$);
9
- var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
10
- var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
11
- var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
12
- var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
13
- var __octaneUniversalPlan0 = universalPlan("nativescript", {
14
- "kind": "host",
15
- "type": "gridlayout",
16
- "propsSlot": 0
17
- });
18
- var CameraView = defineUniversalComponent("nativescript", function CameraView(props) {
19
- const host = useRef(null, _h$0);
20
- const active = props.active !== false;
21
- const facing = props.facing ?? "back";
22
- useImperativeHandle(props.ref, () => ({ native: host.current.ios ?? host.current.nativeViewProtected }), [], _h$1);
23
- const bindHost = useCallback((view) => {
24
- host.current = view;
25
- }, [], _h$2);
26
- useEffect(() => {
27
- const view = host.current;
28
- if (!view || !active) return;
29
- let disposed = false;
30
- let stopPreview;
31
- const fail = (message) => props.onError?.({ message });
32
- if (!cameraHardwarePresent()) {
33
- fail("camera unsupported");
34
- return;
35
- }
36
- ensureCameraPermission().then((granted) => {
37
- if (disposed) return;
38
- if (!granted) {
39
- fail("camera permission denied");
40
- return;
41
- }
42
- try {
43
- stopPreview = startCameraPreview(view, facing, props.onReady);
44
- } catch (error) {
45
- fail(error?.message ?? "camera error");
46
- }
47
- });
48
- return () => {
49
- disposed = true;
50
- stopPreview?.();
51
- };
52
- }, [active, facing], _h$3);
53
- useEffect(() => {
54
- const view = host.current;
55
- if (!view) return;
56
- applyEscapeProps(view.ios ?? view.nativeViewProtected, props);
57
- }, [props.ref, props.ios], _h$4);
58
- return universalValue(__octaneUniversalPlan0, [universalProps([
59
- [
60
- "set",
61
- "ref",
62
- bindHost
63
- ],
64
- [
65
- "set",
66
- "id",
67
- props.id
68
- ],
69
- [
70
- "set",
71
- "rows",
72
- "*"
73
- ],
74
- [
75
- "set",
76
- "columns",
77
- "*"
78
- ],
79
- [
80
- "set",
81
- "className",
82
- cx("vx-layers", "vx-cameraview", props.className)
83
- ],
84
- [
85
- "set",
86
- "style",
87
- {
88
- backgroundColor: "#000",
89
- ...props.style
90
- }
91
- ],
92
- ["spread", layoutChildProps(props)],
93
- [
94
- "set",
95
- "accessible",
96
- props.accessible
97
- ],
98
- [
99
- "set",
100
- "accessibilityLabel",
101
- props.accessibilityLabel
102
- ],
103
- [
104
- "set",
105
- "accessibilityRole",
106
- nativeAccessibilityRole(props.accessibilityRole)
107
- ],
108
- [
109
- "set",
110
- "accessibilityHint",
111
- props.accessibilityHint
112
- ],
113
- [
114
- "set",
115
- "accessibilityValue",
116
- props.accessibilityValue
117
- ],
118
- [
119
- "set",
120
- "accessibilityState",
121
- nativeAccessibilityState(props.accessibilityState)
122
- ],
123
- [
124
- "set",
125
- "accessibilityLiveRegion",
126
- props.accessibilityLiveRegion
127
- ]
128
- ])]);
129
- }, { "module": "@nativescript-community/octane" });
130
- //#endregion
131
- export { CameraView };
@@ -1,45 +0,0 @@
1
- import { Application, isAndroid, isIOS } from "@nativescript/core";
2
- //#region src/camera-permission.mobile.ts
3
- var requestCode = 7719;
4
- function cameraHardwarePresent() {
5
- if (isIOS) return typeof globalThis.AVCaptureDevice !== "undefined";
6
- if (!isAndroid) return false;
7
- try {
8
- return (Application.android?.context ?? Application.android?.startActivity)?.getPackageManager()?.hasSystemFeature("android.hardware.camera.any") !== false;
9
- } catch {
10
- return true;
11
- }
12
- }
13
- async function ensureCameraPermission() {
14
- try {
15
- if (isIOS) {
16
- const av = globalThis.AVCaptureDevice;
17
- const mediaType = globalThis.AVMediaTypeVideo ?? "vide";
18
- const status = av?.authorizationStatusForMediaType?.(mediaType);
19
- if (status === 3) return true;
20
- if (status === 1 || status === 2) return false;
21
- return await new Promise((resolve) => av.requestAccessForMediaTypeCompletionHandler(mediaType, resolve));
22
- }
23
- if (isAndroid) {
24
- const permission = "android.permission.CAMERA";
25
- const activity = Application.android.foregroundActivity ?? Application.android.startActivity;
26
- if (activity?.checkSelfPermission?.(permission) === android.content.pm.PackageManager.PERMISSION_GRANTED) return true;
27
- if (!activity) return false;
28
- return await new Promise((resolve) => {
29
- const currentRequest = requestCode++;
30
- const onResult = (args) => {
31
- if (args.requestCode !== currentRequest) return;
32
- Application.android.off(Application.android.activityRequestPermissionsEvent, onResult);
33
- resolve(args.grantResults?.[0] === android.content.pm.PackageManager.PERMISSION_GRANTED);
34
- };
35
- Application.android.on(Application.android.activityRequestPermissionsEvent, onResult);
36
- activity.requestPermissions([permission], currentRequest);
37
- });
38
- }
39
- } catch {
40
- return false;
41
- }
42
- return false;
43
- }
44
- //#endregion
45
- export { cameraHardwarePresent, ensureCameraPermission };
@@ -1,45 +0,0 @@
1
- //#region src/camera-session.ios.ts
2
- /** Attach an AVFoundation preview layer to a NativeScript UIView host. */
3
- function startCameraPreview(view, facing, onReady) {
4
- const av = globalThis;
5
- const mediaType = av.AVMediaTypeVideo ?? "vide";
6
- const desiredPosition = facing === "front" ? av.AVCaptureDevicePositionFront ?? 2 : av.AVCaptureDevicePositionBack ?? 1;
7
- const devices = av.AVCaptureDevice.devicesWithMediaType(mediaType);
8
- let device = null;
9
- for (let index = 0; index < devices.count; index++) {
10
- const candidate = devices.objectAtIndex(index);
11
- if (candidate.position === desiredPosition) {
12
- device = candidate;
13
- break;
14
- }
15
- }
16
- if (!device) throw new Error("camera unavailable");
17
- const session = av.AVCaptureSession.new();
18
- const input = av.AVCaptureDeviceInput.deviceInputWithDeviceError(device);
19
- if (!input || !session.canAddInput(input)) throw new Error("camera input unavailable");
20
- session.addInput(input);
21
- const layer = av.AVCaptureVideoPreviewLayer.layerWithSession(session);
22
- layer.videoGravity = "resizeAspectFill";
23
- const connection = layer.connection;
24
- if (connection) {
25
- connection.automaticallyAdjustsVideoMirroring = false;
26
- if (connection.isVideoMirroringSupported) connection.isVideoMirrored = facing === "front";
27
- }
28
- const nativeView = view.ios ?? view.nativeViewProtected;
29
- if (!nativeView?.layer) throw new Error("camera preview host unavailable");
30
- nativeView.layer.addSublayer(layer);
31
- const updateFrame = () => {
32
- layer.frame = nativeView.bounds;
33
- };
34
- view.on?.("layoutChanged", updateFrame);
35
- updateFrame();
36
- session.startRunning();
37
- onReady?.();
38
- return () => {
39
- view.off?.("layoutChanged", updateFrame);
40
- session.stopRunning();
41
- layer.removeFromSuperlayer();
42
- };
43
- }
44
- //#endregion
45
- 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,2 +0,0 @@
1
- import { CameraView } from "./src/CameraView";
2
- export { CameraView };