@chipmobilesdk/rn-camera 0.2.0 → 0.3.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/CHANGELOG.md +207 -93
- package/README.md +390 -364
- package/package.json +52 -47
- package/skills/sdk-rn-camera/SKILL.md +76 -75
- package/src/config/types.ts +143 -149
- package/src/core/capabilities.ts +41 -44
- package/src/core/captureController.ts +120 -120
- package/src/core/controller.ts +522 -518
- package/src/core/sessionMachine.ts +104 -111
- package/src/diagnostics/errors.ts +94 -92
- package/src/diagnostics/logger.ts +47 -31
- package/src/geometry/coordinateMapping.ts +129 -129
- package/src/index.ts +78 -78
- package/src/native/cameraEngine.ts +7 -2
- package/src/native/visionCameraPeer.ts +30 -0
- package/src/permissions/permissionPort.ts +24 -20
- package/src/react/CameraPreview.tsx +137 -125
- package/src/react/VisionCameraFeed.tsx +181 -178
- package/src/react/hooks.ts +56 -58
- package/src/storage/artifactStore.ts +141 -141
- package/src/testing/conformance.ts +140 -0
- package/src/testing/index.ts +27 -0
- package/src/testing/testCameraAdapter.ts +246 -256
|
@@ -1,125 +1,137 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Camera preview surface (FR-009..FR-013, UX-001, UX-003).
|
|
3
|
-
*
|
|
4
|
-
* Renders the camera feed and nothing else: no shutter button, no permission
|
|
5
|
-
* copy, no error text, and no theme dependency. Consuming apps draw their own
|
|
6
|
-
* controls and overlays through `children`, positioned with the geometry
|
|
7
|
-
* reported by `onGeometryChange`.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
-
import {
|
|
12
|
-
View,
|
|
13
|
-
type AccessibilityRole,
|
|
14
|
-
type LayoutChangeEvent,
|
|
15
|
-
type StyleProp,
|
|
16
|
-
type ViewStyle,
|
|
17
|
-
} from 'react-native';
|
|
18
|
-
|
|
19
|
-
import type { PreviewGeometry, ResizeMode } from '../config/types';
|
|
20
|
-
import type { CameraController } from '../core/controller';
|
|
21
|
-
import { useCameraState } from './hooks';
|
|
22
|
-
import type { VisionCameraFeedProps } from './VisionCameraFeed';
|
|
23
|
-
|
|
24
|
-
export interface CameraPreviewProps {
|
|
25
|
-
readonly controller: CameraController;
|
|
26
|
-
readonly resizeMode?: ResizeMode;
|
|
27
|
-
readonly style?: StyleProp<ViewStyle>;
|
|
28
|
-
readonly onGeometryChange?: (geometry: PreviewGeometry) => void;
|
|
29
|
-
/** Forwarded untouched; the package never overrides consumer a11y (UX-003). */
|
|
30
|
-
readonly accessibilityLabel?: string;
|
|
31
|
-
readonly accessibilityRole?: AccessibilityRole;
|
|
32
|
-
/** Overlay layer rendered above the feed. */
|
|
33
|
-
readonly children?: React.ReactNode;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
return
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Camera preview surface (FR-009..FR-013, UX-001, UX-003).
|
|
3
|
+
*
|
|
4
|
+
* Renders the camera feed and nothing else: no shutter button, no permission
|
|
5
|
+
* copy, no error text, and no theme dependency. Consuming apps draw their own
|
|
6
|
+
* controls and overlays through `children`, positioned with the geometry
|
|
7
|
+
* reported by `onGeometryChange`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
|
11
|
+
import {
|
|
12
|
+
View,
|
|
13
|
+
type AccessibilityRole,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
|
|
19
|
+
import type { PreviewGeometry, ResizeMode } from '../config/types';
|
|
20
|
+
import type { CameraController } from '../core/controller';
|
|
21
|
+
import { useCameraState } from './hooks';
|
|
22
|
+
import type { VisionCameraFeedProps } from './VisionCameraFeed';
|
|
23
|
+
|
|
24
|
+
export interface CameraPreviewProps {
|
|
25
|
+
readonly controller: CameraController;
|
|
26
|
+
readonly resizeMode?: ResizeMode;
|
|
27
|
+
readonly style?: StyleProp<ViewStyle>;
|
|
28
|
+
readonly onGeometryChange?: (geometry: PreviewGeometry) => void;
|
|
29
|
+
/** Forwarded untouched; the package never overrides consumer a11y (UX-003). */
|
|
30
|
+
readonly accessibilityLabel?: string;
|
|
31
|
+
readonly accessibilityRole?: AccessibilityRole;
|
|
32
|
+
/** Overlay layer rendered above the feed. */
|
|
33
|
+
readonly children?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The feed module failed to load; distinct from `null`, which means not requested. */
|
|
37
|
+
const FEED_UNAVAILABLE = 'unavailable' as const;
|
|
38
|
+
|
|
39
|
+
/** Nominal capture dimensions until the engine reports the real format. */
|
|
40
|
+
const DEFAULT_IMAGE_WIDTH = 1920;
|
|
41
|
+
const DEFAULT_IMAGE_HEIGHT = 1080;
|
|
42
|
+
|
|
43
|
+
export const CameraPreview: React.FC<CameraPreviewProps> = ({
|
|
44
|
+
controller,
|
|
45
|
+
resizeMode = 'cover',
|
|
46
|
+
style,
|
|
47
|
+
onGeometryChange,
|
|
48
|
+
accessibilityLabel,
|
|
49
|
+
accessibilityRole,
|
|
50
|
+
children,
|
|
51
|
+
}) => {
|
|
52
|
+
const [layout, setLayout] = useState<{ width: number; height: number } | null>(null);
|
|
53
|
+
// Subscribed, not read once: a plain `controller.state` read only refreshed
|
|
54
|
+
// when the parent happened to re-render, so the feed could stay unmounted
|
|
55
|
+
// after the session became active (or stay mounted after it stopped).
|
|
56
|
+
const state = useCameraState(controller);
|
|
57
|
+
|
|
58
|
+
// Tell the controller a surface exists so foregrounding can restart the
|
|
59
|
+
// session, and clear it on unmount so the camera is released (FR-016). Both
|
|
60
|
+
// calls are no-ops on an idle or disposed controller.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
controller.setSurfaceMounted(true);
|
|
63
|
+
return () => {
|
|
64
|
+
controller.setSurfaceMounted(false);
|
|
65
|
+
controller.stop().catch(() => undefined);
|
|
66
|
+
};
|
|
67
|
+
}, [controller]);
|
|
68
|
+
|
|
69
|
+
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
70
|
+
const { width, height } = event.nativeEvent.layout;
|
|
71
|
+
setLayout({ width, height });
|
|
72
|
+
}, []);
|
|
73
|
+
|
|
74
|
+
const geometry = useMemo<PreviewGeometry | null>(() => {
|
|
75
|
+
if (!layout || layout.width === 0 || layout.height === 0) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
imageWidth: DEFAULT_IMAGE_WIDTH,
|
|
80
|
+
imageHeight: DEFAULT_IMAGE_HEIGHT,
|
|
81
|
+
previewWidth: layout.width,
|
|
82
|
+
previewHeight: layout.height,
|
|
83
|
+
resizeMode,
|
|
84
|
+
orientation: 0,
|
|
85
|
+
isMirrored: state.cameraPosition === 'front',
|
|
86
|
+
};
|
|
87
|
+
}, [layout, resizeMode, state.cameraPosition]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (geometry && onGeometryChange) {
|
|
91
|
+
onGeometryChange(geometry);
|
|
92
|
+
}
|
|
93
|
+
}, [geometry, onGeometryChange]);
|
|
94
|
+
|
|
95
|
+
const feedRequested = state.session === 'active' && state.permission === 'granted';
|
|
96
|
+
|
|
97
|
+
// Lazy require so importing this package never pulls in the native module
|
|
98
|
+
// for consumers that only use the pure utilities or the test adapter.
|
|
99
|
+
const Feed = useMemo(() => {
|
|
100
|
+
if (!feedRequested) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
return require('./VisionCameraFeed')
|
|
105
|
+
.VisionCameraFeed as React.ComponentType<VisionCameraFeedProps>;
|
|
106
|
+
} catch {
|
|
107
|
+
// Reported, not swallowed (CAM-21): the session fails with
|
|
108
|
+
// `cameraUnavailable` instead of showing a black preview.
|
|
109
|
+
return FEED_UNAVAILABLE;
|
|
110
|
+
}
|
|
111
|
+
}, [feedRequested]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (Feed === FEED_UNAVAILABLE) {
|
|
115
|
+
controller.reportSurfaceUnavailable();
|
|
116
|
+
}
|
|
117
|
+
}, [Feed, controller]);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<View
|
|
121
|
+
style={style}
|
|
122
|
+
onLayout={onLayout}
|
|
123
|
+
accessibilityLabel={accessibilityLabel}
|
|
124
|
+
accessibilityRole={accessibilityRole}
|
|
125
|
+
testID="rn-camera-preview"
|
|
126
|
+
>
|
|
127
|
+
{Feed && Feed !== FEED_UNAVAILABLE ? (
|
|
128
|
+
<Feed
|
|
129
|
+
cameraPosition={state.cameraPosition}
|
|
130
|
+
resizeMode={resizeMode}
|
|
131
|
+
torchEnabled={state.torchEnabled}
|
|
132
|
+
/>
|
|
133
|
+
) : null}
|
|
134
|
+
{children}
|
|
135
|
+
</View>
|
|
136
|
+
);
|
|
137
|
+
};
|
|
@@ -1,178 +1,181 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The live VisionCamera surface rendered inside `CameraPreview`.
|
|
3
|
-
*
|
|
4
|
-
* Split into its own module so `CameraPreview` can require it lazily: consumers
|
|
5
|
-
* that only use the pure utilities or the test adapter never load the native
|
|
6
|
-
* module, while the hooks this component needs (`useCameraDevice`,
|
|
7
|
-
* `usePhotoOutput`) can still be called normally.
|
|
8
|
-
*
|
|
9
|
-
* It also owns capture. In VisionCamera v5 the photo output belongs to the
|
|
10
|
-
* session the `<Camera>` view configures, so the output only exists while this
|
|
11
|
-
* surface is mounted; it is published to the engine through `visionSurface`.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
|
-
import { StyleSheet } from 'react-native';
|
|
16
|
-
import {
|
|
17
|
-
Camera,
|
|
18
|
-
useCameraDevice,
|
|
19
|
-
usePhotoOutput,
|
|
20
|
-
type CameraPhotoOutput,
|
|
21
|
-
} from 'react-native-vision-camera';
|
|
22
|
-
|
|
23
|
-
import {
|
|
24
|
-
known,
|
|
25
|
-
unavailable,
|
|
26
|
-
type CameraPosition,
|
|
27
|
-
type CapturedImage,
|
|
28
|
-
type CaptureRequest,
|
|
29
|
-
type
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
readonly
|
|
38
|
-
readonly
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* The live VisionCamera surface rendered inside `CameraPreview`.
|
|
3
|
+
*
|
|
4
|
+
* Split into its own module so `CameraPreview` can require it lazily: consumers
|
|
5
|
+
* that only use the pure utilities or the test adapter never load the native
|
|
6
|
+
* module, while the hooks this component needs (`useCameraDevice`,
|
|
7
|
+
* `usePhotoOutput`) can still be called normally.
|
|
8
|
+
*
|
|
9
|
+
* It also owns capture. In VisionCamera v5 the photo output belongs to the
|
|
10
|
+
* session the `<Camera>` view configures, so the output only exists while this
|
|
11
|
+
* surface is mounted; it is published to the engine through `visionSurface`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
15
|
+
import { StyleSheet } from 'react-native';
|
|
16
|
+
import {
|
|
17
|
+
Camera,
|
|
18
|
+
useCameraDevice,
|
|
19
|
+
usePhotoOutput,
|
|
20
|
+
type CameraPhotoOutput,
|
|
21
|
+
} from 'react-native-vision-camera';
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
known,
|
|
25
|
+
unavailable,
|
|
26
|
+
type CameraPosition,
|
|
27
|
+
type CapturedImage,
|
|
28
|
+
type CaptureRequest,
|
|
29
|
+
type Maybe,
|
|
30
|
+
type ResizeMode,
|
|
31
|
+
} from '../config/types';
|
|
32
|
+
import { DEFAULT_QUALITY } from '../core/captureController';
|
|
33
|
+
import { CameraError, isCameraError } from '../diagnostics/errors';
|
|
34
|
+
import { registerVisionSurface } from '../native/visionSurface';
|
|
35
|
+
|
|
36
|
+
export interface VisionCameraFeedProps {
|
|
37
|
+
readonly cameraPosition: CameraPosition;
|
|
38
|
+
readonly resizeMode: ResizeMode;
|
|
39
|
+
readonly torchEnabled: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const styles = StyleSheet.create({
|
|
43
|
+
feed: { flex: 1 },
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const MIME_TYPES: Record<string, string> = {
|
|
47
|
+
jpeg: 'image/jpeg',
|
|
48
|
+
heic: 'image/heic',
|
|
49
|
+
dng: 'image/x-adobe-dng',
|
|
50
|
+
tiff: 'image/tiff',
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/** VisionCamera reports orientation as a compass value; the contract wants degrees. */
|
|
54
|
+
const ORIENTATION_DEGREES: Record<string, number> = {
|
|
55
|
+
up: 0,
|
|
56
|
+
right: 90,
|
|
57
|
+
down: 180,
|
|
58
|
+
left: 270,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
function orientationOf(orientation: string): Maybe<number> {
|
|
62
|
+
const degrees = ORIENTATION_DEGREES[orientation];
|
|
63
|
+
return degrees === undefined ? unavailable() : known(degrees);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function toFileUri(path: string): string {
|
|
67
|
+
return path.startsWith('file://') ? path : `file://${path}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function toFilePath(uri: string): string {
|
|
71
|
+
return uri.startsWith('file://') ? uri.slice('file://'.length) : uri;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Native failures carry no code and may name a file path; never surface them raw. */
|
|
75
|
+
function asCameraError(error: unknown, code: 'captureFailed' | 'storageFailure'): CameraError {
|
|
76
|
+
return isCameraError(error) ? error : new CameraError(code);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function capturePhoto(
|
|
80
|
+
photoOutput: CameraPhotoOutput,
|
|
81
|
+
request: CaptureRequest,
|
|
82
|
+
cameraPosition: CameraPosition,
|
|
83
|
+
): Promise<CapturedImage> {
|
|
84
|
+
let photo: Awaited<ReturnType<CameraPhotoOutput['capturePhoto']>>;
|
|
85
|
+
try {
|
|
86
|
+
photo = await photoOutput.capturePhoto({ flashMode: request.flashMode ?? 'off' }, {});
|
|
87
|
+
} catch (error) {
|
|
88
|
+
throw asCameraError(error, 'captureFailed');
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
// A destination makes the artifact app-owned, so it is written straight
|
|
92
|
+
// there; otherwise it goes to a temporary file (FR-026a).
|
|
93
|
+
let path: string;
|
|
94
|
+
if (request.destination) {
|
|
95
|
+
try {
|
|
96
|
+
await photo.saveToFileAsync(toFilePath(request.destination));
|
|
97
|
+
} catch {
|
|
98
|
+
throw new CameraError(
|
|
99
|
+
'storageFailure',
|
|
100
|
+
'Could not write the capture to the requested destination',
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
path = request.destination;
|
|
104
|
+
} else {
|
|
105
|
+
// VisionCamera's own temporary directory. The controller moves the file
|
|
106
|
+
// into the artifact store, when it has one, so cleanup can reach it.
|
|
107
|
+
try {
|
|
108
|
+
path = await photo.saveToTemporaryFileAsync();
|
|
109
|
+
} catch (error) {
|
|
110
|
+
throw asCameraError(error, 'storageFailure');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
uri: toFileUri(path),
|
|
116
|
+
mimeType: MIME_TYPES[photo.containerFormat] ?? 'application/octet-stream',
|
|
117
|
+
width: photo.width,
|
|
118
|
+
height: photo.height,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
cameraPosition: request.cameraPosition ?? cameraPosition,
|
|
121
|
+
// Ownership is decided by the controller from the original request;
|
|
122
|
+
// this value is overwritten there and is only a safe default.
|
|
123
|
+
ownership: request.destination ? 'appOwned' : 'packageTemporary',
|
|
124
|
+
// Quality is fixed on the photo output, not per capture, and nothing here
|
|
125
|
+
// resizes. Report what was actually applied rather than echo the hints.
|
|
126
|
+
effectiveQuality: DEFAULT_QUALITY,
|
|
127
|
+
effectiveMaxDimension: unavailable(),
|
|
128
|
+
// The native layer reports neither back after a save, and guessing them
|
|
129
|
+
// would be worse than saying so (FR-022).
|
|
130
|
+
fileSize: unavailable(),
|
|
131
|
+
orientation: orientationOf(photo.orientation),
|
|
132
|
+
};
|
|
133
|
+
} finally {
|
|
134
|
+
// Large native buffers must not wait for the JS GC.
|
|
135
|
+
photo.dispose();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const VisionCameraFeed: React.FC<VisionCameraFeedProps> = ({
|
|
140
|
+
cameraPosition,
|
|
141
|
+
resizeMode,
|
|
142
|
+
torchEnabled,
|
|
143
|
+
}) => {
|
|
144
|
+
const device = useCameraDevice(cameraPosition);
|
|
145
|
+
const photoOutput = usePhotoOutput({ quality: DEFAULT_QUALITY });
|
|
146
|
+
|
|
147
|
+
const surface = useMemo(
|
|
148
|
+
() => ({
|
|
149
|
+
capture: (request: CaptureRequest) => capturePhoto(photoOutput, request, cameraPosition),
|
|
150
|
+
}),
|
|
151
|
+
[photoOutput, cameraPosition],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
useEffect(() => registerVisionSurface(surface), [surface]);
|
|
155
|
+
|
|
156
|
+
// VisionCamera applies `torchMode` on mount, before the camera is active, and
|
|
157
|
+
// leaves the native "Camera is not active" rejection unhandled — one red
|
|
158
|
+
// error per session start. Leave the prop unset until the torch is first
|
|
159
|
+
// turned on; after that 'off' must be sent explicitly to turn it back off.
|
|
160
|
+
const [torchTouched, setTorchTouched] = useState(false);
|
|
161
|
+
if (torchEnabled && !torchTouched) {
|
|
162
|
+
setTorchTouched(true);
|
|
163
|
+
}
|
|
164
|
+
const torchMode = torchEnabled ? 'on' : torchTouched ? 'off' : undefined;
|
|
165
|
+
|
|
166
|
+
if (!device) {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return (
|
|
171
|
+
<Camera
|
|
172
|
+
style={styles.feed}
|
|
173
|
+
device={device}
|
|
174
|
+
outputs={[photoOutput]}
|
|
175
|
+
isActive={true}
|
|
176
|
+
resizeMode={resizeMode}
|
|
177
|
+
// Left unset until the app first touches the torch, so the device's own default stands.
|
|
178
|
+
{...(torchMode === undefined ? {} : { torchMode })}
|
|
179
|
+
/>
|
|
180
|
+
);
|
|
181
|
+
};
|