zxing-js / ngx-scanner

Angular QR code, Barcode, DataMatrix, scanner component using ZXing.
https://zxing-js.github.io/ngx-scanner/
MIT License
642 stars 228 forks source link

Scanner failing to restart #397

Open sGerli opened 3 years ago

sGerli commented 3 years ago

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):

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

wall-street-dev commented 3 years ago

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

sergey-dudik commented 3 years ago

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

werthdavid commented 3 years ago

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!

sGerli commented 3 years ago

On my app I opted to keep the scanner hot all the time (I just hide it and disable the scan results when not needed), that reduces the error probability to initial load. Although it's not a fix that workaround has worked ok in my use case.

hhmx commented 3 years ago

Hi Guys,

I don't know if I have the same problem right away, but my app only works with version 3.0.0. In my app there is a drop-down list where you can choose a camera. After I installed version 3.2.0 and when I choose a camera, I get the following error message:

scan

Is that really a bug?

Does anyone have the same problem?

Thanks in advance!

siir commented 3 years ago

If it helps, I have been able to pinpoint this happens when the camera is what I am calling "loading." For me, the error has always meant that the camera device is in use.

Once the scanner gets an enabled=true, there is some delay before the video feed is actually visible. If the component is destroyed (navigation change, enabled=false, etc) during this period, I get what I am now calling a "ghost camera" because the video feed from the camera is still in use. Sadly, there is nothing for me to grab and tell it to stop, even though it is within my app (through the zxing scanner component).

What I have done as a workaround (I have an angular app) is I have created a service that has a scanner status. It tracks when the camera is starting (when passing true to enabled property) and when its actually started. I bind to the scanner's camerasFound event to know when the video feed is live. I then use this service in my app to disable buttons and actions that would cause the component to be destroyed while the camera is starting.

Not ideal, but it has allowed me to prevent my user base from getting the "ghost camera" event and having to refresh my app constantly. Maybe this information can also help in figuring out what is going on as it took a bit of trial and error to figure out how and where I was constantly getting this error.

dannycandra commented 3 years ago

I think setting [enable] to false on OnDestroy event fix this problem on Version 3.1.3

Could someone confirm this? I no longer have this problem.

Code example

template <zxing-scanner [enable]=scannerEnable [device]="currentDevice" (camerasFound)="camerasFoundHandler($event)" (scanSuccess)=onScanSuccess($event)></zxing-scanner>

ts ngOnDestroy(): void { this.scannerEnable = false; }

darkedges commented 3 years ago

I think setting [enable] to false on OnDestroy event fix this problem on Version 3.1.3

This does not work for me (Samsung A12, Angular 11.2.14) on version

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

When I switch between pages I am seeing

It was not possible to play the video. DOMException: The play() request was interrupted by a new load request. 
Trying to play video that is already playing.
Unhandled Promise rejection: The associated Track is in an invalid state ; Zone: <root> ; Task: Promise.then ; Value: DOMException: The associated Track is in an invalid state undefined
@zxing/ngx-scanner Error when asking for permission. DOMException: Could not start video source
@zxing/ngx-scanner Couldn't read the device(s)'s stream, it's probably in use by another app.
@zxing/ngx-scanner Error when asking for permission. DOMException: Could not start video source
@zxing/ngx-scanner Couldn't read the device(s)'s stream, it's probably in use by another app.
darkedges commented 3 years ago

Upgrading @zxing/library to ^0.18.6 i.e

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

resolve my issue. I can move between components without any errors.

sGerli commented 3 years ago

I've tried all those workarounds and they've helped a bit but I still get many errors.

NotReadableError: Could not start video source

Uncaught (in promise): Error: No scanning is running at the time. Error: No scanning is running at the time.

Cannot read properties of undefined (reading 'stop')

What I did since this issue came to my attention is to keep the scanner component always open in the background, because if it gets closed the user has to reload the browser every time.

dannycandra commented 3 years ago

The issue is still occurring on my side. Seems like the camera is still being used. This only happen when I rapidly switch pages.

I checked the source a bit and I wonder could it be caused by async init() at ngOnInit() ?

Maybe the tracks didnt get stopped properly in this case.

Could someone enlighten us on this?

shreya-sawardekar commented 3 years ago

@siir could you please share code snippet of your workaround for the solution?

siir commented 3 years ago

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
  }
}
siir commented 3 years ago

I would like to add that after upgrade to ngx-scanner 3.2 and ng 11 (from 3.0 and 10.2 respectively), i am having issues where the camera feed not starting. There appears to be a promise rejection error inside ngx-scanner when navigating away from a component with the scanner in it. Something about the track is in an invalid state. More soon.

shreya-sawardekar commented 3 years ago

@siir Thank you for the detailed explanation.

I had one more query with respect to scanning barcodes. Have you tried scanning data matrix codes? I am trying the same and unable to scan it.

If I take a pic of the data matrix code, zoom-in and scan then the scanner is able to scan successfully.

Attached data matrix code I am trying to scan.IMG_20211108_164402.jpg

siir commented 3 years ago

That looks pretty small. I would wager to guess that its too small for the camera to focus in properly and the barcode library is not detecting anything to scan. Can you try a larger version just to make sure?

I test with this site: Tec-it Data Matrix Generator

shreya-sawardekar commented 3 years ago

@siir I generated the same data matrix code with Tec-it Data Matrix Generator and I am able to scan it.

However, my application needs to scan data matrix of the size I shared. Is there any way this could be achieved?

sGerli commented 3 years ago

I think that we're going out of topic with the last comments on this issue

shreya-sawardekar commented 3 years ago

@sGerli Yes. I will create/discuss it in a separate relevant issue.