QR Scanner on a custom camera screen using Capacitorjs and Angular

Ariweriokuma Excellence Pere
3 min readMay 24, 2024

--

Photo by Hillary Black on Unsplash

In one of my mediums, I discussed the implementation of custom camera screen section on your Capacitorjs and Angular application. You can find that here.

In this medium, I will discuss how to integrate QR code scanner on the custom camera screen. Because I am using a custom camera screen section, @capacitor-mlkit/barcode-scanning will be difficult to implement because it takes up the whole screen. I believe you can use the readBarcodesFromImage(...)from @capacitor-mlkit/barcode-scanning which I encourage you to try, but for this I will be using a different approach.

I will be using a library called jsQr and because I am focused on only QR code, this is a bit streamlined to my goal. I will be attaching this code to the previous project which can be found here.

  1. Install jsQR npm install jsqr --save

2. Create a QR scanner service responsible for scanning images

import { Injectable } from '@angular/core';
import jsQR from 'jsqr';
import { Subject } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class BarcodeService {
private _qrValue = new Subject<string | null>();
public qrValue = this._qrValue.asObservable();

constructor() { }

captureFrame(videoElement: HTMLVideoElement): void {
try {
// Create a canvas to draw image
const canvas = document.createElement("canvas");
const context: CanvasRenderingContext2D = canvas.getContext("2d") || {} as CanvasRenderingContext2D ;

// Start drawing the image
canvas.width = videoElement.videoWidth;
canvas.height = videoElement.videoHeight;
context?.drawImage(videoElement, 0, 0);
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);

// scan the image created
const code = jsQR(imageData.data, imageData.width, imageData.height);

if (code) {
console.log("Decoded QR code:", code.data);
this._qrValue.next(code.data);
// Stop capturing frames (implementation omitted for brevity)
}

} catch (error) {

}
}
}

The main purpose of this service is to take in a video html element and scan a single frame. If what is scanned is a qrcode and it is a success, then a notification is sent to any listener via the observable (using a signal will be better here).

3. Integrate into the component.ts file

I updated the file by adding the following


import { NgClass, NgIf } from '@angular/common';
import { Component, ElementRef, OnDestroy, OnInit, viewChild } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { CameraPreviewService } from './service/camera-preview.service';
import { Subscription } from 'rxjs';
import { BarcodeService } from './service/barcode.service';

@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgIf, NgClass],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [
CameraPreviewService,
BarcodeService,
]
})
export class AppComponent implements OnInit, OnDestroy {

videoElement = viewChild<ElementRef>('videoElement');

videoStream: MediaStream | null = null;

videoSubscription?: Subscription;
qrCodeSubscription?: Subscription;

timeInterval: any = null;

constructor(
private cameraPreviewService: CameraPreviewService,
private barcodeService: BarcodeService
) {}

ngOnInit(): void {
this.listenForChanges();
}

openCamera(): void {
this.cameraPreviewService.openCamera();
}

closeCamera(): void {
this.cameraPreviewService.closeCamera();
this.stopPeriodicalScan();
}

private listenForChanges(): void {
this.videoSubscription = this.cameraPreviewService.stream.subscribe({
next: (stream) => {
this.videoStream = stream;
this.displayCamera();
}
});

this.qrCodeSubscription = this.barcodeService.qrValue.subscribe({
next: (code) => {
alert(code);
this.closeCamera();
}
})
}

private displayCamera() {
const ele = this.videoElement()?.nativeElement as HTMLVideoElement;

if (!this.videoStream || !ele) {
this.closeCamera();
return;
}

ele.srcObject = this.videoStream;
ele.onloadedmetadata = () => {
ele.play();
}

this.beginPeriodicalScan(ele);
}

private beginPeriodicalScan(ele: HTMLVideoElement): void {
ele.addEventListener('pause', () => ele.play());
this.timeInterval = setInterval(() => {
this.barcodeService.captureFrame(ele);
}, 1000)
}

private stopPeriodicalScan(): void {
if (this.timeInterval) { clearInterval(this.timeInterval); }

const ele = this.videoElement()?.nativeElement as HTMLVideoElement;
if (ele) {
ele.removeEventListener('pause', () => {});
}
}

ngOnDestroy(): void {
this.videoSubscription?.unsubscribe();
this.qrCodeSubscription?.unsubscribe();
this.closeCamera();
}
}

Several updates can be seen from the code above.

In summary, we running the settimeinterval JS function to get video element and scan for qr code. If there is any success, then the camera closes including the settimeinterval.

The final code can be founc here.

--

--