@octane-xplat/camera 0.0.0-nightly.g2e22171
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/LICENSE +21 -0
- package/README.md +135 -0
- package/package.json +104 -0
- package/platforms/android/AndroidManifest.xml +6 -0
- package/platforms/android/include.gradle +14 -0
- package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +84 -0
- package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
- package/platforms/ios/Info.plist +10 -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 +150 -0
- package/src/CameraView.ios.tsrx +102 -0
- package/src/CameraView.macos.tsrx +95 -0
- package/src/CameraView.mobile.ts +6 -0
- package/src/CameraView.web.tsrx +132 -0
- package/src/CameraView.windows.tsrx +126 -0
- package/src/camera-permission.mobile.ts +91 -0
- package/src/camera-permission.windows.ts +76 -0
- package/src/camera-session.ios.ts +65 -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 +36 -0
- package/src/index.web.ts +34 -0
- package/src/layout-child.ts +81 -0
- package/src/layout-child.web.ts +66 -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 +77 -0
- 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 +48 -0
- package/types/props.d.ts +61 -0
- package/types/session-core.d.ts +139 -0
- package/types/synthetic-session.d.ts +51 -0
- package/types/types.d.ts +290 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alec Larson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
`AVCaptureSession` + `AVCaptureVideoPreviewLayer` on macOS (AppKit), and
|
|
10
|
+
`MediaCapture` + `CaptureElement` on the Windows (WinUI 3) target.
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
import { CameraView } from '@octane-xplat/camera'
|
|
14
|
+
|
|
15
|
+
export function Preview() {
|
|
16
|
+
return (
|
|
17
|
+
<CameraView
|
|
18
|
+
facing="back"
|
|
19
|
+
active
|
|
20
|
+
onReady={() => console.log('Preview running')}
|
|
21
|
+
onError={(error) => console.log(error.message)}
|
|
22
|
+
/>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
This is the preview surface only — for one-shot still capture and library
|
|
28
|
+
picking use [`@octane-xplat/media`](../media/README.md). Permissions are
|
|
29
|
+
requested when the session starts; `onError` is how a denial shows up.
|
|
30
|
+
`ref` hands you the platform surface (`HTMLVideoElement` on web) for
|
|
31
|
+
controls the shared props don't cover, and the `ios`/`android`/`web`/
|
|
32
|
+
`windows` escape props apply after the shared props. On macOS the leaf is a
|
|
33
|
+
compiled Swift `XplatCameraHost` that owns the capture session; the app bundle
|
|
34
|
+
needs `NSCameraUsageDescription` (and `NSMicrophoneUsageDescription` for
|
|
35
|
+
audio) declared through `xplat.targets.macos.package.infoPlist`.
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
// Preview.web.tsx — browser properties belong in this platform file.
|
|
39
|
+
import { CameraView } from '@octane-xplat/camera'
|
|
40
|
+
|
|
41
|
+
export function Preview() {
|
|
42
|
+
return (
|
|
43
|
+
<CameraView
|
|
44
|
+
web={{ playsInline: true }}
|
|
45
|
+
ref={(handle) => console.log(handle.native)}
|
|
46
|
+
onError={(error) => console.log(error.message)}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Tap-to-focus
|
|
53
|
+
|
|
54
|
+
`CameraView` reports where the user tapped as a normalized point —
|
|
55
|
+
`{x: 0, y: 0}` is the top-left corner of the preview, `{x: 1, y: 1}` the
|
|
56
|
+
bottom-right. Hand the point to a session's `focusAtPoint` to meter one
|
|
57
|
+
autofocus and auto-exposure pass there:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import { CameraView, createCameraSession } from '@octane-xplat/camera'
|
|
61
|
+
|
|
62
|
+
const session = createCameraSession()
|
|
63
|
+
|
|
64
|
+
export function FocusablePreview() {
|
|
65
|
+
return <CameraView session={session} onTap={(point) => session.focusAtPoint(point)} />
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`onTap` is a pure notification — it never takes over the gesture, so
|
|
70
|
+
double-tap and pan recognizers layered over the preview keep working.
|
|
71
|
+
`focusAtPoint` needs a session (it drives the camera the preview
|
|
72
|
+
attaches) and quietly does nothing on cameras that cannot meter at a
|
|
73
|
+
point — front cameras are often fixed-focus. Check
|
|
74
|
+
`capabilities().focusPointOfInterest` and `exposurePointOfInterest`
|
|
75
|
+
before showing focus UI. Metering lands on iOS, Android, and macOS; the
|
|
76
|
+
tap event fires on iOS, Android, and web.
|
|
77
|
+
|
|
78
|
+
## Movie recording
|
|
79
|
+
|
|
80
|
+
`createCameraSession` creates a shared camera owner: the same session can
|
|
81
|
+
drive a `CameraView` preview and one movie-recording attempt at a time.
|
|
82
|
+
Pass the session to `CameraView` instead of `facing`; the preview attaches
|
|
83
|
+
the camera the session owns. Recording requires an attached, ready preview.
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
import { CameraView, createCameraSession } from '@octane-xplat/camera'
|
|
87
|
+
|
|
88
|
+
const session = createCameraSession({ camera: 'default' })
|
|
89
|
+
|
|
90
|
+
export function Recorder() {
|
|
91
|
+
return <CameraView session={session} />
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
A take settles after the movie is finalized, metadata is verified, and local
|
|
96
|
+
storage is committed. Native output lives in app-private files — including the
|
|
97
|
+
packaged Linux WebKitGTK host, which writes under the app's data directory;
|
|
98
|
+
Web uses persistent origin storage. Reopen it with `session.openOutput(output)`.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
const take = session.startRecording({ maximumDurationMs: 30_000 })
|
|
102
|
+
// … later, on a stop press or a maximum-duration limit:
|
|
103
|
+
const outcome = await take.stop()
|
|
104
|
+
if (outcome.kind === 'clip') {
|
|
105
|
+
const clip = outcome.clip
|
|
106
|
+
console.log(clip.durationMs, clip.width, clip.height, clip.output)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
One preview and one recording per session; a second `startRecording` throws
|
|
111
|
+
`CameraCaptureError` with `kind: 'busy'`. Check `session.capabilities()` for
|
|
112
|
+
`supported`/`available` before presenting capture UI, and `session.snapshot()`
|
|
113
|
+
or `session.subscribe` for state. iOS, Android, Web, macOS, the Windows WinUI
|
|
114
|
+
target, and the packaged Linux WebKitGTK host have recording adapters; Web
|
|
115
|
+
requires persistent origin storage.
|
|
116
|
+
macOS writes app-private MOV files through `AVCaptureMovieFileOutput` and
|
|
117
|
+
reports finalized track metadata; on macOS 14+ the four cardinal orientations
|
|
118
|
+
are lockable. Inside a qualified `xplat` WKWebView host the Web backend stores
|
|
119
|
+
output in the app's private storage root instead of IndexedDB. Windows records
|
|
120
|
+
through `MediaCapture` into app-private MP4 files and is pending real-host
|
|
121
|
+
qualification. On Linux the GJS/WebKitGTK host owns media permissions and
|
|
122
|
+
writes MP4 clips under the app's data directory.
|
|
123
|
+
See [Record and reopen a camera movie](../../docs/app/movie-capture.md) for
|
|
124
|
+
explicit permission actions, audio, local output, and qualification limits.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
console.log(await session.permissions())
|
|
128
|
+
// Call from an app button:
|
|
129
|
+
await session.requestPermission('camera')
|
|
130
|
+
console.log(await session.capabilities())
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Guide: [Building screens](../../docs/app/primitives.md) (leaf components);
|
|
134
|
+
component index: [`docs/app/components.md`](../../docs/app/components.md).
|
|
135
|
+
Exercised by [`CameraDemo`](../demos/src/CameraDemo.tsrx).
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octane-xplat/camera",
|
|
3
|
+
"version": "0.0.0-nightly.g2e22171",
|
|
4
|
+
"description": "Live camera preview for Octane xplat — web getUserMedia + native capture (AVCapture / CameraX)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"android",
|
|
7
|
+
"camera",
|
|
8
|
+
"camera-preview",
|
|
9
|
+
"cross-platform",
|
|
10
|
+
"ios",
|
|
11
|
+
"nativescript",
|
|
12
|
+
"octane",
|
|
13
|
+
"photo",
|
|
14
|
+
"video-capture",
|
|
15
|
+
"web",
|
|
16
|
+
"xplat"
|
|
17
|
+
],
|
|
18
|
+
"homepage": "https://github.com/octane-xplat/octane-xplat/tree/main/packages/camera",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/octane-xplat/octane-xplat",
|
|
23
|
+
"directory": "packages/camera"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"types",
|
|
27
|
+
"src",
|
|
28
|
+
"platforms",
|
|
29
|
+
"!src/**/*.test.*",
|
|
30
|
+
"!src/**/*.spec.*"
|
|
31
|
+
],
|
|
32
|
+
"type": "module",
|
|
33
|
+
"exports": {
|
|
34
|
+
".": {
|
|
35
|
+
"types": "./types/index.d.ts",
|
|
36
|
+
"linux": "./src/index.web.ts",
|
|
37
|
+
"web": "./src/index.web.ts",
|
|
38
|
+
"native": "./src/index.ts",
|
|
39
|
+
"default": "./src/index.ts"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"publishConfig": {
|
|
43
|
+
"access": "public"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@octane-xplat/platform": "0.0.0-nightly.g2e22171",
|
|
47
|
+
"mediabunny": "1.61.3"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@nativescript-community/octane": "0.2.5",
|
|
51
|
+
"@nativescript/core": "9.1.3",
|
|
52
|
+
"@octane-xplat/macos-renderer": "0.0.0-nightly.g2e22171",
|
|
53
|
+
"@octanejs/vite-plugin": "0.1.61",
|
|
54
|
+
"octane": "0.11.0",
|
|
55
|
+
"tsrx-typegen": "0.3.0",
|
|
56
|
+
"typescript": "5.9.3",
|
|
57
|
+
"vite": "8.3.0",
|
|
58
|
+
"vitest": "4.1.11"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@nativescript-community/octane": ">=0.2.1 <1",
|
|
62
|
+
"@nativescript/core": ">=9.1.0 <10",
|
|
63
|
+
"octane": ">=0.6.3 <1"
|
|
64
|
+
},
|
|
65
|
+
"peerDependenciesMeta": {
|
|
66
|
+
"@nativescript-community/octane": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@nativescript/core": {
|
|
70
|
+
"optional": true
|
|
71
|
+
},
|
|
72
|
+
"octane": {
|
|
73
|
+
"optional": true
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
"nativescript": {
|
|
77
|
+
"platforms": {
|
|
78
|
+
"ios": "7.0.0",
|
|
79
|
+
"android": "7.0.0"
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
"xplat": {
|
|
83
|
+
"macos": {
|
|
84
|
+
"swiftHeaderImports": [
|
|
85
|
+
"AppKit/AppKit.h"
|
|
86
|
+
],
|
|
87
|
+
"frameworks": [
|
|
88
|
+
"AVFoundation",
|
|
89
|
+
"CoreMedia",
|
|
90
|
+
"AppKit"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
"scripts": {
|
|
95
|
+
"typegen": "tsc -p tsconfig.types.json",
|
|
96
|
+
"test:packed": "node tests/packed-consumer.mjs",
|
|
97
|
+
"pack:check": "tsrx-typegen --pack-check",
|
|
98
|
+
"test": "vitest run && node --test tests/*.test.mjs",
|
|
99
|
+
"test:macos-photo": "node tests/photo-native.macos.mjs",
|
|
100
|
+
"test:macos-photo-hardware": "node tests/photo-hardware.macos.mjs",
|
|
101
|
+
"test:web": "node tests/browser-conformance.web.mjs",
|
|
102
|
+
"test:webkitgtk": "node tests/webkitgtk-conformance.linux.mjs"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
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-feature android:name="android.hardware.microphone" android:required="false" />
|
|
4
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
5
|
+
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
|
6
|
+
</manifest>
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
implementation "androidx.camera:camera-video:${cameraXVersion}"
|
|
14
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
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.MeteringPoint;
|
|
8
|
+
import androidx.camera.core.Preview;
|
|
9
|
+
import androidx.camera.view.PreviewView;
|
|
10
|
+
import androidx.lifecycle.Lifecycle;
|
|
11
|
+
import androidx.lifecycle.LifecycleOwner;
|
|
12
|
+
import androidx.lifecycle.LifecycleRegistry;
|
|
13
|
+
|
|
14
|
+
public final class CameraPreviewView extends FrameLayout implements LifecycleOwner {
|
|
15
|
+
private final LifecycleRegistry lifecycle;
|
|
16
|
+
private final PreviewView previewView;
|
|
17
|
+
private boolean cameraActive = true;
|
|
18
|
+
private boolean attached;
|
|
19
|
+
|
|
20
|
+
public CameraPreviewView(Context context) {
|
|
21
|
+
super(context);
|
|
22
|
+
lifecycle = new LifecycleRegistry(this);
|
|
23
|
+
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
|
|
24
|
+
previewView = new PreviewView(context);
|
|
25
|
+
previewView.setScaleType(PreviewView.ScaleType.FILL_CENTER);
|
|
26
|
+
addView(
|
|
27
|
+
previewView,
|
|
28
|
+
new FrameLayout.LayoutParams(
|
|
29
|
+
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
30
|
+
ViewGroup.LayoutParams.MATCH_PARENT
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@Override
|
|
36
|
+
public Lifecycle getLifecycle() {
|
|
37
|
+
return lifecycle;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public void setCameraActive(boolean active) {
|
|
41
|
+
cameraActive = active;
|
|
42
|
+
updateLifecycle();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public Preview.SurfaceProvider getSurfaceProvider() {
|
|
46
|
+
return previewView.getSurfaceProvider();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Converts a normalized view-space point (0-1, top-left origin) into a
|
|
50
|
+
* CameraX metering point. The PreviewView factory accounts for scale
|
|
51
|
+
* type, rotation, and front-camera mirroring. */
|
|
52
|
+
public MeteringPoint meteringPoint(float x, float y) {
|
|
53
|
+
return previewView.getMeteringPointFactory().createPoint(
|
|
54
|
+
x * previewView.getWidth(), y * previewView.getHeight());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public androidx.lifecycle.LiveData<PreviewView.StreamState> getStreamState() {
|
|
58
|
+
return previewView.getPreviewStreamState();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Override
|
|
62
|
+
protected void onAttachedToWindow() {
|
|
63
|
+
super.onAttachedToWindow();
|
|
64
|
+
attached = true;
|
|
65
|
+
updateLifecycle();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
@Override
|
|
69
|
+
protected void onDetachedFromWindow() {
|
|
70
|
+
attached = false;
|
|
71
|
+
updateLifecycle();
|
|
72
|
+
super.onDetachedFromWindow();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private void updateLifecycle() {
|
|
76
|
+
boolean shouldStart = cameraActive && attached;
|
|
77
|
+
boolean isStarted = lifecycle.getCurrentState().isAtLeast(Lifecycle.State.STARTED);
|
|
78
|
+
if (shouldStart && !isStarted) {
|
|
79
|
+
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_START);
|
|
80
|
+
} else if (!shouldStart && isStarted) {
|
|
81
|
+
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_STOP);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|