expo-camera
Version:
A React component that renders a preview for the device's either front or back camera. Camera's parameters like zoom, auto focus, white balance and flash mode are adjustable. With expo-camera, one can also take photos and record videos that are saved to t
261 lines • 10.8 kB
JavaScript
import { Platform, UnavailabilityError } from 'expo-modules-core';
import { Component, createRef } from 'react';
import ExpoCamera from './ExpoCamera';
import CameraManager from './ExpoCameraManager';
import { ConversionTables, ensureNativeProps } from './utils/props';
const EventThrottleMs = 500;
const _PICTURE_SAVED_CALLBACKS = {};
let loggedRenderingChildrenWarning = false;
let _GLOBAL_PICTURE_ID = 1;
function ensurePictureOptions(options) {
if (!options || typeof options !== 'object') {
return {};
}
if (options.quality === undefined) {
options.quality = 1;
}
if (options.mirror) {
console.warn('The `mirror` option is deprecated. Please use the `mirror` prop on the `CameraView` instead.');
}
if (options.onPictureSaved) {
const id = _GLOBAL_PICTURE_ID++;
_PICTURE_SAVED_CALLBACKS[id] = options.onPictureSaved;
options.id = id;
options.fastMode = true;
}
return options;
}
function ensureRecordingOptions(options = {}) {
if (!options || typeof options !== 'object') {
return {};
}
if (options.mirror) {
console.warn('The `mirror` option is deprecated. Please use the `mirror` prop on the `CameraView` instead.');
}
return options;
}
function _onPictureSaved({ nativeEvent, }) {
const { id, data } = nativeEvent;
const callback = _PICTURE_SAVED_CALLBACKS[id];
if (callback) {
callback(data);
delete _PICTURE_SAVED_CALLBACKS[id];
}
}
export default class CameraView extends Component {
/**
* Property that determines if the current device has the ability to use `DataScannerViewController` (iOS 16+) or the Google code scanner (Android).
*/
static isModernBarcodeScannerAvailable = CameraManager.isModernBarcodeScannerAvailable;
/**
* Check whether the current device has a camera. This is useful for web and simulators cases.
* This isn't influenced by the Permissions API (all platforms), or HTTP usage (in the browser).
* You will still need to check if the native permission has been accepted.
* @platform web
*/
static async isAvailableAsync() {
if (!CameraManager.isAvailableAsync) {
throw new UnavailabilityError('expo-camera', 'isAvailableAsync');
}
return CameraManager.isAvailableAsync();
}
// @needsAudit
/**
* Queries the device for the available video codecs that can be used in video recording.
* @return A promise that resolves to a list of strings that represents available codecs.
* @platform ios
*/
static async getAvailableVideoCodecsAsync() {
if (!CameraManager.getAvailableVideoCodecsAsync) {
throw new UnavailabilityError('Camera', 'getAvailableVideoCodecsAsync');
}
return CameraManager.getAvailableVideoCodecsAsync();
}
/**
* Get picture sizes that are supported by the device.
* @return Returns a Promise that resolves to an array of strings representing picture sizes that can be passed to `pictureSize` prop.
* The list varies across Android devices but is the same for every iOS.
*/
async getAvailablePictureSizesAsync() {
return (await this._cameraRef.current?.getAvailablePictureSizes()) ?? [];
}
/**
* Returns the available lenses for the currently selected camera.
*
* @return Returns a Promise that resolves to an array of strings representing the lens type that can be passed to `selectedLens` prop.
* @platform ios
*/
async getAvailableLensesAsync() {
return (await this._cameraRef.current?.getAvailableLenses()) ?? [];
}
/**
* Returns an object with the supported features of the camera on the current device.
*/
getSupportedFeatures() {
return {
isModernBarcodeScannerAvailable: CameraManager.isModernBarcodeScannerAvailable,
toggleRecordingAsyncAvailable: CameraManager.toggleRecordingAsyncAvailable,
};
}
/**
* Resumes the camera preview.
*/
async resumePreview() {
return this._cameraRef.current?.resumePreview();
}
/**
* Pauses the camera preview. It is not recommended to use `takePictureAsync` when preview is paused.
*/
async pausePreview() {
return this._cameraRef.current?.pausePreview();
}
// Values under keys from this object will be transformed to native options
static ConversionTables = ConversionTables;
static defaultProps = {
zoom: 0,
facing: 'back',
enableTorch: false,
mode: 'picture',
flash: 'off',
};
_cameraHandle;
_cameraRef = createRef();
_lastEvents = {};
_lastEventsTimes = {};
async takePictureAsync(options) {
const pictureOptions = ensurePictureOptions(options);
if (Platform.OS === 'ios' && options?.pictureRef) {
return this._cameraRef.current?.takePictureRef?.(options);
}
return this._cameraRef.current?.takePicture(pictureOptions);
}
/**
* On Android, we will use the [Google code scanner](https://developers.google.com/ml-kit/vision/barcode-scanning/code-scanner).
* On iOS, presents a modal view controller that uses the [`DataScannerViewController`](https://developer.apple.com/documentation/visionkit/scanning_data_with_the_camera) available on iOS 16+.
* @platform android
* @platform ios
*/
static async launchScanner(options) {
if (!options) {
options = { barcodeTypes: [] };
}
if (Platform.OS !== 'web' && CameraView.isModernBarcodeScannerAvailable) {
await CameraManager.launchScanner(options);
}
}
/**
* Dismiss the scanner presented by `launchScanner`.
* > **info** On Android, the scanner is dismissed automatically when a barcode is scanned.
* @platform ios
*/
static async dismissScanner() {
if (Platform.OS !== 'web' && CameraView.isModernBarcodeScannerAvailable) {
await CameraManager.dismissScanner();
}
}
/**
* Invokes the `listener` function when a bar code has been successfully scanned. The callback is provided with
* an object of the `ScanningResult` shape, where the `type` refers to the bar code type that was scanned and the `data` is the information encoded in the bar code
* (in this case of QR codes, this is often a URL). See [`BarcodeType`](#barcodetype) for supported values.
* @param listener Invoked with the [ScanningResult](#scanningresult) when a bar code has been successfully scanned.
*
* @platform ios
* @platform android
*/
static onModernBarcodeScanned(listener) {
return CameraManager.addListener('onModernBarcodeScanned', listener);
}
/**
* Starts recording a video that will be saved to cache directory. Videos are rotated to match device's orientation.
* Flipping camera during a recording results in stopping it.
* @param options A map of `CameraRecordingOptions` type.
* @return Returns a Promise that resolves to an object containing video file `uri` property and a `codec` property on iOS.
* The Promise is returned if `stopRecording` was invoked, one of `maxDuration` and `maxFileSize` is reached or camera preview is stopped.
* @platform android
* @platform ios
*/
async recordAsync(options) {
const recordingOptions = ensureRecordingOptions(options);
return this._cameraRef.current?.record(recordingOptions);
}
/**
* Pauses or resumes the video recording. Only has an effect if there is an active recording. On `iOS`, this method only supported on `iOS` 18.
*
* @example
* ```ts
* const { toggleRecordingAsyncAvailable } = getSupportedFeatures()
*
* return (
* {toggleRecordingAsyncAvailable && (
* <Button title="Toggle Recording" onPress={toggleRecordingAsync} />
* )}
* )
* ```
*/
async toggleRecordingAsync() {
return this._cameraRef.current?.toggleRecording();
}
/**
* Stops recording if any is in progress.
* @platform android
* @platform ios
*/
stopRecording() {
this._cameraRef.current?.stopRecording();
}
_onCameraReady = () => {
if (this.props.onCameraReady) {
this.props.onCameraReady();
}
};
_onAvailableLensesChanged = ({ nativeEvent }) => {
if (this.props.onAvailableLensesChanged) {
this.props.onAvailableLensesChanged(nativeEvent);
}
};
_onMountError = ({ nativeEvent }) => {
if (this.props.onMountError) {
this.props.onMountError(nativeEvent);
}
};
_onResponsiveOrientationChanged = ({ nativeEvent, }) => {
if (this.props.onResponsiveOrientationChanged) {
this.props.onResponsiveOrientationChanged(nativeEvent);
}
};
_onObjectDetected = (callback) => ({ nativeEvent }) => {
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) => {
if (ref) {
// TODO(Bacon): Unify these - perhaps with hooks?
if (Platform.OS === 'web') {
this._cameraHandle = ref;
}
}
};
render() {
const nativeProps = ensureNativeProps(this.props);
const onBarcodeScanned = this.props.onBarcodeScanned
? this._onObjectDetected(this.props.onBarcodeScanned)
: undefined;
// @ts-expect-error
if (nativeProps.children && !loggedRenderingChildrenWarning) {
console.warn('The <CameraView> component does not support children. This may lead to inconsistent behaviour or crashes. If you want to render content on top of the Camera, consider using absolute positioning.');
loggedRenderingChildrenWarning = true;
}
return (<ExpoCamera {...nativeProps} ref={this._cameraRef} onCameraReady={this._onCameraReady} onMountError={this._onMountError} onBarcodeScanned={onBarcodeScanned} onAvailableLensesChanged={this._onAvailableLensesChanged} onPictureSaved={_onPictureSaved} onResponsiveOrientationChanged={this._onResponsiveOrientationChanged}/>);
}
}
//# sourceMappingURL=CameraView.js.map