1 |
|
2 | import React from 'react';
|
3 | import PropTypes from 'prop-types';
|
4 | import { mapValues } from 'lodash';
|
5 | import {
|
6 | findNodeHandle,
|
7 | NativeModules,
|
8 | ViewPropTypes,
|
9 | Platform,
|
10 | requireNativeComponent,
|
11 | } from 'react-native';
|
12 |
|
13 | import type { FaceFeature } from './FaceDetector';
|
14 |
|
15 | type PictureOptions = {
|
16 | quality?: number,
|
17 | };
|
18 |
|
19 | type TrackedFaceFeature = FaceFeature & {
|
20 | faceID?: number,
|
21 | };
|
22 |
|
23 | type RecordingOptions = {
|
24 | maxDuration?: number,
|
25 | maxFileSize?: number,
|
26 | quality?: number | string,
|
27 | };
|
28 |
|
29 | type EventCallbackArgumentsType = {
|
30 | nativeEvent: Object,
|
31 | };
|
32 |
|
33 | type MountErrorNativeEventType = {
|
34 | message: string,
|
35 | };
|
36 |
|
37 | type PropsType = ViewPropTypes & {
|
38 | zoom?: number,
|
39 | ratio?: string,
|
40 | focusDepth?: number,
|
41 | type?: number | string,
|
42 | onCameraReady?: Function,
|
43 | onBarCodeRead?: Function,
|
44 | faceDetectionMode?: number,
|
45 | flashMode?: number | string,
|
46 | barCodeTypes?: Array<string | number>,
|
47 | whiteBalance?: number | string,
|
48 | faceDetectionLandmarks?: number,
|
49 | autoFocus?: string | boolean | number,
|
50 | faceDetectionClassifications?: number,
|
51 | onMountError?: MountErrorNativeEventType => void,
|
52 | onFacesDetected?: ({ faces: Array<TrackedFaceFeature> }) => void,
|
53 | };
|
54 |
|
55 | const CameraManager: Object =
|
56 | NativeModules.ExponentCameraManager || NativeModules.ExponentCameraModule;
|
57 |
|
58 | const EventThrottleMs = 500;
|
59 |
|
60 | export default class Camera extends React.Component<PropsType> {
|
61 | static Constants = {
|
62 | Type: CameraManager.Type,
|
63 | FlashMode: CameraManager.FlashMode,
|
64 | AutoFocus: CameraManager.AutoFocus,
|
65 | WhiteBalance: CameraManager.WhiteBalance,
|
66 | VideoQuality: CameraManager.VideoQuality,
|
67 | BarCodeType: CameraManager.BarCodeType,
|
68 | FaceDetection: CameraManager.FaceDetection,
|
69 | };
|
70 |
|
71 |
|
72 | static ConversionTables = {
|
73 | type: CameraManager.Type,
|
74 | flashMode: CameraManager.FlashMode,
|
75 | autoFocus: CameraManager.AutoFocus,
|
76 | whiteBalance: CameraManager.WhiteBalance,
|
77 | faceDetectionMode: CameraManager.FaceDetection.Mode,
|
78 | faceDetectionLandmarks: CameraManager.FaceDetection.Landmarks,
|
79 | faceDetectionClassifications: CameraManager.FaceDetection.Classifications,
|
80 | };
|
81 |
|
82 | static propTypes = {
|
83 | ...ViewPropTypes,
|
84 | zoom: PropTypes.number,
|
85 | ratio: PropTypes.string,
|
86 | focusDepth: PropTypes.number,
|
87 | onMountError: PropTypes.func,
|
88 | onCameraReady: PropTypes.func,
|
89 | onBarCodeRead: PropTypes.func,
|
90 | onFacesDetected: PropTypes.func,
|
91 | faceDetectionMode: PropTypes.number,
|
92 | faceDetectionLandmarks: PropTypes.number,
|
93 | faceDetectionClassifications: PropTypes.number,
|
94 | barCodeTypes: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),
|
95 | type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
96 | flashMode: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
97 | whiteBalance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
98 | autoFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
|
99 | };
|
100 |
|
101 | static defaultProps: Object = {
|
102 | zoom: 0,
|
103 | ratio: '4:3',
|
104 | focusDepth: 0,
|
105 | type: CameraManager.Type.back,
|
106 | autoFocus: CameraManager.AutoFocus.on,
|
107 | flashMode: CameraManager.FlashMode.off,
|
108 | whiteBalance: CameraManager.WhiteBalance.auto,
|
109 | faceDetectionMode: CameraManager.FaceDetection.fast,
|
110 | barCodeTypes: Object.values(CameraManager.BarCodeType),
|
111 | faceDetectionLandmarks: CameraManager.FaceDetection.Landmarks.none,
|
112 | faceDetectionClassifications: CameraManager.FaceDetection.Classifications.none,
|
113 | };
|
114 |
|
115 | _cameraRef: ?Object;
|
116 | _cameraHandle: ?number;
|
117 | _lastEvents: { [string]: string };
|
118 | _lastEventsTimes: { [string]: Date };
|
119 |
|
120 | constructor(props: PropsType) {
|
121 | super(props);
|
122 | this._lastEvents = {};
|
123 | this._lastEventsTimes = {};
|
124 | }
|
125 |
|
126 | async takePictureAsync(options?: PictureOptions) {
|
127 | if (!options) {
|
128 | options = {};
|
129 | }
|
130 | if (!options.quality) {
|
131 | options.quality = 1;
|
132 | }
|
133 | return await CameraManager.takePicture(options, this._cameraHandle);
|
134 | }
|
135 |
|
136 | async getSupportedRatiosAsync() {
|
137 | if (Platform.OS === 'android') {
|
138 | return await CameraManager.getSupportedRatios(this._cameraHandle);
|
139 | } else {
|
140 | throw new Error('Ratio is not supported on iOS');
|
141 | }
|
142 | }
|
143 |
|
144 | async recordAsync(options?: RecordingOptions) {
|
145 | if (!options || typeof options !== 'object') {
|
146 | options = {};
|
147 | } else if (typeof options.quality === 'string') {
|
148 | options.quality = Camera.Constants.VideoQuality[options.quality];
|
149 | }
|
150 | return await CameraManager.record(options, this._cameraHandle);
|
151 | }
|
152 |
|
153 | stopRecording() {
|
154 | CameraManager.stopRecording(this._cameraHandle);
|
155 | }
|
156 |
|
157 | _onCameraReady = () => {
|
158 | if (this.props.onCameraReady) {
|
159 | this.props.onCameraReady();
|
160 | }
|
161 | };
|
162 |
|
163 | _onMountError = ({ nativeEvent }: { nativeEvent: MountErrorNativeEventType }) => {
|
164 | if (this.props.onMountError) {
|
165 | this.props.onMountError(nativeEvent);
|
166 | }
|
167 | };
|
168 |
|
169 | _onObjectDetected = (callback: ?Function) => ({ nativeEvent }: EventCallbackArgumentsType) => {
|
170 | const { type } = nativeEvent;
|
171 | if (
|
172 | this._lastEvents[type] &&
|
173 | this._lastEventsTimes[type] &&
|
174 | JSON.stringify(nativeEvent) === this._lastEvents[type] &&
|
175 | new Date() - this._lastEventsTimes[type] < EventThrottleMs
|
176 | ) {
|
177 | return;
|
178 | }
|
179 |
|
180 | if (callback) {
|
181 | callback(nativeEvent);
|
182 | this._lastEventsTimes[type] = new Date();
|
183 | this._lastEvents[type] = JSON.stringify(nativeEvent);
|
184 | }
|
185 | };
|
186 |
|
187 | _setReference = (ref: ?Object) => {
|
188 | if (ref) {
|
189 | this._cameraRef = ref;
|
190 | this._cameraHandle = findNodeHandle(ref);
|
191 | } else {
|
192 | this._cameraRef = null;
|
193 | this._cameraHandle = null;
|
194 | }
|
195 | };
|
196 |
|
197 | render() {
|
198 | const nativeProps = this._convertNativeProps(this.props);
|
199 |
|
200 | return (
|
201 | <ExponentCamera
|
202 | {...nativeProps}
|
203 | ref={this._setReference}
|
204 | onCameraReady={this._onCameraReady}
|
205 | onMountError={this._onMountError}
|
206 | onBarCodeRead={this._onObjectDetected(this.props.onBarCodeRead)}
|
207 | onFacesDetected={this._onObjectDetected(this.props.onFacesDetected)}
|
208 | />
|
209 | );
|
210 | }
|
211 |
|
212 | _convertNativeProps(props: PropsType) {
|
213 | const newProps = mapValues(props, this._convertProp);
|
214 |
|
215 | if (props.onBarCodeRead) {
|
216 | newProps.barCodeScannerEnabled = true;
|
217 | }
|
218 |
|
219 | if (props.onFacesDetected) {
|
220 | newProps.faceDetectorEnabled = true;
|
221 | }
|
222 |
|
223 | if (Platform.OS === 'ios') {
|
224 | delete newProps.ratio;
|
225 | }
|
226 |
|
227 | return newProps;
|
228 | }
|
229 |
|
230 | _convertProp(value: *, key: string): * {
|
231 | if (typeof value === 'string' && Camera.ConversionTables[key]) {
|
232 | return Camera.ConversionTables[key][value];
|
233 | }
|
234 |
|
235 | return value;
|
236 | }
|
237 | }
|
238 |
|
239 | export const Constants = Camera.Constants;
|
240 |
|
241 | const ExponentCamera = requireNativeComponent('ExponentCamera', Camera, {
|
242 | nativeOnly: {
|
243 | onCameraReady: true,
|
244 | onMountError: true,
|
245 | onBarCodeRead: true,
|
246 | onFaceDetected: true,
|
247 | faceDetectorEnabled: true,
|
248 | barCodeScannerEnabled: true,
|
249 | },
|
250 | });
|