
Wie lassen sich beschädigte Waren direkt im Browser fotografieren, ohne zusätzliche Software? Der Beitrag zeigt eine praxisnahe Lösung mit Angular, Node.js und der Browser-Schnittstelle getUserMedia. Behandelt werden der Aufbau einer sauberen Navigation, die Implementierung einer Kamerakomponente zur Bildaufnahme sowie die korrekte Freigabe der Kamera über die ngOnDestroy-Methode. Das Szenario ist besonders relevant für Retourenmanagement, Schadenserfassung und Qualitätssicherung.
Ein Kunde äußerte den Wunsch, beschädigte Waren direkt über eine im Browser integrierte Webcam und ohne den Einsatz zusätzlicher Software fotografieren zu können. Dieses Szenario ist in zahlreichen geschäftlichen Kontexten von hoher Relevanz, beispielsweise im Bereich des Retourenmanagements, der Schadenserfassung oder der Qualitätssicherung.
Im vorliegenden Blog-Artikel wird eine exemplarische Lösung für dieses Anwendungsbeispiel vorgestellt. Mithilfe von Angular, Node.js und Visual Studio Code wird eine Webanwendung entwickelt, die es ermöglicht, über die Kamera des Endgeräts Bilder aufzunehmen und unmittelbar im Frontend anzuzeigen.
Bevor die Implementierung beginnt, wird eine passende Entwicklungsumgebung benötigt. Dafür müssen folgende Dinge gegeben sein:
Um die Anwendung zu erstellen, können über das Terminal die folgenden Befehle ausgeführt werden:
ng new angular-example-app --style=scss
cd angular-example-app
code .
Serverseitiges Rendering muss bei der Erstellung der App nicht ausgewählt werden.
Die Anwendung kann über den Befehl ng serve lokal gestartet und mit Strg + c gestoppt werden.
Um Angular-Komponenten anzulegen, werden die folgenden Befehle ausgeführt:
ng generate component navigation-bar --standalone
ng generate component image-manager
Für eine saubere Implementierung wird zuerst eine Navigation angelegt, über die im Anschluss zur Kamerakomponente navigiert werden kann.
Hierfür reicht eine Nav mit mehreren ul-Elementen:
<nav class="navbar">
<ul>
<li><a (click)="navigateTo('/')">Home</a></li>
<li><a (click)="navigateTo('/manager')">Image-Manager</a></li>
</ul>
</nav>
Der Nav wird eine navbar-Klasse gegeben, die anschließend im SCSS angepasst wird.
Für die Navbar soll sichergestellt werden, dass sie beim Scrollen oben bleibt und vor anderen Elementen angezeigt wird, die sich auf der gleichen Ebene befinden können.
.navbar {
background-color: #333; /* Dunkler Balken */
padding: 1em; /* Abstand für den Text */
width: 100%; /* Breite auf die gesamte Seite strecken */
position: fixed; /* Navbar bleibt oben, auch beim Scrollen */
top: 0; /* Ganz oben auf der Seite */
left: 0; /* Links ausgerichtet */
z-index: 1000; /* Navbar bleibt über anderen Elementen */
}
Die Elemente in der Liste wirken übersichtlicher, wenn sie ohne Punkte angezeigt werden.
.navbar ul {
list-style-type: none; /* Keine Punkte vor den Listenelementen */
margin: 0;
padding: 0;
display: flex; /* Elemente nebeneinander */
justify-content: center; /* Zentriere die Navigation in der Mitte des Balkens */
}
Danach wird ein Abstand zwischen den Links eingefügt:
.navbar li {
margin-right: 1.5em; /* Abstand zwischen den Links */
}
Damit der Text leichter erkennbar ist, wird er weiß dargestellt und in seiner Größe angepasst:
.navbar a {
color: white; /* Weißer Text */
text-decoration: none; /* Keine Unterstreichung */
font-size: 1.2em; /* Etwas größere Schrift */
padding: 0.5em;
}
Um erkenntlich zu machen, dass die Maus einen Link auswählt, wird ein Hover-Effekt hinzugefügt:
.navbar a:hover {
text-decoration: underline; /* Unterstreiche den Link beim Hover */
cursor:pointer;
}
Anschließend wird eine Methode zur Navigation hinzugefügt:
1import { Component } from '@angular/core';
2import { NavigationEnd, Router } from '@angular/router';
3
4@Component({
5 selector: 'app-navigation-bar',
6 standalone: true,
7 imports: [],
8 templateUrl: './navigation-bar.component.html',
9 styleUrl: './navigation-bar.component.scss'
10})
11
12
13export class NavigationBarComponent {
14 constructor(private router: Router) {
15
16 }
17
18 public navigateTo(route: string): void {
19 let self = this;
20 self.router.navigate([route]); // Programmatic navigation
21
22 }
23}
Für das HTML wird ein Container erstellt, der auf der einen Seite die Kamera durch ein videoElement und das Bild durch ein canvasElement darstellt und auf der anderen Seite eine Liste der aufgenommenen Bilder anzeigt.
1<div class="container">
2 <div class="webcam-container">
3 <button (click)="startWebcam()">Open Webcam</button>
4
5 <video #videoElement width="320" height="240" autoplay></video>
6 <canvas #canvasElement width="320" height="240"></canvas>
7
8 <!-- Button to capture an image -->
9 <button (click)="takeImage()">Take Image</button>
10 </div>
11
12 <!-- New container for displaying captured images -->
13
14 <div class="image-list-container">
15 <h3>Captured Images</h3>
16 <div *ngIf="capturedImages.length === 0">No images captured yet.</div>
17 <div class="image-list">
18 <img *ngFor="let img of capturedImages" [src]="img" class="captured-image"/>
19 </div>
20 </div>
21 </div>
Damit die Elemente geordneter aussehen, wird eine SCSS-Datei hinzugefügt.
1.container {
2 margin-top:80px;
3 display: flex;
4 flex-direction: row;
5 justify-content: space-between;
6 align-items: flex-start;
7 }
8
9 .webcam-container {
10 display: flex;
11 flex-direction: column;
12 align-items: center;
13 margin-right: 20px;
14 }
15
16 video, canvas {
17 border: 2px solid #333;
18 margin: 10px;
19 }
20
21 button {
22 margin-bottom: 10px;
23 padding: 10px 20px;
24 font-size: 16px;
25 cursor: pointer;
26 }
27
28 .image-list-container {
29 display: flex;
30 flex-direction: column;
31 align-items: center;
32 width: 300px;
33 }
34
35 .image-list {
36 display: flex;
37 flex-direction: column;
38 align-items: center;
39 width: 100%;
40 }
41
42 .captured-image {
43 width: 100%;
44 height: auto;
45 border: 1px solid #333;
46 margin: 10px 0;
47 }
In der TypeScript-Datei wird eine Methode erstellt, um die Kamera zu starten. Durch navigator.mediaDevices.getUserMedia() wird die Berechtigung für die Webcam angefragt, um sie danach zu verwenden.
In der takeImage()-Methode wird das aktuelle Bild der Kamera aufgenommen und anschließend der Liste mit Bildern hinzugefügt.
Die ngOnDestroy()-Methode wird ausgeführt, wenn die Komponente nicht mehr gebraucht (zerstört) wird, z. B. wenn der Tab mit der Kamerakomponente geschlossen wird. Dort wird sichergestellt, dass die Kamera beendet wird, da sie nicht mehr benötigt wird.
1import { Component, ViewChild, ElementRef, OnDestroy } from '@angular/core';
2import { CommonModule } from '@angular/common';
3
4@Component({
5 selector: 'app-image-manager',
6 standalone: true,
7 imports: [CommonModule],
8 templateUrl: './image-manager.component.html',
9 styleUrl: './image-manager.component.scss'
10})
11
12export class ImageManagerComponent implements OnDestroy{
13
14 @ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;
15 @ViewChild('canvasElement') canvasElement!: ElementRef<HTMLCanvasElement>;
16
17 stream: MediaStream | null = null;
18 capturedImages: string[] = []; // Array to store captured image URLs
19
20
21startWebcam(): void {
22 if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
23 navigator.mediaDevices.getUserMedia({ video: true })
24 .then((stream) => {
25 this.stream = stream;
26 this.videoElement.nativeElement.srcObject = stream;
27 })
28
29 .catch((error) => {
30 console.error('Error accessing the webcam:', error);
31 });
32
33 } else {
34 alert('Webcam not supported in this browser.');
35 }
36 }
37
38 takeImage(): void {
39 const canvas = this.canvasElement.nativeElement;
40 const video = this.videoElement.nativeElement;
41 const context = canvas.getContext('2d');
42
43 if (context) {
44 context.drawImage(video, 0, 0, canvas.width, canvas.height);
45 const imageUrl = canvas.toDataURL('image/png');
46 this.capturedImages.push(imageUrl);
47 }
48 }
49
50 ngOnDestroy(): void {
51 if (this.stream) {
52 this.stream.getTracks().forEach(track => track.stop());
53 }
54 }
55}
Damit die Kamerakomponente aufgerufen werden kann und die Navigation funktioniert, muss die AppComponent angepasst werden.
Hier wird das aktuelle HTML gegen Folgendes ausgetauscht:
<app-navigation-bar></app-navigation-bar>
<router-outlet></router-outlet>
Hier werden Referenzen zu den benötigten Komponenten hinzugefügt:
import { NavigationEnd, Router, RouterOutlet } from '@angular/router';
import { NavigationBarComponent } from './navigation-bar/navigation-bar.component';
Danach muss Folgendes im Konstruktor stehen:
1constructor(private router: Router) {
2 this.router.events.subscribe(event => {
3 if (event instanceof NavigationEnd) {
4 console.log('Navigated to:', event.urlAfterRedirects);
5 }
6 });
7 }
In app.routes.ts werden nun die Routen konfiguriert:
1import { Routes } from '@angular/router';
2import { AppComponent } from './app.component';
3import { ImageManagerComponent } from './image-manager/image-manager.component';
4export const routes: Routes = [
5 {path:'', component: AppComponent},
6 {path:'manager', component: ImageManagerComponent}
7];
Alternativ kann eine Home-Komponente erstellt werden, die als Startseite aufgerufen und anstelle der AppComponent eingebunden wird. Die vorgestellte Konfiguration ist jedoch ebenfalls funktionsfähig.


Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.
Praxiswissen, Tipps und Neuigkeiten für den Mittelstand: kompakt und ohne Spam. Abmelden jederzeit möglich.