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
287 lines • 10.6 kB
JavaScript
/* eslint-env browser */
import invariant from 'invariant';
import { CameraType, ImageType } from './CameraModule.types';
import * as Utils from './CameraUtils';
import * as CapabilityUtils from './CapabilityUtils';
import { FacingModeToCameraType, PictureSizes } from './constants';
import { isBackCameraAvailableAsync, isFrontCameraAvailableAsync } from './UserMediaManager';
export { ImageType, CameraType };
const VALID_SETTINGS_KEYS = [
'autoFocus',
'flashMode',
'exposureCompensation',
'colorTemperature',
'iso',
'brightness',
'contrast',
'saturation',
'sharpness',
'focusDistance',
'whiteBalance',
'zoom',
];
class CameraModule {
constructor(videoElement) {
this.videoElement = videoElement;
this.onCameraReady = () => { };
this.onMountError = () => { };
this.stream = null;
this.settings = null;
this.isStartingCamera = false;
this.cameraType = CameraType.front;
this.webCameraSettings = {
autoFocus: 'continuous',
flashMode: 'off',
whiteBalance: 'continuous',
zoom: 1,
};
// TODO: Bacon: we don't even use ratio in native...
this.getAvailablePictureSizes = async (ratio) => {
return PictureSizes;
};
this.getAvailableCameraTypesAsync = async () => {
if (!navigator.mediaDevices.enumerateDevices) {
return [];
}
const devices = await navigator.mediaDevices.enumerateDevices();
const types = await Promise.all([
(await isFrontCameraAvailableAsync(devices)) && CameraType.front,
(await isBackCameraAvailableAsync()) && CameraType.back,
]);
return types.filter(Boolean);
};
if (this.videoElement) {
this.videoElement.addEventListener('loadedmetadata', () => {
this.syncTrackCapabilities();
});
}
}
get type() {
return this.cameraType;
}
async updateWebCameraSettingsAsync(nextSettings) {
const changes = {};
for (const key of Object.keys(nextSettings)) {
if (!VALID_SETTINGS_KEYS.includes(key))
continue;
const nextValue = nextSettings[key];
if (nextValue !== this.webCameraSettings[key]) {
changes[key] = nextValue;
}
}
// Only update the native camera if changes were found
const hasChanges = !!Object.keys(changes).length;
this.webCameraSettings = { ...this.webCameraSettings, ...changes };
if (hasChanges) {
await this.syncTrackCapabilities(changes);
}
return hasChanges;
}
async setTypeAsync(value) {
if (value === this.cameraType) {
return;
}
this.cameraType = value;
await this.resumePreview();
}
setPictureSize(value) {
if (value === this.pictureSize) {
return;
}
invariant(PictureSizes.includes(value), `expo-camera: CameraModule.setPictureSize(): invalid size supplied ${value}, expected one of: ${PictureSizes.join(', ')}`);
// TODO: Bacon: IMP
// const [width, height] = value.split('x');
// const aspectRatio = parseFloat(width) / parseFloat(height);
this.pictureSize = value;
}
isTorchAvailable() {
return isCapabilityAvailable(this.videoElement, 'torch');
}
isZoomAvailable() {
return isCapabilityAvailable(this.videoElement, 'zoom');
}
// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
async onCapabilitiesReady(track, settings = {}) {
const capabilities = track.getCapabilities();
// Create an empty object because if you set a constraint that isn't available an error will be thrown.
const constraints = {};
// TODO: Bacon: Add `pointsOfInterest` support
const clampedValues = [
'exposureCompensation',
'colorTemperature',
'iso',
'brightness',
'contrast',
'saturation',
'sharpness',
'focusDistance',
'zoom',
];
for (const property of clampedValues) {
if (capabilities[property]) {
constraints[property] = convertNormalizedSetting(capabilities[property], settings[property]);
}
}
const _validatedConstrainedValue = (key, propName, converter) => validatedConstrainedValue(key, propName, converter(settings[propName]), capabilities, settings, this.cameraType);
if (capabilities.focusMode && settings.autoFocus !== undefined) {
constraints.focusMode = _validatedConstrainedValue('focusMode', 'autoFocus', CapabilityUtils.convertAutoFocusJSONToNative);
}
if (capabilities.torch && settings.flashMode !== undefined) {
constraints.torch = _validatedConstrainedValue('torch', 'flashMode', CapabilityUtils.convertFlashModeJSONToNative);
}
if (capabilities.whiteBalanceMode && settings.whiteBalance !== undefined) {
constraints.whiteBalanceMode = _validatedConstrainedValue('whiteBalanceMode', 'whiteBalance', CapabilityUtils.convertWhiteBalanceJSONToNative);
}
await track.applyConstraints({ advanced: [constraints] });
}
async applyVideoConstraints(constraints) {
if (!this.stream || !this.stream.getVideoTracks) {
return false;
}
return await applyConstraints(this.stream.getVideoTracks(), constraints);
}
async applyAudioConstraints(constraints) {
if (!this.stream || !this.stream.getAudioTracks) {
return false;
}
return await applyConstraints(this.stream.getAudioTracks(), constraints);
}
async syncTrackCapabilities(settings = {}) {
if (this.stream && this.stream.getVideoTracks) {
await Promise.all(this.stream.getVideoTracks().map(track => this.onCapabilitiesReady(track, settings)));
}
}
setStream(stream) {
this.stream = stream;
this.settings = stream ? stream.getTracks()[0].getSettings() : null;
setVideoSource(this.videoElement, stream);
}
getActualCameraType() {
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() {
if (!this.stream) {
await this.resumePreview();
}
}
async resumePreview() {
if (this.isStartingCamera) {
return null;
}
this.isStartingCamera = true;
try {
this.stopAsync();
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) {
const base64 = Utils.captureImage(this.videoElement, config);
const 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;
}
stopAsync() {
stopMediaStream(this.stream);
this.setStream(null);
}
}
function stopMediaStream(stream) {
if (!stream)
return;
if (stream.getAudioTracks)
stream.getAudioTracks().forEach(track => track.stop());
if (stream.getVideoTracks)
stream.getVideoTracks().forEach(track => track.stop());
if (isMediaStreamTrack(stream))
stream.stop();
}
function setVideoSource(video, stream) {
try {
video.srcObject = stream;
}
catch (_) {
if (stream) {
video.src = window.URL.createObjectURL(stream);
}
else if (typeof video.src === 'string') {
window.URL.revokeObjectURL(video.src);
}
}
}
async function applyConstraints(tracks, constraints) {
try {
await Promise.all(tracks.map(async (track) => {
await track.applyConstraints({ advanced: [constraints] });
}));
return true;
}
catch (_) {
return false;
}
}
function isCapabilityAvailable(video, keyName) {
const stream = video.srcObject;
if (stream instanceof MediaStream) {
const videoTrack = stream.getVideoTracks()[0];
if (typeof videoTrack.getCapabilities === 'undefined') {
return false;
}
const capabilities = videoTrack.getCapabilities();
return !!capabilities[keyName];
}
return false;
}
function isMediaStreamTrack(input) {
return typeof input.stop === 'function';
}
function convertNormalizedSetting(range, value) {
if (!value)
return;
// convert the normalized incoming setting to the native camera zoom range
const converted = convertRange(value, [range.min, range.max]);
// clamp value so we don't get an error
return Math.min(range.max, Math.max(range.min, converted));
}
function convertRange(value, r2, r1 = [0, 1]) {
return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
}
function validatedConstrainedValue(constraintKey, settingsKey, convertedSetting, capabilities, settings, cameraType) {
const setting = settings[settingsKey];
if (Array.isArray(capabilities[constraintKey]) &&
convertedSetting &&
!capabilities[constraintKey].includes(convertedSetting)) {
console.warn(` { ${settingsKey}: "${setting}" } (converted to "${convertedSetting}" in the browser) is not supported for camera type "${cameraType}" in your browser. Using the default value instead.`);
return undefined;
}
return convertedSetting;
}
export default CameraModule;
//# sourceMappingURL=CameraModule.js.map