import { afterNextRender, ChangeDetectionStrategy, Component, DestroyRef, ElementRef, inject, signal, viewChild, } from '@angular/core'; import { AfricaniesIconComponent } from '@africanies/africanies-icons'; import { ButtonComponent } from '../../button/button.component'; import { AfricaniesOverlayRef } from '../../overlay/africanies-overlay-ref'; type CameraFacing = 'environment' | 'user'; /** * Live webcam capture dialog for {@link FileUploadComponent}. * * Uses `navigator.mediaDevices.getUserMedia` so desktop playgrounds and * HTTPS apps can take a photo without relying on the mobile-only * `` attribute. Closes with a JPEG {@link File}, or * `undefined` when dismissed. * * Prefer opening via {@link ModalService} so the stream is owned by this * dialog and torn down on close / destroy. */ @Component({ selector: 'africanies-camera-capture-dialog', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, imports: [AfricaniesIconComponent, ButtonComponent], template: ` `, }) /** * Live webcam capture dialog for {@link FileUploadComponent}. */ export class CameraCaptureDialogComponent { private readonly destroyRef = inject(DestroyRef); private readonly ref = inject(AfricaniesOverlayRef); private readonly preview = viewChild>('preview'); private readonly snapshot = viewChild>('snapshot'); protected readonly titleId = `africanies-camera-title-${Math.random().toString(36).slice(2, 10)}`; protected readonly hintId = `${this.titleId}-hint`; protected readonly errorId = `${this.titleId}-error`; protected readonly starting = signal(true); protected readonly capturing = signal(false); protected readonly streamActive = signal(false); protected readonly error = signal(null); private stream: MediaStream | null = null; private facing: CameraFacing = 'environment'; constructor() { this.destroyRef.onDestroy(() => this.stopStream()); afterNextRender(() => { void this.startStream(); }); } /** Dismiss without a file. */ protected cancel(): void { this.stopStream(); this.ref.close(); } /** Flip between rear and front cameras when the device exposes both. */ protected switchFacing(): void { this.facing = this.facing === 'environment' ? 'user' : 'environment'; void this.startStream(); } /** Freeze the current frame into a JPEG {@link File} and close. */ protected capture(): void { const video = this.preview()?.nativeElement; const canvas = this.snapshot()?.nativeElement; if (!video || !canvas || !this.streamActive() || this.capturing()) { return; } const width = video.videoWidth; const height = video.videoHeight; if (!width || !height) { this.error.set('Camera is not ready yet. Try again in a moment.'); return; } this.capturing.set(true); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); if (!ctx) { this.capturing.set(false); this.error.set('Could not capture this frame. Try another browser.'); return; } ctx.drawImage(video, 0, 0, width, height); canvas.toBlob( (blob) => { this.capturing.set(false); if (!blob) { this.error.set('Could not create the photo. Try again.'); return; } const file = new File([blob], `camera-${Date.now()}.jpg`, { type: 'image/jpeg', lastModified: Date.now(), }); this.stopStream(); this.ref.close(file); }, 'image/jpeg', 0.92, ); } private async startStream(): Promise { this.starting.set(true); this.error.set(null); this.streamActive.set(false); this.stopStream(); if ( typeof navigator === 'undefined' || !navigator.mediaDevices?.getUserMedia ) { this.starting.set(false); this.error.set( 'Camera capture is not available in this browser. Use Choose files instead.', ); return; } try { this.stream = await navigator.mediaDevices.getUserMedia({ audio: false, video: { facingMode: { ideal: this.facing }, width: { ideal: 1280 }, height: { ideal: 720 }, }, }); const video = this.preview()?.nativeElement; if (!video) { this.stopStream(); this.starting.set(false); this.error.set('Camera preview failed to start.'); return; } video.srcObject = this.stream; await video.play().catch(() => undefined); this.streamActive.set(true); this.starting.set(false); } catch { this.starting.set(false); this.streamActive.set(false); this.error.set( 'Could not access the camera. Check permissions and try again.', ); } } private stopStream(): void { const video = this.preview()?.nativeElement; if (video) { video.srcObject = null; } if (this.stream) { for (const track of this.stream.getTracks()) { track.stop(); } this.stream = null; } this.streamActive.set(false); } }