scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
941 lines • 41.2 kB
JavaScript
import { CameraResolutionConstraint } from "./barcodePicker/cameraManager";
import { BrowserCompatibility } from "./browserCompatibility";
import { BrowserHelper } from "./browserHelper";
import { Camera } from "./camera";
import { CustomError } from "./customError";
import { Logger } from "./logger";
import { UnsupportedBrowserError } from "./unsupportedBrowserError";
/**
* A helper object to interact with cameras.
*/
export var CameraAccess;
(function (CameraAccess) {
/**
* @hidden
*
* Standard error names mapping.
*/
const standardErrorNamesMapping = new Map([
["DeviceCaptureError", "AbortError"],
["NotSupportedError", "AbortError"],
["ScreenCaptureError", "AbortError"],
["TabCaptureError", "AbortError"],
["TypeError", "AbortError"],
["InvalidStateError", "NotAllowedError"],
["MediaDeviceFailedDueToShutdown", "NotAllowedError"],
["MediaDeviceKillSwitchOn", "NotAllowedError"],
["PermissionDeniedError", "NotAllowedError"],
["PermissionDismissedError", "NotAllowedError"],
["DevicesNotFoundError", "NotFoundError"],
["SourceUnavailableError", "NotReadableError"],
["TrackStartError", "NotReadableError"],
["ConstraintNotSatisfiedError", "OverconstrainedError"],
]);
/**
* @hidden
*
* Handle localized camera labels. Supported languages:
* English, German, French, Spanish (spain), Portuguese (brasil), Portuguese (portugal), Italian,
* Chinese (simplified), Chinese (traditional), Japanese, Russian, Turkish, Dutch, Arabic, Thai, Swedish,
* Danish, Vietnamese, Norwegian, Polish, Finnish, Indonesian, Hebrew, Greek, Romanian, Hungarian, Czech,
* Catalan, Slovak, Ukraininan, Croatian, Malay, Hindi.
*/
const backCameraKeywords = [
"rear",
"back",
"rück",
"arrière",
"trasera",
"trás",
"traseira",
"posteriore",
"后面",
"後面",
"背面",
"后置",
"後置",
"背置",
"задней",
"الخلفية",
"후",
"arka",
"achterzijde",
"หลัง",
"baksidan",
"bagside",
"sau",
"bak",
"tylny",
"takakamera",
"belakang",
"אחורית",
"πίσω",
"spate",
"hátsó",
"zadní",
"darrere",
"posterior",
"zadná",
"задня",
"stražnja",
"belakang",
"बैक",
];
/**
* @hidden
*
* Handle localized iOS Wide front (main) camera labels. Supported languages:
* English, German, French, Spanish (spain), Portuguese (brasil), Portuguese (portugal), Italian,
* Chinese (simplified), Chinese (traditional), Japanese, Russian, Turkish, Dutch, Arabic, Thai, Swedish,
* Danish, Vietnamese, Norwegian, Polish, Finnish, Indonesian, Hebrew, Greek, Romanian, Hungarian, Czech,
* Catalan, Slovak, Ukraininan, Croatian, Malay, Hindi.
*/
const iosWideFrontCameraLabels = new Set([
"Front Camera",
"Frontkamera",
"Appareil photo avant",
"Cámara frontal",
"Câmera Frontal",
"Câmara frontal",
"Fotocamera (anteriore)",
"前置相机",
"前置相機",
"前面カメラ",
"Камера на передней панели",
"Ön Kamera",
"Camera aan voorzijde",
"الكاميرا الأمامية",
"กล้องด้านหน้า",
"Kamera på framsidan",
"Forsidekamera",
"Camera mặt trước",
"Kamera foran",
"Przedni aparat",
"Etukamera",
"Kamera Depan",
"מצלמה קדמית",
"Μπροστινή κάμερα",
"Cameră față",
"Elülső kamera",
"Přední fotoaparát",
"Càmera frontal",
"Predná kamera",
"Передня камера",
"Prednja kamera",
"Kamera Depan",
"फ़्रंट कैमरा",
]);
/**
* @hidden
*
* Handle localized iOS Wide back (main) camera labels. Supported languages:
* English, German, French, Spanish (spain), Portuguese (brasil), Portuguese (portugal), Italian,
* Chinese (simplified), Chinese (traditional), Japanese, Russian, Turkish, Dutch, Arabic, Thai, Swedish,
* Danish, Vietnamese, Norwegian, Polish, Finnish, Indonesian, Hebrew, Greek, Romanian, Hungarian, Czech,
* Catalan, Slovak, Ukraininan, Croatian, Malay, Hindi.
*/
const iosWideBackCameraLabels = new Set([
"Back Camera",
"Rückkamera",
"Caméra arrière",
"Cámara trasera",
"Câmera Traseira",
"Câmara traseira",
"Fotocamera (posteriore)",
"后置相机",
"後置相機",
"背面カメラ",
"Камера на задней панели",
"Arka Kamera",
"Camera aan achterzijde",
"الكاميرا الخلفية",
"กล้องด้านหลัง",
"Kamera på baksidan",
"Bagsidekamera",
"Camera mặt sau",
"Kamera bak",
"Tylny aparat",
"Takakamera",
"Kamera Belakang",
"מצלמה אחורית",
"Πίσω κάμερα",
"Cameră spate",
"Hátsó kamera",
"Zadní fotoaparát",
"Càmera posterior",
"Zadná kamera",
"Задня камера",
"Stražnja kamera",
"Kamera Belakang",
"बैक कैमरा",
]);
/**
* @hidden
*
* Handle localized iOS UltraWide back camera labels. Supported languages:
* English, German, French, Spanish (spain), Portuguese (brasil), Portuguese (portugal), Italian,
* Chinese (simplified), Chinese (traditional), Japanese, Russian, Turkish, Dutch, Arabic, Thai, Swedish,
* Danish, Vietnamese, Norwegian, Polish, Finnish, Indonesian, Hebrew, Greek, Romanian, Hungarian, Czech,
* Catalan, Slovak, Ukraininan, Croatian, Malay, Hindi.
*/
const iosUltraWideBackCameraLabels = new Set([
"Back Ultra Wide Camera",
"Rückseitige Ultra-Weitwinkelkamera",
"Appareil photo ultra grand angle arrière",
"Cámara trasera con ultra gran angular",
"Câmera Ultra-Angular Traseira",
"Câmara ultra grande angular traseira",
"Fotocamera con ultra-grandangolo (posteriore)",
"后置超广角相机",
"後置超廣角相機",
"背面超広角カメラ",
"Задняя сверхширокоугольная камера",
"Ultra Geniş Kamera Arka Yüzü",
"Ultrabrede camera aan achterzijde",
"كاميرا خلفية عريضة جدًا",
"กล้องด้านหลังมุมกว้างอัลตร้า",
"Ultravidvinkelkamera på baksidan",
"Ultravidvinkelkameraet på bagsiden",
"Camera cực rộng mặt sau",
"Ultravidvinkelkamera bak",
"Tylny aparat ultraszerokokątny",
"Ultralaajakulmainen takakamera",
"Kamera Ultra Lebar Belakang",
"מצלמה אולטרה רחבה אחורית",
"Πίσω υπερευρεία κάμερα",
"Cameră cu obiectiv ultra‑superangular spate",
"Hátsó, ultraszéles látószögű kamera",
"Zadní ultra širokoúhlý fotoaparát",
"Càmera posterior amb ultra gran angular",
"Zadná ultraširokouhlá kamera",
"Задня надширококутна камера",
"Stražnja ultra široka kamera",
"Kamera Ultralebar Belakang",
"बैक अल्ट्रा वाइड कैमरा",
]);
/**
* @hidden
*
* Handle localized iOS Back Dual Wide camera labels.
* Supported Languages:
* English, German, French, Spanish, Portuguese (Brazil), Portuguese (Portugal), Italian,
* Chinese(Simplified), Chinese Traditional, Japanese, Russian, Turkish, Dutch, Arabic, Thai,
* Swedish, Danish, Vietnamese, Norwegian Bokmål, Polish, Finnish, Indonesian, Hebrew, Greek,
* Romanian, Hungarian, Czech, Catalan, Slovak, Ukrainian, Croatian, Malay, Hindi
*/
const iosBackDualWideCameraLabels = new Set([
"Back Dual Wide Camera",
"Rückseitige Dual-Weitwinkelkamera",
"Double appareil photo grand angle arrière",
"Cámara trasera dual con gran angular",
"Câmera Dupla Grande-Angular Traseira",
"Câmara grande angular dupla traseira",
"Fotocamera doppia con grandangolo (posteriore)",
"后置双广角镜头",
"後置雙廣角相機",
"背面デュアル広角カメラ",
"Задняя двойная широкоугольная камера",
"Çift Geniş Kamera Arka Yüzü",
"كاميرا خلفية مزدوجة عريضة",
"กล้องคู่ด้านหลังมุมกว้าง",
"Dubbel vidvinkelkamera på baksidan",
"Camera kép rộng mặt sau",
"Dobbelt vidvinkelkamera bak",
"Tylny dwuobiektywowy aparat szerokokątny",
"Laajakulmainen kaksoistakakamera",
"Kamera Lebar Belakang Ganda",
"מצלמה כפולה רחבה אחורית",
"Πίσω διπλή ευρεία κάμερα",
"Cameră dublă cu obiectiv superangular spate",
"Hátsó, kettős, széles látószögű kamera",
"Zadní duální širokoúhlý fotoaparát",
"Càmera dual posterior amb gran angular",
"Zadná duálna širokouhlá kamera",
"Задня здвоєна ширококутна камера",
"Stražnja dvostruka široka kamera",
"Dwikamera Lebar Belakang",
"बैक ड्युअल वाइड कैमरा",
]);
/**
* @hidden
*
* The (cached) list of available video devices, updated when [[getCameras]] is called for the first time and after
* subsequent calls with the *refreshDevices* parameter enabled. The contained devices' order never changes, howver
* their deviceIds could change when they are retrieved again after a camera access and stop in some situations.
*/
let availableVideoDevices;
/**
* @hidden
*
* Whether the currently cached available devices are out of date because of a `devicechange` event.
*/
let outdatedDevices = false;
/**
* @hidden
*
* Overrides for main camera for a given camera type on a desktop/laptop device, set when accessing an initial camera.
*/
CameraAccess.mainCameraForTypeOverridesOnDesktop = new Map();
/**
* @hidden
*
* To be accessed directly only for tests.
*
* The mapping from deviceIds to camera objects.
*/
CameraAccess.deviceIdToCameraObjects = new Map();
/**
* @hidden
*
* To be accessed directly only for tests.
*
* The list of inaccessible deviceIds.
*/
CameraAccess.inaccessibleDeviceIds = new Set();
/**
* @hidden
*
* Listen to `devicechange` events.
*/
function deviceChangeListener() {
outdatedDevices = true;
}
/**
* @hidden
*
* @param label The camera label.
* @returns Whether the label mentions the camera being a back-facing one.
*/
function isBackCameraLabel(label) {
const lowercaseLabel = label.toLowerCase();
return backCameraKeywords.some((keyword) => {
return lowercaseLabel.includes(keyword);
});
}
/**
* @hidden
*
* @param label The camera label.
* @returns Whether the label identifies the camera being the iOS Wide front (main) camera one.
*/
function isIOSWideFrontCameraLabel(label) {
return iosWideFrontCameraLabels.has(label);
}
CameraAccess.isIOSWideFrontCameraLabel = isIOSWideFrontCameraLabel;
/**
* @hidden
*
* @param label The camera label.
* @returns Whether the label identifies the camera being the iOS Wide back (main) camera one.
*/
function isIOSWideBackCameraLabel(label) {
return iosWideBackCameraLabels.has(label);
}
CameraAccess.isIOSWideBackCameraLabel = isIOSWideBackCameraLabel;
/**
*
* @param label The camera label.
* @returns Whether the label identifies the camera being the iOS Back Dual camera one.
*/
function isIOSBackDualWideCameraLabel(label) {
return iosBackDualWideCameraLabels.has(label);
}
CameraAccess.isIOSBackDualWideCameraLabel = isIOSBackDualWideCameraLabel;
/**
* @hidden
*
* @param label The camera label.
* @returns Whether the label identifies the camera being the iOS UltraWide back camera one.
*/
function isIOSUltraWideBackCameraLabel(label) {
return iosUltraWideBackCameraLabels.has(label);
}
CameraAccess.isIOSUltraWideBackCameraLabel = isIOSUltraWideBackCameraLabel;
/**
* @hidden
*
* Map non-standard error names to standard ones.
*
* @param error The error object.
*/
function mapNonStandardErrorName(error) {
let name;
if (error.message === "Invalid constraint") {
name = "OverconstrainedError";
}
else {
name = standardErrorNamesMapping.get(error.name) ?? error.name;
}
Object.defineProperty(error, "name", {
value: name,
});
}
/**
* @hidden
*
* Get the main camera for the given camera type.
*
* @param cameras The array of available [[Camera]] objects.
* @param cameraType The wanted camera type.
* @returns The main camera matching the wanted camera type.
*/
function getMainCameraForType(cameras, cameraType) {
let mainCameraForType;
if (BrowserHelper.isDesktopDevice()) {
// When the device is a desktop/laptop, the overridden camera for the given type or, if not present, the first
// camera of the given type is the main one.
if (CameraAccess.mainCameraForTypeOverridesOnDesktop.has(cameraType)) {
mainCameraForType = CameraAccess.mainCameraForTypeOverridesOnDesktop.get(cameraType);
}
else {
// Note that if the device is a desktop/laptop, with no labels all cameras are assumed to be front ones,
// so this will return the first camera as the main front one and none for the back one.
mainCameraForType = cameras.filter((camera) => {
return camera.cameraType === cameraType;
})[0];
}
}
else {
const allHaveBlankLabel = cameras.every((camera) => {
return camera.label === "";
});
const allHaveNonEmptyLabel = cameras.every((camera) => {
return camera.label !== "";
});
const someHaveLabel = cameras.length > 1 && !allHaveBlankLabel && !allHaveNonEmptyLabel;
if (allHaveBlankLabel) {
// When no camera label is available cameras are already in front to back order, assume main front camera is the
// first one and main back camera is the last one.
mainCameraForType = cameras[cameraType === Camera.Type.FRONT ? 0 : cameras.length - 1];
}
else if (someHaveLabel) {
// When only a few cameras have labels, we may be in a webview where only labels from accessed stream are
// available.
const cameraOfType = cameras.filter((camera) => {
return camera.cameraType === cameraType;
});
if (cameraOfType.length === 1) {
mainCameraForType = cameraOfType[0];
}
else if (cameraOfType.length > 1) {
// Assume main front camera is the first one and main back camera is the last one.
mainCameraForType = cameraOfType[cameraType === Camera.Type.FRONT ? 0 : cameraOfType.length - 1];
}
}
else {
if (BrowserHelper.userAgentInfo.getOS().name === "iOS") {
if (cameraType === Camera.Type.BACK) {
// If available find the Back Dual Camera and return it as the main world facing camera
const backDualWideCamera = cameras.find((camera) => {
return isIOSBackDualWideCameraLabel(camera.label);
});
if (backDualWideCamera != null) {
return backDualWideCamera;
}
// If available find the Wide Back Camera and return it as the main world facing camera
const wideBackCamera = cameras.find((camera) => {
return isIOSWideBackCameraLabel(camera.label);
});
if (wideBackCamera != null) {
return wideBackCamera;
}
}
else {
// If available find the Wide front camera and return it as the main user facing camera
const wideFrontCamera = cameras.find((camera) => {
return isIOSWideFrontCameraLabel(camera.label);
});
if (wideFrontCamera != null) {
return wideFrontCamera;
}
}
}
mainCameraForType = cameras
.filter((camera) => {
return camera.cameraType === cameraType;
})
// sort so that camera list looks like ['camera1 0', 'camera1 1', 'camera2 0', 'camera2 1']
.sort((camera1, camera2) => {
return camera1.label.localeCompare(camera2.label);
})[0];
}
}
return mainCameraForType;
}
CameraAccess.getMainCameraForType = getMainCameraForType;
/**
* @hidden
*
* Sort the given cameras in order of priority of access based on the given camera type.
*
* @param cameras The array of available [[Camera]] objects.
* @param cameraType The preferred camera type.
* @returns The sorted cameras.
*/
function sortCamerasForCameraType(cameras, cameraType) {
function prioritizeMainCameraOverride(prioritizedCameras, currentCameraType) {
const mainCameraOverride = CameraAccess.mainCameraForTypeOverridesOnDesktop.get(currentCameraType);
if (mainCameraOverride != null && prioritizedCameras.includes(mainCameraOverride)) {
prioritizedCameras = prioritizedCameras.filter((camera) => {
return camera !== mainCameraOverride;
});
prioritizedCameras.unshift(mainCameraOverride);
}
return prioritizedCameras;
}
let frontCameras = cameras.filter((camera) => {
return camera.cameraType === Camera.Type.FRONT;
});
let backCameras = cameras.filter((camera) => {
return camera.cameraType === Camera.Type.BACK;
});
if (BrowserHelper.isDesktopDevice()) {
// When the device is a desktop/laptop, the cameras for each type are already ordered, we move the overrides
// first if present and change front / back group order if needed.
frontCameras = prioritizeMainCameraOverride(frontCameras, Camera.Type.FRONT);
backCameras = prioritizeMainCameraOverride(backCameras, Camera.Type.BACK);
}
else if (cameras.every((camera) => {
return camera.label === "";
})) {
// When no camera label is available cameras are already in front to back order, we assume front cameras are
// ordered and back cameras are in reversed order (try to access last first), and we change front / back group
// order if needed.
backCameras.reverse();
}
else {
frontCameras.sort((camera1, camera2) => {
return camera1.label.localeCompare(camera2.label);
});
backCameras.sort((camera1, camera2) => {
return camera1.label.localeCompare(camera2.label);
});
if (BrowserHelper.userAgentInfo.getOS().name === "iOS") {
// If available, find the Wide front camera and move it as the top priority for front cameras
void frontCameras.some((camera, index) => {
return isIOSWideFrontCameraLabel(camera.label) && frontCameras.unshift(frontCameras.splice(index, 1)[0]) >= 0;
});
// If available, find the UltraWide back camera and move it as the second priority for back cameras
// (it's moved to top priority here, but the Wide back camera will take the first position in a few lines)
void backCameras.some((camera, index) => {
return (isIOSUltraWideBackCameraLabel(camera.label) && backCameras.unshift(backCameras.splice(index, 1)[0]) >= 0);
});
// If available, find the Wide back camera and move it as the top priority for back cameras
void backCameras.some((camera, index) => {
return isIOSWideBackCameraLabel(camera.label) && backCameras.unshift(backCameras.splice(index, 1)[0]) >= 0;
});
// If available, find the back dual wide camera and move it as the top priority for back cameras
void backCameras.some((camera, index) => {
return (isIOSBackDualWideCameraLabel(camera.label) && backCameras.unshift(backCameras.splice(index, 1)[0]) >= 0);
});
}
}
return cameraType === Camera.Type.FRONT ? [...frontCameras, ...backCameras] : [...backCameras, ...frontCameras];
}
CameraAccess.sortCamerasForCameraType = sortCamerasForCameraType;
/**
* @hidden
*
* Adjusts the camera's information based on the given currently active video stream.
*
* @param mediaStream The currently active `MediaStream` object.
* @param camera The currently active [[Camera]] object associated with the video stream.
*/
function adjustCameraFromMediaStream(mediaStream, camera) {
const videoTracks = mediaStream.getVideoTracks();
if (videoTracks.length !== 0) {
const mediaStreamTrack = videoTracks[0];
let mediaTrackSettings;
if (typeof mediaStreamTrack.getSettings === "function") {
mediaTrackSettings = mediaStreamTrack.getSettings();
if (mediaTrackSettings?.facingMode != null && mediaTrackSettings.facingMode.length > 0) {
camera.cameraType =
mediaTrackSettings.facingMode === "environment" ? Camera.Type.BACK : Camera.Type.FRONT;
}
}
if (mediaStreamTrack.label != null && mediaStreamTrack.label.length > 0) {
camera.label = mediaStreamTrack.label;
}
}
}
CameraAccess.adjustCameraFromMediaStream = adjustCameraFromMediaStream;
/**
* @hidden
*
* @param devices The list of available devices.
* @returns The extracted list of accessible camera objects initialized from the given devices.
*/
function extractAccessibleCamerasFromDevices(devices) {
function createCamera(videoDevice, index, videoDevices) {
if (CameraAccess.deviceIdToCameraObjects.has(videoDevice.deviceId)) {
return CameraAccess.deviceIdToCameraObjects.get(videoDevice.deviceId);
}
const label = videoDevice.label ?? "";
let cameraType;
if (!BrowserHelper.isDesktopDevice() &&
videoDevices.every((device) => {
return device.label === "" && !CameraAccess.deviceIdToCameraObjects.has(device.deviceId);
})) {
// When the device is not a desktop/laptop and no camera label is available, assume the camera is a front one
// if it's the only one or comes in the first half of the list of cameras (if an odd number of cameras is
// available, it's more likely to have more back than front ones).
cameraType =
videoDevices.length === 1 || index + 1 <= Math.floor(videoDevices.length / 2)
? Camera.Type.FRONT
: Camera.Type.BACK;
}
else {
// Note that if the device is a desktop/laptop, unless the label specifies a back camera, a front one is assumed
cameraType = isBackCameraLabel(label) ? Camera.Type.BACK : Camera.Type.FRONT;
}
return {
deviceId: videoDevice.deviceId,
label,
cameraType,
};
}
const cameras = devices
.map(createCamera)
.map((camera) => {
// If it's the initial camera, do nothing
if (camera.deviceId !== "") {
CameraAccess.deviceIdToCameraObjects.set(camera.deviceId, camera);
}
return camera;
})
.filter((camera) => {
// Ignore infrared cameras as they often fail to be accessed and are not useful in any case
return !/\b(?:ir|infrared)\b/i.test(camera.label);
})
.filter((camera) => {
return !CameraAccess.inaccessibleDeviceIds.has(camera.deviceId);
});
if (!BrowserHelper.isDesktopDevice() &&
cameras.length > 1 &&
!cameras.some((camera) => {
return camera.cameraType === Camera.Type.BACK;
})) {
// When the device is not a desktop/laptop check if cameras are labeled with resolution information, if that's the
// case, take the higher - resolution one, otherwise pick the last camera (it's not true that the primary camera
// is first in most scenarios) and mark it as the back one.
let backCameraIndex = cameras.length - 1;
const cameraResolutions = cameras.map((camera) => {
const match = camera.label.match(/\b([0-9]+)MP?\b/i);
if (match != null) {
return parseInt(match[1], 10);
}
return NaN;
});
if (!cameraResolutions.some((cameraResolution) => {
return isNaN(cameraResolution);
})) {
backCameraIndex = cameraResolutions.lastIndexOf(Math.max(...cameraResolutions));
}
cameras[backCameraIndex].cameraType = Camera.Type.BACK;
}
return cameras;
}
/**
* @hidden
*
* @returns The stream, if necessary, accessed to provide access to complete device information
*/
async function getStreamForDeviceAccessPermission() {
// If there are video devices and all of them have no label it means we need to access a camera before we can get
// the needed information
if (availableVideoDevices != null &&
availableVideoDevices.length > 0 &&
availableVideoDevices.every((device) => {
return device.label === "" && !CameraAccess.deviceIdToCameraObjects.has(device.deviceId);
})) {
try {
return await navigator.mediaDevices.getUserMedia({
video: true,
audio: false,
});
}
catch {
// Ignored
}
}
}
/**
* @hidden
*
* Checks and adjust cameras' deviceId information and related information if a change is detected. We can rely on the
* fact that devices are returned in the same order even when deviceId information changes.
*
* @param oldAvailableDevices The old list of available devices before deviceId information was refreshed.
* @param newAvailableDevices The new list of available devices after deviceId information was refreshed.
*/
function checkAndUpdateCameraDeviceIdInformation(oldAvailableDevices, newAvailableDevices) {
if (newAvailableDevices.length > 0 &&
oldAvailableDevices.length === newAvailableDevices.length &&
!newAvailableDevices.every((device, index) => {
return oldAvailableDevices[index].deviceId === device.deviceId;
})) {
const deviceIdChanges = {};
oldAvailableDevices.forEach((device, index) => {
const camera = CameraAccess.deviceIdToCameraObjects.get(device.deviceId);
if (camera == null || camera.label !== (newAvailableDevices[index].label ?? "")) {
return;
}
const newDeviceId = newAvailableDevices[index].deviceId;
deviceIdChanges[camera.deviceId] = newDeviceId;
if (CameraAccess.inaccessibleDeviceIds.has(camera.deviceId)) {
CameraAccess.inaccessibleDeviceIds.add(newDeviceId);
}
camera.deviceId = newDeviceId;
CameraAccess.deviceIdToCameraObjects.set(newDeviceId, camera);
});
Logger.log(Logger.Level.DEBUG, "Detected updated camera deviceId information and updated it accordingly", deviceIdChanges);
}
}
/**
* Get a list of cameras (if any) available on the device, a camera access permission is requested to the user
* the first time this method is called if needed.
*
* If the browser is incompatible the returned promise is rejected with a `UnsupportedBrowserError` error.
*
* When refreshing available devices, if updated deviceId information is detected, cameras' deviceId are updated
* accordingly. This could happen after a camera access and stop in some situations.
*
* @param refreshDevices Force a call to refresh available video devices even when information is already available.
* @param cameraAlreadyAccessed Hint that a camera has already been accessed before, avoiding a possible initial
* camera access permission request on the first call, in cases this cannot be already reliably detected.
* @returns A promise resolving to the array of available [[Camera]] objects (could be empty).
*/
async function getCameras(refreshDevices = false, cameraAlreadyAccessed = false) {
const browserCompatibility = BrowserHelper.checkBrowserCompatibility();
if (!browserCompatibility.fullSupport) {
throw new UnsupportedBrowserError(browserCompatibility);
}
// This will add the listeners only once in case of multiple calls: identical listeners are ignored
if (typeof navigator.mediaDevices.addEventListener === "function") {
navigator.mediaDevices.addEventListener("devicechange", deviceChangeListener);
}
else {
// It's not a problem if this event is not supported, this would just never trigger
navigator.mediaDevices.ondevicechange = deviceChangeListener;
}
if (availableVideoDevices == null || refreshDevices || outdatedDevices) {
outdatedDevices = false;
let stream;
const oldAvailableDevices = availableVideoDevices ?? [];
availableVideoDevices = [];
try {
availableVideoDevices = await enumerateVideoDevices();
if (!cameraAlreadyAccessed) {
stream = await getStreamForDeviceAccessPermission();
if (stream != null) {
availableVideoDevices = await enumerateVideoDevices();
}
}
Logger.log(Logger.Level.DEBUG, "Camera list (devices):", ...availableVideoDevices);
checkAndUpdateCameraDeviceIdInformation(oldAvailableDevices, availableVideoDevices);
}
catch (error) {
mapNonStandardErrorName(error);
throw error;
}
finally {
if (stream != null) {
stream.getVideoTracks().forEach((track) => {
track.stop();
});
}
}
}
const cameras = extractAccessibleCamerasFromDevices(availableVideoDevices);
Logger.log(Logger.Level.DEBUG, "Camera list (cameras): ", ...cameras);
// Return a copy of the array to allow for array mutations in other functions
return [...cameras];
}
CameraAccess.getCameras = getCameras;
/**
* @hidden
*
* Call `navigator.mediaDevices.getUserMedia` asynchronously in a `setTimeout` call.
*
* @param getUserMediaParams The parameters for the `navigator.mediaDevices.getUserMedia` call.
* @returns A promise resolving when the camera is accessed.
*/
function getUserMediaDelayed(getUserMediaParams) {
Logger.log(Logger.Level.DEBUG, "Attempt to access camera (parameters):", getUserMediaParams.video);
return new Promise((resolve, reject) => {
window.setTimeout(() => {
(navigator.mediaDevices.getUserMedia(getUserMediaParams) ??
Promise.reject(new CustomError({ name: "AbortError" })))
.then(resolve)
.catch(reject);
}, 0);
});
}
/**
* @hidden
*
* Get the *getUserMedia* *video* parameters to be used given a resolution fallback level and the browser used.
*
* @param cameraResolutionConstraint The resolution constraint.
* @returns The resulting *getUserMedia* *video* parameters.
*/
function getUserMediaVideoParams(cameraResolutionConstraint) {
const userMediaVideoParams = {
// @ts-ignore
resizeMode: "none",
};
switch (cameraResolutionConstraint) {
case CameraResolutionConstraint.ULTRA_HD:
return {
...userMediaVideoParams,
width: { min: 3200, ideal: 3840, max: 4096 },
height: { min: 1800, ideal: 2160, max: 2400 },
};
case CameraResolutionConstraint.FULL_HD:
return {
...userMediaVideoParams,
width: { min: 1400, ideal: 1920, max: 2160 },
height: { min: 900, ideal: 1080, max: 1440 },
};
case CameraResolutionConstraint.HD:
return {
...userMediaVideoParams,
width: { min: 960, ideal: 1280, max: 1440 },
height: { min: 480, ideal: 720, max: 960 },
};
case CameraResolutionConstraint.SD:
return {
...userMediaVideoParams,
width: { min: 640, ideal: 640, max: 800 },
height: { min: 480, ideal: 480, max: 600 },
};
case CameraResolutionConstraint.NONE:
default:
return {};
}
}
CameraAccess.getUserMediaVideoParams = getUserMediaVideoParams;
/**
* @hidden
*
* Try to access a given camera for video input at the given resolution level.
*
* If a camera is inaccessible because of errors, then it's added to the inaccessible device list. If the specific
* error is of type `OverconstrainedError` or `NotReadableError` however, this procedure is done later on via a
* separate external logic; also, in case of an error of type `NotAllowedError` (permission denied) this procedure is
* not executed, in order to possibly recover if and when the user allows the camera to be accessed again.
* This is done to allow checking if the camera can still be accessed via an updated deviceId when deviceId
* information changes, or if it should then be confirmed to be considered inaccessible.
*
* Depending on parameters, device features and user permissions for camera access, any of the following errors
* could be the rejected result of the returned promise:
* - `AbortError`
* - `NotAllowedError`
* - `NotFoundError`
* - `NotReadableError`
* - `SecurityError`
* - `OverconstrainedError`
*
* @param cameraResolutionConstraint The resolution constraint.
* @param camera The camera to try to access for video input.
* @returns A promise resolving to the `MediaStream` object coming from the accessed camera.
*/
async function accessCameraStream(cameraResolutionConstraint, camera) {
Logger.log(Logger.Level.DEBUG, "Attempt to access camera (camera):", camera);
const getUserMediaParams = {
audio: false,
video: getUserMediaVideoParams(cameraResolutionConstraint),
};
// If it's the initial camera, use the given cameraType, otherwise use the given deviceId
if (camera.deviceId === "") {
getUserMediaParams.video.facingMode = {
ideal: camera.cameraType === Camera.Type.BACK ? "environment" : "user",
};
}
else {
getUserMediaParams.video.deviceId = {
exact: camera.deviceId,
};
}
try {
const mediaStream = await getUserMediaDelayed(getUserMediaParams);
adjustCameraFromMediaStream(mediaStream, camera);
return mediaStream;
}
catch (error) {
mapNonStandardErrorName(error);
if (!["OverconstrainedError", "NotReadableError", "NotAllowedError"].includes(error.name)) {
markCameraAsInaccessible(camera);
}
throw error;
}
}
CameraAccess.accessCameraStream = accessCameraStream;
/**
* @hidden
*
* Mark a camera to be inaccessible and thus excluded from the camera list returned by [[getCameras]].
*
* @param camera The camera to mark to be inaccessible.
*/
function markCameraAsInaccessible(camera) {
// If it's the initial camera, do nothing
if (camera.deviceId !== "") {
Logger.log(Logger.Level.DEBUG, "Camera marked to be inaccessible:", camera);
CameraAccess.inaccessibleDeviceIds.add(camera.deviceId);
}
}
CameraAccess.markCameraAsInaccessible = markCameraAsInaccessible;
/**
* @hidden
*
* Get a list of available video devices in a cross-browser compatible way.
*
* @returns A promise resolving to the `MediaDeviceInfo` array of all available video devices.
*/
async function enumerateVideoDevices() {
let devices;
if (typeof navigator.enumerateDevices === "function") {
devices = await navigator.enumerateDevices();
}
else if (typeof navigator.mediaDevices?.enumerateDevices === "function") {
devices = await navigator.mediaDevices.enumerateDevices();
}
else {
try {
if (window.MediaStreamTrack?.getSources == null) {
throw new Error();
}
devices = await new Promise((resolve) => {
window.MediaStreamTrack?.getSources?.(resolve);
});
devices = devices
.filter((device) => {
return device.kind.toLowerCase() === "video" || device.kind.toLowerCase() === "videoinput";
})
.map((device) => {
return {
deviceId: device.deviceId ?? "",
groupId: device.groupId,
kind: "videoinput",
label: device.label,
toJSON: /* istanbul ignore next */ function () {
return this;
},
};
});
}
catch {
throw new UnsupportedBrowserError({
fullSupport: false,
scannerSupport: true,
missingFeatures: [BrowserCompatibility.Feature.MEDIA_DEVICES],
});
}
}
return devices.filter((device) => {
return device.kind === "videoinput";
});
}
})(CameraAccess || (CameraAccess = {}));
//# sourceMappingURL=cameraAccess.js.map