@octane-xplat/camera 0.8.0 → 0.10.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 +49 -0
- package/dist/native/CameraView.android.js +8 -7
- package/dist/native/CameraView.ios.js +8 -7
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/CameraView.web.tsrx.js +18 -14
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +17 -17
- package/src/CameraView.android.tsrx +5 -4
- package/src/CameraView.ios.tsrx +5 -4
- package/src/CameraView.web.tsrx +6 -3
- package/src/CameraView.windows.ts +5 -0
- package/src/camera-permission.mobile.ts +31 -20
- package/src/camera-session.ios.ts +18 -7
- package/src/layout-child.ts +29 -10
- package/src/layout-child.web.ts +33 -9
- package/src/props.ts +3 -1
- package/types/props.d.ts +2 -1
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# `@octane-xplat/camera`
|
|
2
|
+
|
|
3
|
+
```sh
|
|
4
|
+
pnpm add @octane-xplat/camera
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Live camera preview for Octane xplat apps: `getUserMedia` video on web,
|
|
8
|
+
`AVCaptureSession` on iOS, CameraX on Android.
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { CameraView } from '@octane-xplat/camera'
|
|
12
|
+
|
|
13
|
+
export function Preview() {
|
|
14
|
+
return (
|
|
15
|
+
<CameraView
|
|
16
|
+
facing="back"
|
|
17
|
+
active
|
|
18
|
+
onReady={() => console.log('Preview running')}
|
|
19
|
+
onError={(error) => console.log(error.message)}
|
|
20
|
+
/>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
This is the preview surface only — for one-shot still capture and library
|
|
26
|
+
picking use [`@octane-xplat/media`](../media/README.md). Permissions are
|
|
27
|
+
requested when the session starts; `onError` is how a denial shows up.
|
|
28
|
+
`ref` hands you the platform surface (`HTMLVideoElement` on web) for
|
|
29
|
+
controls the shared props don't cover, and the `ios`/`android`/`web`
|
|
30
|
+
escape props apply after the shared props.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
// Preview.web.tsx — browser properties belong in this platform file.
|
|
34
|
+
import { CameraView } from '@octane-xplat/camera'
|
|
35
|
+
|
|
36
|
+
export function Preview() {
|
|
37
|
+
return (
|
|
38
|
+
<CameraView
|
|
39
|
+
web={{ playsInline: true }}
|
|
40
|
+
ref={(handle) => console.log(handle.native)}
|
|
41
|
+
onError={(error) => console.log(error.message)}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Guide: [Building screens](../../docs/app/primitives.md) (leaf components);
|
|
48
|
+
component index: [`docs/app/components.md`](../../docs/app/components.md).
|
|
49
|
+
Exercised by [`CameraDemo`](../demos/src/CameraDemo.tsrx).
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { cameraHardwarePresent, ensureCameraPermission } from "./camera-permission.mobile.js";
|
|
2
2
|
import { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState } from "./layout-child.js";
|
|
3
3
|
import { cx } from "./cx.mobile.js";
|
|
4
|
-
import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
|
|
4
|
+
import { defineUniversalComponent, hookSlots, registerElement, universalPlan, universalProps, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
|
|
5
5
|
import { Application, View } from "@nativescript/core";
|
|
6
6
|
//#region src/CameraView.android.tsrx
|
|
7
|
-
var _hs$ = /* @__PURE__ */ hookSlots(
|
|
7
|
+
var _hs$ = /* @__PURE__ */ hookSlots(5);
|
|
8
8
|
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
9
9
|
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
10
10
|
var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
|
|
11
11
|
var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
|
|
12
|
+
var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
|
|
12
13
|
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
13
14
|
"kind": "host",
|
|
14
15
|
"type": "gridlayout",
|
|
@@ -34,9 +35,10 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
34
35
|
const host = useRef(null, _h$0);
|
|
35
36
|
const active = props.active !== false;
|
|
36
37
|
const facing = props.facing ?? "back";
|
|
38
|
+
useImperativeHandle(props.ref, () => ({ native: host.current.android }), [], _h$1);
|
|
37
39
|
const bindHost = useCallback((view) => {
|
|
38
40
|
host.current = view;
|
|
39
|
-
}, [], _h$
|
|
41
|
+
}, [], _h$2);
|
|
40
42
|
useEffect(() => {
|
|
41
43
|
const nativeView = host.current?.android;
|
|
42
44
|
if (!nativeView) return;
|
|
@@ -87,13 +89,12 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
87
89
|
provider.unbind(preview);
|
|
88
90
|
} catch {}
|
|
89
91
|
};
|
|
90
|
-
}, [active, facing], _h$
|
|
92
|
+
}, [active, facing], _h$3);
|
|
91
93
|
useEffect(() => {
|
|
92
94
|
const view = host.current;
|
|
93
95
|
if (!view) return;
|
|
94
96
|
applyEscapeProps(view.android, props);
|
|
95
|
-
|
|
96
|
-
}, [props.bind, props.android], _h$3);
|
|
97
|
+
}, [props.ref, props.android], _h$4);
|
|
97
98
|
return universalValue(__octaneUniversalPlan0, [universalProps([
|
|
98
99
|
[
|
|
99
100
|
"set",
|
|
@@ -113,7 +114,7 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
113
114
|
[
|
|
114
115
|
"set",
|
|
115
116
|
"className",
|
|
116
|
-
cx("vx-
|
|
117
|
+
cx("vx-layers", "vx-cameraview", props.className)
|
|
117
118
|
],
|
|
118
119
|
[
|
|
119
120
|
"set",
|
|
@@ -2,13 +2,14 @@ import { cameraHardwarePresent, ensureCameraPermission } from "./camera-permissi
|
|
|
2
2
|
import { startCameraPreview } from "./camera-session.ios.js";
|
|
3
3
|
import { applyEscapeProps, layoutChildProps, nativeAccessibilityRole, nativeAccessibilityState } from "./layout-child.js";
|
|
4
4
|
import { cx } from "./cx.mobile.js";
|
|
5
|
-
import { defineUniversalComponent, hookSlots, universalPlan, universalProps, universalValue, useCallback, useEffect, useRef } from "@nativescript-community/octane";
|
|
5
|
+
import { defineUniversalComponent, hookSlots, universalPlan, universalProps, universalValue, useCallback, useEffect, useImperativeHandle, useRef } from "@nativescript-community/octane";
|
|
6
6
|
//#region src/CameraView.ios.tsrx
|
|
7
|
-
var _hs$ = /* @__PURE__ */ hookSlots(
|
|
7
|
+
var _hs$ = /* @__PURE__ */ hookSlots(5);
|
|
8
8
|
var _h$0 = /* @__PURE__ */ Symbol(_hs$);
|
|
9
9
|
var _h$1 = /* @__PURE__ */ Symbol(_hs$ + 1);
|
|
10
10
|
var _h$2 = /* @__PURE__ */ Symbol(_hs$ + 2);
|
|
11
11
|
var _h$3 = /* @__PURE__ */ Symbol(_hs$ + 3);
|
|
12
|
+
var _h$4 = /* @__PURE__ */ Symbol(_hs$ + 4);
|
|
12
13
|
var __octaneUniversalPlan0 = universalPlan("nativescript", {
|
|
13
14
|
"kind": "host",
|
|
14
15
|
"type": "gridlayout",
|
|
@@ -18,9 +19,10 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
18
19
|
const host = useRef(null, _h$0);
|
|
19
20
|
const active = props.active !== false;
|
|
20
21
|
const facing = props.facing ?? "back";
|
|
22
|
+
useImperativeHandle(props.ref, () => ({ native: host.current.ios ?? host.current.nativeViewProtected }), [], _h$1);
|
|
21
23
|
const bindHost = useCallback((view) => {
|
|
22
24
|
host.current = view;
|
|
23
|
-
}, [], _h$
|
|
25
|
+
}, [], _h$2);
|
|
24
26
|
useEffect(() => {
|
|
25
27
|
const view = host.current;
|
|
26
28
|
if (!view || !active) return;
|
|
@@ -47,13 +49,12 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
47
49
|
disposed = true;
|
|
48
50
|
stopPreview?.();
|
|
49
51
|
};
|
|
50
|
-
}, [active, facing], _h$
|
|
52
|
+
}, [active, facing], _h$3);
|
|
51
53
|
useEffect(() => {
|
|
52
54
|
const view = host.current;
|
|
53
55
|
if (!view) return;
|
|
54
56
|
applyEscapeProps(view.ios ?? view.nativeViewProtected, props);
|
|
55
|
-
|
|
56
|
-
}, [props.bind, props.ios], _h$3);
|
|
57
|
+
}, [props.ref, props.ios], _h$4);
|
|
57
58
|
return universalValue(__octaneUniversalPlan0, [universalProps([
|
|
58
59
|
[
|
|
59
60
|
"set",
|
|
@@ -78,7 +79,7 @@ var CameraView = defineUniversalComponent("nativescript", function CameraView(pr
|
|
|
78
79
|
[
|
|
79
80
|
"set",
|
|
80
81
|
"className",
|
|
81
|
-
cx("vx-
|
|
82
|
+
cx("vx-layers", "vx-cameraview", props.className)
|
|
82
83
|
],
|
|
83
84
|
[
|
|
84
85
|
"set",
|
|
@@ -1,12 +1,12 @@
|
|
|
1
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";
|
|
2
|
+
import { __s, bag15, bag5, child, clone, delegateEvents, evt2e, evt2u, hookSlots, ifBlock, markDangerouslySetInnerHTMLChildren, queueRefAttach, queueRefDetach, setStyleProperty, snapshotSpread, template, useEffect, useImperativeHandle, useRef } from "octane";
|
|
3
3
|
import { bindSignalHostPropSources, enableSignalBindings, replaceRef, setAttributeIfChanged } from "octane/internal/client";
|
|
4
4
|
//#region src/CameraView.web.tsrx
|
|
5
5
|
enableSignalBindings(1, true);
|
|
6
6
|
delegateEvents(["playing"]);
|
|
7
7
|
var _t$0 = /* @__PURE__ */ template("<video autoplay=\"\" playsinline=\"\" style=\"display: block; width: 100%; height: 100%; object-fit: cover;\"></video>");
|
|
8
8
|
var _t$1 = /* @__PURE__ */ template("<div><!></div>");
|
|
9
|
-
var _hs$ = /* @__PURE__ */ hookSlots(
|
|
9
|
+
var _hs$ = /* @__PURE__ */ hookSlots(5);
|
|
10
10
|
var __event = (__eventArg, readyFired, props) => {
|
|
11
11
|
if (readyFired.current) return;
|
|
12
12
|
readyFired.current = true;
|
|
@@ -19,18 +19,19 @@ var _rm$1 = [
|
|
|
19
19
|
];
|
|
20
20
|
function __then$0(__props, __s, __extra) {
|
|
21
21
|
const facing = __extra[0];
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const
|
|
22
|
+
const hostRef = __extra[1];
|
|
23
|
+
const props = __extra[2];
|
|
24
|
+
const readyFired = __extra[3];
|
|
25
|
+
const video = __extra[4];
|
|
25
26
|
let _b = __s.slots[0];
|
|
26
27
|
if (_b === void 0) {
|
|
27
28
|
let _m0, _m1, _m4;
|
|
28
29
|
const _root = clone(_t$0, void 0, "||");
|
|
29
30
|
queueRefAttach(__s, _m1 = (element) => {
|
|
31
|
+
hostRef.current = element;
|
|
30
32
|
video.current = element;
|
|
31
33
|
if (!element) return;
|
|
32
34
|
applyEscapeProps(element, props);
|
|
33
|
-
props.bind?.({ native: element });
|
|
34
35
|
}, _m0 = _root);
|
|
35
36
|
(__s.cleanups ??= []).push(() => _b && queueRefDetach(_b.b, _b.a));
|
|
36
37
|
_m4 = evt2e(_root, "$$playing", __event, readyFired, props);
|
|
@@ -39,10 +40,10 @@ function __then$0(__props, __s, __extra) {
|
|
|
39
40
|
} else {
|
|
40
41
|
{
|
|
41
42
|
const _r = (element) => {
|
|
43
|
+
hostRef.current = element;
|
|
42
44
|
video.current = element;
|
|
43
45
|
if (!element) return;
|
|
44
46
|
applyEscapeProps(element, props);
|
|
45
|
-
props.bind?.({ native: element });
|
|
46
47
|
};
|
|
47
48
|
if (_r !== _b.b) _b.b = replaceRef(__s, _b.b, _r, _b.a);
|
|
48
49
|
}
|
|
@@ -63,10 +64,11 @@ var _rm$3 = [
|
|
|
63
64
|
"m"
|
|
64
65
|
];
|
|
65
66
|
var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
|
|
66
|
-
const
|
|
67
|
+
const hostRef = useRef(null, _hs$ + 0);
|
|
68
|
+
const video = useRef(null, _hs$ + 1);
|
|
67
69
|
const facing = props.facing ?? "back";
|
|
68
70
|
const active = props.active !== false;
|
|
69
|
-
const readyFired = useRef(false, _hs$ +
|
|
71
|
+
const readyFired = useRef(false, _hs$ + 2);
|
|
70
72
|
useEffect(() => {
|
|
71
73
|
const element = video.current;
|
|
72
74
|
if (!element || !active) return;
|
|
@@ -97,14 +99,15 @@ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
|
|
|
97
99
|
element.srcObject = null;
|
|
98
100
|
if (stream) for (const track of stream.getTracks()) track.stop();
|
|
99
101
|
};
|
|
100
|
-
}, [active, facing], _hs$ +
|
|
102
|
+
}, [active, facing], _hs$ + 3);
|
|
103
|
+
useImperativeHandle(props.ref, () => ({ native: hostRef.current }), void 0, _hs$ + 4);
|
|
101
104
|
let _b = __s.slots[0];
|
|
102
105
|
if (_b === void 0) {
|
|
103
106
|
let _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14;
|
|
104
107
|
const _root = clone(_t$1);
|
|
105
108
|
_m0 = props.id;
|
|
106
109
|
_m1 = [
|
|
107
|
-
"vx-
|
|
110
|
+
"vx-layers",
|
|
108
111
|
"vx-cameraview",
|
|
109
112
|
props.className
|
|
110
113
|
];
|
|
@@ -181,7 +184,7 @@ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
|
|
|
181
184
|
"aria-live",
|
|
182
185
|
_m11
|
|
183
186
|
]
|
|
184
|
-
], "b:
|
|
187
|
+
], "b:25f1aaa9", true);
|
|
185
188
|
markDangerouslySetInnerHTMLChildren(_root);
|
|
186
189
|
_m14 = child(_root);
|
|
187
190
|
_b = bag15(__s, _root, _m0, _m1, _m2, _m3, _m4, _m5, _m6, _m7, _m8, _m9, _m10, _m11, _m12, _m13, _m14);
|
|
@@ -193,7 +196,7 @@ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
|
|
|
193
196
|
}
|
|
194
197
|
{
|
|
195
198
|
const _v = [
|
|
196
|
-
"vx-
|
|
199
|
+
"vx-layers",
|
|
197
200
|
"vx-cameraview",
|
|
198
201
|
props.className
|
|
199
202
|
];
|
|
@@ -298,12 +301,13 @@ var CameraView = /* @__PURE__ */ __s(function CameraView(props, __s, __extra) {
|
|
|
298
301
|
"aria-live",
|
|
299
302
|
_b.l
|
|
300
303
|
]
|
|
301
|
-
], "b:
|
|
304
|
+
], "b:25f1aaa9", true);
|
|
302
305
|
}
|
|
303
306
|
{
|
|
304
307
|
const __ifCondition = active;
|
|
305
308
|
ifBlock(__s, 1, _b.m, __ifCondition, __then$0, null, _b.o, __ifCondition ? [
|
|
306
309
|
facing,
|
|
310
|
+
hostRef,
|
|
307
311
|
props,
|
|
308
312
|
readyFired,
|
|
309
313
|
video
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/camera",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Live camera preview for Octane xplat — web getUserMedia + native capture (AVCapture / CameraX)",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"xplat",
|
|
8
|
-
"cross-platform",
|
|
6
|
+
"android",
|
|
9
7
|
"camera",
|
|
10
8
|
"camera-preview",
|
|
9
|
+
"cross-platform",
|
|
10
|
+
"ios",
|
|
11
|
+
"nativescript",
|
|
12
|
+
"octane",
|
|
11
13
|
"photo",
|
|
12
14
|
"video-capture",
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"android",
|
|
16
|
-
"web"
|
|
15
|
+
"web",
|
|
16
|
+
"xplat"
|
|
17
17
|
],
|
|
18
|
-
"license": "MIT",
|
|
19
18
|
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/camera",
|
|
19
|
+
"license": "MIT",
|
|
20
20
|
"repository": {
|
|
21
21
|
"type": "git",
|
|
22
|
-
"url": "
|
|
22
|
+
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
23
23
|
"directory": "packages/camera"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
@@ -29,12 +29,6 @@
|
|
|
29
29
|
"platforms"
|
|
30
30
|
],
|
|
31
31
|
"type": "module",
|
|
32
|
-
"nativescript": {
|
|
33
|
-
"platforms": {
|
|
34
|
-
"ios": "7.0.0",
|
|
35
|
-
"android": "7.0.0"
|
|
36
|
-
}
|
|
37
|
-
},
|
|
38
32
|
"exports": {
|
|
39
33
|
".": {
|
|
40
34
|
"types": "./types/index.d.ts",
|
|
@@ -51,7 +45,7 @@
|
|
|
51
45
|
"@nativescript/core": "9.1.2",
|
|
52
46
|
"@octanejs/vite-plugin": "0.1.61",
|
|
53
47
|
"octane": "0.6.3",
|
|
54
|
-
"tsrx-typegen": "0.2.
|
|
48
|
+
"tsrx-typegen": "0.2.2",
|
|
55
49
|
"typescript": "5.9.3",
|
|
56
50
|
"vite": "8.3.0"
|
|
57
51
|
},
|
|
@@ -71,6 +65,12 @@
|
|
|
71
65
|
"optional": true
|
|
72
66
|
}
|
|
73
67
|
},
|
|
68
|
+
"nativescript": {
|
|
69
|
+
"platforms": {
|
|
70
|
+
"ios": "7.0.0",
|
|
71
|
+
"android": "7.0.0"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
74
|
"scripts": {
|
|
75
75
|
"build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
|
|
76
76
|
"typegen": "tsc -p tsconfig.types.json",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'octane'
|
|
2
|
+
import { useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
|
|
3
3
|
import { Application, View } from '@nativescript/core'
|
|
4
4
|
import { registerElement } from '@nativescript-community/octane'
|
|
5
5
|
import type { Attributes } from '@nativescript-community/octane/intrinsics'
|
|
@@ -28,6 +28,8 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
28
28
|
const host = useRef<any>(null)
|
|
29
29
|
const active = props.active !== false
|
|
30
30
|
const facing = props.facing ?? 'back'
|
|
31
|
+
useImperativeHandle(props.ref, () => ({ native: host.current.android }), []);
|
|
32
|
+
|
|
31
33
|
const bindHost = useCallback((view: any) => {
|
|
32
34
|
host.current = view
|
|
33
35
|
}, [])
|
|
@@ -102,14 +104,13 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
102
104
|
const view = host.current
|
|
103
105
|
if (!view) return
|
|
104
106
|
applyEscapeProps(view.android, props)
|
|
105
|
-
|
|
106
|
-
}, [props.bind, props.android])
|
|
107
|
+
}, [props.ref, props.android])
|
|
107
108
|
|
|
108
109
|
<gridlayout
|
|
109
110
|
id={props.id}
|
|
110
111
|
rows="*"
|
|
111
112
|
columns="*"
|
|
112
|
-
className={cx('vx-
|
|
113
|
+
className={cx('vx-layers', 'vx-cameraview', props.className)}
|
|
113
114
|
style={{ backgroundColor: '#000', ...props.style }}
|
|
114
115
|
{...layoutChildProps(props)}
|
|
115
116
|
accessible={props.accessible}
|
package/src/CameraView.ios.tsrx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** @jsxImportSource @nativescript-community/octane */
|
|
2
|
-
import { useCallback, useEffect, useRef } from 'octane'
|
|
2
|
+
import { useImperativeHandle, useCallback, useEffect, useRef } from 'octane'
|
|
3
3
|
import type { CameraViewProps } from './props'
|
|
4
4
|
import { cameraHardwarePresent, ensureCameraPermission } from './camera-permission.mobile'
|
|
5
5
|
import { startCameraPreview } from './camera-session.ios'
|
|
@@ -11,6 +11,8 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
11
11
|
const host = useRef<any>(null)
|
|
12
12
|
const active = props.active !== false
|
|
13
13
|
const facing = props.facing ?? 'back'
|
|
14
|
+
useImperativeHandle(props.ref, () => ({ native: host.current.ios ?? host.current.nativeViewProtected }), []);
|
|
15
|
+
|
|
14
16
|
const bindHost = useCallback((view: any) => {
|
|
15
17
|
host.current = view
|
|
16
18
|
}, [])
|
|
@@ -51,15 +53,14 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
51
53
|
const view = host.current
|
|
52
54
|
if (!view) return
|
|
53
55
|
applyEscapeProps(view.ios ?? view.nativeViewProtected, props)
|
|
54
|
-
|
|
55
|
-
}, [props.bind, props.ios])
|
|
56
|
+
}, [props.ref, props.ios])
|
|
56
57
|
|
|
57
58
|
<gridlayout
|
|
58
59
|
ref={bindHost}
|
|
59
60
|
id={props.id}
|
|
60
61
|
rows="*"
|
|
61
62
|
columns="*"
|
|
62
|
-
className={cx('vx-
|
|
63
|
+
className={cx('vx-layers', 'vx-cameraview', props.className)}
|
|
63
64
|
style={{ backgroundColor: '#000', ...props.style }}
|
|
64
65
|
{...layoutChildProps(props)}
|
|
65
66
|
accessible={props.accessible}
|
package/src/CameraView.web.tsrx
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { useEffect, useRef } from 'octane'
|
|
1
|
+
import { useImperativeHandle, useEffect, useRef } from 'octane'
|
|
2
2
|
import type { CameraViewProps } from './props'
|
|
3
3
|
import { applyEscapeProps, layoutChildProps } from './layout-child.web'
|
|
4
4
|
|
|
5
5
|
/** Live web preview backed by getUserMedia. */
|
|
6
6
|
export function CameraView(props: CameraViewProps) @{
|
|
7
|
+
const hostRef = useRef<any>(null);
|
|
7
8
|
const video = useRef<HTMLVideoElement | null>(null)
|
|
8
9
|
const facing = props.facing ?? 'back'
|
|
9
10
|
const active = props.active !== false
|
|
@@ -52,9 +53,11 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
52
53
|
}
|
|
53
54
|
}, [active, facing])
|
|
54
55
|
|
|
56
|
+
useImperativeHandle(props.ref, () => ({ native: hostRef.current }), undefined);
|
|
57
|
+
|
|
55
58
|
<div
|
|
56
59
|
id={props.id}
|
|
57
|
-
className={['vx-
|
|
60
|
+
className={['vx-layers', 'vx-cameraview', props.className]}
|
|
58
61
|
{...layoutChildProps(props, {
|
|
59
62
|
position: 'relative',
|
|
60
63
|
overflow: 'hidden',
|
|
@@ -74,10 +77,10 @@ export function CameraView(props: CameraViewProps) @{
|
|
|
74
77
|
@if (active) {
|
|
75
78
|
<video
|
|
76
79
|
ref={(element: HTMLVideoElement | null) => {
|
|
80
|
+
hostRef.current = element;
|
|
77
81
|
video.current = element
|
|
78
82
|
if (!element) return
|
|
79
83
|
applyEscapeProps(element, props)
|
|
80
|
-
props.bind?.({ native: element })
|
|
81
84
|
}}
|
|
82
85
|
muted={true}
|
|
83
86
|
autoplay={true}
|
|
@@ -3,11 +3,17 @@ import { Application, isAndroid, isIOS } from '@nativescript/core'
|
|
|
3
3
|
let requestCode = 7719
|
|
4
4
|
|
|
5
5
|
export function cameraHardwarePresent(): boolean {
|
|
6
|
-
if (isIOS) {
|
|
7
|
-
|
|
6
|
+
if (isIOS) {
|
|
7
|
+
return typeof (globalThis as any).AVCaptureDevice !== 'undefined'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (!isAndroid) {
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
8
14
|
try {
|
|
9
15
|
const context = Application.android?.context ?? Application.android?.startActivity
|
|
10
|
-
|
|
16
|
+
return context?.getPackageManager()?.hasSystemFeature('android.hardware.camera.any') !== false
|
|
11
17
|
} catch {
|
|
12
18
|
return true
|
|
13
19
|
}
|
|
@@ -19,8 +25,14 @@ export async function ensureCameraPermission(): Promise<boolean> {
|
|
|
19
25
|
const av = (globalThis as any).AVCaptureDevice
|
|
20
26
|
const mediaType = (globalThis as any).AVMediaTypeVideo ?? 'vide'
|
|
21
27
|
const status = av?.authorizationStatusForMediaType?.(mediaType)
|
|
22
|
-
if (status === 3) {
|
|
23
|
-
|
|
28
|
+
if (status === 3) {
|
|
29
|
+
return true
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (status === 1 || status === 2) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
24
36
|
return await new Promise<boolean>((resolve) =>
|
|
25
37
|
av.requestAccessForMediaTypeCompletionHandler(mediaType, resolve),
|
|
26
38
|
)
|
|
@@ -32,28 +44,27 @@ export async function ensureCameraPermission(): Promise<boolean> {
|
|
|
32
44
|
if (
|
|
33
45
|
activity?.checkSelfPermission?.(permission) ===
|
|
34
46
|
android.content.pm.PackageManager.PERMISSION_GRANTED
|
|
35
|
-
) {
|
|
47
|
+
) {
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!activity) {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
36
54
|
|
|
37
|
-
if (!activity) {return false}
|
|
38
55
|
return await new Promise<boolean>((resolve) => {
|
|
39
56
|
const currentRequest = requestCode++
|
|
40
57
|
const onResult = (args: any) => {
|
|
41
|
-
if (args.requestCode !== currentRequest) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
)
|
|
58
|
+
if (args.requestCode !== currentRequest) {
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
Application.android.off(Application.android.activityRequestPermissionsEvent, onResult)
|
|
46
63
|
|
|
47
|
-
resolve(
|
|
48
|
-
args.grantResults?.[0] ===
|
|
49
|
-
android.content.pm.PackageManager.PERMISSION_GRANTED,
|
|
50
|
-
)
|
|
64
|
+
resolve(args.grantResults?.[0] === android.content.pm.PackageManager.PERMISSION_GRANTED)
|
|
51
65
|
}
|
|
52
66
|
|
|
53
|
-
Application.android.on(
|
|
54
|
-
Application.android.activityRequestPermissionsEvent,
|
|
55
|
-
onResult,
|
|
56
|
-
)
|
|
67
|
+
Application.android.on(Application.android.activityRequestPermissionsEvent, onResult)
|
|
57
68
|
|
|
58
69
|
activity.requestPermissions([permission], currentRequest)
|
|
59
70
|
})
|
|
@@ -6,9 +6,10 @@ export function startCameraPreview(
|
|
|
6
6
|
): () => void {
|
|
7
7
|
const av = globalThis as any
|
|
8
8
|
const mediaType = av.AVMediaTypeVideo ?? 'vide'
|
|
9
|
-
const desiredPosition =
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const desiredPosition =
|
|
10
|
+
facing === 'front'
|
|
11
|
+
? (av.AVCaptureDevicePositionFront ?? 2)
|
|
12
|
+
: (av.AVCaptureDevicePositionBack ?? 1)
|
|
12
13
|
|
|
13
14
|
const devices = av.AVCaptureDevice.devicesWithMediaType(mediaType)
|
|
14
15
|
let device: any = null
|
|
@@ -20,22 +21,32 @@ export function startCameraPreview(
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
if (!device) {
|
|
24
|
+
if (!device) {
|
|
25
|
+
throw new Error('camera unavailable')
|
|
26
|
+
}
|
|
24
27
|
|
|
25
28
|
const session = av.AVCaptureSession.new()
|
|
26
29
|
const input = av.AVCaptureDeviceInput.deviceInputWithDeviceError(device)
|
|
27
|
-
if (!input || !session.canAddInput(input)) {
|
|
30
|
+
if (!input || !session.canAddInput(input)) {
|
|
31
|
+
throw new Error('camera input unavailable')
|
|
32
|
+
}
|
|
33
|
+
|
|
28
34
|
session.addInput(input)
|
|
29
35
|
const layer = av.AVCaptureVideoPreviewLayer.layerWithSession(session)
|
|
30
36
|
layer.videoGravity = 'resizeAspectFill'
|
|
31
37
|
const connection = layer.connection
|
|
32
38
|
if (connection) {
|
|
33
39
|
connection.automaticallyAdjustsVideoMirroring = false
|
|
34
|
-
if (connection.isVideoMirroringSupported) {
|
|
40
|
+
if (connection.isVideoMirroringSupported) {
|
|
41
|
+
connection.isVideoMirrored = facing === 'front'
|
|
42
|
+
}
|
|
35
43
|
}
|
|
36
44
|
|
|
37
45
|
const nativeView = view.ios ?? view.nativeViewProtected
|
|
38
|
-
if (!nativeView?.layer) {
|
|
46
|
+
if (!nativeView?.layer) {
|
|
47
|
+
throw new Error('camera preview host unavailable')
|
|
48
|
+
}
|
|
49
|
+
|
|
39
50
|
nativeView.layer.addSublayer(layer)
|
|
40
51
|
const updateFrame = () => {
|
|
41
52
|
layer.frame = nativeView.bounds
|
package/src/layout-child.ts
CHANGED
|
@@ -17,16 +17,23 @@ export function layoutChildProps(props: CameraViewProps): Record<string, any> {
|
|
|
17
17
|
'alignSelf',
|
|
18
18
|
'order',
|
|
19
19
|
] as const) {
|
|
20
|
-
if (props[key] !== undefined) {
|
|
20
|
+
if (props[key] !== undefined) {
|
|
21
|
+
result[key] = props[key]
|
|
22
|
+
}
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
return result
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
export function applyEscapeProps(view: any, props: { ios?: any; android?: any }): void {
|
|
27
|
-
if (!view) {
|
|
29
|
+
if (!view) {
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
|
|
28
33
|
const bag = isIOS ? props.ios : props.android
|
|
29
|
-
if (bag) {
|
|
34
|
+
if (bag) {
|
|
35
|
+
Object.assign(view, bag)
|
|
36
|
+
}
|
|
30
37
|
}
|
|
31
38
|
|
|
32
39
|
const nativeRoles: Record<string, string> = {
|
|
@@ -51,12 +58,24 @@ export function nativeAccessibilityRole(role?: string): string | undefined {
|
|
|
51
58
|
return role ? nativeRoles[role] : undefined
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export function nativeAccessibilityState(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (state?.disabled) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
export function nativeAccessibilityState(
|
|
62
|
+
state?: CameraViewProps['accessibilityState'],
|
|
63
|
+
): string | undefined {
|
|
64
|
+
if (state?.disabled) {
|
|
65
|
+
return 'disabled'
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (state?.selected) {
|
|
69
|
+
return 'selected'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (state?.checked === true) {
|
|
73
|
+
return 'checked'
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (state?.checked === false) {
|
|
77
|
+
return 'unchecked'
|
|
78
|
+
}
|
|
79
|
+
|
|
61
80
|
return undefined
|
|
62
81
|
}
|
package/src/layout-child.web.ts
CHANGED
|
@@ -14,21 +14,45 @@ export function layoutChildProps(props: CameraViewProps, baseStyle: any = props.
|
|
|
14
14
|
style.gridColumn = `span ${props.colSpan}`
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
if (props.left !== undefined) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (props.
|
|
22
|
-
|
|
17
|
+
if (props.left !== undefined) {
|
|
18
|
+
style.left = props.left
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (props.top !== undefined) {
|
|
22
|
+
style.top = props.top
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (props.flexGrow !== undefined) {
|
|
26
|
+
style.flexGrow = props.flexGrow
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (props.flexShrink !== undefined) {
|
|
30
|
+
style.flexShrink = props.flexShrink
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (props.alignSelf !== undefined) {
|
|
34
|
+
style.alignSelf = props.alignSelf
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (props.order !== undefined) {
|
|
38
|
+
style.order = props.order
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
return { style }
|
|
24
42
|
}
|
|
25
43
|
|
|
26
44
|
export function applyEscapeProps(element: any, props: { web?: any }): void {
|
|
27
|
-
if (!element || !props.web) {
|
|
45
|
+
if (!element || !props.web) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
28
49
|
for (const [key, value] of Object.entries(props.web)) {
|
|
29
50
|
if (key.includes('-')) {
|
|
30
|
-
if (value == null || value === false) {
|
|
31
|
-
|
|
51
|
+
if (value == null || value === false) {
|
|
52
|
+
element.removeAttribute(key)
|
|
53
|
+
} else {
|
|
54
|
+
element.setAttribute(key, value === true ? '' : String(value))
|
|
55
|
+
}
|
|
32
56
|
} else if (key in element) {
|
|
33
57
|
try {
|
|
34
58
|
element[key] = value
|
package/src/props.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime'
|
|
2
|
+
|
|
1
3
|
export type CameraAccessibilityRole =
|
|
2
4
|
| 'button'
|
|
3
5
|
| 'link'
|
|
@@ -54,7 +56,7 @@ export interface CameraViewProps {
|
|
|
54
56
|
onReady?: () => void
|
|
55
57
|
onError?: (error: { message?: string }) => void
|
|
56
58
|
/** Native preview surface for platform-specific controls. */
|
|
57
|
-
|
|
59
|
+
ref?: Octane.Ref<CameraViewHandle>
|
|
58
60
|
/** Platform-specific properties applied to the preview surface. */
|
|
59
61
|
ios?: Record<string, any>
|
|
60
62
|
android?: Record<string, any>
|
package/types/props.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
1
2
|
export type CameraAccessibilityRole = 'button' | 'link' | 'search' | 'image' | 'heading' | 'adjustable' | 'summary' | 'text' | 'none' | 'progressbar' | 'checkbox' | 'switch' | 'radio' | 'spinbutton' | 'tab';
|
|
2
3
|
export interface CameraViewHandle {
|
|
3
4
|
/** The native preview surface (`HTMLVideoElement` on web). */
|
|
@@ -39,7 +40,7 @@ export interface CameraViewProps {
|
|
|
39
40
|
message?: string;
|
|
40
41
|
}) => void;
|
|
41
42
|
/** Native preview surface for platform-specific controls. */
|
|
42
|
-
|
|
43
|
+
ref?: Octane.Ref<CameraViewHandle>;
|
|
43
44
|
/** Platform-specific properties applied to the preview surface. */
|
|
44
45
|
ios?: Record<string, any>;
|
|
45
46
|
android?: Record<string, any>;
|