search
Vue

Vue.js Webcam Integration: Photo Capture and Video Recording Made Easy

Master webcam functionality in Vue.js with Composition API. Capture photos, record videos, apply filters, and manage camera devices with practical examples.

person By Gautam Sharma
calendar_today December 29, 2024
schedule 15 min read
Vue JavaScript Webcam Composition API MediaStream

Learn how to integrate webcam functionality in Vue.js applications. Capture photos, record videos, switch cameras, and implement advanced features using Composition API and reactive state management.

Install Dependencies

No external packages needed. Use native browser APIs.

npm create vue@latest camera-app
cd camera-app
npm install

Basic Webcam Display

Simple webcam preview with Composition API.

WebcamPreview.vue:

<template>
  <div class="webcam-preview">
    <div v-if="loading" class="loading">
      Starting camera...
    </div>

    <div v-if="error" class="error">
      {{ error }}
    </div>

    <video
      ref="videoRef"
      autoplay
      playsinline
      class="video-feed"
    ></video>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const videoRef = ref(null);
const loading = ref(true);
const error = ref(null);
let stream = null;

const startCamera = async () => {
  try {
    stream = await navigator.mediaDevices.getUserMedia({
      video: {
        width: { ideal: 1280 },
        height: { ideal: 720 },
        facingMode: 'user'
      },
      audio: false
    });

    if (videoRef.value) {
      videoRef.value.srcObject = stream;
    }

    loading.value = false;
  } catch (err) {
    error.value = 'Failed to access camera: ' + err.message;
    loading.value = false;
  }
};

const stopCamera = () => {
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
  }
};

onMounted(() => {
  startCamera();
});

onUnmounted(() => {
  stopCamera();
});
</script>

<style scoped>
.webcam-preview {
  max-width: 800px;
  margin: 0 auto;
}

.video-feed {
  width: 100%;
  border-radius: 12px;
  background: #000;
}

.loading, .error {
  padding: 40px;
  text-align: center;
  border-radius: 8px;
}

.error {
  background: #fee;
  color: #c33;
}
</style>

Photo Capture Component

Take photos and download them instantly.

PhotoCapture.vue:

<template>
  <div class="photo-capture">
    <div class="camera-wrapper">
      <video
        ref="videoRef"
        autoplay
        playsinline
        @loadedmetadata="onVideoLoaded"
      ></video>

      <canvas ref="canvasRef" style="display: none;"></canvas>
    </div>

    <div class="actions">
      <button @click="capturePhoto" class="btn-capture">
        📸 Capture Photo
      </button>
      <button
        @click="downloadPhoto"
        :disabled="!lastPhoto"
        class="btn-download"
      >
        ⬇️ Download
      </button>
    </div>

    <div v-if="lastPhoto" class="preview">
      <h3>Last Captured</h3>
      <img :src="lastPhoto" alt="Captured photo">
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const videoRef = ref(null);
const canvasRef = ref(null);
const lastPhoto = ref(null);
const videoReady = ref(false);

let stream = null;

const initCamera = async () => {
  try {
    stream = await navigator.mediaDevices.getUserMedia({
      video: { width: 1280, height: 720 },
      audio: false
    });

    if (videoRef.value) {
      videoRef.value.srcObject = stream;
    }
  } catch (err) {
    console.error('Camera error:', err);
    alert('Could not access camera');
  }
};

const onVideoLoaded = () => {
  videoReady.value = true;
};

const capturePhoto = () => {
  if (!videoReady.value) return;

  const video = videoRef.value;
  const canvas = canvasRef.value;

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

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

  lastPhoto.value = canvas.toDataURL('image/jpeg', 0.95);
};

const downloadPhoto = () => {
  if (!lastPhoto.value) return;

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

onMounted(() => {
  initCamera();
});

onUnmounted(() => {
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
  }
});
</script>

<style scoped>
.photo-capture {
  max-width: 900px;
  margin: 0 auto;
  padding: 20px;
}

.camera-wrapper {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  margin-bottom: 20px;
  background: #000;
}

video {
  width: 100%;
  display: block;
}

.actions {
  display: flex;
  gap: 15px;
  margin-bottom: 30px;
}

.actions button {
  flex: 1;
  padding: 16px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: transform 0.1s;
}

.actions button:active {
  transform: scale(0.98);
}

.btn-capture {
  background: #10b981;
  color: white;
}

.btn-download {
  background: #3b82f6;
  color: white;
}

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

.preview {
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}

.preview h3 {
  margin-top: 0;
}

.preview img {
  width: 100%;
  border-radius: 8px;
}
</style>

Video Recording with Composable

Create reusable video recorder composable.

composables/useVideoRecorder.js:

import { ref } from 'vue';

export function useVideoRecorder() {
  const isRecording = ref(false);
  const recordedVideos = ref([]);
  const recordingDuration = ref(0);

  let mediaRecorder = null;
  let recordedChunks = [];
  let startTime = 0;
  let timerInterval = null;

  const startRecording = (stream) => {
    recordedChunks = [];
    recordingDuration.value = 0;

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

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

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

    mediaRecorder.onstop = () => {
      saveVideo();
    };

    mediaRecorder.start();
    isRecording.value = true;
    startTime = Date.now();

    timerInterval = setInterval(() => {
      recordingDuration.value = Math.floor((Date.now() - startTime) / 1000);
    }, 1000);
  };

  const stopRecording = () => {
    if (mediaRecorder && isRecording.value) {
      mediaRecorder.stop();
      isRecording.value = false;

      if (timerInterval) {
        clearInterval(timerInterval);
      }
    }
  };

  const saveVideo = () => {
    const blob = new Blob(recordedChunks, { type: 'video/webm' });
    const url = URL.createObjectURL(blob);

    recordedVideos.value.push({
      id: Date.now(),
      url,
      duration: recordingDuration.value,
      timestamp: new Date()
    });

    recordedChunks = [];
  };

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

  const deleteVideo = (videoId) => {
    const video = recordedVideos.value.find(v => v.id === videoId);
    if (video) {
      URL.revokeObjectURL(video.url);
    }
    recordedVideos.value = recordedVideos.value.filter(v => v.id !== videoId);
  };

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

  return {
    isRecording,
    recordedVideos,
    recordingDuration,
    startRecording,
    stopRecording,
    downloadVideo,
    deleteVideo,
    formatTime
  };
}

VideoRecorder.vue:

<template>
  <div class="video-recorder">
    <div class="camera-section">
      <div class="video-wrapper">
        <video
          ref="videoRef"
          autoplay
          playsinline
          muted
        ></video>

        <div v-if="isRecording" class="recording-badge">
          <span class="pulse-dot"></span>
          <span>REC {{ formatTime(recordingDuration) }}</span>
        </div>
      </div>

      <div class="controls">
        <button
          v-if="!isRecording"
          @click="handleStartRecording"
          class="btn-record"
        >
          ⏺ Start Recording
        </button>
        <button
          v-else
          @click="stopRecording"
          class="btn-stop"
        >
          ⏹ Stop Recording
        </button>
      </div>
    </div>

    <div v-if="recordedVideos.length > 0" class="recordings">
      <h2>Recorded Videos ({{ recordedVideos.length }})</h2>
      <div class="video-list">
        <div
          v-for="video in recordedVideos"
          :key="video.id"
          class="video-item"
        >
          <video :src="video.url" controls></video>
          <div class="video-details">
            <span>Duration: {{ formatTime(video.duration) }}</span>
            <span>{{ formatDate(video.timestamp) }}</span>
          </div>
          <div class="video-actions">
            <button @click="downloadVideo(video)">Download</button>
            <button @click="deleteVideo(video.id)" class="btn-danger">
              Delete
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useVideoRecorder } from '../composables/useVideoRecorder';

const videoRef = ref(null);
let stream = null;

const {
  isRecording,
  recordedVideos,
  recordingDuration,
  startRecording,
  stopRecording,
  downloadVideo,
  deleteVideo,
  formatTime
} = useVideoRecorder();

const initCamera = async () => {
  try {
    stream = await navigator.mediaDevices.getUserMedia({
      video: { width: 1280, height: 720 },
      audio: true
    });

    if (videoRef.value) {
      videoRef.value.srcObject = stream;
    }
  } catch (err) {
    console.error('Camera error:', err);
  }
};

const handleStartRecording = () => {
  if (stream) {
    startRecording(stream);
  }
};

const formatDate = (date) => {
  return new Intl.DateTimeFormat('en-US', {
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit'
  }).format(date);
};

onMounted(() => {
  initCamera();
});

onUnmounted(() => {
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
  }
  stopRecording();
});
</script>

<style scoped>
.video-recorder {
  max-width: 1000px;
  margin: 0 auto;
  padding: 20px;
}

.camera-section {
  background: white;
  border-radius: 16px;
  padding: 24px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  margin-bottom: 40px;
}

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

video {
  width: 100%;
  display: block;
}

.recording-badge {
  position: absolute;
  top: 20px;
  right: 20px;
  background: rgba(239, 68, 68, 0.95);
  color: white;
  padding: 10px 20px;
  border-radius: 24px;
  font-weight: 700;
  display: flex;
  align-items: center;
  gap: 10px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

.pulse-dot {
  width: 12px;
  height: 12px;
  background: white;
  border-radius: 50%;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.3; }
}

.controls button {
  width: 100%;
  padding: 18px;
  border: none;
  border-radius: 10px;
  font-size: 18px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.btn-record {
  background: #ef4444;
  color: white;
}

.btn-record:hover {
  background: #dc2626;
}

.btn-stop {
  background: #64748b;
  color: white;
}

.btn-stop:hover {
  background: #475569;
}

.recordings h2 {
  margin-bottom: 24px;
  color: #1f2937;
}

.video-list {
  display: grid;
  gap: 24px;
}

.video-item {
  background: white;
  border-radius: 12px;
  padding: 20px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}

.video-item video {
  border-radius: 8px;
  margin-bottom: 12px;
}

.video-details {
  display: flex;
  justify-content: space-between;
  color: #6b7280;
  font-size: 14px;
  margin-bottom: 12px;
}

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

.video-actions button {
  flex: 1;
  padding: 10px;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 600;
  background: #3b82f6;
  color: white;
}

.btn-danger {
  background: #ef4444 !important;
}
</style>

Camera Switcher with Options API

Switch between front and back cameras.

CameraSwitcher.vue:

<template>
  <div class="camera-switcher">
    <div class="device-select">
      <label>Camera:</label>
      <select v-model="selectedDevice" @change="switchCamera">
        <option
          v-for="device in devices"
          :key="device.deviceId"
          :value="device.deviceId"
        >
          {{ device.label || `Camera ${device.deviceId.slice(0, 8)}` }}
        </option>
      </select>
    </div>

    <div class="camera-view">
      <video ref="video" autoplay playsinline></video>
    </div>

    <div class="quick-switch">
      <button @click="toggleCamera" class="btn-switch">
        🔄 Flip Camera
      </button>
      <button @click="takeSnapshot" class="btn-snap">
        📷 Snapshot
      </button>
    </div>

    <div v-if="snapshot" class="snapshot-preview">
      <img :src="snapshot">
      <button @click="downloadSnapshot">Download Snapshot</button>
    </div>
  </div>
</template>

<script>
export default {
  name: 'CameraSwitcher',
  data() {
    return {
      devices: [],
      selectedDevice: null,
      currentStream: null,
      snapshot: null,
      facingMode: 'user'
    };
  },
  async mounted() {
    await this.loadDevices();
    if (this.devices.length > 0) {
      this.selectedDevice = this.devices[0].deviceId;
      await this.startCamera();
    }
  },
  beforeUnmount() {
    this.stopCamera();
  },
  methods: {
    async loadDevices() {
      const allDevices = await navigator.mediaDevices.enumerateDevices();
      this.devices = allDevices.filter(d => d.kind === 'videoinput');
    },
    async startCamera() {
      this.stopCamera();

      try {
        const constraints = {
          video: {
            deviceId: this.selectedDevice ?
              { exact: this.selectedDevice } :
              { facingMode: this.facingMode }
          },
          audio: false
        };

        this.currentStream = await navigator.mediaDevices.getUserMedia(constraints);
        this.$refs.video.srcObject = this.currentStream;
      } catch (err) {
        console.error('Error accessing camera:', err);
        alert('Failed to access camera');
      }
    },
    stopCamera() {
      if (this.currentStream) {
        this.currentStream.getTracks().forEach(track => track.stop());
        this.currentStream = null;
      }
    },
    async switchCamera() {
      await this.startCamera();
    },
    async toggleCamera() {
      this.facingMode = this.facingMode === 'user' ? 'environment' : 'user';

      const envDevice = this.devices.find(d =>
        d.label.toLowerCase().includes('back') ||
        d.label.toLowerCase().includes('rear')
      );
      const userDevice = this.devices.find(d =>
        d.label.toLowerCase().includes('front') ||
        d.label.toLowerCase().includes('user')
      );

      if (this.facingMode === 'environment' && envDevice) {
        this.selectedDevice = envDevice.deviceId;
      } else if (this.facingMode === 'user' && userDevice) {
        this.selectedDevice = userDevice.deviceId;
      }

      await this.startCamera();
    },
    takeSnapshot() {
      const video = this.$refs.video;
      const canvas = document.createElement('canvas');

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

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

      this.snapshot = canvas.toDataURL('image/jpeg');
    },
    downloadSnapshot() {
      const link = document.createElement('a');
      link.href = this.snapshot;
      link.download = `snapshot-${Date.now()}.jpg`;
      link.click();
    }
  }
};
</script>

<style scoped>
.camera-switcher {
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

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

.device-select label {
  display: block;
  margin-bottom: 8px;
  font-weight: 600;
  color: #374151;
}

.device-select select {
  width: 100%;
  padding: 12px;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  font-size: 16px;
  background: white;
}

.camera-view {
  border-radius: 12px;
  overflow: hidden;
  margin-bottom: 20px;
  background: #000;
}

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

.quick-switch {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
  margin-bottom: 30px;
}

.quick-switch button {
  padding: 14px;
  border: none;
  border-radius: 8px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
}

.btn-switch {
  background: #8b5cf6;
  color: white;
}

.btn-snap {
  background: #10b981;
  color: white;
}

.snapshot-preview {
  background: white;
  padding: 20px;
  border-radius: 12px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}

.snapshot-preview img {
  width: 100%;
  border-radius: 8px;
  margin-bottom: 12px;
}

.snapshot-preview button {
  width: 100%;
  padding: 12px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  font-weight: 600;
}
</style>

Manage multiple photos with thumbnails.

PhotoGallery.vue:

<template>
  <div class="gallery-app">
    <div class="camera">
      <video ref="videoRef" autoplay playsinline></video>
      <button @click="addPhoto" class="capture-btn">
        <span class="icon">📸</span>
        <span>Capture</span>
        <span class="badge" v-if="photos.length > 0">{{ photos.length }}</span>
      </button>
    </div>

    <canvas ref="canvasRef" style="display: none;"></canvas>

    <div class="toolbar" v-if="photos.length > 0">
      <button @click="downloadAll">
        Download All ({{ photos.length }})
      </button>
      <button @click="clearGallery" class="btn-clear">
        Clear Gallery
      </button>
    </div>

    <div class="gallery" v-if="photos.length > 0">
      <TransitionGroup name="photo">
        <div
          v-for="photo in photos"
          :key="photo.id"
          class="photo-card"
        >
          <img :src="photo.url" :alt="`Photo ${photo.id}`">
          <div class="photo-overlay">
            <button @click="downloadPhoto(photo)" class="btn-icon">
              ⬇️
            </button>
            <button @click="removePhoto(photo.id)" class="btn-icon">
              🗑️
            </button>
          </div>
          <div class="photo-time">
            {{ formatTime(photo.timestamp) }}
          </div>
        </div>
      </TransitionGroup>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue';

const videoRef = ref(null);
const canvasRef = ref(null);
const photos = ref([]);

let stream = null;

const startCamera = async () => {
  stream = await navigator.mediaDevices.getUserMedia({
    video: { width: 1280, height: 720 },
    audio: false
  });

  if (videoRef.value) {
    videoRef.value.srcObject = stream;
  }
};

const addPhoto = () => {
  const video = videoRef.value;
  const canvas = canvasRef.value;

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

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

  photos.value.unshift({
    id: Date.now(),
    url: canvas.toDataURL('image/jpeg', 0.92),
    timestamp: new Date()
  });
};

const downloadPhoto = (photo) => {
  const link = document.createElement('a');
  link.href = photo.url;
  link.download = `photo-${photo.id}.jpg`;
  link.click();
};

const removePhoto = (id) => {
  photos.value = photos.value.filter(p => p.id !== id);
};

const downloadAll = () => {
  photos.value.forEach((photo, index) => {
    setTimeout(() => downloadPhoto(photo), index * 100);
  });
};

const clearGallery = () => {
  if (confirm('Delete all photos?')) {
    photos.value = [];
  }
};

const formatTime = (date) => {
  return new Intl.DateTimeFormat('en-US', {
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit'
  }).format(date);
};

onMounted(() => {
  startCamera();
});

onUnmounted(() => {
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
  }
});
</script>

<style scoped>
.gallery-app {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
}

.camera {
  position: relative;
  border-radius: 16px;
  overflow: hidden;
  margin-bottom: 24px;
  background: #000;
}

.camera video {
  width: 100%;
  display: block;
}

.capture-btn {
  position: absolute;
  bottom: 30px;
  left: 50%;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 16px 32px;
  background: rgba(255, 255, 255, 0.95);
  border: none;
  border-radius: 50px;
  font-size: 18px;
  font-weight: 700;
  cursor: pointer;
  box-shadow: 0 8px 24px rgba(0,0,0,0.3);
  transition: transform 0.1s;
}

.capture-btn:active {
  transform: translateX(-50%) scale(0.95);
}

.capture-btn .icon {
  font-size: 24px;
}

.badge {
  background: #ef4444;
  color: white;
  padding: 4px 10px;
  border-radius: 12px;
  font-size: 14px;
}

.toolbar {
  display: flex;
  gap: 12px;
  margin-bottom: 24px;
}

.toolbar button {
  flex: 1;
  padding: 14px;
  border: none;
  border-radius: 10px;
  font-weight: 600;
  cursor: pointer;
  background: #3b82f6;
  color: white;
}

.btn-clear {
  background: #ef4444 !important;
}

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

.photo-card {
  position: relative;
  border-radius: 12px;
  overflow: hidden;
  background: white;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  transition: transform 0.2s;
}

.photo-card:hover {
  transform: translateY(-4px);
}

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

.photo-overlay {
  position: absolute;
  top: 10px;
  right: 10px;
  display: flex;
  gap: 8px;
}

.btn-icon {
  width: 40px;
  height: 40px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.95);
  cursor: pointer;
  font-size: 18px;
  box-shadow: 0 2px 8px rgba(0,0,0,0.2);
}

.photo-time {
  padding: 12px;
  background: white;
  font-size: 14px;
  color: #6b7280;
  text-align: center;
}

.photo-enter-active {
  animation: photoIn 0.3s;
}

.photo-leave-active {
  animation: photoOut 0.3s;
}

@keyframes photoIn {
  from {
    opacity: 0;
    transform: scale(0.8);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes photoOut {
  from {
    opacity: 1;
    transform: scale(1);
  }
  to {
    opacity: 0;
    transform: scale(0.8);
  }
}
</style>

Complete App with Filters

Advanced camera app with real-time filters.

CameraApp.vue:

<template>
  <div class="camera-app">
    <header>
      <h1>Vue Camera Pro</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>
      <div class="filter-bar" v-if="mode === 'photo'">
        <button
          v-for="filter in filters"
          :key="filter.name"
          :class="{ active: selectedFilter === filter.value }"
          @click="selectedFilter = filter.value"
        >
          {{ filter.name }}
        </button>
      </div>

      <component
        :is="currentComponent"
        :filter="selectedFilter"
      />
    </main>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';
import PhotoCapture from './PhotoCapture.vue';
import VideoRecorder from './VideoRecorder.vue';

const mode = ref('photo');
const selectedFilter = ref('none');

const filters = [
  { name: 'None', value: 'none' },
  { name: 'B&W', value: 'grayscale(100%)' },
  { name: 'Sepia', value: 'sepia(100%)' },
  { name: 'Bright', value: 'brightness(130%)' },
  { name: 'Vintage', value: 'sepia(50%) contrast(120%)' }
];

const currentComponent = computed(() => {
  return mode.value === 'photo' ? PhotoCapture : VideoRecorder;
});
</script>

<style scoped>
.camera-app {
  min-height: 100vh;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  padding-bottom: 40px;
}

header {
  background: rgba(255, 255, 255, 0.95);
  backdrop-filter: blur(10px);
  padding: 24px;
  box-shadow: 0 4px 12px rgba(0,0,0,0.1);
  margin-bottom: 30px;
}

header h1 {
  margin: 0 0 20px 0;
  text-align: center;
  background: linear-gradient(135deg, #667eea, #764ba2);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  font-size: 32px;
}

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

.mode-tabs button {
  padding: 12px 36px;
  border: 2px solid #667eea;
  background: white;
  border-radius: 25px;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: all 0.2s;
}

.mode-tabs button.active {
  background: linear-gradient(135deg, #667eea, #764ba2);
  color: white;
  border-color: transparent;
}

.filter-bar {
  display: flex;
  gap: 10px;
  justify-content: center;
  margin-bottom: 24px;
  padding: 0 20px;
  flex-wrap: wrap;
}

.filter-bar button {
  padding: 8px 20px;
  border: 2px solid rgba(255,255,255,0.3);
  background: rgba(255,255,255,0.1);
  backdrop-filter: blur(10px);
  color: white;
  border-radius: 20px;
  cursor: pointer;
  font-weight: 600;
  transition: all 0.2s;
}

.filter-bar button.active {
  background: white;
  color: #667eea;
  border-color: white;
}

main {
  padding: 0 20px;
}
</style>

Quick Reference

Start Camera:

const stream = await navigator.mediaDevices.getUserMedia({
  video: { width: 1280, height: 720 },
  audio: false
});
videoRef.value.srcObject = stream;

Capture Photo:

const canvas = canvasRef.value;
const ctx = canvas.getContext('2d');
ctx.drawImage(videoRef.value, 0, 0);
const dataUrl = canvas.toDataURL('image/jpeg');

Record Video:

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

Download File:

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

Stop Camera:

stream.getTracks().forEach(track => track.stop());

Best Practices

  1. Always clean up streams in onUnmounted
  2. Use refs for DOM elements
  3. Handle errors with try-catch
  4. Provide loading states
  5. Use composables for reusable logic
  6. Implement proper TypeScript types
  7. Add transitions for better UX
  8. Clean up object URLs
  9. Request permissions early
  10. Test on mobile devices

Browser Compatibility

Works in modern browsers:

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

Requires HTTPS in production (except localhost).

Conclusion

Vue.js provides reactive state management perfect for camera applications. Use Composition API for modern code structure, create reusable composables, and leverage Vue’s reactivity for real-time UI updates. Always handle cleanup properly and provide good user feedback.

Gautam Sharma

About Gautam Sharma

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

Related Articles

Vue

Vue.js Tutorial to Integrate jsPDF Library to Edit PDF in Browser

Complete guide to using jsPDF in Vue.js applications for creating and editing PDF documents directly in the browser.

December 28, 2024
Vue

Vue.js Scan & Generate QRCodes in Browser

Quick guide to generating and scanning QR codes in Vue.js browser applications. Simple examples with qrcode.vue and html5-qrcode.

December 31, 2024
Vue

Fix: defineProps is not defined in Vue.js Error

Learn how to fix the 'defineProps is not defined' error in Vue.js applications. This comprehensive guide covers Composition API, TypeScript, and best practices.

January 2, 2026