@octane-xplat/camera 0.7.3 → 0.9.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 +31 -0
- package/dist/native/camera-permission.mobile.js +1 -1
- package/dist/native/octane-client-build.json +1 -1
- package/dist/web/octane-client-build.json +1 -1
- package/package.json +22 -18
- 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/types/index.d.ts +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# `@octane-xplat/camera`
|
|
2
|
+
|
|
3
|
+
Live camera preview for Octane xplat apps: `getUserMedia` video on web,
|
|
4
|
+
`AVCaptureSession` on iOS, CameraX on Android.
|
|
5
|
+
|
|
6
|
+
```sh
|
|
7
|
+
pnpm add @octane-xplat/camera
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```tsx
|
|
11
|
+
import { CameraView } from '@octane-xplat/camera'
|
|
12
|
+
|
|
13
|
+
;<CameraView
|
|
14
|
+
facing="back" // 'front' | 'back', default 'back'
|
|
15
|
+
active={streaming} // default true — false releases the session
|
|
16
|
+
onReady={() => console.log('preview running')}
|
|
17
|
+
onError={(e) => console.log(e.message)}
|
|
18
|
+
bind={(h) => (handle = h)} // h.native is the preview surface
|
|
19
|
+
/>
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
This is the preview surface only — for one-shot still capture and library
|
|
23
|
+
picking use [`@octane-xplat/media`](../media/README.md). Permissions are
|
|
24
|
+
requested when the session starts; `onError` is how a denial shows up.
|
|
25
|
+
`bind` hands you the platform surface (`HTMLVideoElement` on web) for
|
|
26
|
+
controls the shared props don't cover, and the `ios`/`android`/`web`
|
|
27
|
+
escape props apply after the shared props.
|
|
28
|
+
|
|
29
|
+
Guide: [Building screens](../../docs/primitives.md) (leaf components);
|
|
30
|
+
component index: [`docs/components.md`](../../docs/components.md).
|
|
31
|
+
Exercised by [`CameraDemo`](../demos/src/CameraDemo.tsrx).
|
|
@@ -5,7 +5,7 @@ function cameraHardwarePresent() {
|
|
|
5
5
|
if (isIOS) return typeof globalThis.AVCaptureDevice !== "undefined";
|
|
6
6
|
if (!isAndroid) return false;
|
|
7
7
|
try {
|
|
8
|
-
return (Application.android?.context ?? Application.android?.startActivity)?.
|
|
8
|
+
return (Application.android?.context ?? Application.android?.startActivity)?.getPackageManager()?.hasSystemFeature("android.hardware.camera.any") !== false;
|
|
9
9
|
} catch {
|
|
10
10
|
return true;
|
|
11
11
|
}
|
package/package.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@octane-xplat/camera",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -47,10 +41,11 @@
|
|
|
47
41
|
"access": "public"
|
|
48
42
|
},
|
|
49
43
|
"devDependencies": {
|
|
50
|
-
"@nativescript-community/octane": "0.2.
|
|
44
|
+
"@nativescript-community/octane": "0.2.4",
|
|
51
45
|
"@nativescript/core": "9.1.2",
|
|
52
46
|
"@octanejs/vite-plugin": "0.1.61",
|
|
53
47
|
"octane": "0.6.3",
|
|
48
|
+
"tsrx-typegen": "0.2.1",
|
|
54
49
|
"typescript": "5.9.3",
|
|
55
50
|
"vite": "8.3.0"
|
|
56
51
|
},
|
|
@@ -70,7 +65,16 @@
|
|
|
70
65
|
"optional": true
|
|
71
66
|
}
|
|
72
67
|
},
|
|
68
|
+
"nativescript": {
|
|
69
|
+
"platforms": {
|
|
70
|
+
"ios": "7.0.0",
|
|
71
|
+
"android": "7.0.0"
|
|
72
|
+
}
|
|
73
|
+
},
|
|
73
74
|
"scripts": {
|
|
74
|
-
"build": "vite build && vite build --mode native &&
|
|
75
|
+
"build": "vite build && vite build --mode native && pnpm typegen && node ../../scripts/check-native-dist.mjs",
|
|
76
|
+
"typegen": "tsc -p tsconfig.types.json",
|
|
77
|
+
"test:packed": "node tests/packed-consumer.mjs",
|
|
78
|
+
"pack:check": "tsrx-typegen --pack-check"
|
|
75
79
|
}
|
|
76
80
|
}
|
|
@@ -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
|
-
return context?.
|
|
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/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { UniversalComponent } from 'octane/universal'
|
|
2
|
-
import type { CameraViewProps } from './props'
|
|
2
|
+
import type { CameraViewProps } from './props.js'
|
|
3
3
|
|
|
4
4
|
export declare const CameraView: UniversalComponent<CameraViewProps>
|
|
5
|
-
export type { CameraViewHandle, CameraViewProps, CameraAccessibilityRole } from './props'
|
|
5
|
+
export type { CameraViewHandle, CameraViewProps, CameraAccessibilityRole } from './props.js'
|