scandit-sdk
Version:
Scandit Barcode Scanner SDK for the Web
592 lines • 27.8 kB
JavaScript
import { Camera } from "./camera";
import { CameraAccess } from "./cameraAccess";
import { CameraManager } from "./cameraManager";
import { CameraSettings } from "./cameraSettings";
import { CustomError } from "./customError";
/**
* @hidden
*/
export var MeteringMode;
(function (MeteringMode) {
MeteringMode["CONTINUOUS"] = "continuous";
MeteringMode["MANUAL"] = "manual";
MeteringMode["NONE"] = "none";
MeteringMode["SINGLE_SHOT"] = "single-shot";
})(MeteringMode || (MeteringMode = {}));
/**
* @hidden
*
* A barcode picker utility class used to handle camera interaction.
*/
export class BarcodePickerCameraManager extends CameraManager {
constructor(triggerFatalError, barcodePickerGui) {
super();
this.postStreamInitializationListener = this.postStreamInitialization.bind(this);
this.videoTrackUnmuteListener = this.videoTrackUnmuteRecovery.bind(this);
this.triggerManualFocusListener = this.triggerManualFocus.bind(this);
this.triggerZoomStartListener = this.triggerZoomStart.bind(this);
this.triggerZoomMoveListener = this.triggerZoomMove.bind(this);
this.triggerFatalError = triggerFatalError;
this.barcodePickerGui = barcodePickerGui;
}
setInteractionOptions(cameraSwitcherEnabled, torchToggleEnabled, tapToFocusEnabled, pinchToZoomEnabled) {
this.cameraSwitcherEnabled = cameraSwitcherEnabled;
this.torchToggleEnabled = torchToggleEnabled;
this.tapToFocusEnabled = tapToFocusEnabled;
this.pinchToZoomEnabled = pinchToZoomEnabled;
}
isCameraSwitcherEnabled() {
return this.cameraSwitcherEnabled;
}
async setCameraSwitcherEnabled(enabled) {
this.cameraSwitcherEnabled = enabled;
if (this.cameraSwitcherEnabled) {
const cameras = await CameraAccess.getCameras();
if (cameras.length > 1) {
this.barcodePickerGui.setCameraSwitcherVisible(true);
}
}
else {
this.barcodePickerGui.setCameraSwitcherVisible(false);
}
}
isTorchToggleEnabled() {
return this.torchToggleEnabled;
}
setTorchToggleEnabled(enabled) {
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);
}
}
isTapToFocusEnabled() {
return this.tapToFocusEnabled;
}
setTapToFocusEnabled(enabled) {
this.tapToFocusEnabled = enabled;
if (this.mediaStream != null) {
if (this.tapToFocusEnabled) {
this.enableTapToFocusListeners();
}
else {
this.disableTapToFocusListeners();
}
}
}
isPinchToZoomEnabled() {
return this.pinchToZoomEnabled;
}
setPinchToZoomEnabled(enabled) {
this.pinchToZoomEnabled = enabled;
if (this.mediaStream != null) {
if (this.pinchToZoomEnabled) {
this.enablePinchToZoomListeners();
}
else {
this.disablePinchToZoomListeners();
}
}
}
setSelectedCamera(camera) {
this.selectedCamera = camera;
}
setSelectedCameraSettings(cameraSettings) {
this.selectedCameraSettings = cameraSettings;
}
async setupCameras() {
if (this.cameraInitializationPromise != null) {
return this.cameraInitializationPromise;
}
const mediaStreamTrack = await this.accessInitialCamera();
const cameras = 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 = CameraAccess.adjustCamerasFromMainCameraStream(mediaStreamTrack, cameras);
if (mainCamera != null) {
this.selectedCamera = mainCamera;
this.updateActiveCameraCurrentResolution(mainCamera);
return Promise.resolve();
}
this.setSelectedCamera();
}
if (this.selectedCamera == null) {
let autoselectedCamera = 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);
}
}
stopStream() {
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;
}
}
applyCameraSettings(cameraSettings) {
this.selectedCameraSettings = cameraSettings;
if (this.activeCamera == null) {
return Promise.reject(new CustomError(BarcodePickerCameraManager.noCameraErrorParameters));
}
return this.initializeCameraWithSettings(this.activeCamera, cameraSettings);
}
reinitializeCamera() {
if (this.activeCamera != null) {
this.initializeCameraWithSettings(this.activeCamera, this.activeCameraSettings).catch(this.triggerFatalError);
}
}
async initializeCameraWithSettings(camera, cameraSettings) {
let existingCameraInitializationPromise = 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;
}
async setTorchEnabled(enabled) {
if (this.mediaStream != null &&
this.mediaTrackCapabilities != null &&
this.mediaTrackCapabilities.torch != null &&
this.mediaTrackCapabilities.torch) {
this.torchEnabled = enabled;
const videoTracks = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
await videoTracks[0].applyConstraints({ advanced: [{ torch: enabled }] });
}
}
}
async toggleTorch() {
this.torchEnabled = !this.torchEnabled;
await this.setTorchEnabled(this.torchEnabled);
}
async setZoom(zoomPercentage, currentZoom) {
if (this.mediaStream != null && this.mediaTrackCapabilities != null && this.mediaTrackCapabilities.zoom != null) {
const videoTracks = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
const zoomRange = this.mediaTrackCapabilities.zoom.max - this.mediaTrackCapabilities.zoom.min;
if (currentZoom == null) {
currentZoom = this.mediaTrackCapabilities.zoom.min;
}
const targetZoom = Math.max(this.mediaTrackCapabilities.zoom.min, Math.min(currentZoom + zoomRange * zoomPercentage, this.mediaTrackCapabilities.zoom.max));
await videoTracks[0].applyConstraints({
advanced: [{ zoom: targetZoom }]
});
}
}
}
accessInitialCamera() {
let initialCameraAccessPromise = Promise.resolve();
if (this.selectedCamera == null) {
// Try to directly access primary (back or only) camera
const primaryCamera = {
deviceId: "",
label: "",
cameraType: Camera.Type.BACK
};
initialCameraAccessPromise = new Promise(async (resolve) => {
try {
await this.initializeCameraWithSettings(primaryCamera, this.selectedCameraSettings);
if (this.mediaStream != null) {
const videoTracks = this.mediaStream.getVideoTracks();
if (videoTracks.length !== 0) {
return resolve(videoTracks[0]);
}
}
}
catch {
// Ignored
}
finally {
resolve();
}
});
}
return initialCameraAccessPromise;
}
updateActiveCameraCurrentResolution(camera) {
this.activeCamera = camera;
this.activeCamera.currentResolution = {
width: this.barcodePickerGui.videoElement.videoWidth,
height: this.barcodePickerGui.videoElement.videoHeight
};
this.barcodePickerGui.setMirrorImageEnabled(this.barcodePickerGui.isMirrorImageEnabled(), false);
}
postStreamInitialization() {
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);
}
videoTrackUnmuteRecovery() {
this.reinitializeCamera();
}
async triggerManualFocusForContinuous() {
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
}
}
async triggerManualFocusForSingleShot() {
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
}
}
async triggerManualFocus(event) {
if (event != null) {
event.preventDefault();
if (event.type === "touchend" && 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 = 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();
}
}
}
}
triggerZoomStart(event) {
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 = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].getConstraints === "function") {
this.pinchToZoomInitialZoom = this.mediaTrackCapabilities.zoom.min;
const currentConstraints = videoTracks[0].getConstraints();
if (currentConstraints.advanced != null) {
const currentZoomConstraint = currentConstraints.advanced.find(constraint => {
return "zoom" in constraint;
});
if (currentZoomConstraint != null && currentZoomConstraint.zoom != null) {
this.pinchToZoomInitialZoom = currentZoomConstraint.zoom;
}
}
}
}
}
async triggerZoomMove(event) {
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);
}
storeStreamCapabilities() {
// istanbul ignore else
if (this.mediaStream != null) {
const videoTracks = this.mediaStream.getVideoTracks();
// istanbul ignore else
if (videoTracks.length !== 0 && typeof videoTracks[0].getCapabilities === "function") {
this.mediaTrackCapabilities = videoTracks[0].getCapabilities();
}
}
}
setupAutofocus() {
window.clearTimeout(this.manualFocusWaitTimeout);
window.clearTimeout(this.manualToAutofocusResumeTimeout);
// istanbul ignore else
if (this.mediaStream != null && this.mediaTrackCapabilities != null) {
const focusModeCapability = 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);
}
}
}
triggerAutoFocus() {
this.triggerFocusMode(MeteringMode.SINGLE_SHOT).catch(
/* istanbul ignore next */ () => {
// Ignored
});
}
triggerFocusMode(focusMode) {
// istanbul ignore else
if (this.mediaStream != null) {
const videoTracks = this.mediaStream.getVideoTracks();
if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
return videoTracks[0].applyConstraints({ advanced: [{ focusMode }] });
}
}
return Promise.reject(undefined);
}
enableTapToFocusListeners() {
["touchend", "mousedown"].forEach(eventName => {
this.barcodePickerGui.videoElement.addEventListener(eventName, this.triggerManualFocusListener);
});
}
enablePinchToZoomListeners() {
this.barcodePickerGui.videoElement.addEventListener("touchstart", this.triggerZoomStartListener);
this.barcodePickerGui.videoElement.addEventListener("touchmove", this.triggerZoomMoveListener);
}
disableTapToFocusListeners() {
["touchend", "mousedown"].forEach(eventName => {
this.barcodePickerGui.videoElement.removeEventListener(eventName, this.triggerManualFocusListener);
});
}
disablePinchToZoomListeners() {
this.barcodePickerGui.videoElement.removeEventListener("touchstart", this.triggerZoomStartListener);
this.barcodePickerGui.videoElement.removeEventListener("touchmove", this.triggerZoomMoveListener);
}
async initializeCameraAndCheckUpdatedSettings(camera, resolutionFallbackLevel) {
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 ||
Object.keys(this.selectedCameraSettings).some(cameraSettingsProperty => {
return (this.selectedCameraSettings[cameraSettingsProperty] !==
this.activeCameraSettings[cameraSettingsProperty]);
}))) {
this.activeCameraSettings = this.selectedCameraSettings;
return this.initializeCameraAndCheckUpdatedSettings(camera, resolutionFallbackLevel);
}
}
finally {
this.cameraInitializationPromise = undefined;
}
}
retryInitializeCameraIfNeeded(camera, resolutionFallbackLevel, resolve, reject, error) {
if (resolutionFallbackLevel < 6) {
return this.initializeCamera(camera, resolutionFallbackLevel + 1)
.then(resolve)
.catch(reject);
}
else {
return reject(error);
}
}
async handleCameraInitializationError(error, resolutionFallbackLevel, camera, resolve, reject) {
// istanbul ignore if
if (error.name === "SourceUnavailableError") {
error.name = "NotReadableError";
}
if (error.message === "Invalid constraint" ||
// tslint:disable-next-line:no-any
(error.name === "OverconstrainedError" && error.constraint === "deviceId")) {
// Camera might have changed deviceId: check for new cameras with same label and type but different deviceId
const cameras = await CameraAccess.getCameras();
const newCamera = 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);
}
initializeCamera(camera, resolutionFallbackLevel = 0) {
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 = 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 = 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);
}
});
}
resolveInitializeCamera(camera, resolve, reject) {
const cameraNotReadableError = 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 = 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);
};
}
}
BarcodePickerCameraManager.cameraAccessTimeoutMs = 4000;
BarcodePickerCameraManager.cameraMetadataCheckTimeoutMs = 4000;
BarcodePickerCameraManager.cameraMetadataCheckIntervalMs = 50;
BarcodePickerCameraManager.getCapabilitiesTimeoutMs = 500;
BarcodePickerCameraManager.autofocusIntervalMs = 1500;
BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs = 5000;
BarcodePickerCameraManager.manualFocusWaitTimeoutMs = 400;
BarcodePickerCameraManager.noCameraErrorParameters = {
name: "NoCameraAvailableError",
message: "No camera available"
};
//# sourceMappingURL=barcodePickerCameraManager.js.map