No articles found
Try different keywords or browse our categories
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.
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>
Photo Gallery Component
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
- Always clean up streams in onUnmounted
- Use refs for DOM elements
- Handle errors with try-catch
- Provide loading states
- Use composables for reusable logic
- Implement proper TypeScript types
- Add transitions for better UX
- Clean up object URLs
- Request permissions early
- 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.
Related Articles
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.
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.
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.