search
Angular

Building a Camera App in Angular: Complete Photo and Video Recorder

Step-by-step guide to building a professional camera application in Angular with photo capture, video recording, filters, and download features.

person By Gautam Sharma
calendar_today December 30, 2024
schedule 13 min read
Angular TypeScript Webcam Camera MediaRecorder

Build a professional camera application in Angular with webcam access, photo capture, video recording, and file downloads. Full TypeScript implementation with services and components.

Setup Angular Project

Create new Angular project with routing and styles.

ng new camera-app
cd camera-app
ng generate component camera
ng generate service camera

Camera Service

Create reusable camera service for stream management.

camera.service.ts:

import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

export interface CameraError {
  type: string;
  message: string;
}

@Injectable({
  providedIn: 'root'
})
export class CameraService {
  private streamSubject = new BehaviorSubject<MediaStream | null>(null);
  private errorSubject = new BehaviorSubject<CameraError | null>(null);

  public stream$ = this.streamSubject.asObservable();
  public error$ = this.errorSubject.asObservable();

  private currentStream: MediaStream | null = null;

  async startCamera(constraints?: MediaStreamConstraints): Promise<void> {
    try {
      const defaultConstraints: MediaStreamConstraints = {
        video: {
          width: { ideal: 1280 },
          height: { ideal: 720 },
          facingMode: 'user'
        },
        audio: false
      };

      const stream = await navigator.mediaDevices.getUserMedia(
        constraints || defaultConstraints
      );

      this.currentStream = stream;
      this.streamSubject.next(stream);
      this.errorSubject.next(null);
    } catch (error: any) {
      this.handleError(error);
    }
  }

  stopCamera(): void {
    if (this.currentStream) {
      this.currentStream.getTracks().forEach(track => track.stop());
      this.currentStream = null;
      this.streamSubject.next(null);
    }
  }

  async getAvailableDevices(): Promise<MediaDeviceInfo[]> {
    const devices = await navigator.mediaDevices.enumerateDevices();
    return devices.filter(device => device.kind === 'videoinput');
  }

  private handleError(error: any): void {
    let errorMessage = 'Failed to access camera';
    let errorType = 'UnknownError';

    if (error.name === 'NotAllowedError') {
      errorMessage = 'Camera access denied by user';
      errorType = 'PermissionDenied';
    } else if (error.name === 'NotFoundError') {
      errorMessage = 'No camera device found';
      errorType = 'DeviceNotFound';
    } else if (error.name === 'NotReadableError') {
      errorMessage = 'Camera already in use';
      errorType = 'DeviceBusy';
    }

    this.errorSubject.next({ type: errorType, message: errorMessage });
  }
}

Photo Capture Component

Component for taking photos with download.

photo-capture.component.ts:

import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';
import { Subject, takeUntil } from 'rxjs';

interface CapturedPhoto {
  id: string;
  dataUrl: string;
  timestamp: Date;
}

@Component({
  selector: 'app-photo-capture',
  templateUrl: './photo-capture.component.html',
  styleUrls: ['./photo-capture.component.css']
})
export class PhotoCaptureComponent implements OnInit, OnDestroy {
  @ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;
  @ViewChild('canvasElement') canvasElement!: ElementRef<HTMLCanvasElement>;

  photos: CapturedPhoto[] = [];
  errorMessage: string | null = null;
  isLoading = true;

  private destroy$ = new Subject<void>();

  constructor(private cameraService: CameraService) {}

  ngOnInit(): void {
    this.initializeCamera();
    this.subscribeToErrors();
  }

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.cameraService.stopCamera();
  }

  private initializeCamera(): void {
    this.cameraService.startCamera();

    this.cameraService.stream$
      .pipe(takeUntil(this.destroy$))
      .subscribe(stream => {
        if (stream && this.videoElement) {
          this.videoElement.nativeElement.srcObject = stream;
          this.isLoading = false;
        }
      });
  }

  private subscribeToErrors(): void {
    this.cameraService.error$
      .pipe(takeUntil(this.destroy$))
      .subscribe(error => {
        if (error) {
          this.errorMessage = error.message;
          this.isLoading = false;
        }
      });
  }

  capturePhoto(): void {
    const video = this.videoElement.nativeElement;
    const canvas = this.canvasElement.nativeElement;

    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;

    const context = canvas.getContext('2d');
    if (context) {
      context.drawImage(video, 0, 0, canvas.width, canvas.height);

      const dataUrl = canvas.toDataURL('image/jpeg', 0.95);

      this.photos.push({
        id: this.generateId(),
        dataUrl,
        timestamp: new Date()
      });
    }
  }

  downloadPhoto(photo: CapturedPhoto): void {
    const link = document.createElement('a');
    link.href = photo.dataUrl;
    link.download = `photo-${photo.id}.jpg`;
    link.click();
  }

  deletePhoto(photoId: string): void {
    this.photos = this.photos.filter(p => p.id !== photoId);
  }

  clearAll(): void {
    this.photos = [];
  }

  private generateId(): string {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }
}

photo-capture.component.html:

<div class="photo-capture-container">
  <div class="camera-section">
    <div *ngIf="isLoading" class="loading">
      <p>Loading camera...</p>
    </div>

    <div *ngIf="errorMessage" class="error-message">
      <h3>Camera Error</h3>
      <p>{{ errorMessage }}</p>
    </div>

    <div class="video-container" *ngIf="!isLoading && !errorMessage">
      <video #videoElement autoplay playsinline></video>
    </div>

    <canvas #canvasElement style="display: none;"></canvas>

    <div class="controls">
      <button
        class="btn-primary"
        (click)="capturePhoto()"
        [disabled]="isLoading || errorMessage !== null">
        📷 Capture Photo
      </button>
      <button
        class="btn-secondary"
        (click)="clearAll()"
        [disabled]="photos.length === 0">
        Clear All ({{ photos.length }})
      </button>
    </div>
  </div>

  <div class="gallery" *ngIf="photos.length > 0">
    <h2>Captured Photos</h2>
    <div class="photo-grid">
      <div *ngFor="let photo of photos" class="photo-card">
        <img [src]="photo.dataUrl" [alt]="'Photo ' + photo.id">
        <div class="photo-info">
          <span class="timestamp">{{ photo.timestamp | date:'short' }}</span>
          <div class="photo-actions">
            <button (click)="downloadPhoto(photo)" class="btn-download">
              ⬇ Download
            </button>
            <button (click)="deletePhoto(photo.id)" class="btn-delete">
              🗑 Delete
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

photo-capture.component.css:

.photo-capture-container {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.camera-section {
  background: #fff;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
  margin-bottom: 30px;
}

.video-container {
  position: relative;
  border-radius: 8px;
  overflow: hidden;
  background: #000;
  margin-bottom: 20px;
}

.video-container video {
  width: 100%;
  height: auto;
  display: block;
}

.controls {
  display: flex;
  gap: 10px;
}

.btn-primary {
  flex: 1;
  padding: 15px 30px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: background 0.2s;
}

.btn-primary:hover:not(:disabled) {
  background: #0056b3;
}

.btn-primary:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.btn-secondary {
  padding: 15px 30px;
  background: #6c757d;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
}

.btn-secondary:hover:not(:disabled) {
  background: #545b62;
}

.loading, .error-message {
  text-align: center;
  padding: 40px;
}

.error-message {
  background: #fee;
  border: 2px solid #fcc;
  border-radius: 8px;
  color: #c33;
}

.gallery h2 {
  margin-bottom: 20px;
}

.photo-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
}

.photo-card {
  background: #fff;
  border-radius: 8px;
  overflow: hidden;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.photo-card img {
  width: 100%;
  height: 200px;
  object-fit: cover;
}

.photo-info {
  padding: 15px;
}

.timestamp {
  display: block;
  color: #666;
  font-size: 14px;
  margin-bottom: 10px;
}

.photo-actions {
  display: flex;
  gap: 10px;
}

.photo-actions button {
  flex: 1;
  padding: 8px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

.btn-download {
  background: #28a745;
  color: white;
}

.btn-delete {
  background: #dc3545;
  color: white;
}

Video Recording Component

Record videos with timer and playback.

video-recorder.component.ts:

import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';
import { Subject, takeUntil, interval } from 'rxjs';

interface RecordedVideo {
  id: string;
  url: string;
  duration: number;
  timestamp: Date;
}

@Component({
  selector: 'app-video-recorder',
  templateUrl: './video-recorder.component.html',
  styleUrls: ['./video-recorder.component.css']
})
export class VideoRecorderComponent implements OnInit, OnDestroy {
  @ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;

  isRecording = false;
  recordingDuration = 0;
  videos: RecordedVideo[] = [];
  errorMessage: string | null = null;

  private mediaRecorder: MediaRecorder | null = null;
  private recordedChunks: Blob[] = [];
  private destroy$ = new Subject<void>();
  private recordingStartTime = 0;

  constructor(private cameraService: CameraService) {}

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

  ngOnDestroy(): void {
    this.destroy$.next();
    this.destroy$.complete();
    this.stopRecording();
    this.cameraService.stopCamera();
  }

  private initializeCamera(): void {
    this.cameraService.startCamera({
      video: { width: 1280, height: 720 },
      audio: true
    });

    this.cameraService.stream$
      .pipe(takeUntil(this.destroy$))
      .subscribe(stream => {
        if (stream && this.videoElement) {
          this.videoElement.nativeElement.srcObject = stream;
        }
      });

    this.cameraService.error$
      .pipe(takeUntil(this.destroy$))
      .subscribe(error => {
        if (error) {
          this.errorMessage = error.message;
        }
      });
  }

  startRecording(): void {
    const stream = this.videoElement.nativeElement.srcObject as MediaStream;
    if (!stream) return;

    this.recordedChunks = [];
    this.recordingDuration = 0;
    this.recordingStartTime = Date.now();

    const options: MediaRecorderOptions = {
      mimeType: 'video/webm;codecs=vp9',
      videoBitsPerSecond: 2500000
    };

    try {
      this.mediaRecorder = new MediaRecorder(stream, options);
    } catch (e) {
      this.mediaRecorder = new MediaRecorder(stream);
    }

    this.mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        this.recordedChunks.push(event.data);
      }
    };

    this.mediaRecorder.onstop = () => {
      this.saveRecording();
    };

    this.mediaRecorder.start();
    this.isRecording = true;

    interval(1000)
      .pipe(takeUntil(this.destroy$))
      .subscribe(() => {
        if (this.isRecording) {
          this.recordingDuration = Math.floor((Date.now() - this.recordingStartTime) / 1000);
        }
      });
  }

  stopRecording(): void {
    if (this.mediaRecorder && this.isRecording) {
      this.mediaRecorder.stop();
      this.isRecording = false;
    }
  }

  private saveRecording(): void {
    const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
    const url = URL.createObjectURL(blob);

    this.videos.push({
      id: this.generateId(),
      url,
      duration: this.recordingDuration,
      timestamp: new Date()
    });

    this.recordedChunks = [];
  }

  downloadVideo(video: RecordedVideo): void {
    const link = document.createElement('a');
    link.href = video.url;
    link.download = `video-${video.id}.webm`;
    link.click();
  }

  deleteVideo(videoId: string): void {
    const video = this.videos.find(v => v.id === videoId);
    if (video) {
      URL.revokeObjectURL(video.url);
    }
    this.videos = this.videos.filter(v => v.id !== videoId);
  }

  formatTime(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = seconds % 60;
    return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
  }

  private generateId(): string {
    return Date.now().toString(36) + Math.random().toString(36).substr(2);
  }
}

video-recorder.component.html:

<div class="video-recorder-container">
  <div class="camera-section">
    <div *ngIf="errorMessage" class="error-message">
      <h3>Error</h3>
      <p>{{ errorMessage }}</p>
    </div>

    <div class="video-preview">
      <video #videoElement autoplay playsinline muted></video>
      <div *ngIf="isRecording" class="recording-indicator">
        <span class="rec-badge">🔴 REC</span>
        <span class="timer">{{ formatTime(recordingDuration) }}</span>
      </div>
    </div>

    <div class="controls">
      <button
        *ngIf="!isRecording"
        class="btn-record"
        (click)="startRecording()"
        [disabled]="errorMessage !== null">
        ⏺ Start Recording
      </button>
      <button
        *ngIf="isRecording"
        class="btn-stop"
        (click)="stopRecording()">
        ⏹ Stop Recording ({{ formatTime(recordingDuration) }})
      </button>
    </div>
  </div>

  <div class="videos-list" *ngIf="videos.length > 0">
    <h2>Recorded Videos ({{ videos.length }})</h2>
    <div class="video-grid">
      <div *ngFor="let video of videos" class="video-card">
        <video [src]="video.url" controls></video>
        <div class="video-info">
          <div class="video-meta">
            <span>Duration: {{ formatTime(video.duration) }}</span>
            <span>{{ video.timestamp | date:'short' }}</span>
          </div>
          <div class="video-actions">
            <button (click)="downloadVideo(video)" class="btn-download">
              Download
            </button>
            <button (click)="deleteVideo(video.id)" class="btn-delete">
              Delete
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

Camera Switcher Component

Switch between multiple cameras.

camera-switcher.component.ts:

import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
import { CameraService } from '../camera.service';

@Component({
  selector: 'app-camera-switcher',
  template: `
    <div class="camera-switcher">
      <div class="device-selector">
        <label>Select Camera:</label>
        <select [(ngModel)]="selectedDeviceId" (change)="switchCamera()">
          <option *ngFor="let device of devices" [value]="device.deviceId">
            {{ device.label || 'Camera ' + device.deviceId.substring(0, 8) }}
          </option>
        </select>
      </div>

      <div class="video-container">
        <video #videoElement autoplay playsinline></video>
      </div>

      <button (click)="capturePhoto()" class="btn-capture">
        Take Photo
      </button>

      <div *ngIf="capturedImage" class="preview">
        <h3>Last Photo</h3>
        <img [src]="capturedImage">
        <button (click)="downloadImage()">Download</button>
      </div>
    </div>
  `,
  styles: [`
    .camera-switcher {
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }

    .device-selector {
      margin-bottom: 20px;
    }

    .device-selector label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
    }

    .device-selector select {
      width: 100%;
      padding: 10px;
      border: 2px solid #ddd;
      border-radius: 6px;
      font-size: 16px;
    }

    .video-container {
      border-radius: 8px;
      overflow: hidden;
      margin-bottom: 20px;
      background: #000;
    }

    .video-container video {
      width: 100%;
      display: block;
    }

    .btn-capture {
      width: 100%;
      padding: 15px;
      background: #007bff;
      color: white;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      cursor: pointer;
    }

    .preview {
      margin-top: 30px;
    }

    .preview img {
      width: 100%;
      border-radius: 8px;
    }
  `]
})
export class CameraSwitcherComponent implements OnInit {
  @ViewChild('videoElement') videoElement!: ElementRef<HTMLVideoElement>;

  devices: MediaDeviceInfo[] = [];
  selectedDeviceId: string = '';
  capturedImage: string | null = null;

  constructor(private cameraService: CameraService) {}

  async ngOnInit() {
    await this.loadDevices();
    if (this.devices.length > 0) {
      this.selectedDeviceId = this.devices[0].deviceId;
      this.switchCamera();
    }
  }

  async loadDevices() {
    this.devices = await this.cameraService.getAvailableDevices();
  }

  switchCamera() {
    this.cameraService.stopCamera();
    this.cameraService.startCamera({
      video: { deviceId: { exact: this.selectedDeviceId } },
      audio: false
    });

    this.cameraService.stream$.subscribe(stream => {
      if (stream && this.videoElement) {
        this.videoElement.nativeElement.srcObject = stream;
      }
    });
  }

  capturePhoto() {
    const video = this.videoElement.nativeElement;
    const canvas = document.createElement('canvas');
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;

    const ctx = canvas.getContext('2d');
    ctx?.drawImage(video, 0, 0);

    this.capturedImage = canvas.toDataURL('image/jpeg');
  }

  downloadImage() {
    if (!this.capturedImage) return;

    const link = document.createElement('a');
    link.href = this.capturedImage;
    link.download = `photo-${Date.now()}.jpg`;
    link.click();
  }
}

Complete Camera App Component

All-in-one camera application.

app.component.ts:

import { Component } from '@angular/core';

type CameraMode = 'photo' | 'video';

@Component({
  selector: 'app-root',
  template: `
    <div class="camera-app">
      <header>
        <h1>📷 Angular Camera App</h1>
        <div class="mode-tabs">
          <button
            [class.active]="mode === 'photo'"
            (click)="mode = 'photo'">
            Photo
          </button>
          <button
            [class.active]="mode === 'video'"
            (click)="mode = 'video'">
            Video
          </button>
        </div>
      </header>

      <main>
        <app-photo-capture *ngIf="mode === 'photo'"></app-photo-capture>
        <app-video-recorder *ngIf="mode === 'video'"></app-video-recorder>
      </main>
    </div>
  `,
  styles: [`
    .camera-app {
      min-height: 100vh;
      background: #f5f5f5;
    }

    header {
      background: white;
      padding: 20px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.1);
      margin-bottom: 30px;
    }

    header h1 {
      margin: 0 0 20px 0;
      text-align: center;
    }

    .mode-tabs {
      display: flex;
      justify-content: center;
      gap: 10px;
    }

    .mode-tabs button {
      padding: 10px 30px;
      border: 2px solid #333;
      background: white;
      border-radius: 8px;
      cursor: pointer;
      font-size: 16px;
      font-weight: 600;
      transition: all 0.2s;
    }

    .mode-tabs button.active {
      background: #333;
      color: white;
    }

    main {
      padding: 0 20px;
    }
  `]
})
export class AppComponent {
  mode: CameraMode = 'photo';
}

Add Photo Filters

Apply filters using canvas context.

import { Component } from '@angular/core';

@Component({
  selector: 'app-photo-filters',
  template: `
    <div class="filters-app">
      <div class="filter-selector">
        <label>Select Filter:</label>
        <select [(ngModel)]="selectedFilter">
          <option value="none">None</option>
          <option value="grayscale(100%)">Grayscale</option>
          <option value="sepia(100%)">Sepia</option>
          <option value="blur(3px)">Blur</option>
          <option value="brightness(150%)">Bright</option>
          <option value="contrast(200%)">Contrast</option>
          <option value="invert(100%)">Invert</option>
        </select>
      </div>

      <div class="video-container" [style.filter]="selectedFilter">
        <video #videoElement autoplay playsinline></video>
      </div>

      <canvas #canvasElement style="display: none;"></canvas>

      <button (click)="captureWithFilter()">
        Capture with {{ getFilterName() }}
      </button>

      <div *ngIf="capturedImage" class="preview">
        <img [src]="capturedImage">
        <button (click)="download()">Download</button>
      </div>
    </div>
  `
})
export class PhotoFiltersComponent {
  selectedFilter = 'none';
  capturedImage: string | null = null;

  getFilterName(): string {
    return this.selectedFilter.split('(')[0] || 'None';
  }

  captureWithFilter() {
    // Implementation similar to previous examples
  }

  download() {
    // Download implementation
  }
}

Module Configuration

Import required modules.

app.module.ts:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { PhotoCaptureComponent } from './photo-capture/photo-capture.component';
import { VideoRecorderComponent } from './video-recorder/video-recorder.component';
import { CameraSwitcherComponent } from './camera-switcher/camera-switcher.component';
import { CameraService } from './camera.service';

@NgModule({
  declarations: [
    AppComponent,
    PhotoCaptureComponent,
    VideoRecorderComponent,
    CameraSwitcherComponent
  ],
  imports: [
    BrowserModule,
    FormsModule
  ],
  providers: [CameraService],
  bootstrap: [AppComponent]
})
export class AppModule { }

Permission Guard

Guard for checking camera permissions.

camera-permission.guard.ts:

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';

@Injectable({
  providedIn: 'root'
})
export class CameraPermissionGuard implements CanActivate {
  constructor(private router: Router) {}

  async canActivate(): Promise<boolean> {
    try {
      const stream = await navigator.mediaDevices.getUserMedia({
        video: true
      });

      stream.getTracks().forEach(track => track.stop());
      return true;
    } catch (error) {
      alert('Camera permission required');
      return false;
    }
  }
}

Snapshot Pipe

Format file sizes for downloads.

filesize.pipe.ts:

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({
  name: 'filesize'
})
export class FileSizePipe implements PipeTransform {
  transform(bytes: number): string {
    if (bytes === 0) return '0 Bytes';

    const k = 1024;
    const sizes = ['Bytes', 'KB', 'MB', 'GB'];
    const i = Math.floor(Math.log(bytes) / Math.log(k));

    return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
  }
}

Testing Components

Unit test for camera service.

camera.service.spec.ts:

import { TestBed } from '@angular/core/testing';
import { CameraService } from './camera.service';

describe('CameraService', () => {
  let service: CameraService;

  beforeEach(() => {
    TestBed.configureTestingModule({});
    service = TestBed.inject(CameraService);
  });

  it('should be created', () => {
    expect(service).toBeTruthy();
  });

  it('should start camera stream', async () => {
    const mockStream = new MediaStream();

    spyOn(navigator.mediaDevices, 'getUserMedia')
      .and.returnValue(Promise.resolve(mockStream));

    await service.startCamera();

    service.stream$.subscribe(stream => {
      expect(stream).toBe(mockStream);
    });
  });

  it('should stop camera stream', () => {
    const mockTrack = jasmine.createSpyObj('MediaStreamTrack', ['stop']);
    const mockStream = new MediaStream();

    spyOn(mockStream, 'getTracks').and.returnValue([mockTrack]);

    service['currentStream'] = mockStream;
    service.stopCamera();

    expect(mockTrack.stop).toHaveBeenCalled();
  });
});

Performance Optimization

Optimize video recording for large files.

export class OptimizedRecorderComponent {
  private recordingConfig = {
    mimeType: 'video/webm;codecs=vp8',
    videoBitsPerSecond: 1000000, // 1 Mbps
    timeSlice: 1000 // Record in 1-second chunks
  };

  startOptimizedRecording(stream: MediaStream) {
    const mediaRecorder = new MediaRecorder(stream, this.recordingConfig);

    mediaRecorder.ondataavailable = (event) => {
      if (event.data.size > 0) {
        // Process chunks immediately
        this.processChunk(event.data);
      }
    };

    mediaRecorder.start(this.recordingConfig.timeSlice);
  }

  private processChunk(chunk: Blob) {
    // Handle chunk (upload, store, etc.)
    console.log('Chunk size:', chunk.size);
  }
}

Quick Reference

Service Methods:

startCamera(constraints?: MediaStreamConstraints)
stopCamera()
getAvailableDevices()

Capture Photo:

const canvas = this.canvasElement.nativeElement;
canvas.getContext('2d')?.drawImage(video, 0, 0);
const dataUrl = canvas.toDataURL('image/jpeg');

Record Video:

const mediaRecorder = new MediaRecorder(stream, options);
mediaRecorder.start();
mediaRecorder.stop();

Download File:

const link = document.createElement('a');
link.href = dataUrl;
link.download = 'file.jpg';
link.click();

RxJS Cleanup:

private destroy$ = new Subject<void>();
ngOnDestroy() {
  this.destroy$.next();
  this.destroy$.complete();
}

Browser Support

Modern browsers with MediaDevices API:

  • Chrome 53+
  • Firefox 36+
  • Safari 11+
  • Edge 79+

HTTPS required for production.

Best Practices

  1. Always clean up streams in ngOnDestroy
  2. Use RxJS operators for stream management
  3. Handle errors with proper user feedback
  4. Implement loading states
  5. Use TypeScript interfaces for type safety
  6. Create reusable services for camera logic
  7. Test camera permissions before access
  8. Optimize recording settings for file size
  9. Provide clear UI for recording state
  10. Clean up object URLs after use

Conclusion

Angular provides powerful tools for building camera applications. Use services for stream management, components for UI logic, and RxJS for reactive state handling. TypeScript ensures type safety throughout the application.

Gautam Sharma

About Gautam Sharma

Full-stack developer and tech blogger sharing coding tutorials and best practices

Related Articles

Angular

Angular 21 jsPDF Example to Edit & Modify PDF Files in Browser

Learn how to integrate jsPDF with Angular 21 to create, edit, and modify PDF documents directly in the browser using standalone components and signals.

December 30, 2024
Angular

How to Fix Property does not exist on type 'never' Error in Angular

Learn how to fix the 'Property does not exist on type never' error in Angular TypeScript. This comprehensive guide covers type guards, unions, and best practices.

January 2, 2026
Angular

Fix: ExpressionChangedAfterItHasBeenCheckedError in Angular - Complete Tutorial

Complete guide to fix ExpressionChangedAfterItHasBeenCheckedError in Angular applications. Learn how to resolve change detection issues with practical solutions, debugging techniques, and best practices for Angular development.

January 8, 2026