import { UnavailabilityError } from '@unimodules/core'; import mapValues from 'lodash/mapValues'; import PropTypes from 'prop-types'; import React from 'react'; import { findNodeHandle, Platform, ViewPropTypes } from 'react-native'; import { CameraCapturedPicture, CameraNativeProps, CameraPictureOptions, CameraProps, CameraRecordingOptions, PermissionResponse, PermissionStatus, PermissionExpiration, BarCodeScanningResult, FaceDetectionResult, CameraMountError, } from './Camera.types'; import ExponentCamera from './ExponentCamera'; import _CameraManager from './ExponentCameraManager'; // TODO: Bacon: Fix multiplatform const CameraManager = _CameraManager as any; const EventThrottleMs = 500; const _PICTURE_SAVED_CALLBACKS = {}; let _GLOBAL_PICTURE_ID = 1; function ensurePictureOptions(options?: CameraPictureOptions): CameraPictureOptions { let pictureOptions = options || {}; if (!pictureOptions || typeof pictureOptions !== 'object') { pictureOptions = {}; } if (!pictureOptions.quality) { pictureOptions.quality = 1; } if (pictureOptions.onPictureSaved) { const id = _GLOBAL_PICTURE_ID++; _PICTURE_SAVED_CALLBACKS[id] = pictureOptions.onPictureSaved; pictureOptions.id = id; pictureOptions.fastMode = true; } return pictureOptions; } function ensureRecordingOptions(options?: CameraRecordingOptions): CameraRecordingOptions { let recordingOptions = options || {}; if (!recordingOptions || typeof recordingOptions !== 'object') { recordingOptions = {}; } else if (typeof recordingOptions.quality === 'string') { recordingOptions.quality = Camera.Constants.VideoQuality[recordingOptions.quality]; } return recordingOptions; } function ensureNativeProps(options?: CameraProps): CameraNativeProps { let props = options || {}; if (!props || typeof props !== 'object') { props = {}; } const newProps: CameraNativeProps = mapValues(props, convertProp); const propsKeys = Object.keys(newProps); // barCodeTypes is deprecated if (!propsKeys.includes('barCodeScannerSettings') && propsKeys.includes('barCodeTypes')) { console.warn( `The "barCodeTypes" prop for Camera is deprecated and will be removed in SDK 34. Use "barCodeScannerSettings" instead.` ); newProps.barCodeScannerSettings = { // @ts-ignore barCodeTypes: newProps.barCodeTypes, }; } if (props.onBarCodeScanned) { newProps.barCodeScannerEnabled = true; } if (props.onFacesDetected) { newProps.faceDetectorEnabled = true; } if (Platform.OS !== 'android') { delete newProps.ratio; delete newProps.useCamera2Api; } return newProps; } function convertProp(value: any, key: string): any { if (typeof value === 'string' && Camera.ConversionTables[key]) { return Camera.ConversionTables[key][value]; } return value; } function _onPictureSaved({ nativeEvent, }: { nativeEvent: { data: CameraCapturedPicture; id: number }; }) { const { id, data } = nativeEvent; const callback = _PICTURE_SAVED_CALLBACKS[id]; if (callback) { callback(data); delete _PICTURE_SAVED_CALLBACKS[id]; } } export default class Camera extends React.Component { static async isAvailableAsync(): Promise { if (!CameraManager.isAvailableAsync) { throw new UnavailabilityError('expo-camera', 'isAvailableAsync'); } return await CameraManager.isAvailableAsync(); } static async getAvailableCameraTypesAsync(): Promise<('front' | 'back')[]> { if (!CameraManager.getAvailableCameraTypesAsync) { throw new UnavailabilityError('expo-camera', 'getAvailableCameraTypesAsync'); } return await CameraManager.getAvailableCameraTypesAsync(); } static Constants = { Type: CameraManager.Type, FlashMode: CameraManager.FlashMode, AutoFocus: CameraManager.AutoFocus, WhiteBalance: CameraManager.WhiteBalance, VideoQuality: CameraManager.VideoQuality, VideoStabilization: CameraManager.VideoStabilization || {}, }; // Values under keys from this object will be transformed to native options static ConversionTables = { type: CameraManager.Type, flashMode: CameraManager.FlashMode, autoFocus: CameraManager.AutoFocus, whiteBalance: CameraManager.WhiteBalance, }; static propTypes = { ...ViewPropTypes, zoom: PropTypes.number, ratio: PropTypes.string, focusDepth: PropTypes.number, onMountError: PropTypes.func, pictureSize: PropTypes.string, onCameraReady: PropTypes.func, useCamera2Api: PropTypes.bool, onBarCodeScanned: PropTypes.func, barCodeScannerSettings: PropTypes.object, onFacesDetected: PropTypes.func, faceDetectorSettings: PropTypes.object, type: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), flashMode: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), videoStabilizationMode: PropTypes.number, whiteBalance: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), autoFocus: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), }; static defaultProps: CameraProps = { zoom: 0, ratio: '4:3', focusDepth: 0, faceDetectorSettings: {}, type: CameraManager.Type.back, autoFocus: CameraManager.AutoFocus.on, flashMode: CameraManager.FlashMode.off, whiteBalance: CameraManager.WhiteBalance.auto, }; static async getPermissionsAsync(): Promise { return CameraManager.getPermissionsAsync(); } static async requestPermissionsAsync(): Promise { return CameraManager.requestPermissionsAsync(); } _cameraHandle?: number | null; _cameraRef?: React.Component | null; _lastEvents: { [eventName: string]: string } = {}; _lastEventsTimes: { [eventName: string]: Date } = {}; async takePictureAsync(options?: CameraPictureOptions): Promise { const pictureOptions = ensurePictureOptions(options); return await CameraManager.takePicture(pictureOptions, this._cameraHandle); } async getSupportedRatiosAsync(): Promise { if (!CameraManager.getSupportedRatios) { throw new UnavailabilityError('Camera', 'getSupportedRatiosAsync'); } return await CameraManager.getSupportedRatios(this._cameraHandle); } async getAvailablePictureSizesAsync(ratio?: string): Promise { if (!CameraManager.getAvailablePictureSizes) { throw new UnavailabilityError('Camera', 'getAvailablePictureSizesAsync'); } return await CameraManager.getAvailablePictureSizes(ratio, this._cameraHandle); } async recordAsync(options?: CameraRecordingOptions): Promise<{ uri: string }> { if (!CameraManager.record) { throw new UnavailabilityError('Camera', 'recordAsync'); } const recordingOptions = ensureRecordingOptions(options); return await CameraManager.record(recordingOptions, this._cameraHandle); } stopRecording() { if (!CameraManager.stopRecording) { throw new UnavailabilityError('Camera', 'stopRecording'); } CameraManager.stopRecording(this._cameraHandle); } pausePreview() { if (!CameraManager.pausePreview) { throw new UnavailabilityError('Camera', 'pausePreview'); } CameraManager.pausePreview(this._cameraHandle); } resumePreview() { if (!CameraManager.resumePreview) { throw new UnavailabilityError('Camera', 'resumePreview'); } CameraManager.resumePreview(this._cameraHandle); } _onCameraReady = () => { if (this.props.onCameraReady) { this.props.onCameraReady(); } }; _onMountError = ({ nativeEvent }: { nativeEvent: { message: string } }) => { if (this.props.onMountError) { this.props.onMountError(nativeEvent); } }; _onObjectDetected = (callback?: Function) => ({ nativeEvent }: { nativeEvent: any }) => { const { type } = nativeEvent; if ( this._lastEvents[type] && this._lastEventsTimes[type] && JSON.stringify(nativeEvent) === this._lastEvents[type] && new Date().getTime() - this._lastEventsTimes[type].getTime() < EventThrottleMs ) { return; } if (callback) { callback(nativeEvent); this._lastEventsTimes[type] = new Date(); this._lastEvents[type] = JSON.stringify(nativeEvent); } }; _setReference = (ref?: React.Component) => { if (ref) { this._cameraRef = ref; // TODO: Bacon: Unify these if (Platform.OS === 'web') { this._cameraHandle = ref as any; } else { this._cameraHandle = findNodeHandle(ref); } } else { this._cameraRef = null; this._cameraHandle = null; } }; render() { const nativeProps = ensureNativeProps(this.props); const onBarCodeScanned = this._onObjectDetected(this.props.onBarCodeScanned); const onFacesDetected = this._onObjectDetected(this.props.onFacesDetected); return ( ); } } export const { Constants, getPermissionsAsync, requestPermissionsAsync } = Camera; export { CameraCapturedPicture, CameraNativeProps, CameraPictureOptions, CameraProps, CameraRecordingOptions, PermissionResponse, PermissionStatus, PermissionExpiration, BarCodeScanningResult, FaceDetectionResult, CameraMountError, };