@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.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +135 -0
  3. package/package.json +104 -0
  4. package/platforms/android/AndroidManifest.xml +6 -0
  5. package/platforms/android/include.gradle +14 -0
  6. package/platforms/android/java/com/octanexplat/camera/CameraPreviewView.java +84 -0
  7. package/platforms/android/java/com/octanexplat/camera/CameraSessionBridge.java +394 -0
  8. package/platforms/ios/Info.plist +10 -0
  9. package/platforms/macos/src/CameraOwnership.swift +22 -0
  10. package/platforms/macos/src/PhotoCaptureLifecycle.swift +34 -0
  11. package/platforms/macos/src/XplatCameraHost.swift +639 -0
  12. package/platforms/macos/src/XplatPhotoCaptureSheet.swift +192 -0
  13. package/src/CameraView.android.tsrx +150 -0
  14. package/src/CameraView.ios.tsrx +102 -0
  15. package/src/CameraView.macos.tsrx +95 -0
  16. package/src/CameraView.mobile.ts +6 -0
  17. package/src/CameraView.web.tsrx +132 -0
  18. package/src/CameraView.windows.tsrx +126 -0
  19. package/src/camera-permission.mobile.ts +91 -0
  20. package/src/camera-permission.windows.ts +76 -0
  21. package/src/camera-session.ios.ts +65 -0
  22. package/src/camera-session.macos.ts +75 -0
  23. package/src/camera-session.windows.ts +126 -0
  24. package/src/host-movie.web.ts +295 -0
  25. package/src/index.ts +36 -0
  26. package/src/index.web.ts +34 -0
  27. package/src/layout-child.ts +81 -0
  28. package/src/layout-child.web.ts +66 -0
  29. package/src/layout-child.windows.ts +79 -0
  30. package/src/movie-metadata.web.ts +84 -0
  31. package/src/movie-storage.web.ts +164 -0
  32. package/src/preview-tap.ts +34 -0
  33. package/src/props.ts +77 -0
  34. package/src/session-backend.android.ts +439 -0
  35. package/src/session-backend.ios.ts +1258 -0
  36. package/src/session-backend.linux.ts +887 -0
  37. package/src/session-backend.macos.ts +707 -0
  38. package/src/session-backend.ts +59 -0
  39. package/src/session-backend.web.ts +891 -0
  40. package/src/session-backend.windows.ts +1599 -0
  41. package/src/session-core.ts +723 -0
  42. package/src/session.ts +37 -0
  43. package/src/synthetic-session.ts +377 -0
  44. package/src/types.ts +357 -0
  45. package/types/index.d.ts +48 -0
  46. package/types/props.d.ts +61 -0
  47. package/types/session-core.d.ts +139 -0
  48. package/types/synthetic-session.d.ts +51 -0
  49. 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
+ }