1/* eslint-env browser */
2import invariant from 'invariant';
3import { CameraType, ImageType, } from './Camera.types';
4import * as CapabilityUtils from './WebCapabilityUtils';
5import { CameraTypeToFacingMode, ImageTypeFormat, MinimumConstraints } from './WebConstants';
6import { requestUserMediaAsync } from './WebUserMediaManager';
7export function getImageSize(videoWidth, videoHeight, scale) {
8    const width = videoWidth * scale;
9    const ratio = videoWidth / width;
10    const height = videoHeight / ratio;
11    return {
12        width,
13        height,
14    };
15}
16export function toDataURL(canvas, imageType, quality) {
17    invariant(Object.values(ImageType).includes(imageType), `expo-camera: ${imageType} is not a valid ImageType. Expected a string from: ${Object.values(ImageType).join(', ')}`);
18    const format = ImageTypeFormat[imageType];
19    if (imageType === ImageType.jpg) {
20        invariant(quality <= 1 && quality >= 0, `expo-camera: ${quality} is not a valid image quality. Expected a number from 0...1`);
21        return canvas.toDataURL(format, quality);
22    }
23    else {
24        return canvas.toDataURL(format);
25    }
26}
27export function hasValidConstraints(preferredCameraType, width, height) {
28    return preferredCameraType !== undefined && width !== undefined && height !== undefined;
29}
30function ensureCameraPictureOptions(config) {
31    const captureOptions = {
32        scale: 1,
33        imageType: ImageType.png,
34        isImageMirror: false,
35    };
36    for (const key in config) {
37        if (key in config && config[key] !== undefined && key in captureOptions) {
38            captureOptions[key] = config[key];
39        }
40    }
41    return captureOptions;
42}
43const DEFAULT_QUALITY = 0.92;
44export function captureImageData(video, pictureOptions = {}) {
45    if (!video || video.readyState !== video.HAVE_ENOUGH_DATA) {
46        return null;
47    }
48    const canvas = captureImageContext(video, pictureOptions);
49    const context = canvas.getContext('2d', { alpha: false });
50    if (!context || !canvas.width || !canvas.height) {
51        return null;
52    }
53    const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
54    return imageData;
55}
56export function captureImageContext(video, { scale = 1, isImageMirror = false }) {
57    const { videoWidth, videoHeight } = video;
58    const { width, height } = getImageSize(videoWidth, videoHeight, scale);
59    // Build the canvas size and draw the camera image to the context from video
60    const canvas = document.createElement('canvas');
61    canvas.width = width;
62    canvas.height = height;
63    const context = canvas.getContext('2d', { alpha: false });
64    if (!context) {
65        // Should never be called
66        throw new Error('Context is not defined');
67    }
68    // sharp image details
69    // context.imageSmoothingEnabled = false;
70    // Flip horizontally (as css transform: rotateY(180deg))
71    if (isImageMirror) {
72        context.setTransform(-1, 0, 0, 1, canvas.width, 0);
73    }
74    context.drawImage(video, 0, 0, width, height);
75    return canvas;
76}
77export function captureImage(video, pictureOptions) {
78    const config = ensureCameraPictureOptions(pictureOptions);
79    const canvas = captureImageContext(video, config);
80    const { imageType, quality = DEFAULT_QUALITY } = config;
81    return toDataURL(canvas, imageType, quality);
82}
83function getSupportedConstraints() {
84    if (navigator.mediaDevices && navigator.mediaDevices.getSupportedConstraints) {
85        return navigator.mediaDevices.getSupportedConstraints();
86    }
87    return null;
88}
89export function getIdealConstraints(preferredCameraType, width, height) {
90    const preferredConstraints = {
91        audio: false,
92        video: {},
93    };
94    if (hasValidConstraints(preferredCameraType, width, height)) {
95        return MinimumConstraints;
96    }
97    const supports = getSupportedConstraints();
98    // TODO(Bacon): Test this
99    if (!supports || !supports.facingMode || !supports.width || !supports.height) {
100        return MinimumConstraints;
101    }
102    if (preferredCameraType && Object.values(CameraType).includes(preferredCameraType)) {
103        const facingMode = CameraTypeToFacingMode[preferredCameraType];
104        if (isWebKit()) {
105            const key = facingMode === 'user' ? 'exact' : 'ideal';
106            preferredConstraints.video.facingMode = {
107                [key]: facingMode,
108            };
109        }
110        else {
111            preferredConstraints.video.facingMode = {
112                ideal: CameraTypeToFacingMode[preferredCameraType],
113            };
114        }
115    }
116    if (isMediaTrackConstraints(preferredConstraints.video)) {
117        preferredConstraints.video.width = width;
118        preferredConstraints.video.height = height;
119    }
120    return preferredConstraints;
121}
122function isMediaTrackConstraints(input) {
123    return input && typeof input.video !== 'boolean';
124}
125/**
126 * Invoke getStreamDevice a second time with the opposing camera type if the preferred type cannot be retrieved.
127 *
128 * @param preferredCameraType
129 * @param preferredWidth
130 * @param preferredHeight
131 */
132export async function getPreferredStreamDevice(preferredCameraType, preferredWidth, preferredHeight) {
133    try {
134        return await getStreamDevice(preferredCameraType, preferredWidth, preferredHeight);
135    }
136    catch (error) {
137        // A hack on desktop browsers to ensure any camera is used.
138        // eslint-disable-next-line no-undef
139        if (error instanceof OverconstrainedError && error.constraint === 'facingMode') {
140            const nextCameraType = preferredCameraType === CameraType.back ? CameraType.front : CameraType.back;
141            return await getStreamDevice(nextCameraType, preferredWidth, preferredHeight);
142        }
143        throw error;
144    }
145}
146export async function getStreamDevice(preferredCameraType, preferredWidth, preferredHeight) {
147    const constraints = getIdealConstraints(preferredCameraType, preferredWidth, preferredHeight);
148    const stream = await requestUserMediaAsync(constraints);
149    return stream;
150}
151export function isWebKit() {
152    return /WebKit/.test(navigator.userAgent) && !/Edg/.test(navigator.userAgent);
153}
154export function compareStreams(a, b) {
155    if (!a || !b) {
156        return false;
157    }
158    const settingsA = a.getTracks()[0].getSettings();
159    const settingsB = b.getTracks()[0].getSettings();
160    return settingsA.deviceId === settingsB.deviceId;
161}
162export function capture(video, settings, config) {
163    const base64 = captureImage(video, config);
164    const capturedPicture = {
165        uri: base64,
166        base64,
167        width: 0,
168        height: 0,
169    };
170    if (settings) {
171        const { width = 0, height = 0 } = settings;
172        capturedPicture.width = width;
173        capturedPicture.height = height;
174        capturedPicture.exif = settings;
175    }
176    if (config.onPictureSaved) {
177        config.onPictureSaved(capturedPicture);
178    }
179    return capturedPicture;
180}
181export async function syncTrackCapabilities(cameraType, stream, settings = {}) {
182    if (stream?.getVideoTracks) {
183        await Promise.all(stream.getVideoTracks().map((track) => onCapabilitiesReady(cameraType, track, settings)));
184    }
185}
186// https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints
187async function onCapabilitiesReady(cameraType, track, settings = {}) {
188    const capabilities = track.getCapabilities();
189    // Create an empty object because if you set a constraint that isn't available an error will be thrown.
190    const constraints = {};
191    // TODO(Bacon): Add `pointsOfInterest` support
192    const clampedValues = [
193        'exposureCompensation',
194        'colorTemperature',
195        'iso',
196        'brightness',
197        'contrast',
198        'saturation',
199        'sharpness',
200        'focusDistance',
201        'zoom',
202    ];
203    for (const property of clampedValues) {
204        if (capabilities[property]) {
205            constraints[property] = convertNormalizedSetting(capabilities[property], settings[property]);
206        }
207    }
208    function validatedInternalConstrainedValue(constraintKey, settingsKey, converter) {
209        const convertedSetting = converter(settings[settingsKey]);
210        return validatedConstrainedValue({
211            constraintKey,
212            settingsKey,
213            convertedSetting,
214            capabilities,
215            settings,
216            cameraType,
217        });
218    }
219    if (capabilities.focusMode && settings.autoFocus !== undefined) {
220        constraints.focusMode = validatedInternalConstrainedValue('focusMode', 'autoFocus', CapabilityUtils.convertAutoFocusJSONToNative);
221    }
222    if (capabilities.torch && settings.flashMode !== undefined) {
223        constraints.torch = validatedInternalConstrainedValue('torch', 'flashMode', CapabilityUtils.convertFlashModeJSONToNative);
224    }
225    if (capabilities.whiteBalanceMode && settings.whiteBalance !== undefined) {
226        constraints.whiteBalanceMode = validatedInternalConstrainedValue('whiteBalanceMode', 'whiteBalance', CapabilityUtils.convertWhiteBalanceJSONToNative);
227    }
228    try {
229        await track.applyConstraints({ advanced: [constraints] });
230    }
231    catch (error) {
232        if (__DEV__)
233            console.warn('Failed to apply constraints', error);
234    }
235}
236export function stopMediaStream(stream) {
237    if (!stream) {
238        return;
239    }
240    if (stream.getAudioTracks) {
241        stream.getAudioTracks().forEach((track) => track.stop());
242    }
243    if (stream.getVideoTracks) {
244        stream.getVideoTracks().forEach((track) => track.stop());
245    }
246    if (isMediaStreamTrack(stream)) {
247        stream.stop();
248    }
249}
250export function setVideoSource(video, stream) {
251    const createObjectURL = window.URL.createObjectURL ?? window.webkitURL.createObjectURL;
252    if (typeof video.srcObject !== 'undefined') {
253        video.srcObject = stream;
254    }
255    else if (typeof video.mozSrcObject !== 'undefined') {
256        video.mozSrcObject = stream;
257    }
258    else if (stream && createObjectURL) {
259        video.src = createObjectURL(stream);
260    }
261    if (!stream) {
262        const revokeObjectURL = window.URL.revokeObjectURL ?? window.webkitURL.revokeObjectURL;
263        const source = video.src ?? video.srcObject ?? video.mozSrcObject;
264        if (revokeObjectURL && typeof source === 'string') {
265            revokeObjectURL(source);
266        }
267    }
268}
269export function isCapabilityAvailable(video, keyName) {
270    const stream = video.srcObject;
271    if (stream instanceof MediaStream) {
272        const videoTrack = stream.getVideoTracks()[0];
273        return videoTrack.getCapabilities?.()?.[keyName];
274    }
275    return false;
276}
277function isMediaStreamTrack(input) {
278    return typeof input.stop === 'function';
279}
280function convertNormalizedSetting(range, value) {
281    if (!value) {
282        return;
283    }
284    // convert the normalized incoming setting to the native camera zoom range
285    const converted = convertRange(value, [range.min, range.max]);
286    // clamp value so we don't get an error
287    return Math.min(range.max, Math.max(range.min, converted));
288}
289function convertRange(value, r2, r1 = [0, 1]) {
290    return ((value - r1[0]) * (r2[1] - r2[0])) / (r1[1] - r1[0]) + r2[0];
291}
292function validatedConstrainedValue(props) {
293    const { constraintKey, settingsKey, convertedSetting, capabilities, settings, cameraType } = props;
294    const setting = settings[settingsKey];
295    if (Array.isArray(capabilities[constraintKey]) &&
296        convertedSetting &&
297        !capabilities[constraintKey].includes(convertedSetting)) {
298        if (__DEV__) {
299            // Only warn in dev mode.
300            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.`);
301        }
302        return undefined;
303    }
304    return convertedSetting;
305}
306//# sourceMappingURL=WebCameraUtils.js.map