1 |
|
2 | import React from 'react';
|
3 | import PropTypes from 'prop-types';
|
4 | import mapValues from 'lodash.mapvalues';
|
5 | import { NativeModulesProxy, requireNativeViewManager } from 'expo-core';
|
6 | import { findNodeHandle, ViewPropTypes, Platform } from 'react-native';
|
7 |
|
8 | type PictureOptions = {
|
9 | quality?: number,
|
10 | base64?: boolean,
|
11 | exif?: boolean,
|
12 | skipProcessing?: boolean,
|
13 | onPictureSaved?: Function,
|
14 |
|
15 | id?: number,
|
16 | fastMode?: boolean,
|
17 | };
|
18 |
|
19 | type RecordingOptions = {
|
20 | maxDuration?: number,
|
21 | maxFileSize?: number,
|
22 | quality?: number | string,
|
23 | };
|
24 |
|
25 | type CapturedPicture = {
|
26 | width: number,
|
27 | height: number,
|
28 | uri: string,
|
29 | base64?: string,
|
30 | exif?: Object,
|
31 | };
|
32 |
|
33 | type RecordingResult = {
|
34 | uri: string,
|
35 | };
|
36 |
|
37 | type EventCallbackArgumentsType = {
|
38 | nativeEvent: Object,
|
39 | };
|
40 |
|
41 | type MountErrorNativeEventType = {
|
42 | message: string,
|
43 | };
|
44 |
|
45 | type PictureSavedNativeEventType = {
|
46 | data: CapturedPicture,
|
47 | id: number,
|
48 | };
|
49 |
|
50 | type PropsType = ViewPropTypes & {
|
51 | zoom?: number,
|
52 | ratio?: string,
|
53 | focusDepth?: number,
|
54 | type?: number | string,
|
55 | onCameraReady?: Function,
|
56 | useCamera2Api?: boolean,
|
57 | flashMode?: number | string,
|
58 | whiteBalance?: number | string,
|
59 | autoFocus?: string | boolean | number,
|
60 | pictureSize?: string,
|
61 | onMountError?: MountErrorNativeEventType => void,
|
62 | barCodeScannerSettings?: {},
|
63 | onBarCodeScanned?: ({ type: string, data: string }) => void,
|
64 | faceDetectorSettings?: {},
|
65 | onFacesDetected?: ({ faces: Array<*> }) => void,
|
66 | };
|
67 |
|
68 | const CameraManager: Object =
|
69 | NativeModulesProxy.ExponentCameraManager || NativeModulesProxy.ExponentCameraModule;
|
70 |
|
71 | const EventThrottleMs = 500;
|
72 |
|
73 | const _PICTURE_SAVED_CALLBACKS = {};
|
74 | let _GLOBAL_PICTURE_ID = 1;
|
75 |
|
76 | export default class Camera extends React.Component<PropsType> {
|
77 | static Constants = {
|
78 | Type: CameraManager.Type,
|
79 | FlashMode: CameraManager.FlashMode,
|
80 | AutoFocus: CameraManager.AutoFocus,
|
81 | WhiteBalance: CameraManager.WhiteBalance,
|
82 | VideoQuality: CameraManager.VideoQuality,
|
83 | };
|
84 |
|
85 |
|
86 | static ConversionTables = {
|
87 | type: CameraManager.Type,
|
88 | flashMode: CameraManager.FlashMode,
|
89 | autoFocus: CameraManager.AutoFocus,
|
90 | whiteBalance: CameraManager.WhiteBalance,
|
91 | };
|
92 |
|
93 | static propTypes = {
|
94 | ...ViewPropTypes,
|
95 | zoom: PropTypes.number,
|
96 | ratio: PropTypes.string,
|
97 | focusDepth: PropTypes.number,
|
98 | onMountError: PropTypes.func,
|
99 | pictureSize: PropTypes.string,
|
100 | onCameraReady: PropTypes.func,
|
101 | useCamera2Api: PropTypes.bool,
|
102 | onBarCodeScanned: PropTypes.func,
|
103 | barCodeScannerSettings: PropTypes.object,
|
104 | onFacesDetected: PropTypes.func,
|
105 | faceDetectorSettings: PropTypes.object,
|
106 | type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
107 | flashMode: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
108 | whiteBalance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
109 | autoFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
110 | };
|
111 |
|
112 | static defaultProps: Object = {
|
113 | zoom: 0,
|
114 | ratio: '4:3',
|
115 | focusDepth: 0,
|
116 | faceDetectorSettings: {},
|
117 | type: CameraManager.Type.back,
|
118 | autoFocus: CameraManager.AutoFocus.on,
|
119 | flashMode: CameraManager.FlashMode.off,
|
120 | whiteBalance: CameraManager.WhiteBalance.auto,
|
121 | };
|
122 |
|
123 | _cameraRef: ?Object;
|
124 | _cameraHandle: ?number;
|
125 | _lastEvents: { [string]: string };
|
126 | _lastEventsTimes: { [string]: Date };
|
127 |
|
128 | constructor(props: PropsType) {
|
129 | super(props);
|
130 | this._lastEvents = {};
|
131 | this._lastEventsTimes = {};
|
132 | }
|
133 |
|
134 | async takePictureAsync(options?: PictureOptions): Promise<CapturedPicture> {
|
135 | if (!options) {
|
136 | options = {};
|
137 | }
|
138 | if (!options.quality) {
|
139 | options.quality = 1;
|
140 | }
|
141 | if (options.onPictureSaved) {
|
142 | const id = _GLOBAL_PICTURE_ID++;
|
143 | _PICTURE_SAVED_CALLBACKS[id] = options.onPictureSaved;
|
144 | options.id = id;
|
145 | options.fastMode = true;
|
146 | }
|
147 | return await CameraManager.takePicture(options, this._cameraHandle);
|
148 | }
|
149 |
|
150 | async getSupportedRatiosAsync(): Promise<Array<string>> {
|
151 | if (Platform.OS === 'android') {
|
152 | return await CameraManager.getSupportedRatios(this._cameraHandle);
|
153 | } else {
|
154 | throw new Error('Ratio is not supported on iOS');
|
155 | }
|
156 | }
|
157 |
|
158 | async getAvailablePictureSizesAsync(ratio?: string): Promise<Array<string>> {
|
159 | return await CameraManager.getAvailablePictureSizes(ratio, this._cameraHandle);
|
160 | }
|
161 |
|
162 | async recordAsync(options?: RecordingOptions): Promise<RecordingResult> {
|
163 | if (!options || typeof options !== 'object') {
|
164 | options = {};
|
165 | } else if (typeof options.quality === 'string') {
|
166 | options.quality = Camera.Constants.VideoQuality[options.quality];
|
167 | }
|
168 | return await CameraManager.record(options, this._cameraHandle);
|
169 | }
|
170 |
|
171 | stopRecording() {
|
172 | CameraManager.stopRecording(this._cameraHandle);
|
173 | }
|
174 |
|
175 | pausePreview() {
|
176 | CameraManager.pausePreview(this._cameraHandle);
|
177 | }
|
178 |
|
179 | resumePreview() {
|
180 | CameraManager.resumePreview(this._cameraHandle);
|
181 | }
|
182 |
|
183 | _onCameraReady = () => {
|
184 | if (this.props.onCameraReady) {
|
185 | this.props.onCameraReady();
|
186 | }
|
187 | };
|
188 |
|
189 | _onMountError = ({ nativeEvent }: { nativeEvent: MountErrorNativeEventType }) => {
|
190 | if (this.props.onMountError) {
|
191 | this.props.onMountError(nativeEvent);
|
192 | }
|
193 | };
|
194 |
|
195 | _onPictureSaved = ({ nativeEvent }: { nativeEvent: PictureSavedNativeEventType }) => {
|
196 | const callback = _PICTURE_SAVED_CALLBACKS[nativeEvent.id];
|
197 | if (callback) {
|
198 | callback(nativeEvent.data);
|
199 | delete _PICTURE_SAVED_CALLBACKS[nativeEvent.id];
|
200 | }
|
201 | };
|
202 |
|
203 | _onObjectDetected = (callback: ?Function) => ({ nativeEvent }: EventCallbackArgumentsType) => {
|
204 | const { type } = nativeEvent;
|
205 | if (
|
206 | this._lastEvents[type] &&
|
207 | this._lastEventsTimes[type] &&
|
208 | JSON.stringify(nativeEvent) === this._lastEvents[type] &&
|
209 | new Date() - this._lastEventsTimes[type] < EventThrottleMs
|
210 | ) {
|
211 | return;
|
212 | }
|
213 |
|
214 | if (callback) {
|
215 | callback(nativeEvent);
|
216 | this._lastEventsTimes[type] = new Date();
|
217 | this._lastEvents[type] = JSON.stringify(nativeEvent);
|
218 | }
|
219 | };
|
220 |
|
221 | _setReference = (ref: ?Object) => {
|
222 | if (ref) {
|
223 | this._cameraRef = ref;
|
224 | this._cameraHandle = findNodeHandle(ref);
|
225 | } else {
|
226 | this._cameraRef = null;
|
227 | this._cameraHandle = null;
|
228 | }
|
229 | };
|
230 |
|
231 | _onBarCodeScanned = () => {
|
232 | const onBarCodeRead = this.props.onBarCodeRead && ((data) => {
|
233 | console.warn("'onBarCodeRead' is deprecated in favour of 'onBarCodeScanned'");
|
234 | return this.props.onBarCodeRead(data);
|
235 | });
|
236 | return this.props.onBarCodeScanned || onBarCodeRead;
|
237 | };
|
238 |
|
239 | render() {
|
240 | const nativeProps = this._convertNativeProps(this.props);
|
241 |
|
242 | return (
|
243 | <ExponentCamera
|
244 | {...nativeProps}
|
245 | ref={this._setReference}
|
246 | onCameraReady={this._onCameraReady}
|
247 | onMountError={this._onMountError}
|
248 | onPictureSaved={this._onPictureSaved}
|
249 | onBarCodeScanned={this._onObjectDetected(this._onBarCodeScanned())}
|
250 | onFacesDetected={this._onObjectDetected(this.props.onFacesDetected)}
|
251 | />
|
252 | );
|
253 | }
|
254 |
|
255 | _convertNativeProps(props: PropsType) {
|
256 | const newProps = mapValues(props, this._convertProp);
|
257 |
|
258 | const propsKeys = Object.keys(newProps);
|
259 | if (!propsKeys.includes("barCodeScannerSettings") && propsKeys.includes("barCodeTypes")) {
|
260 | newProps.barCodeScannerSettings = {
|
261 | barCodeTypes: newProps.barCodeTypes,
|
262 | };
|
263 | }
|
264 |
|
265 | if (props.onBarCodeScanned || props.onBarCodeRead) {
|
266 | newProps.barCodeScannerEnabled = true;
|
267 | }
|
268 |
|
269 | if (props.onFacesDetected) {
|
270 | newProps.faceDetectorEnabled = true;
|
271 | }
|
272 |
|
273 | if (Platform.OS === 'ios') {
|
274 | delete newProps.ratio;
|
275 | delete newProps.useCamera2Api;
|
276 | }
|
277 |
|
278 | return newProps;
|
279 | }
|
280 |
|
281 | _convertProp(value: *, key: string): * {
|
282 | if (typeof value === 'string' && Camera.ConversionTables[key]) {
|
283 | return Camera.ConversionTables[key][value];
|
284 | }
|
285 |
|
286 | return value;
|
287 | }
|
288 | }
|
289 |
|
290 | export const Constants = Camera.Constants;
|
291 |
|
292 | const ExponentCamera = requireNativeViewManager('ExponentCamera', Camera);
|