scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
773 lines (686 loc) • 28.5 kB
text/typescript
import { BarcodePickerGui } from "./barcodePickerGui";
import { Camera } from "./camera";
import { CameraAccess } from "./cameraAccess";
import { CameraManager } from "./cameraManager";
import { CameraSettings } from "./cameraSettings";
import { CustomError } from "./customError";
/**
* @hidden
*/
export enum MeteringMode {
CONTINUOUS = "continuous",
MANUAL = "manual",
NONE = "none",
SINGLE_SHOT = "single-shot"
}
/**
* @hidden
*/
export interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities {
focusMode?: MeteringMode[];
torch?: boolean;
zoom?: {
max: number;
min: number;
step: number;
};
}
/**
* @hidden
*/
export interface ExtendedMediaTrackConstraintSet extends MediaTrackConstraintSet {
torch?: boolean;
zoom?: number;
}
/**
* @hidden
*
* A barcode picker utility class used to handle camera interaction.
*/
export class BarcodePickerCameraManager extends CameraManager {
private static readonly cameraAccessTimeoutMs: number = 4000;
private static readonly cameraMetadataCheckTimeoutMs: number = 4000;
private static readonly cameraMetadataCheckIntervalMs: number = 50;
private static readonly getCapabilitiesTimeoutMs: number = 500;
private static readonly autofocusIntervalMs: number = 1500;
private static readonly manualToAutofocusResumeTimeoutMs: number = 5000;
private static readonly manualFocusWaitTimeoutMs: number = 400;
private static readonly noCameraErrorParameters: { name: string; message: string } = {
name: "NoCameraAvailableError",
message: "No camera available"
};
private readonly triggerFatalError: (error: Error) => void;
private readonly barcodePickerGui: BarcodePickerGui;
private readonly postStreamInitializationListener: () => void = this.postStreamInitialization.bind(this);
private readonly videoTrackUnmuteListener: () => void = this.videoTrackUnmuteRecovery.bind(this);
private readonly triggerManualFocusListener: () => void = this.triggerManualFocus.bind(this);
private readonly triggerZoomStartListener: () => void = this.triggerZoomStart.bind(this);
private readonly triggerZoomMoveListener: () => void = this.triggerZoomMove.bind(this);
private selectedCameraSettings?: CameraSettings;
private mediaStream?: MediaStream;
private mediaTrackCapabilities?: ExtendedMediaTrackCapabilities;
private cameraAccessTimeout: number;
private cameraMetadataCheckInterval: number;
private getCapabilitiesTimeout: number;
private autofocusInterval: number;
private manualToAutofocusResumeTimeout: number;
private manualFocusWaitTimeout: number;
private cameraSwitcherEnabled: boolean;
private torchToggleEnabled: boolean;
private tapToFocusEnabled: boolean;
private pinchToZoomEnabled: boolean;
private pinchToZoomDistance?: number;
private pinchToZoomInitialZoom: number;
private torchEnabled: boolean;
private cameraInitializationPromise?: Promise<void>;
constructor(triggerFatalError: (error: Error) => void, barcodePickerGui: BarcodePickerGui) {
super();
this.triggerFatalError = triggerFatalError;
this.barcodePickerGui = barcodePickerGui;
}
public setInteractionOptions(
cameraSwitcherEnabled: boolean,
torchToggleEnabled: boolean,
tapToFocusEnabled: boolean,
pinchToZoomEnabled: boolean
): void {
this.cameraSwitcherEnabled = cameraSwitcherEnabled;
this.torchToggleEnabled = torchToggleEnabled;
this.tapToFocusEnabled = tapToFocusEnabled;
this.pinchToZoomEnabled = pinchToZoomEnabled;
}
public isCameraSwitcherEnabled(): boolean {
return this.cameraSwitcherEnabled;
}
public async setCameraSwitcherEnabled(enabled: boolean): Promise<void> {
this.cameraSwitcherEnabled = enabled;
if (this.cameraSwitcherEnabled) {
const cameras: Camera[] = await CameraAccess.getCameras();
if (cameras.length > 1) {
this.barcodePickerGui.setCameraSwitcherVisible(true);
}
} else {
this.barcodePickerGui.setCameraSwitcherVisible(false);
}
}
public isTorchToggleEnabled(): boolean {
return this.torchToggleEnabled;
}
public setTorchToggleEnabled(enabled: boolean): void {
this.torchToggleEnabled = enabled;
if (this.torchToggleEnabled) {
if (
this.mediaStream != null &&
this.mediaTrackCapabilities != null &&
this.mediaTrackCapabilities.torch != null &&
this.mediaTrackCapabilities.torch
) {
this.barcodePickerGui.setTorchTogglerVisible(true);
}
} else {
this.barcodePickerGui.setTorchTogglerVisible(false);
}
}
public isTapToFocusEnabled(): boolean {
return this.tapToFocusEnabled;
}
public setTapToFocusEnabled(enabled: boolean): void {
this.tapToFocusEnabled = enabled;
if (this.mediaStream != null) {
if (this.tapToFocusEnabled) {
this.enableTapToFocusListeners();
} else {
this.disableTapToFocusListeners();
}
}
}
public isPinchToZoomEnabled(): boolean {
return this.pinchToZoomEnabled;
}
public setPinchToZoomEnabled(enabled: boolean): void {
this.pinchToZoomEnabled = enabled;
if (this.mediaStream != null) {
if (this.pinchToZoomEnabled) {
this.enablePinchToZoomListeners();
} else {
this.disablePinchToZoomListeners();
}
}
}
public setSelectedCamera(camera?: Camera): void {
this.selectedCamera = camera;
}
public setSelectedCameraSettings(cameraSettings?: CameraSettings): void {
this.selectedCameraSettings = cameraSettings;
}
public async setupCameras(): Promise<void> {
if (this.cameraInitializationPromise != null) {
return this.cameraInitializationPromise;
}
const mediaStreamTrack: void | MediaStreamTrack = await this.accessInitialCamera();
const cameras: Camera[] = await CameraAccess.getCameras();
if (this.cameraSwitcherEnabled && cameras.length > 1) {
this.barcodePickerGui.setCameraSwitcherVisible(true);
}
if (mediaStreamTrack != null) {
// We successfully accessed a camera, check if it's really the main (back or only) camera
const mainCamera: Camera | undefined = CameraAccess.adjustCamerasFromMainCameraStream(mediaStreamTrack, cameras);
if (mainCamera != null) {
this.selectedCamera = mainCamera;
this.updateActiveCameraCurrentResolution(mainCamera);
return Promise.resolve();
}
this.setSelectedCamera();
}
if (this.selectedCamera == null) {
let autoselectedCamera: Camera | undefined = cameras
.filter(camera => {
return camera.cameraType === Camera.Type.BACK;
})
.sort((camera1, camera2) => {
return camera1.label.localeCompare(camera2.label);
})[0];
if (autoselectedCamera == null) {
autoselectedCamera = cameras[0];
if (autoselectedCamera == null) {
throw new CustomError(BarcodePickerCameraManager.noCameraErrorParameters);
}
}
return this.initializeCameraWithSettings(autoselectedCamera, this.selectedCameraSettings);
} else {
return this.initializeCameraWithSettings(this.selectedCamera, this.selectedCameraSettings);
}
}
public stopStream(): void {
if (this.activeCamera != null) {
this.activeCamera.currentResolution = undefined;
}
this.activeCamera = undefined;
if (this.mediaStream != null) {
window.clearTimeout(this.cameraAccessTimeout);
window.clearInterval(this.cameraMetadataCheckInterval);
window.clearTimeout(this.getCapabilitiesTimeout);
window.clearTimeout(this.manualFocusWaitTimeout);
window.clearTimeout(this.manualToAutofocusResumeTimeout);
window.clearInterval(this.autofocusInterval);
this.mediaStream.getVideoTracks().forEach(track => {
track.stop();
});
this.mediaStream = undefined;
this.mediaTrackCapabilities = undefined;
}
}
public applyCameraSettings(cameraSettings?: CameraSettings): Promise<void> {
this.selectedCameraSettings = cameraSettings;
if (this.activeCamera == null) {
return Promise.reject(new CustomError(BarcodePickerCameraManager.noCameraErrorParameters));
}
return this.initializeCameraWithSettings(this.activeCamera, cameraSettings);
}
public reinitializeCamera(): void {
if (this.activeCamera != null) {
this.initializeCameraWithSettings(this.activeCamera, this.activeCameraSettings).catch(this.triggerFatalError);
}
}
public async initializeCameraWithSettings(camera: Camera, cameraSettings?: CameraSettings): Promise<void> {
let existingCameraInitializationPromise: Promise<void> = Promise.resolve();
if (this.cameraInitializationPromise != null) {
existingCameraInitializationPromise = this.cameraInitializationPromise;
}
await existingCameraInitializationPromise;
this.setSelectedCamera(camera);
this.selectedCameraSettings = this.activeCameraSettings = cameraSettings;
if (cameraSettings != null && cameraSettings.resolutionPreference === CameraSettings.ResolutionPreference.FULL_HD) {
this.cameraInitializationPromise = this.initializeCameraAndCheckUpdatedSettings(camera);
} else {
this.cameraInitializationPromise = this.initializeCameraAndCheckUpdatedSettings(camera, 3);
}
return this.cameraInitializationPromise;
}
public async setTorchEnabled(enabled: boolean): Promise<void> {
if (
this.mediaStream != null &&
this.mediaTrackCapabilities != null &&
this.mediaTrackCapabilities.torch != null &&
this.mediaTrackCapabilities.torch
) {
this.torchEnabled = enabled;
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
await videoTracks[0].applyConstraints({ advanced: <ExtendedMediaTrackConstraintSet[]>[{ torch: enabled }] });
}
}
}
public async toggleTorch(): Promise<void> {
this.torchEnabled = !this.torchEnabled;
await this.setTorchEnabled(this.torchEnabled);
}
public async setZoom(zoomPercentage: number, currentZoom?: number): Promise<void> {
if (this.mediaStream != null && this.mediaTrackCapabilities != null && this.mediaTrackCapabilities.zoom != null) {
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
const zoomRange: number = this.mediaTrackCapabilities.zoom.max - this.mediaTrackCapabilities.zoom.min;
if (currentZoom == null) {
currentZoom = this.mediaTrackCapabilities.zoom.min;
}
const targetZoom: number = Math.max(
this.mediaTrackCapabilities.zoom.min,
Math.min(currentZoom + zoomRange * zoomPercentage, this.mediaTrackCapabilities.zoom.max)
);
await videoTracks[0].applyConstraints({
advanced: <ExtendedMediaTrackConstraintSet[]>[{ zoom: targetZoom }]
});
}
}
}
private accessInitialCamera(): Promise<void | MediaStreamTrack> {
let initialCameraAccessPromise: Promise<void | MediaStreamTrack> = Promise.resolve();
if (this.selectedCamera == null) {
// Try to directly access primary (back or only) camera
const primaryCamera: Camera = {
deviceId: "",
label: "",
cameraType: Camera.Type.BACK
};
initialCameraAccessPromise = new Promise(async resolve => {
try {
await this.initializeCameraWithSettings(primaryCamera, this.selectedCameraSettings);
if (this.mediaStream != null) {
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
if (videoTracks.length !== 0) {
return resolve(videoTracks[0]);
}
}
} catch {
// Ignored
} finally {
resolve();
}
});
}
return initialCameraAccessPromise;
}
private updateActiveCameraCurrentResolution(camera: Camera): void {
this.activeCamera = camera;
this.activeCamera.currentResolution = {
width: this.barcodePickerGui.videoElement.videoWidth,
height: this.barcodePickerGui.videoElement.videoHeight
};
this.barcodePickerGui.setMirrorImageEnabled(this.barcodePickerGui.isMirrorImageEnabled(), false);
}
private postStreamInitialization(): void {
window.clearTimeout(this.getCapabilitiesTimeout);
this.getCapabilitiesTimeout = window.setTimeout(() => {
this.storeStreamCapabilities();
this.setupAutofocus();
if (
this.torchToggleEnabled &&
this.mediaStream != null &&
this.mediaTrackCapabilities != null &&
this.mediaTrackCapabilities.torch != null &&
this.mediaTrackCapabilities.torch
) {
this.barcodePickerGui.setTorchTogglerVisible(true);
}
}, BarcodePickerCameraManager.getCapabilitiesTimeoutMs);
}
private videoTrackUnmuteRecovery(): void {
this.reinitializeCamera();
}
private async triggerManualFocusForContinuous(): Promise<void> {
this.manualToAutofocusResumeTimeout = window.setTimeout(async () => {
await this.triggerFocusMode(MeteringMode.CONTINUOUS);
}, BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs);
try {
await this.triggerFocusMode(MeteringMode.CONTINUOUS);
this.manualFocusWaitTimeout = window.setTimeout(async () => {
await this.triggerFocusMode(MeteringMode.MANUAL);
}, BarcodePickerCameraManager.manualFocusWaitTimeoutMs);
} catch {
// istanbul ignore next
}
}
private async triggerManualFocusForSingleShot(): Promise<void> {
window.clearInterval(this.autofocusInterval);
this.manualToAutofocusResumeTimeout = window.setTimeout(() => {
this.autofocusInterval = window.setInterval(
this.triggerAutoFocus.bind(this),
BarcodePickerCameraManager.autofocusIntervalMs
);
}, BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs);
try {
await this.triggerFocusMode(MeteringMode.SINGLE_SHOT);
} catch {
// istanbul ignore next
}
}
private async triggerManualFocus(event?: MouseEvent | TouchEvent): Promise<void> {
if (event != null) {
event.preventDefault();
if (event.type === "touchend" && (<TouchEvent>event).touches.length !== 0) {
return;
}
// Check if we were using pinch-to-zoom
if (this.pinchToZoomDistance != null) {
this.pinchToZoomDistance = undefined;
return;
}
}
window.clearTimeout(this.manualFocusWaitTimeout);
window.clearTimeout(this.manualToAutofocusResumeTimeout);
if (this.mediaStream != null && this.mediaTrackCapabilities != null) {
const focusModeCapability: MeteringMode[] | undefined = this.mediaTrackCapabilities.focusMode;
if (focusModeCapability instanceof Array && focusModeCapability.includes(MeteringMode.SINGLE_SHOT)) {
if (
focusModeCapability.includes(MeteringMode.CONTINUOUS) &&
focusModeCapability.includes(MeteringMode.MANUAL)
) {
await this.triggerManualFocusForContinuous();
} else if (!focusModeCapability.includes(MeteringMode.CONTINUOUS)) {
await this.triggerManualFocusForSingleShot();
}
}
}
}
private triggerZoomStart(event?: TouchEvent): void {
if (event == null || event.touches.length !== 2) {
return;
}
event.preventDefault();
this.pinchToZoomDistance = Math.hypot(
(event.touches[1].screenX - event.touches[0].screenX) / screen.width,
(event.touches[1].screenY - event.touches[0].screenY) / screen.height
);
if (this.mediaStream != null && this.mediaTrackCapabilities != null && this.mediaTrackCapabilities.zoom != null) {
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].getConstraints === "function") {
this.pinchToZoomInitialZoom = this.mediaTrackCapabilities.zoom.min;
const currentConstraints: MediaTrackConstraints = videoTracks[0].getConstraints();
if (currentConstraints.advanced != null) {
const currentZoomConstraint: ExtendedMediaTrackConstraintSet | undefined = currentConstraints.advanced.find(
constraint => {
return "zoom" in constraint;
}
);
if (currentZoomConstraint != null && currentZoomConstraint.zoom != null) {
this.pinchToZoomInitialZoom = currentZoomConstraint.zoom;
}
}
}
}
}
private async triggerZoomMove(event?: TouchEvent): Promise<void> {
if (this.pinchToZoomDistance == null || event == null || event.touches.length !== 2) {
return;
}
event.preventDefault();
await this.setZoom(
(Math.hypot(
(event.touches[1].screenX - event.touches[0].screenX) / screen.width,
(event.touches[1].screenY - event.touches[0].screenY) / screen.height
) -
this.pinchToZoomDistance) *
2,
this.pinchToZoomInitialZoom
);
}
private storeStreamCapabilities(): void {
// istanbul ignore else
if (this.mediaStream != null) {
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].getCapabilities === "function") {
this.mediaTrackCapabilities = videoTracks[0].getCapabilities();
}
}
}
private setupAutofocus(): void {
window.clearTimeout(this.manualFocusWaitTimeout);
window.clearTimeout(this.manualToAutofocusResumeTimeout);
// istanbul ignore else
if (this.mediaStream != null && this.mediaTrackCapabilities != null) {
const focusModeCapability: MeteringMode[] | undefined = this.mediaTrackCapabilities.focusMode;
if (
focusModeCapability instanceof Array &&
!focusModeCapability.includes(MeteringMode.CONTINUOUS) &&
focusModeCapability.includes(MeteringMode.SINGLE_SHOT)
) {
window.clearInterval(this.autofocusInterval);
this.autofocusInterval = window.setInterval(
this.triggerAutoFocus.bind(this),
BarcodePickerCameraManager.autofocusIntervalMs
);
}
}
}
private triggerAutoFocus(): void {
this.triggerFocusMode(MeteringMode.SINGLE_SHOT).catch(
/* istanbul ignore next */ () => {
// Ignored
}
);
}
private triggerFocusMode(focusMode: MeteringMode): Promise<void> {
// istanbul ignore else
if (this.mediaStream != null) {
const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
return videoTracks[0].applyConstraints({ advanced: <MediaTrackConstraintSet[]>(<unknown>[{ focusMode }]) });
}
}
return Promise.reject(undefined);
}
private enableTapToFocusListeners(): void {
["touchend", "mousedown"].forEach(eventName => {
this.barcodePickerGui.videoElement.addEventListener(eventName, this.triggerManualFocusListener);
});
}
private enablePinchToZoomListeners(): void {
this.barcodePickerGui.videoElement.addEventListener("touchstart", this.triggerZoomStartListener);
this.barcodePickerGui.videoElement.addEventListener("touchmove", this.triggerZoomMoveListener);
}
private disableTapToFocusListeners(): void {
["touchend", "mousedown"].forEach(eventName => {
this.barcodePickerGui.videoElement.removeEventListener(eventName, this.triggerManualFocusListener);
});
}
private disablePinchToZoomListeners(): void {
this.barcodePickerGui.videoElement.removeEventListener("touchstart", this.triggerZoomStartListener);
this.barcodePickerGui.videoElement.removeEventListener("touchmove", this.triggerZoomMoveListener);
}
private async initializeCameraAndCheckUpdatedSettings(
camera: Camera,
resolutionFallbackLevel?: number
): Promise<void> {
try {
await this.initializeCamera(camera, resolutionFallbackLevel);
// Check if due to asynchronous behaviour camera settings were changed while camera was initialized
if (
this.selectedCameraSettings !== this.activeCameraSettings &&
(this.selectedCameraSettings == null ||
this.activeCameraSettings == null ||
(<(keyof CameraSettings)[]>Object.keys(this.selectedCameraSettings)).some(cameraSettingsProperty => {
return (
(<CameraSettings>this.selectedCameraSettings)[cameraSettingsProperty] !==
(<CameraSettings>this.activeCameraSettings)[cameraSettingsProperty]
);
}))
) {
this.activeCameraSettings = this.selectedCameraSettings;
return this.initializeCameraAndCheckUpdatedSettings(camera, resolutionFallbackLevel);
}
} finally {
this.cameraInitializationPromise = undefined;
}
}
private retryInitializeCameraIfNeeded(
camera: Camera,
resolutionFallbackLevel: number,
resolve: (value?: void | PromiseLike<void> | undefined) => void,
reject: (reason?: Error) => void,
error: Error
): Promise<void> | void {
if (resolutionFallbackLevel < 6) {
return this.initializeCamera(camera, resolutionFallbackLevel + 1)
.then(resolve)
.catch(reject);
} else {
return reject(error);
}
}
private async handleCameraInitializationError(
error: Error,
resolutionFallbackLevel: number,
camera: Camera,
resolve: (value?: void | PromiseLike<void> | undefined) => void,
reject: (reason?: Error) => void
): Promise<void> {
// istanbul ignore if
if (error.name === "SourceUnavailableError") {
error.name = "NotReadableError";
}
if (
error.message === "Invalid constraint" ||
// tslint:disable-next-line:no-any
(error.name === "OverconstrainedError" && (<any>error).constraint === "deviceId")
) {
// Camera might have changed deviceId: check for new cameras with same label and type but different deviceId
const cameras: Camera[] = await CameraAccess.getCameras();
const newCamera: Camera | undefined = cameras.find(currentCamera => {
return (
currentCamera.label === camera.label &&
currentCamera.cameraType === camera.cameraType &&
currentCamera.deviceId !== camera.deviceId
);
});
if (newCamera == null) {
return this.retryInitializeCameraIfNeeded(camera, resolutionFallbackLevel, resolve, reject, error);
} else {
return this.initializeCamera(newCamera, resolutionFallbackLevel)
.then(resolve)
.catch(reject);
}
}
if (
["PermissionDeniedError", "PermissionDismissedError", "NotAllowedError", "NotFoundError", "AbortError"].includes(
error.name
)
) {
// Camera is not accessible at all
return reject(error);
}
return this.retryInitializeCameraIfNeeded(camera, resolutionFallbackLevel, resolve, reject, error);
}
private initializeCamera(camera: Camera, resolutionFallbackLevel: number = 0): Promise<void> {
if (camera == null) {
return Promise.reject(new CustomError(BarcodePickerCameraManager.noCameraErrorParameters));
}
this.stopStream();
this.torchEnabled = false;
this.barcodePickerGui.setTorchTogglerVisible(false);
return new Promise(async (resolve, reject) => {
try {
const stream: MediaStream = await CameraAccess.accessCameraStream(resolutionFallbackLevel, camera);
// Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
if (typeof stream.getTracks()[0].getSettings === "function") {
const mediaTrackSettings: MediaTrackSettings = stream.getTracks()[0].getSettings();
if (
mediaTrackSettings.width != null &&
mediaTrackSettings.height != null &&
(mediaTrackSettings.width === 2 || mediaTrackSettings.height === 2)
) {
if (resolutionFallbackLevel === 6) {
return reject(
new CustomError({ name: "NotReadableError", message: "Could not initialize camera correctly" })
);
} else {
return this.initializeCamera(camera, resolutionFallbackLevel + 1)
.then(resolve)
.catch(reject);
}
}
}
this.mediaStream = stream;
this.mediaStream.getVideoTracks().forEach(track => {
// Reinitialize camera on weird pause/resumption coming from the OS
// This will add the listener only once in the case of multiple calls, identical listeners are ignored
track.addEventListener("unmute", this.videoTrackUnmuteListener);
});
// This will add the listener only once in the case of multiple calls, identical listeners are ignored
this.barcodePickerGui.videoElement.addEventListener("loadedmetadata", this.postStreamInitializationListener);
if (this.tapToFocusEnabled) {
this.enableTapToFocusListeners();
}
if (this.pinchToZoomEnabled) {
this.enablePinchToZoomListeners();
}
this.resolveInitializeCamera(camera, resolve, reject);
this.barcodePickerGui.videoElement.srcObject = stream;
this.barcodePickerGui.videoElement.load();
this.barcodePickerGui.playVideo();
} catch (error) {
await this.handleCameraInitializationError(error, resolutionFallbackLevel, camera, resolve, reject);
}
});
}
private resolveInitializeCamera(camera: Camera, resolve: () => void, reject: (reason: Error) => void): void {
const cameraNotReadableError: Error = new CustomError({
name: "NotReadableError",
message: "Could not initialize camera correctly"
});
window.clearTimeout(this.cameraAccessTimeout);
this.cameraAccessTimeout = window.setTimeout(() => {
this.stopStream();
reject(cameraNotReadableError);
}, BarcodePickerCameraManager.cameraAccessTimeoutMs);
this.barcodePickerGui.videoElement.onresize = () => {
this.updateActiveCameraCurrentResolution(camera);
};
this.barcodePickerGui.videoElement.onloadeddata = () => {
this.barcodePickerGui.videoElement.onloadeddata = null;
window.clearTimeout(this.cameraAccessTimeout);
// Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
// Also detect failed camera access with no error but also no video stream provided
if (
this.barcodePickerGui.videoElement.videoWidth > 2 &&
this.barcodePickerGui.videoElement.videoHeight > 2 &&
this.barcodePickerGui.videoElement.currentTime > 0
) {
if (camera.deviceId !== "") {
this.updateActiveCameraCurrentResolution(camera);
}
return resolve();
}
const cameraMetadataCheckStartTime: number = performance.now();
window.clearInterval(this.cameraMetadataCheckInterval);
this.cameraMetadataCheckInterval = window.setInterval(() => {
// Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
// Also detect failed camera access with no error but also no video stream provided
if (
this.barcodePickerGui.videoElement.videoWidth === 2 ||
this.barcodePickerGui.videoElement.videoHeight === 2 ||
this.barcodePickerGui.videoElement.currentTime === 0
) {
if (
performance.now() - cameraMetadataCheckStartTime >
BarcodePickerCameraManager.cameraMetadataCheckTimeoutMs
) {
window.clearInterval(this.cameraMetadataCheckInterval);
this.stopStream();
return reject(cameraNotReadableError);
}
return;
}
window.clearInterval(this.cameraMetadataCheckInterval);
if (camera.deviceId !== "") {
this.updateActiveCameraCurrentResolution(camera);
this.barcodePickerGui.videoElement.dispatchEvent(new Event("canplay"));
}
return resolve();
}, BarcodePickerCameraManager.cameraMetadataCheckIntervalMs);
};
}
}