@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.
- package/dist/native/CameraView.android.js +165 -0
- package/dist/native/CameraView.ios.js +130 -0
- package/dist/native/camera-permission.mobile.js +45 -0
- package/dist/native/camera-session.ios.js +45 -0
- package/dist/native/cx.mobile.js +8 -0
- package/dist/native/index.js +2 -0
- package/dist/native/layout-child.js +53 -0
- package/dist/native/octane-client-build.json +8 -0
- package/dist/web/CameraView.web.tsrx.js +314 -0
- package/dist/web/index.js +2 -0
- package/dist/web/layout-child.web.js +29 -0
- package/dist/web/octane-client-build.json +8 -0
- package/package.json +61 -0
- package/platforms/android/AndroidManifest.xml +4 -0
- package/platforms/android/include.gradle +13 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +71 -0
- package/platforms/ios/Info.plist +8 -0
- package/src/CameraView.android.tsrx +125 -0
- package/src/CameraView.ios.tsrx +73 -0
- package/src/CameraView.mobile.ts +6 -0
- package/src/CameraView.web.tsrx +100 -0
- package/src/camera-permission.mobile.ts +66 -0
- package/src/camera-session.ios.ts +54 -0
- package/src/cx.mobile.ts +5 -0
- package/src/index.ts +4 -0
- package/src/index.web.ts +2 -0
- package/src/layout-child.ts +62 -0
- package/src/layout-child.web.ts +42 -0
- package/src/props.ts +62 -0
- package/types/index.d.ts +5 -0
- package/types/props.d.ts +47 -0
|
@@ -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,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 };
|
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,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
|
+
}
|