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
265 lines (225 loc) • 7.26 kB
text/typescript
import invariant from 'invariant';
import { PictureOptions } from '../Camera.types';
import { CameraType, CapturedPicture, CaptureOptions, ImageType } from './CameraModule.types';
import * as Utils from './CameraUtils';
import { FacingModeToCameraType, PictureSizes } from './constants';
import * as CapabilityUtils from './CapabilityUtils';
export { ImageType, CameraType, CaptureOptions };
/*
* TODO: Bacon: Add more props for Android
*
* aspectRatio: { min (0.00033), max (4032) }
* colorTemperature: MediaSettingsRange (max: 7000, min: 2850, step: 50)
* exposureCompensation: MediaSettingsRange (max: 2, min: -2, step: 0.1666666716337204)
* exposureMode: 'continuous' | 'manual'
* frameRate: { min: (1), max: (60) }
* iso: MediaSettingsRange (max: 3200, min: 50, step: 1)
* width: { min: 1, max}
* height: { min: 1, max}
*/
type OnCameraReadyListener = () => void;
type OnMountErrorListener = ({ nativeEvent: Error }) => void;
class CameraModule {
videoElement: HTMLVideoElement;
stream: MediaStream | null = null;
settings: MediaTrackSettings | null = null;
onCameraReady: OnCameraReadyListener = () => {};
onMountError: OnMountErrorListener = () => {};
_pictureSize?: string;
_isStartingCamera = false;
_autoFocus: string = 'continuous';
get autoFocus(): string {
return this._autoFocus;
}
async setAutoFocusAsync(value: string): Promise<void> {
if (value === this.autoFocus) {
return;
}
this._autoFocus = value;
await this._syncTrackCapabilities();
}
_flashMode: string = 'off';
get flashMode(): string {
return this._flashMode;
}
async setFlashModeAsync(value: string): Promise<void> {
if (value === this.flashMode) {
return;
}
this._flashMode = value;
await this._syncTrackCapabilities();
}
_whiteBalance: string = 'continuous';
get whiteBalance(): string {
return this._whiteBalance;
}
async setWhiteBalanceAsync(value: string): Promise<void> {
if (value === this.whiteBalance) {
return;
}
this._whiteBalance = value;
await this._syncTrackCapabilities();
}
_cameraType: CameraType = CameraType.front;
get type(): CameraType {
return this._cameraType;
}
async setTypeAsync(value: CameraType) {
if (value === this._cameraType) {
return;
}
this._cameraType = value;
await this.resumePreview();
}
_zoom: number = 1;
get zoom(): number {
return this._zoom;
}
async setZoomAsync(value: number): Promise<void> {
if (value === this.zoom) {
return;
}
//TODO: Bacon: IMP on non-android devices
this._zoom = value;
await this._syncTrackCapabilities();
}
setPictureSize(value: string) {
if (value === this._pictureSize) {
return;
}
invariant(
PictureSizes.includes(value),
`expo-camera: CameraModule.setPictureSize(): invalid size supplied ${value}, expected one of: ${PictureSizes.join(
', '
)}`
);
const [width, height] = value.split('x');
//TODO: Bacon: IMP
const aspectRatio = parseFloat(width) / parseFloat(height);
this._pictureSize = value;
}
constructor(videoElement: HTMLVideoElement) {
this.videoElement = videoElement;
if (this.videoElement) {
this.videoElement.addEventListener('loadedmetadata', () => {
this._syncTrackCapabilities();
});
}
}
async onCapabilitiesReady(track: MediaStreamTrack): Promise<void> {
const capabilities = track.getCapabilities() as any;
// Create an empty object because if you set a constraint that isn't available an error will be thrown.
const constraints: {
zoom?: number;
torch?: boolean;
whiteBalance?: string;
focusMode?: string;
} = {};
if (capabilities.zoom) {
// TODO: Bacon: We should have some async method for getting the (min, max, step) externally
const { min, max } = capabilities.zoom;
constraints.zoom = Math.min(max, Math.max(min, this._zoom));
}
if (capabilities.focusMode) {
constraints.focusMode = CapabilityUtils.convertAutoFocusJSONToNative(this.autoFocus);
}
if (capabilities.torch) {
constraints.torch = CapabilityUtils.convertFlashModeJSONToNative(this.flashMode);
}
if (capabilities.whiteBalance) {
constraints.whiteBalance = this.whiteBalance;
}
await track.applyConstraints({ advanced: [constraints] as any });
}
async _syncTrackCapabilities(): Promise<void> {
if (this.stream) {
await Promise.all(this.stream.getTracks().map(track => this.onCapabilitiesReady(track)));
}
}
setVideoSource(stream: MediaStream | MediaSource | Blob | null): void {
if ('srcObject' in this.videoElement) {
this.videoElement.srcObject = stream;
} else {
// TODO: Bacon: Check if needed
(this.videoElement['src'] as any) = window.URL.createObjectURL(stream);
}
}
setSettings(stream: MediaStream | null): void {
this.settings = null;
if (stream && this.stream) {
this.settings = this.stream.getTracks()[0].getSettings();
}
}
setStream(stream: MediaStream | null): void {
this.stream = stream;
this.setSettings(stream);
this.setVideoSource(stream);
}
getActualCameraType(): CameraType | null {
if (this.settings) {
// On desktop no value will be returned, in this case we should assume the cameraType is 'front'
const { facingMode = 'user' } = this.settings;
return FacingModeToCameraType[facingMode];
}
return null;
}
async ensureCameraIsRunningAsync(): Promise<void> {
if (!this.stream) {
await this.resumePreview();
}
}
async resumePreview(): Promise<MediaStream | null> {
if (this._isStartingCamera) {
return null;
}
this._isStartingCamera = true;
try {
this.pausePreview();
const stream = await Utils.getStreamDevice(this.type);
this.setStream(stream);
this._isStartingCamera = false;
this.onCameraReady();
return stream;
} catch (error) {
this._isStartingCamera = false;
this.onMountError({ nativeEvent: error });
}
return null;
}
takePicture(config: PictureOptions): CapturedPicture {
const base64 = Utils.captureImage(this.videoElement, config);
const capturedPicture: CapturedPicture = {
uri: base64,
base64,
width: 0,
height: 0,
};
if (this.settings) {
const { width = 0, height = 0 } = this.settings;
capturedPicture.width = width;
capturedPicture.height = height;
// TODO: Bacon: verify/enforce exif shape.
capturedPicture.exif = this.settings;
}
if (config.onPictureSaved) {
config.onPictureSaved({ nativeEvent: { data: capturedPicture, id: config.id } });
}
return capturedPicture;
}
pausePreview(): void {
if (!this.stream) {
return;
}
this.stream.getTracks().forEach(track => track.stop());
this.setStream(null);
}
// TODO: Bacon: we don't even use ratio in native...
getAvailablePictureSizes = async (ratio: string): Promise<string[]> => {
return PictureSizes;
};
unmount = () => {
this.settings = null;
this.stream = null;
};
}
export default CameraModule;