1 | import { BarcodePickerGui } from "./barcodePickerGui";
|
2 | import { Camera } from "./camera";
|
3 | import { CameraAccess } from "./cameraAccess";
|
4 | import { CameraManager } from "./cameraManager";
|
5 | import { CameraSettings } from "./cameraSettings";
|
6 | import { CustomError } from "./customError";
|
7 |
|
8 |
|
9 |
|
10 |
|
11 | export enum MeteringMode {
|
12 | CONTINUOUS = "continuous",
|
13 | MANUAL = "manual",
|
14 | NONE = "none",
|
15 | SINGLE_SHOT = "single-shot"
|
16 | }
|
17 |
|
18 |
|
19 |
|
20 |
|
21 | export 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 |
|
33 |
|
34 | export interface ExtendedMediaTrackConstraintSet extends MediaTrackConstraintSet {
|
35 | torch?: boolean;
|
36 | zoom?: number;
|
37 | }
|
38 |
|
39 |
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | export 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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
491 | if (this.mediaStream != null) {
|
492 | const videoTracks: MediaStreamTrack[] = this.mediaStream.getVideoTracks();
|
493 |
|
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 |
|
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 | () => {
|
523 |
|
524 | }
|
525 | );
|
526 | }
|
527 |
|
528 | private triggerFocusMode(focusMode: MeteringMode): Promise<void> {
|
529 |
|
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 |
|
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 |
|
613 | if (error.name === "SourceUnavailableError") {
|
614 | error.name = "NotReadableError";
|
615 | }
|
616 | if (
|
617 | error.message === "Invalid constraint" ||
|
618 |
|
619 | (error.name === "OverconstrainedError" && (<any>error).constraint === "deviceId")
|
620 | ) {
|
621 |
|
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 |
|
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 |
|
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 |
|
683 |
|
684 | track.addEventListener("unmute", this.videoTrackUnmuteListener);
|
685 | });
|
686 |
|
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 |
|
725 |
|
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 |
|
743 |
|
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 | }
|