@octane-xplat/camera 0.7.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.
@@ -0,0 +1,314 @@
1
+ import { applyEscapeProps, layoutChildProps } from "./layout-child.web.js";
2
+ import { __s, bag15, bag5, child, clone, delegateEvents, evt2e, evt2u, hookSlots, ifBlock, markDangerouslySetInnerHTMLChildren, queueRefAttach, queueRefDetach, setStyleProperty, snapshotSpread, template, useEffect, useRef } from "octane";
3
+ import { bindSignalHostPropSources, enableSignalBindings, replaceRef, setAttributeIfChanged } from "octane/internal/client";
4
+ //#region src/CameraView.web.tsrx
5
+ enableSignalBindings(1, true);
6
+ delegateEvents(["playing"]);
7
+ var _t$0 = /* @__PURE__ */ template("<video autoplay=\"\" playsinline=\"\" style=\"display: block; width: 100%; height: 100%; object-fit: cover;\"></video>");
8
+ var _t$1 = /* @__PURE__ */ template("<div><!></div>");
9
+ var _hs$ = /* @__PURE__ */ hookSlots(3);
10
+ var __event = (__eventArg, readyFired, props) => {
11
+ if (readyFired.current) return;
12
+ readyFired.current = true;
13
+ props.onReady?.();
14
+ };
15
+ var _rm$1 = [
16
+ "r",
17
+ "b",
18
+ "a"
19
+ ];
20
+ function __then$0(__props, __s, __extra) {
21
+ const facing = __extra[0];
22
+ const props = __extra[1];
23
+ const readyFired = __extra[2];
24
+ const video = __extra[3];
25
+ let _b = __s.slots[0];
26
+ if (_b === void 0) {
27
+ let _m0, _m1, _m4;
28
+ const _root = clone(_t$0, void 0, "||");
29
+ queueRefAttach(__s, _m1 = (element) => {
30
+ video.current = element;
31
+ if (!element) return;
32
+ applyEscapeProps(element, props);
33
+ props.bind?.({ native: element });
34
+ }, _m0 = _root);
35
+ (__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
36
+ _m4 = evt2e(_root, "$$playing", __event, readyFired, props);
37
+ _b = bag5(__s, _root, _m0, _m1, void 0, __s, _m4);
38
+ __s.refFields = _rm$1;
39
+ } else {
40
+ {
41
+ const _r = (element) => {
42
+ video.current = element;
43
+ if (!element) return;
44
+ applyEscapeProps(element, props);
45
+ props.bind?.({ native: element });
46
+ };
47
+ if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
48
+ }
49
+ evt2u(_b.e, __event, readyFired, props);
50
+ }
51
+ _b.c = setAttributeIfChanged(true, _b.c, _b.a, "muted");
52
+ {
53
+ const _v = facing === "front" ? "scaleX(-1)" : void 0;
54
+ if (_b.d !== _v) {
55
+ setStyleProperty(_b.a, "transform", _v, "display: block; width: 100%; height: 100%; object-fit: cover;", _b.d);
56
+ _b.d = _v;
57
+ }
58
+ }
59
+ }
60
+ var _rm$3 = [
61
+ "s",
62
+ "n",
63
+ "m"
64
+ ];
65
+ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
66
+ const video = useRef(null, _hs$ + 0);
67
+ const facing = props.facing ?? "back";
68
+ const active = props.active !== false;
69
+ const readyFired = useRef(false, _hs$ + 1);
70
+ useEffect(() => {
71
+ const element = video.current;
72
+ if (!element || !active) return;
73
+ const mediaDevices = navigator.mediaDevices;
74
+ if (!mediaDevices?.getUserMedia) {
75
+ props.onError?.({ message: "camera unsupported" });
76
+ return;
77
+ }
78
+ let stream = null;
79
+ let disposed = false;
80
+ readyFired.current = false;
81
+ mediaDevices.getUserMedia({
82
+ video: { facingMode: facing === "front" ? "user" : "environment" },
83
+ audio: false
84
+ }).then((nextStream) => {
85
+ if (disposed) {
86
+ for (const track of nextStream.getTracks()) track.stop();
87
+ return;
88
+ }
89
+ stream = nextStream;
90
+ element.srcObject = nextStream;
91
+ element.play()?.catch(() => {});
92
+ }).catch((error) => {
93
+ props.onError?.({ message: error?.name === "NotAllowedError" ? "camera permission denied" : String(error?.name ?? error ?? "camera error") });
94
+ });
95
+ return () => {
96
+ disposed = true;
97
+ element.srcObject = null;
98
+ if (stream) for (const track of stream.getTracks()) track.stop();
99
+ };
100
+ }, [active, facing], _hs$ + 2);
101
+ let _b = __s.slots[0];
102
+ if (_b === void 0) {
103
+ let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14;
104
+ const _root = clone(_t$1);
105
+ _m0 = props.id;
106
+ _m1 = [
107
+ "vx-stack",
108
+ "vx-cameraview",
109
+ props.className
110
+ ];
111
+ _m2 = snapshotSpread(layoutChildProps(props, {
112
+ position: "relative",
113
+ overflow: "hidden",
114
+ backgroundColor: "#000",
115
+ ...props.style
116
+ }));
117
+ _m3 = props.accessibilityRole;
118
+ _m4 = props.accessibilityLabel;
119
+ _m5 = props.accessible === false ? true : void 0;
120
+ _m6 = props.accessibilityHint;
121
+ _m7 = props.accessibilityValue;
122
+ _m8 = props.accessibilityState?.disabled;
123
+ _m9 = props.accessibilityState?.selected;
124
+ _m10 = props.accessibilityState?.checked;
125
+ _m11 = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
126
+ _m12 = _root;
127
+ _m13 = bindSignalHostPropSources(__s, void 0, _root, [
128
+ [
129
+ false,
130
+ "id",
131
+ _m0
132
+ ],
133
+ [
134
+ false,
135
+ "className",
136
+ _m1
137
+ ],
138
+ [true, _m2],
139
+ [
140
+ false,
141
+ "role",
142
+ _m3
143
+ ],
144
+ [
145
+ false,
146
+ "aria-label",
147
+ _m4
148
+ ],
149
+ [
150
+ false,
151
+ "aria-hidden",
152
+ _m5
153
+ ],
154
+ [
155
+ false,
156
+ "aria-description",
157
+ _m6
158
+ ],
159
+ [
160
+ false,
161
+ "aria-valuetext",
162
+ _m7
163
+ ],
164
+ [
165
+ false,
166
+ "aria-disabled",
167
+ _m8
168
+ ],
169
+ [
170
+ false,
171
+ "aria-selected",
172
+ _m9
173
+ ],
174
+ [
175
+ false,
176
+ "aria-checked",
177
+ _m10
178
+ ],
179
+ [
180
+ false,
181
+ "aria-live",
182
+ _m11
183
+ ]
184
+ ], "b:83c7570e", true);
185
+ markDangerouslySetInnerHTMLChildren(_root);
186
+ _m14 = child(_root);
187
+ _b = bag15(__s, _root, _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14);
188
+ __s.refFields = _rm$3;
189
+ } else {
190
+ {
191
+ const _v = props.id;
192
+ if (_b.a !== _v) _b.a = _v;
193
+ }
194
+ {
195
+ const _v = [
196
+ "vx-stack",
197
+ "vx-cameraview",
198
+ props.className
199
+ ];
200
+ if (_b.b !== _v) _b.b = _v;
201
+ }
202
+ _b.c = snapshotSpread(layoutChildProps(props, {
203
+ position: "relative",
204
+ overflow: "hidden",
205
+ backgroundColor: "#000",
206
+ ...props.style
207
+ }));
208
+ {
209
+ const _v = props.accessibilityRole;
210
+ if (_b.d !== _v) _b.d = _v;
211
+ }
212
+ {
213
+ const _v = props.accessibilityLabel;
214
+ if (_b.e !== _v) _b.e = _v;
215
+ }
216
+ {
217
+ const _v = props.accessible === false ? true : void 0;
218
+ if (_b.f !== _v) _b.f = _v;
219
+ }
220
+ {
221
+ const _v = props.accessibilityHint;
222
+ if (_b.g !== _v) _b.g = _v;
223
+ }
224
+ {
225
+ const _v = props.accessibilityValue;
226
+ if (_b.h !== _v) _b.h = _v;
227
+ }
228
+ {
229
+ const _v = props.accessibilityState?.disabled;
230
+ if (_b.i !== _v) _b.i = _v;
231
+ }
232
+ {
233
+ const _v = props.accessibilityState?.selected;
234
+ if (_b.j !== _v) _b.j = _v;
235
+ }
236
+ {
237
+ const _v = props.accessibilityState?.checked;
238
+ if (_b.k !== _v) _b.k = _v;
239
+ }
240
+ {
241
+ const _v = props.accessibilityLiveRegion === "none" ? "off" : props.accessibilityLiveRegion;
242
+ if (_b.l !== _v) _b.l = _v;
243
+ }
244
+ _b.n = bindSignalHostPropSources(__s, _b.n, _b.m, [
245
+ [
246
+ false,
247
+ "id",
248
+ _b.a
249
+ ],
250
+ [
251
+ false,
252
+ "className",
253
+ _b.b
254
+ ],
255
+ [true, _b.c],
256
+ [
257
+ false,
258
+ "role",
259
+ _b.d
260
+ ],
261
+ [
262
+ false,
263
+ "aria-label",
264
+ _b.e
265
+ ],
266
+ [
267
+ false,
268
+ "aria-hidden",
269
+ _b.f
270
+ ],
271
+ [
272
+ false,
273
+ "aria-description",
274
+ _b.g
275
+ ],
276
+ [
277
+ false,
278
+ "aria-valuetext",
279
+ _b.h
280
+ ],
281
+ [
282
+ false,
283
+ "aria-disabled",
284
+ _b.i
285
+ ],
286
+ [
287
+ false,
288
+ "aria-selected",
289
+ _b.j
290
+ ],
291
+ [
292
+ false,
293
+ "aria-checked",
294
+ _b.k
295
+ ],
296
+ [
297
+ false,
298
+ "aria-live",
299
+ _b.l
300
+ ]
301
+ ], "b:83c7570e", true);
302
+ }
303
+ {
304
+ const __ifCondition = active;
305
+ ifBlock(__s, 1, _b.m, __ifCondition, __then$0, null, _b.o, __ifCondition ? [
306
+ facing,
307
+ props,
308
+ readyFired,
309
+ video
310
+ ] : void 0);
311
+ }
312
+ });
313
+ //#endregion
314
+ export { CameraView };
@@ -0,0 +1,2 @@
1
+ import { CameraView } from "./CameraView.web.tsrx.js";
2
+ export { CameraView };
@@ -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,8 @@
1
+ {
2
+ "version": 1,
3
+ "buildId": "002634c0-7c1b-4fae-9ed8-5c3aabdc04da",
4
+ "mode": "production",
5
+ "capabilities": {
6
+ "independentHydration": false
7
+ }
8
+ }
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@octane-xplat/camera",
3
+ "version": "0.7.0",
4
+ "description": "Live camera preview leaf for Octane xplat",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/octane-xplat/octane-xplat.git",
8
+ "directory": "packages/camera"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "types",
13
+ "src",
14
+ "platforms"
15
+ ],
16
+ "type": "module",
17
+ "nativescript": {
18
+ "platforms": {
19
+ "ios": "7.0.0",
20
+ "android": "7.0.0"
21
+ }
22
+ },
23
+ "exports": {
24
+ ".": {
25
+ "types": "./types/index.d.ts",
26
+ "web": "./dist/web/index.js",
27
+ "native": "./dist/native/index.js",
28
+ "default": "./dist/native/index.js"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "devDependencies": {
35
+ "@nativescript-community/octane": "0.2.1",
36
+ "@nativescript/core": "9.1.2",
37
+ "@octanejs/vite-plugin": "0.1.61",
38
+ "octane": "0.6.3",
39
+ "typescript": "5.9.3",
40
+ "vite": "8.3.0"
41
+ },
42
+ "peerDependencies": {
43
+ "@nativescript-community/octane": ">=0.2.1 <1",
44
+ "@nativescript/core": ">=9.1.0 <10",
45
+ "octane": ">=0.6.3 <1"
46
+ },
47
+ "peerDependenciesMeta": {
48
+ "@nativescript-community/octane": {
49
+ "optional": true
50
+ },
51
+ "@nativescript/core": {
52
+ "optional": true
53
+ },
54
+ "octane": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "scripts": {
59
+ "build": "vite build && vite build --mode native && tsc -p tsconfig.types.json && node ../../scripts/check-native-dist.mjs"
60
+ }
61
+ }
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <uses-feature android:name="android.hardware.camera.any" android:required="false" />
3
+ <uses-permission android:name="android.permission.CAMERA" />
4
+ </manifest>
@@ -0,0 +1,13 @@
1
+ allprojects {
2
+ repositories {
3
+ google()
4
+ mavenCentral()
5
+ }
6
+ }
7
+
8
+ dependencies {
9
+ def cameraXVersion = '1.5.3'
10
+ implementation "androidx.camera:camera-camera2:${cameraXVersion}"
11
+ implementation "androidx.camera:camera-lifecycle:${cameraXVersion}"
12
+ implementation "androidx.camera:camera-view:${cameraXVersion}"
13
+ }
@@ -0,0 +1,71 @@
1
+ package com.octanexplat.camera;
2
+
3
+ import android.content.Context;
4
+ import android.view.ViewGroup;
5
+ import android.widget.FrameLayout;
6
+
7
+ import androidx.camera.core.Preview;
8
+ import androidx.camera.view.PreviewView;
9
+ import androidx.lifecycle.Lifecycle;
10
+ import androidx.lifecycle.LifecycleOwner;
11
+ import androidx.lifecycle.LifecycleRegistry;
12
+
13
+ public final class CameraPreviewView extends FrameLayout implements LifecycleOwner {
14
+ private final LifecycleRegistry lifecycle;
15
+ private final PreviewView previewView;
16
+ private boolean cameraActive = true;
17
+ private boolean attached;
18
+
19
+ public CameraPreviewView(Context context) {
20
+ super(context);
21
+ lifecycle = new LifecycleRegistry(this);
22
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
23
+ previewView = new PreviewView(context);
24
+ previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
25
+ addView(
26
+ previewView,
27
+ new FrameLayout.LayoutParams(
28
+ ViewGroup.LayoutParams.MATCH_PARENT,
29
+ ViewGroup.LayoutParams.MATCH_PARENT
30
+ )
31
+ );
32
+ }
33
+
34
+ @Override
35
+ public Lifecycle getLifecycle() {
36
+ return lifecycle;
37
+ }
38
+
39
+ public void setCameraActive(boolean active) {
40
+ cameraActive = active;
41
+ updateLifecycle();
42
+ }
43
+
44
+ public Preview.SurfaceProvider getSurfaceProvider() {
45
+ return previewView.getSurfaceProvider();
46
+ }
47
+
48
+ @Override
49
+ protected void onAttachedToWindow() {
50
+ super.onAttachedToWindow();
51
+ attached = true;
52
+ updateLifecycle();
53
+ }
54
+
55
+ @Override
56
+ protected void onDetachedFromWindow() {
57
+ attached = false;
58
+ updateLifecycle();
59
+ super.onDetachedFromWindow();
60
+ }
61
+
62
+ private void updateLifecycle() {
63
+ boolean shouldStart = cameraActive && attached;
64
+ boolean isStarted = lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED);
65
+ if (shouldStart && !isStarted) {
66
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
67
+ } else if (!shouldStart && isStarted) {
68
+ lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
69
+ }
70
+ }
71
+ }
@@ -0,0 +1,8 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>NSCameraUsageDescription</key>
6
+ <string>Camera access is required to show a live preview.</string>
7
+ </dict>
8
+ </plist>
@@ -0,0 +1,125 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { useCallback, useEffect, useRef } from 'octane'
3
+ import { Application, View } from '@nativescript/core'
4
+ import { registerElement } from '@nativescript-community/octane'
5
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
6
+ import type { CameraViewProps } from './props'
7
+ import { cameraHardwarePresent, ensureCameraPermission } from './camera-permission.mobile'
8
+ import { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState } from './layout-child'
9
+ import { cx } from './cx.mobile'
10
+
11
+ class CameraPreview extends View {
12
+ createNativeView() {
13
+ const nativeClass = (globalThis as any).com.octanexplat.camera.CameraPreviewView
14
+ return new nativeClass(this._context)
15
+ }
16
+ }
17
+
18
+ declare module '@nativescript-community/octane/intrinsics' {
19
+ interface NativeScriptElements {
20
+ camerapreview: Attributes<typeof CameraPreview>
21
+ }
22
+ }
23
+
24
+ registerElement('camerapreview', CameraPreview)
25
+
26
+ /** Live Android preview backed by CameraX. */
27
+ export function CameraView(props: CameraViewProps) @{
28
+ const host = useRef<any>(null)
29
+ const active = props.active !== false
30
+ const facing = props.facing ?? 'back'
31
+ const bindHost = useCallback((view: any) => {
32
+ host.current = view
33
+ }, [])
34
+
35
+ useEffect(() => {
36
+ const nativeView = host.current?.android
37
+ if (!nativeView) return
38
+ let disposed = false
39
+ let provider: any
40
+ let preview: any
41
+ nativeView.setCameraActive(active)
42
+ if (!active) return () => nativeView.setCameraActive(false)
43
+
44
+ const fail = (message: string) => props.onError?.({ message })
45
+ if (!cameraHardwarePresent()) {
46
+ fail('camera unsupported')
47
+ return () => nativeView.setCameraActive(false)
48
+ }
49
+
50
+ ensureCameraPermission().then((granted) => {
51
+ if (disposed) return
52
+ if (!granted) {
53
+ fail('camera permission denied')
54
+ return
55
+ }
56
+
57
+ try {
58
+ const context = Application.android.context
59
+ const cameraX = (globalThis as any).androidx.camera
60
+ const future = cameraX.lifecycle.ProcessCameraProvider.getInstance(context)
61
+ const executor = (globalThis as any).androidx.core.content.ContextCompat.getMainExecutor(context)
62
+ future.addListener(
63
+ new java.lang.Runnable({
64
+ run() {
65
+ if (disposed) return
66
+ try {
67
+ provider = future.get()
68
+ const selectorClass = cameraX.core.CameraSelector
69
+ const lens = facing === 'front'
70
+ ? selectorClass.LENS_FACING_FRONT
71
+ : selectorClass.LENS_FACING_BACK
72
+
73
+ const selector = new selectorClass.Builder().requireLensFacing(lens).build()
74
+ preview = new cameraX.core.Preview.Builder().build()
75
+ preview.setSurfaceProvider(nativeView.getSurfaceProvider())
76
+ provider.bindToLifecycle(nativeView, selector, preview)
77
+ props.onReady?.()
78
+ } catch (error: any) {
79
+ fail(error?.message ?? 'camera error')
80
+ }
81
+ },
82
+ }),
83
+ executor,
84
+ )
85
+ } catch (error: any) {
86
+ fail(error?.message ?? 'camera error')
87
+ }
88
+ })
89
+
90
+ return () => {
91
+ disposed = true
92
+ nativeView.setCameraActive(false)
93
+ if (provider && preview) {
94
+ try {
95
+ provider.unbind(preview)
96
+ } catch {}
97
+ }
98
+ }
99
+ }, [active, facing])
100
+
101
+ useEffect(() => {
102
+ const view = host.current
103
+ if (!view) return
104
+ applyEscapeProps(view.android, props)
105
+ props.bind?.({ native: view.android })
106
+ }, [props.bind, props.android])
107
+
108
+ <gridlayout
109
+ id={props.id}
110
+ rows="*"
111
+ columns="*"
112
+ className={cx('vx-stack', 'vx-cameraview', props.className)}
113
+ style={{ backgroundColor: '#000', ...props.style }}
114
+ {...layoutChildProps(props)}
115
+ accessible={props.accessible}
116
+ accessibilityLabel={props.accessibilityLabel}
117
+ accessibilityRole={nativeAccessibilityRole(props.accessibilityRole) as any}
118
+ accessibilityHint={props.accessibilityHint}
119
+ accessibilityValue={props.accessibilityValue}
120
+ accessibilityState={nativeAccessibilityState(props.accessibilityState) as any}
121
+ accessibilityLiveRegion={props.accessibilityLiveRegion as any}
122
+ >
123
+ <camerapreview ref={bindHost} row={0} col={0} />
124
+ </gridlayout>
125
+ }