ngx-scanner: Scanner failing to restart

Describe the bug After successfully scanning, navigating away and then back to scanner page it fails to open on some devices. It throws the error NotReadableError NotReadableError: Could not start video source.

And then

Error: Uncaught (in promise): Error: No scanning is running at the time.
Error: No scanning is running at the time.
    at O.getScannerControls (https://cenyth.events/12-es2015.2dbdf0a52407038865e6.js:1:665378)
    at t.scanStop (https://cenyth.events/12-es...

The only way to make it work again is to reload the page through the browser.

Expected behavior Scanner should restart successfully after reopening the scanner page and not require a refresh.

Smartphone (please complete the following information):

  • Device: Amazon Kindle
  • OS: Android 5.1.1
  • Browser: Amazon Silk 8.9.27
  • Version v3.1.3

Additional context Angular: 11.2.11 @zxing/browser: 0.0.7 @zxing/library: ^0.18.3 @zxing/ngx-scanner: ^3.1.3

As far as I know this didn’t happen a few versions ago with Angular 10

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 2
  • Comments: 19 (1 by maintainers)

Most upvoted comments

So I was hoping 3.2.0 would fix the issue which is obviously not the case… Thanks for the information, I’ll see what I can do… PRs much appreciated!

Happening on my end as well. There’s a PR already in place, any chance to get a new build anytime soon?

I wrote a wrapper component to hold the zxing scanner and related services. It makes it possible to hook into the zxing scanner consistently, and I do this for most npm packages I’m using so I can change out packages by changing this one component instead of having to change everything that consumes the 3rd party package directly.

I’ve tried to trim this down as much as possible to give the relevant parts. This will feel like a lot (perhaps it is) to just have a way to cancel navigation while the camera is between the starting up step (initial load/enable=true) and started step (camerasFound event fires), but its what worked for me. Feel free to simplify or improve and share back.

The key is using the scanner service in your child/individual components that can navigate the router to a new route while the camera is starting. The example here, some-other-component-consuming-scanner.component has both the scanner and an a tag, but you can have navigation points in other components depending on how specialized your app components get.

scanner-camera.component.html

<zxing-scanner #zxing 
  *ngIf="scannerEnabled"
  [autofocusEnabled]="true"
  [autostart]="true"
  [enable]="true"
  (camerasFound)="camerasFound($event)"
  (camerasNotFound)="camerasNotFound($event)"
  (scanSuccess)="onscan($event)"
  (scanError)="onerror($event,'e')"
></zxing-scanner>

scanner-camera.component.ts

import { Component, OnInit, Output, ViewChild, EventEmitter, Input, OnDestroy } from '@angular/core';
import { ZXingScannerComponent } from '@zxing/ngx-scanner';
import { Subject, BehaviorSubject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { ScannerService } from './scanner.service';

export enum ScannerStatus {
  STARTING = 'STARTING',
  STARTED = 'STARTED',
  STOPPED = 'STOPPED',
  ERROR = 'ERROR',
  ERROR_INUSE = 'ERROR_INUSE'
}

@Component({
  selector: 'app-scanner-camera',
  templateUrl: './scanner-camera.component.html',
  styleUrls: ['./scanner-camera.component.css']
})
export class ScannerCameraComponent implements OnInit, OnDestroy {
  private componentDestroyed = new Subject<boolean>();

  @Input() format: string | string[];
  @Output() scanned: EventEmitter<string> = new EventEmitter();
  @Output() scannerStatus: EventEmitter<ScannerStatus> = new EventEmitter();

  // local scanner status subject for tracking what condition the camera is in
  private _scannerStatusSubject = new BehaviorSubject<ScannerStatus>(undefined);
  private get scannerStatus$() { return this._scannerStatusSubject.asObservable(); }

  @ViewChild('zxing') zxing: ZXingScannerComponent;

  scannerEnabled = false;
  errorMessage: string;

  private _scannerStartUpError: boolean;
  get scannerStartUpError() { return this._scannerStartUpError; }

  // used to know if there is a scanner camera starting somewhere else in the application
  get scannerStarting(): boolean { return this.scannerService.scannerStarting; }

  // to control (i.e. pause/play) the video feed of the camera
  private get _video() { return this.zxing?.previewElemRef?.nativeElement; }

  constructor(
    private scannerService: ScannerService
  ) { }

  ngOnInit() {
    // subscribe to local scanner status to pass new status to service
    this.scannerStatus$.pipe(takeUntil(this.componentDestroyed))
      .subscribe(s => setTimeout(() => { this.scannerService.scannerStatus = s; }));

    // hook into scanner actions from the service
    this.scannerService.scannerActions$.pipe(takeUntil(this.componentDestroyed))
      .subscribe(s => {
        switch (s) {
          case 'pause': this.pause(); break;
          case 'play': this.resume(); break;
        }
      });

    // Check for existence of a camera/ask permissions on first time then "start" camera
    this.checkForCameras();
  }

  ngOnDestroy() {
    this.scannerStatusEnded();
    this.componentDestroyed.next(true);
    this.componentDestroyed.unsubscribe();
  }

  // Check for existence of a camera/ask permissions on first time then "start" camera
  private checkForCameras(): void {
    if (!navigator.mediaDevices) {
      this.scannerStatusError('No Cameras were found / MediaDevices not defined', {startUpError: true});
    }
    if (!navigator.mediaDevices.enumerateDevices) {
      this.scannerStatusError('No Cameras were found / EnumerateDevices not defined', {startUpError: true});
    }
    navigator.mediaDevices.enumerateDevices().then(r => {
      if (r.filter(d => d.kind === 'videoinput')) {
        this.scannerStatusStarting(); // set status to starting
      } else {
        this.scannerStatusError('No Cameras were found', {startUpError: true});
      }
    }).catch(error => this.scannerStatusError(error, {startUpError: true}));
  }

  //#region - status setting functions
  private scannerStatusStarting(): void {
    this.scannerEnabled = true;
    this._scannerStatusSubject.next(ScannerStatus.STARTING);
  }
  private scannerStatusStarted(): void {
    this.scannerEnabled = true;
    this._scannerStatusSubject.next(ScannerStatus.STARTED);
  }
  private scannerStatusStopped(): void {
    this.scannerEnabled = false;
    this._scannerStatusSubject.next(ScannerStatus.STOPPED);
  }
  private scannerStatusEnded(): void {
    this._scannerStatusSubject.next(undefined);
  }
  private scannerStatusError(errorMessage, opts?: {inUse?: boolean, startUpError?: boolean}): void {
    if (!!errorMessage) {
      this.errorMessage = errorMessage;
      this._scannerStartUpError = opts?.startUpError;
      this._scannerStatusSubject.next(opts?.inUse ? ScannerStatus.ERROR_INUSE : ScannerStatus.ERROR);
    } else {
      this.errorMessage = undefined;
    }
  }
  //#endregion

  //#region - scanner event hooks
  public onscan(scanResult: string): void {
    this.scanned.emit(scanResult);
  }

  public onstart(): void {
    this.scannerStatusStarted();
  }

  public onerror(e: Error, s): void {
    if (!!e && e.name !== 'NotFoundException') {
      this.scannerStatusError(e.message);
    }
  }

  public camerasFound(c): void {
    setTimeout(() => this.onstart(), 500);
  }

  public camerasNotFound($event) {
    // this fires when the camera is in use by a previous scanner in "ghost" mode when its destroyed before starting
    if ($event instanceof DOMException) {
      const e: DOMException = $event;

      let errorMessage = `${e.message}.`;
      if (e.name === 'NotReadableError') {
        errorMessage += ` Something else is likely using the camera or there is a ghost camera incident. You can try refreshing the app to reset this app's ghost camera usage.`;
      }
      this.scannerStatusError(errorMessage, {inUse: true, startUpError: true});
    } else {
      this.scannerStatusError($event, {startUpError: true});
    }
  }
  //#endregion

  //#region - scanner/camera controls
  private pause(): void {
    if (this.scannerStarting) {
      setTimeout(() => this.pause(), 100);
      return;
    }
    this._video?.pause();
  }

  private resume(): void {
    this._video?.play();
  }

  public reset(): void {
    this.scannerEnabled = false;
    this.errorMessage = '';
    this.scannerStatusStarting();

    // start video if its in dom but still paused
    const video = this._video;
    if (video && !(video.currentTime > 0 && !video.paused && !video.ended && video.readyState > 2)) {
      video.play();
    }
  }
  //#endregion
}

scanner-service.ts

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';
import { ScannerStatus } from './scanner-status.enum';

export type ScannerActions = 'pause' | 'play';
@Injectable({
  providedIn: 'root'
})
export class ScannerService {
  //#region - Scanner Status
    private _scannerStatusSubject = new BehaviorSubject<ScannerStatus>(undefined);
    get scannerStatus$(): Observable<ScannerStatus> {
      return this._scannerStatusSubject.asObservable();
    }
    set scannerStatus(val: ScannerStatus) {
      this._scannerStatusSubject.next(val);
    }
    get scannerStatus(): ScannerStatus {
      return this._scannerStatusSubject.value;
    }
    get scannerStarting(): boolean {
      return this.scannerStatus === ScannerStatus.STARTING;
    }
    get scannerExists(): boolean {
      return !(this.scannerStatus === undefined);
    }
  //#endregion
  private _scannerActions = new BehaviorSubject<ScannerActions>(undefined);
  get scannerActions$() { return this._scannerActions.asObservable(); }

  constructor() { }

  pause(): void {
    this._scannerActions.next('pause');
  }
  play(): void {
    this._scannerActions.next('play');
  }
  destroyed(): void {
    this._scannerActions.next(undefined);
    this._scannerStatusSubject.next(undefined);
  }
}

some-other-component-consuming-scanner.component.html

<app-scanner (scanned)="scanned($event)"></app-scanner>

<a mat-fab color="primary" class="floating-mat-fab" [disabled]="scannerStarting" (click)="scannerStarting ? undefined : stopComplete()">
  <mat-icon>done</mat-icon>
</a>

some-other-component-consuming-scanner.component.ts

@Component({
  selector: 'some-other-component-consuming-scanner',
  templateUrl: './some-other-component-consuming-scanner.component.html',
  styleUrls: ['./some-other-component-consuming-scanner.component.css']
})
export class SomeOtherComponentConsumingScanner implements OnInit, OnDestroy {
  private componentDestroyed: Subject<boolean> = new Subject<boolean>();

  private _scannerStarting: boolean;
  get scannerStarting(): boolean { return this._scannerStarting; }
  get showScanner(): boolean { return !!this.validRacks; }

  constructor(
    private scannerService: ScannerService,
  ) { }

  ngOnInit(): void {
    this.scannerService.scannerStatus$.pipe(takeUntil(this.componentDestroyed))
      .subscribe(s => this._scannerStarting = this.scannerService.scannerStarting);
  }

  ngOnDestroy() {
    this.componentDestroyed.next(true);
    this.componentDestroyed.unsubscribe();
  }

  scanned(result) {
    // do something with result
  }

  navigate() {
    if (this.scannerStarting) { return; }
    // navigate if not starting
  }
}

I have the same problem on Android. Is there any plans for the fix?

"@zxing/browser": "0.0.9"
"@zxing/library": "^0.18.6"
"@zxing/ngx-scanner": "3.2.0"

Reload helps.

<zxing-scanner [enable]="enable" [autostart]="true" [timeBetweenScans]="2000" [delayBetweenScanSuccess]="2000" (scanSuccess)="onScanSuccess($event)" (permissionResponse)="onPermissionResponse($event)" (hasDevices)="onHasDevices($event)" [formats]="['QR_CODE']"> </zxing-scanner>

image