Custom camera screen section using Capacitorjs and Angular
While I was working on a mobile cross platform application using Capacitorjs and Angular, I came across one of the UI requirement which involved displaying the feedback gotten from the mobile camera into a custom div section.
This was quite a challenge since the plugin I found; @capacitor-community/camera-preview took the entire screen. To make matters worse, I had to preview the camera on a custom section and perform QR scanning. @capacitor-mlkit/barcode-scanning was able to call the Camera and perform scanning, however, it took the whole screen which was not was I was looking for. QR scanning will be discussed later.
In this medium, I will discuss how I was able to accomplish the task of using a custom div section to display the camera using capacitorjs and angular.
Step 1: Create New Angular project
ng new custom-camera
If you have not installed Angular cli, you can follow the step by step guide from here.
Step 1.5: Add Tailwind (Optional)
This is optional but it helps a lot in utilizing some common CSS structures for different screen sizes. A step by step guide can be found here.
Step 2: Integrate Capacitor
You can get the step by step guide for adding Capacitor to the Angular Application here (For angular follow the “Add Capacitor to your web app”).
Step 3: Ensure Android and IOS permission for access to the mobile camera
For Android in the AndroidManifest.xml file
<uses-permission android:name=”android.permission.CAMERA” />
For IOS in the info.plist (preferably from XCode)
<key>NSCameraUsageDescription</key>
<string>To take amazing pictures</string>
Step 4: Update capacitor.json to allow the use of the JavaScript function navigator.mediaDevices.getUserMedia
to access the camera on both Android and IOS.
In your capacitor.json file, add this line of code.
import type { CapacitorConfig } from '@capacitor/cli';
const config: CapacitorConfig = {
appId: 'com.cameracu.online',
appName: 'camera',
webDir: 'dist',
server: {
hostname: 'localhost',
iosScheme: 'https',
androidScheme: 'https'
},
};
export default config;
This is because navigator.mediaDevices.getUserMedia
requires some security measures. You can read more about that from the documentation.
Step 5: Create a service cameraPreviewService to store the logic for accessing the camera.
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class CameraPreviewService {
private _stream = new Subject<MediaStream | null>();
public stream = this._stream.asObservable();
private internalStream: MediaStream | null = null;
constructor() { }
openCamera() {
navigator.mediaDevices.getUserMedia({
video: {
facingMode: 'environment'
}
})
.then(stream => {
this._stream.next(stream);
this.internalStream = stream;
}).catch(error => {
this._stream.next(null);
this.internalStream = null;
// Display error if need be
});
}
closeCamera() {
if (!this.internalStream) { return; }
this.internalStream.getVideoTracks().forEach(x => x.stop());
this.internalStream = null;
this._stream.next(null);
}
}
The heart of this project is the use ofnavigator.mediaDevices.getUserMedia
which opens the camera and returns a video stream. Without this, the whole process will not be possible.
Here, we start by using a Subject (_stream
and stream
) to store the video stream in order to share with components that require video stream. You can also use Signals here.
internalStream
is used within the service to confirm the state of the video stream is active or not and also helps in closing the camera.
Finally, we have the openCamera
method which start the camera and the closeCamera
method which stops the camera. You can also add a logic to check if camera is supported before calling the openCamera
, I did not do that in the project.
Step 6: Update the app.component.ts file
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';
@Component({
selector: 'app-root',
standalone: true,
imports: [RouterOutlet, NgIf, NgClass],
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
providers: [
CameraPreviewService
]
})
export class AppComponent implements OnInit, OnDestroy {
videoElement = viewChild<ElementRef>('videoElement');
videoStream: MediaStream | null = null;
videoSubscription?: Subscription;
constructor(
private cameraPreviewService: CameraPreviewService
) {}
ngOnInit(): void {
this.listenForChanges();
}
openCamera(): void {
this.cameraPreviewService.openCamera();
}
closeCamera(): void {
this.cameraPreviewService.closeCamera();
}
private listenForChanges(): void {
this.videoSubscription = this.cameraPreviewService.stream.subscribe({
next: (stream) => {
this.videoStream = stream;
this.displayCamera();
}
});
}
private displayCamera() {
const ele = this.videoElement()?.nativeElement as HTMLVideoElement;
if (!this.videoStream || !ele) {
this.closeCamera();
return;
}
ele.srcObject = this.videoStream;
ele.onloadedmetadata = () => {
ele.play();
}
}
ngOnDestroy(): void {
this.videoSubscription?.unsubscribe();
}
}
There are a lot of ways to implement this and I encourage you to use what is convenient. The basic gist of the code above is I am listening for video streams (be it null or an actual video stream object). If I get a stream, I display it else I close the camera.
Step 7: Update the app.component.html file
<section class="w-screen h-screen flex justify-center items-center bg-gray-200">
<div class="flex flex-col">
<!-- This will hold the custom camera view -->
<div class="w-52 h-52 bg-gray-600 relative">
<video [ngClass]="{
'invisible': !videoStream
}" #videoElement mute class="absolute w-full h-full"></video>
</div>
<!-- This will control the camera -->
<div>
@if (videoStream) {
<button (click)="closeCamera()" class="bg-red-600 w-full py-4 text-center text-white">Close camera</button>
} @else {
<button (click)="openCamera()" class="bg-blue-600 w-full py-4 text-center text-white">Open camera</button>
}
</div>
</div>
</section>
Lastly, we update the html to have and display our camera via the video stream on our custom section which is the video element. With this we can customise the video element however we see fit.
The complete code can be found here.
Thanks and happy coding.
PS: this is my first.