@octane-xplat/camera 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +89 -3
- package/package.json +35 -11
- package/platforms/android/AndroidManifest.xml +2 -0
- package/platforms/android/include.gradle +1 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +13 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
- package/platforms/ios/Info.plist +2 -0
- package/platforms/macos/src/CameraOwnership.swift +22 -0
- package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
- package/platforms/macos/src/XplatCameraHost.swift +639 -0
- package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
- package/src/CameraView.android.tsrx +27 -3
- package/src/CameraView.ios.tsrx +32 -4
- package/src/CameraView.macos.tsrx +95 -0
- package/src/CameraView.web.tsrx +31 -2
- package/src/CameraView.windows.tsrx +126 -0
- package/src/camera-permission.mobile.ts +18 -4
- package/src/camera-permission.windows.ts +76 -0
- package/src/camera-session.macos.ts +75 -0
- package/src/camera-session.windows.ts +126 -0
- package/src/host-movie.web.ts +295 -0
- package/src/index.ts +32 -0
- package/src/index.web.ts +32 -0
- package/src/layout-child.windows.ts +79 -0
- package/src/movie-metadata.web.ts +84 -0
- package/src/movie-storage.web.ts +164 -0
- package/src/preview-tap.ts +34 -0
- package/src/props.ts +14 -1
- package/src/session-backend.android.ts +439 -0
- package/src/session-backend.ios.ts +1258 -0
- package/src/session-backend.linux.ts +887 -0
- package/src/session-backend.macos.ts +707 -0
- package/src/session-backend.ts +59 -0
- package/src/session-backend.web.ts +891 -0
- package/src/session-backend.windows.ts +1599 -0
- package/src/session-core.ts +723 -0
- package/src/session.ts +37 -0
- package/src/synthetic-session.ts +377 -0
- package/src/types.ts +357 -0
- package/types/index.d.ts +43 -0
- package/types/props.d.ts +14 -1
- package/types/session-core.d.ts +139 -0
- package/types/synthetic-session.d.ts +51 -0
- package/types/types.d.ts +290 -0
- package/dist/native/CameraView.android.js +0 -166
- package/dist/native/CameraView.ios.js +0 -131
- package/dist/native/camera-permission.mobile.js +0 -45
- package/dist/native/camera-session.ios.js +0 -45
- package/dist/native/cx.mobile.js +0 -9
- package/dist/native/index.js +0 -2
- package/dist/native/layout-child.js +0 -53
- package/dist/native/octane-client-build.json +0 -8
- package/dist/web/CameraView.web.tsrx.js +0 -317
- package/dist/web/index.js +0 -2
- package/dist/web/layout-child.web.js +0 -29
- package/dist/web/octane-client-build.json +0 -8
- package/src/CameraView.windows.ts +0 -5
- package/src/cx.mobile.ts +0 -6
package/types/types.d.ts
ADDED
|
@@ -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 };
|
package/dist/native/cx.mobile.js
DELETED
|
@@ -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 };
|
package/dist/native/index.js
DELETED