UNPKG

28.5 kBPlain TextView Raw
1import { BarcodePickerGui } from "./barcodePickerGui";
2import { Camera } from "./camera";
3import { CameraAccess } from "./cameraAccess";
4import { CameraManager } from "./cameraManager";
5import { CameraSettings } from "./cameraSettings";
6import { CustomError } from "./customError";
7
8/**
9 * @hidden
10 */
11export enum MeteringMode {
12 CONTINUOUS = "continuous",
13 MANUAL = "manual",
14 NONE = "none",
15 SINGLE_SHOT = "single-shot"
16}
17
18/**
19 * @hidden
20 */
21export interface ExtendedMediaTrackCapabilities extends MediaTrackCapabilities {
22 focusMode?: MeteringMode[];
23 torch?: boolean;
24 zoom?: {
25 max: number;
26 min: number;
27 step: number;
28 };
29}
30
31/**
32 * @hidden
33 */
34export interface ExtendedMediaTrackConstraintSet extends MediaTrackConstraintSet {
35 torch?: boolean;
36 zoom?: number;
37}
38
39/**
40 * @hidden
41 *
42 * A barcode picker utility class used to handle camera interaction.
43 */
44export class BarcodePickerCameraManager extends CameraManager {
45 private static readonly cameraAccessTimeoutMs: number = 4000;
46 private static readonly cameraMetadataCheckTimeoutMs: number = 4000;
47 private static readonly cameraMetadataCheckIntervalMs: number = 50;
48 private static readonly getCapabilitiesTimeoutMs: number = 500;
49 private static readonly autofocusIntervalMs: number = 1500;
50 private static readonly manualToAutofocusResumeTimeoutMs: number = 5000;
51 private static readonly manualFocusWaitTimeoutMs: number = 400;
52 private static readonly noCameraErrorParameters: { name: string; message: string } = {
53 name: "NoCameraAvailableError",
54 message: "No camera available"
55 };
56
57 private readonly triggerFatalError: (error: Error) => void;
58 private readonly barcodePickerGui: BarcodePickerGui;
59 private readonly postStreamInitializationListener: () => void = this.postStreamInitialization.bind(this);
60 private readonly videoTrackUnmuteListener: () => void = this.videoTrackUnmuteRecovery.bind(this);
61 private readonly triggerManualFocusListener: () => void = this.triggerManualFocus.bind(this);
62 private readonly triggerZoomStartListener: () => void = this.triggerZoomStart.bind(this);
63 private readonly triggerZoomMoveListener: () => void = this.triggerZoomMove.bind(this);
64
65 private selectedCameraSettings?: CameraSettings;
66 private mediaStream?: MediaStream;
67 private mediaTrackCapabilities?: ExtendedMediaTrackCapabilities;
68 private cameraAccessTimeout: number;
69 private cameraMetadataCheckInterval: number;
70 private getCapabilitiesTimeout: number;
71 private autofocusInterval: number;
72 private manualToAutofocusResumeTimeout: number;
73 private manualFocusWaitTimeout: number;
74 private cameraSwitcherEnabled: boolean;
75 private torchToggleEnabled: boolean;
76 private tapToFocusEnabled: boolean;
77 private pinchToZoomEnabled: boolean;
78 private pinchToZoomDistance?: number;
79 private pinchToZoomInitialZoom: number;
80 private torchEnabled: boolean;
81 private cameraInitializationPromise?: Promise<void>;
82
83 constructor(triggerFatalError: (error: Error) => void, barcodePickerGui: BarcodePickerGui) {
84 super();
85 this.triggerFatalError = triggerFatalError;
86 this.barcodePickerGui = barcodePickerGui;
87 }
88
89 public setInteractionOptions(
90 cameraSwitcherEnabled: boolean,
91 torchToggleEnabled: boolean,
92 tapToFocusEnabled: boolean,
93 pinchToZoomEnabled: boolean
94 ): void {
95 this.cameraSwitcherEnabled = cameraSwitcherEnabled;
96 this.torchToggleEnabled = torchToggleEnabled;
97 this.tapToFocusEnabled = tapToFocusEnabled;
98 this.pinchToZoomEnabled = pinchToZoomEnabled;
99 }
100
101 public isCameraSwitcherEnabled(): boolean {
102 return this.cameraSwitcherEnabled;
103 }
104
105 public async setCameraSwitcherEnabled(enabled: boolean): Promise<void> {
106 this.cameraSwitcherEnabled = enabled;
107
108 if (this.cameraSwitcherEnabled) {
109 const cameras: Camera[] = await CameraAccess.getCameras();
110 if (cameras.length > 1) {
111 this.barcodePickerGui.setCameraSwitcherVisible(true);
112 }
113 } else {
114 this.barcodePickerGui.setCameraSwitcherVisible(false);
115 }
116 }
117
118 public isTorchToggleEnabled(): boolean {
119 return this.torchToggleEnabled;
120 }
121
122 public setTorchToggleEnabled(enabled: boolean): void {
123 this.torchToggleEnabled = enabled;
124
125 if (this.torchToggleEnabled) {
126 if (
127 this.mediaStream != null &&
128 this.mediaTrackCapabilities != null &&
129 this.mediaTrackCapabilities.torch != null &&
130 this.mediaTrackCapabilities.torch
131 ) {
132 this.barcodePickerGui.setTorchTogglerVisible(true);
133 }
134 } else {
135 this.barcodePickerGui.setTorchTogglerVisible(false);
136 }
137 }
138
139 public isTapToFocusEnabled(): boolean {
140 return this.tapToFocusEnabled;
141 }
142
143 public setTapToFocusEnabled(enabled: boolean): void {
144 this.tapToFocusEnabled = enabled;
145
146 if (this.mediaStream != null) {
147 if (this.tapToFocusEnabled) {
148 this.enableTapToFocusListeners();
149 } else {
150 this.disableTapToFocusListeners();
151 }
152 }
153 }
154
155 public isPinchToZoomEnabled(): boolean {
156 return this.pinchToZoomEnabled;
157 }
158
159 public setPinchToZoomEnabled(enabled: boolean): void {
160 this.pinchToZoomEnabled = enabled;
161
162 if (this.mediaStream != null) {
163 if (this.pinchToZoomEnabled) {
164 this.enablePinchToZoomListeners();
165 } else {
166 this.disablePinchToZoomListeners();
167 }
168 }
169 }
170
171 public setSelectedCamera(camera?: Camera): void {
172 this.selectedCamera = camera;
173 }
174
175 public setSelectedCameraSettings(cameraSettings?: CameraSettings): void {
176 this.selectedCameraSettings = cameraSettings;
177 }
178
179 public async setupCameras(): Promise<void> {
180 if (this.cameraInitializationPromise != null) {
181 return this.cameraInitializationPromise;
182 }
183
184 const mediaStreamTrack: void | MediaStreamTrack = await this.accessInitialCamera();
185 const cameras: Camera[] = await CameraAccess.getCameras();
186
187 if (this.cameraSwitcherEnabled && cameras.length > 1) {
188 this.barcodePickerGui.setCameraSwitcherVisible(true);
189 }
190
191 if (mediaStreamTrack != null) {
192 // We successfully accessed a camera, check if it's really the main (back or only) camera
193 const mainCamera: Camera | undefined = CameraAccess.adjustCamerasFromMainCameraStream(mediaStreamTrack, cameras);
194 if (mainCamera != null) {
195 this.selectedCamera = mainCamera;
196 this.updateActiveCameraCurrentResolution(mainCamera);
197
198 return Promise.resolve();
199 }
200 this.setSelectedCamera();
201 }
202
203 if (this.selectedCamera == null) {
204 let autoselectedCamera: Camera | undefined = cameras
205 .filter(camera => {
206 return camera.cameraType === Camera.Type.BACK;
207 })
208 .sort((camera1, camera2) => {
209 return camera1.label.localeCompare(camera2.label);
210 })[0];
211 if (autoselectedCamera == null) {
212 autoselectedCamera = cameras[0];
213 if (autoselectedCamera == null) {
214 throw new CustomError(BarcodePickerCameraManager.noCameraErrorParameters);
215 }
216 }
217
218 return this.initializeCameraWithSettings(autoselectedCamera, this.selectedCameraSettings);
219 } else {
220 return this.initializeCameraWithSettings(this.selectedCamera, this.selectedCameraSettings);
221 }
222 }
223
224 public stopStream(): void {
225 if (this.activeCamera != null) {
226 this.activeCamera.currentResolution = undefined;
227 }
228
229 this.activeCamera = undefined;
230
231 if (this.mediaStream != null) {
232 window.clearTimeout(this.cameraAccessTimeout);
233 window.clearInterval(this.cameraMetadataCheckInterval);
234 window.clearTimeout(this.getCapabilitiesTimeout);
235 window.clearTimeout(this.manualFocusWaitTimeout);
236 window.clearTimeout(this.manualToAutofocusResumeTimeout);
237 window.clearInterval(this.autofocusInterval);
238 this.mediaStream.getVideoTracks().forEach(track => {
239 track.stop();
240 });
241 this.mediaStream = undefined;
242 this.mediaTrackCapabilities = undefined;
243 }
244 }
245
246 public applyCameraSettings(cameraSettings?: CameraSettings): Promise<void> {
247 this.selectedCameraSettings = cameraSettings;
248
249 if (this.activeCamera == null) {
250 return Promise.reject(new CustomError(BarcodePickerCameraManager.noCameraErrorParameters));
251 }
252
253 return this.initializeCameraWithSettings(this.activeCamera, cameraSettings);
254 }
255
256 public reinitializeCamera(): void {
257 if (this.activeCamera != null) {
258 this.initializeCameraWithSettings(this.activeCamera, this.activeCameraSettings).catch(this.triggerFatalError);
259 }
260 }
261
262 public async initializeCameraWithSettings(camera: Camera, cameraSettings?: CameraSettings): Promise<void> {
263 let existingCameraInitializationPromise: Promise<void> = Promise.resolve();
264 if (this.cameraInitializationPromise != null) {
265 existingCameraInitializationPromise = this.cameraInitializationPromise;
266 }
267
268 await existingCameraInitializationPromise;
269 this.setSelectedCamera(camera);
270 this.selectedCameraSettings = this.activeCameraSettings = cameraSettings;
271 if (cameraSettings != null && cameraSettings.resolutionPreference === CameraSettings.ResolutionPreference.FULL_HD) {
272 this.cameraInitializationPromise = this.initializeCameraAndCheckUpdatedSettings(camera);
273 } else {
274 this.cameraInitializationPromise = this.initializeCameraAndCheckUpdatedSettings(camera, 3);
275 }
276
277 return this.cameraInitializationPromise;
278 }
279
280 public async setTorchEnabled(enabled: boolean): Promise<void> {
281 if (
282 this.mediaStream != null &&
283 this.mediaTrackCapabilities != null &&
284 this.mediaTrackCapabilities.torch != null &&
285 this.mediaTrackCapabilities.torch
286 ) {
287 this.torchEnabled = enabled;
288 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
289 // istanbul ignore else
290 if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
291 await videoTracks[0].applyConstraints({ advanced: <ExtendedMediaTrackConstraintSet[]>[{ torch: enabled }] });
292 }
293 }
294 }
295
296 public async toggleTorch(): Promise<void> {
297 this.torchEnabled = !this.torchEnabled;
298 await this.setTorchEnabled(this.torchEnabled);
299 }
300
301 public async setZoom(zoomPercentage: number, currentZoom?: number): Promise<void> {
302 if (this.mediaStream != null && this.mediaTrackCapabilities != null && this.mediaTrackCapabilities.zoom != null) {
303 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
304 // istanbul ignore else
305 if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
306 const zoomRange: number = this.mediaTrackCapabilities.zoom.max - this.mediaTrackCapabilities.zoom.min;
307 if (currentZoom == null) {
308 currentZoom = this.mediaTrackCapabilities.zoom.min;
309 }
310 const targetZoom: number = Math.max(
311 this.mediaTrackCapabilities.zoom.min,
312 Math.min(currentZoom + zoomRange * zoomPercentage, this.mediaTrackCapabilities.zoom.max)
313 );
314 await videoTracks[0].applyConstraints({
315 advanced: <ExtendedMediaTrackConstraintSet[]>[{ zoom: targetZoom }]
316 });
317 }
318 }
319 }
320
321 private accessInitialCamera(): Promise<void | MediaStreamTrack> {
322 let initialCameraAccessPromise: Promise<void | MediaStreamTrack> = Promise.resolve();
323
324 if (this.selectedCamera == null) {
325 // Try to directly access primary (back or only) camera
326 const primaryCamera: Camera = {
327 deviceId: "",
328 label: "",
329 cameraType: Camera.Type.BACK
330 };
331
332 initialCameraAccessPromise = new Promise(async resolve => {
333 try {
334 await this.initializeCameraWithSettings(primaryCamera, this.selectedCameraSettings);
335 if (this.mediaStream != null) {
336 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
337 if (videoTracks.length !== 0) {
338 return resolve(videoTracks[0]);
339 }
340 }
341 } catch {
342 // Ignored
343 } finally {
344 resolve();
345 }
346 });
347 }
348
349 return initialCameraAccessPromise;
350 }
351
352 private updateActiveCameraCurrentResolution(camera: Camera): void {
353 this.activeCamera = camera;
354 this.activeCamera.currentResolution = {
355 width: this.barcodePickerGui.videoElement.videoWidth,
356 height: this.barcodePickerGui.videoElement.videoHeight
357 };
358 this.barcodePickerGui.setMirrorImageEnabled(this.barcodePickerGui.isMirrorImageEnabled(), false);
359 }
360
361 private postStreamInitialization(): void {
362 window.clearTimeout(this.getCapabilitiesTimeout);
363 this.getCapabilitiesTimeout = window.setTimeout(() => {
364 this.storeStreamCapabilities();
365 this.setupAutofocus();
366 if (
367 this.torchToggleEnabled &&
368 this.mediaStream != null &&
369 this.mediaTrackCapabilities != null &&
370 this.mediaTrackCapabilities.torch != null &&
371 this.mediaTrackCapabilities.torch
372 ) {
373 this.barcodePickerGui.setTorchTogglerVisible(true);
374 }
375 }, BarcodePickerCameraManager.getCapabilitiesTimeoutMs);
376 }
377
378 private videoTrackUnmuteRecovery(): void {
379 this.reinitializeCamera();
380 }
381
382 private async triggerManualFocusForContinuous(): Promise<void> {
383 this.manualToAutofocusResumeTimeout = window.setTimeout(async () => {
384 await this.triggerFocusMode(MeteringMode.CONTINUOUS);
385 }, BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs);
386
387 try {
388 await this.triggerFocusMode(MeteringMode.CONTINUOUS);
389 this.manualFocusWaitTimeout = window.setTimeout(async () => {
390 await this.triggerFocusMode(MeteringMode.MANUAL);
391 }, BarcodePickerCameraManager.manualFocusWaitTimeoutMs);
392 } catch {
393 // istanbul ignore next
394 }
395 }
396
397 private async triggerManualFocusForSingleShot(): Promise<void> {
398 window.clearInterval(this.autofocusInterval);
399
400 this.manualToAutofocusResumeTimeout = window.setTimeout(() => {
401 this.autofocusInterval = window.setInterval(
402 this.triggerAutoFocus.bind(this),
403 BarcodePickerCameraManager.autofocusIntervalMs
404 );
405 }, BarcodePickerCameraManager.manualToAutofocusResumeTimeoutMs);
406
407 try {
408 await this.triggerFocusMode(MeteringMode.SINGLE_SHOT);
409 } catch {
410 // istanbul ignore next
411 }
412 }
413
414 private async triggerManualFocus(event?: MouseEvent | TouchEvent): Promise<void> {
415 if (event != null) {
416 event.preventDefault();
417 if (event.type === "touchend" && (<TouchEvent>event).touches.length !== 0) {
418 return;
419 }
420 // Check if we were using pinch-to-zoom
421 if (this.pinchToZoomDistance != null) {
422 this.pinchToZoomDistance = undefined;
423
424 return;
425 }
426 }
427 window.clearTimeout(this.manualFocusWaitTimeout);
428 window.clearTimeout(this.manualToAutofocusResumeTimeout);
429 if (this.mediaStream != null && this.mediaTrackCapabilities != null) {
430 const focusModeCapability: MeteringMode[] | undefined = this.mediaTrackCapabilities.focusMode;
431 if (focusModeCapability instanceof Array && focusModeCapability.includes(MeteringMode.SINGLE_SHOT)) {
432 if (
433 focusModeCapability.includes(MeteringMode.CONTINUOUS) &&
434 focusModeCapability.includes(MeteringMode.MANUAL)
435 ) {
436 await this.triggerManualFocusForContinuous();
437 } else if (!focusModeCapability.includes(MeteringMode.CONTINUOUS)) {
438 await this.triggerManualFocusForSingleShot();
439 }
440 }
441 }
442 }
443
444 private triggerZoomStart(event?: TouchEvent): void {
445 if (event == null || event.touches.length !== 2) {
446 return;
447 }
448 event.preventDefault();
449 this.pinchToZoomDistance = Math.hypot(
450 (event.touches[1].screenX - event.touches[0].screenX) / screen.width,
451 (event.touches[1].screenY - event.touches[0].screenY) / screen.height
452 );
453 if (this.mediaStream != null && this.mediaTrackCapabilities != null && this.mediaTrackCapabilities.zoom != null) {
454 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
455 // istanbul ignore else
456 if (videoTracks.length !== 0 && typeof videoTracks[0].getConstraints === "function") {
457 this.pinchToZoomInitialZoom = this.mediaTrackCapabilities.zoom.min;
458 const currentConstraints: MediaTrackConstraints = videoTracks[0].getConstraints();
459 if (currentConstraints.advanced != null) {
460 const currentZoomConstraint: ExtendedMediaTrackConstraintSet | undefined = currentConstraints.advanced.find(
461 constraint => {
462 return "zoom" in constraint;
463 }
464 );
465 if (currentZoomConstraint != null && currentZoomConstraint.zoom != null) {
466 this.pinchToZoomInitialZoom = currentZoomConstraint.zoom;
467 }
468 }
469 }
470 }
471 }
472
473 private async triggerZoomMove(event?: TouchEvent): Promise<void> {
474 if (this.pinchToZoomDistance == null || event == null || event.touches.length !== 2) {
475 return;
476 }
477 event.preventDefault();
478 await this.setZoom(
479 (Math.hypot(
480 (event.touches[1].screenX - event.touches[0].screenX) / screen.width,
481 (event.touches[1].screenY - event.touches[0].screenY) / screen.height
482 ) -
483 this.pinchToZoomDistance) *
484 2,
485 this.pinchToZoomInitialZoom
486 );
487 }
488
489 private storeStreamCapabilities(): void {
490 // istanbul ignore else
491 if (this.mediaStream != null) {
492 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
493 // istanbul ignore else
494 if (videoTracks.length !== 0 && typeof videoTracks[0].getCapabilities === "function") {
495 this.mediaTrackCapabilities = videoTracks[0].getCapabilities();
496 }
497 }
498 }
499
500 private setupAutofocus(): void {
501 window.clearTimeout(this.manualFocusWaitTimeout);
502 window.clearTimeout(this.manualToAutofocusResumeTimeout);
503 // istanbul ignore else
504 if (this.mediaStream != null && this.mediaTrackCapabilities != null) {
505 const focusModeCapability: MeteringMode[] | undefined = this.mediaTrackCapabilities.focusMode;
506 if (
507 focusModeCapability instanceof Array &&
508 !focusModeCapability.includes(MeteringMode.CONTINUOUS) &&
509 focusModeCapability.includes(MeteringMode.SINGLE_SHOT)
510 ) {
511 window.clearInterval(this.autofocusInterval);
512 this.autofocusInterval = window.setInterval(
513 this.triggerAutoFocus.bind(this),
514 BarcodePickerCameraManager.autofocusIntervalMs
515 );
516 }
517 }
518 }
519
520 private triggerAutoFocus(): void {
521 this.triggerFocusMode(MeteringMode.SINGLE_SHOT).catch(
522 /* istanbul ignore next */ () => {
523 // Ignored
524 }
525 );
526 }
527
528 private triggerFocusMode(focusMode: MeteringMode): Promise<void> {
529 // istanbul ignore else
530 if (this.mediaStream != null) {
531 const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
532 if (videoTracks.length !== 0 && typeof videoTracks[0].applyConstraints === "function") {
533 return videoTracks[0].applyConstraints({ advanced: <MediaTrackConstraintSet[]>(<unknown>[{ focusMode }]) });
534 }
535 }
536
537 return Promise.reject(undefined);
538 }
539
540 private enableTapToFocusListeners(): void {
541 ["touchend", "mousedown"].forEach(eventName => {
542 this.barcodePickerGui.videoElement.addEventListener(eventName, this.triggerManualFocusListener);
543 });
544 }
545
546 private enablePinchToZoomListeners(): void {
547 this.barcodePickerGui.videoElement.addEventListener("touchstart", this.triggerZoomStartListener);
548 this.barcodePickerGui.videoElement.addEventListener("touchmove", this.triggerZoomMoveListener);
549 }
550
551 private disableTapToFocusListeners(): void {
552 ["touchend", "mousedown"].forEach(eventName => {
553 this.barcodePickerGui.videoElement.removeEventListener(eventName, this.triggerManualFocusListener);
554 });
555 }
556
557 private disablePinchToZoomListeners(): void {
558 this.barcodePickerGui.videoElement.removeEventListener("touchstart", this.triggerZoomStartListener);
559 this.barcodePickerGui.videoElement.removeEventListener("touchmove", this.triggerZoomMoveListener);
560 }
561
562 private async initializeCameraAndCheckUpdatedSettings(
563 camera: Camera,
564 resolutionFallbackLevel?: number
565 ): Promise<void> {
566 try {
567 await this.initializeCamera(camera, resolutionFallbackLevel);
568 // Check if due to asynchronous behaviour camera settings were changed while camera was initialized
569 if (
570 this.selectedCameraSettings !== this.activeCameraSettings &&
571 (this.selectedCameraSettings == null ||
572 this.activeCameraSettings == null ||
573 (<(keyof CameraSettings)[]>Object.keys(this.selectedCameraSettings)).some(cameraSettingsProperty => {
574 return (
575 (<CameraSettings>this.selectedCameraSettings)[cameraSettingsProperty] !==
576 (<CameraSettings>this.activeCameraSettings)[cameraSettingsProperty]
577 );
578 }))
579 ) {
580 this.activeCameraSettings = this.selectedCameraSettings;
581
582 return this.initializeCameraAndCheckUpdatedSettings(camera, resolutionFallbackLevel);
583 }
584 } finally {
585 this.cameraInitializationPromise = undefined;
586 }
587 }
588
589 private retryInitializeCameraIfNeeded(
590 camera: Camera,
591 resolutionFallbackLevel: number,
592 resolve: (value?: void | PromiseLike<void> | undefined) => void,
593 reject: (reason?: Error) => void,
594 error: Error
595 ): Promise<void> | void {
596 if (resolutionFallbackLevel < 6) {
597 return this.initializeCamera(camera, resolutionFallbackLevel + 1)
598 .then(resolve)
599 .catch(reject);
600 } else {
601 return reject(error);
602 }
603 }
604
605 private async handleCameraInitializationError(
606 error: Error,
607 resolutionFallbackLevel: number,
608 camera: Camera,
609 resolve: (value?: void | PromiseLike<void> | undefined) => void,
610 reject: (reason?: Error) => void
611 ): Promise<void> {
612 // istanbul ignore if
613 if (error.name === "SourceUnavailableError") {
614 error.name = "NotReadableError";
615 }
616 if (
617 error.message === "Invalid constraint" ||
618 // tslint:disable-next-line:no-any
619 (error.name === "OverconstrainedError" && (<any>error).constraint === "deviceId")
620 ) {
621 // Camera might have changed deviceId: check for new cameras with same label and type but different deviceId
622 const cameras: Camera[] = await CameraAccess.getCameras();
623 const newCamera: Camera | undefined = cameras.find(currentCamera => {
624 return (
625 currentCamera.label === camera.label &&
626 currentCamera.cameraType === camera.cameraType &&
627 currentCamera.deviceId !== camera.deviceId
628 );
629 });
630 if (newCamera == null) {
631 return this.retryInitializeCameraIfNeeded(camera, resolutionFallbackLevel, resolve, reject, error);
632 } else {
633 return this.initializeCamera(newCamera, resolutionFallbackLevel)
634 .then(resolve)
635 .catch(reject);
636 }
637 }
638 if (
639 ["PermissionDeniedError", "PermissionDismissedError", "NotAllowedError", "NotFoundError", "AbortError"].includes(
640 error.name
641 )
642 ) {
643 // Camera is not accessible at all
644 return reject(error);
645 }
646
647 return this.retryInitializeCameraIfNeeded(camera, resolutionFallbackLevel, resolve, reject, error);
648 }
649
650 private initializeCamera(camera: Camera, resolutionFallbackLevel: number = 0): Promise<void> {
651 if (camera == null) {
652 return Promise.reject(new CustomError(BarcodePickerCameraManager.noCameraErrorParameters));
653 }
654 this.stopStream();
655 this.torchEnabled = false;
656 this.barcodePickerGui.setTorchTogglerVisible(false);
657
658 return new Promise(async (resolve, reject) => {
659 try {
660 const stream: MediaStream = await CameraAccess.accessCameraStream(resolutionFallbackLevel, camera);
661 // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
662 if (typeof stream.getTracks()[0].getSettings === "function") {
663 const mediaTrackSettings: MediaTrackSettings = stream.getTracks()[0].getSettings();
664 if (
665 mediaTrackSettings.width != null &&
666 mediaTrackSettings.height != null &&
667 (mediaTrackSettings.width === 2 || mediaTrackSettings.height === 2)
668 ) {
669 if (resolutionFallbackLevel === 6) {
670 return reject(
671 new CustomError({ name: "NotReadableError", message: "Could not initialize camera correctly" })
672 );
673 } else {
674 return this.initializeCamera(camera, resolutionFallbackLevel + 1)
675 .then(resolve)
676 .catch(reject);
677 }
678 }
679 }
680 this.mediaStream = stream;
681 this.mediaStream.getVideoTracks().forEach(track => {
682 // Reinitialize camera on weird pause/resumption coming from the OS
683 // This will add the listener only once in the case of multiple calls, identical listeners are ignored
684 track.addEventListener("unmute", this.videoTrackUnmuteListener);
685 });
686 // This will add the listener only once in the case of multiple calls, identical listeners are ignored
687 this.barcodePickerGui.videoElement.addEventListener("loadedmetadata", this.postStreamInitializationListener);
688 if (this.tapToFocusEnabled) {
689 this.enableTapToFocusListeners();
690 }
691 if (this.pinchToZoomEnabled) {
692 this.enablePinchToZoomListeners();
693 }
694 this.resolveInitializeCamera(camera, resolve, reject);
695 this.barcodePickerGui.videoElement.srcObject = stream;
696 this.barcodePickerGui.videoElement.load();
697 this.barcodePickerGui.playVideo();
698 } catch (error) {
699 await this.handleCameraInitializationError(error, resolutionFallbackLevel, camera, resolve, reject);
700 }
701 });
702 }
703
704 private resolveInitializeCamera(camera: Camera, resolve: () => void, reject: (reason: Error) => void): void {
705 const cameraNotReadableError: Error = new CustomError({
706 name: "NotReadableError",
707 message: "Could not initialize camera correctly"
708 });
709
710 window.clearTimeout(this.cameraAccessTimeout);
711 this.cameraAccessTimeout = window.setTimeout(() => {
712 this.stopStream();
713 reject(cameraNotReadableError);
714 }, BarcodePickerCameraManager.cameraAccessTimeoutMs);
715
716 this.barcodePickerGui.videoElement.onresize = () => {
717 this.updateActiveCameraCurrentResolution(camera);
718 };
719
720 this.barcodePickerGui.videoElement.onloadeddata = () => {
721 this.barcodePickerGui.videoElement.onloadeddata = null;
722 window.clearTimeout(this.cameraAccessTimeout);
723
724 // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
725 // Also detect failed camera access with no error but also no video stream provided
726 if (
727 this.barcodePickerGui.videoElement.videoWidth > 2 &&
728 this.barcodePickerGui.videoElement.videoHeight > 2 &&
729 this.barcodePickerGui.videoElement.currentTime > 0
730 ) {
731 if (camera.deviceId !== "") {
732 this.updateActiveCameraCurrentResolution(camera);
733 }
734
735 return resolve();
736 }
737
738 const cameraMetadataCheckStartTime: number = performance.now();
739
740 window.clearInterval(this.cameraMetadataCheckInterval);
741 this.cameraMetadataCheckInterval = window.setInterval(() => {
742 // Detect weird browser behaviour that on unsupported resolution returns a 2x2 video instead
743 // Also detect failed camera access with no error but also no video stream provided
744 if (
745 this.barcodePickerGui.videoElement.videoWidth === 2 ||
746 this.barcodePickerGui.videoElement.videoHeight === 2 ||
747 this.barcodePickerGui.videoElement.currentTime === 0
748 ) {
749 if (
750 performance.now() - cameraMetadataCheckStartTime >
751 BarcodePickerCameraManager.cameraMetadataCheckTimeoutMs
752 ) {
753 window.clearInterval(this.cameraMetadataCheckInterval);
754 this.stopStream();
755
756 return reject(cameraNotReadableError);
757 }
758
759 return;
760 }
761
762 window.clearInterval(this.cameraMetadataCheckInterval);
763 if (camera.deviceId !== "") {
764 this.updateActiveCameraCurrentResolution(camera);
765 this.barcodePickerGui.videoElement.dispatchEvent(new Event("canplay"));
766 }
767
768 return resolve();
769 }, BarcodePickerCameraManager.cameraMetadataCheckIntervalMs);
770 };
771 }
772}