feat(*): send images from website
This commit is contained in:
144
frontend/src/lib/components/CaptureCountdown.svelte
Normal file
144
frontend/src/lib/components/CaptureCountdown.svelte
Normal file
@@ -0,0 +1,144 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
oncomplete: (base64: string, mediaType: string, thumbnailBase64: string) => void;
|
||||
oncancel: () => void;
|
||||
}
|
||||
|
||||
let { oncomplete, oncancel }: Props = $props();
|
||||
|
||||
let count = $state(3);
|
||||
let videoElement: HTMLVideoElement | null = $state(null);
|
||||
let stream: MediaStream | null = $state(null);
|
||||
let error: string | null = $state(null);
|
||||
let cancelled = false;
|
||||
let countdownInterval: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
async function startCamera() {
|
||||
if (cancelled) return;
|
||||
|
||||
if (!navigator.mediaDevices?.getUserMedia) {
|
||||
error = 'Camera not supported (requires HTTPS)';
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const constraints: MediaStreamConstraints = {
|
||||
video: {
|
||||
facingMode: { ideal: 'environment' },
|
||||
width: { ideal: 1920 },
|
||||
height: { ideal: 1080 }
|
||||
},
|
||||
audio: false
|
||||
};
|
||||
|
||||
stream = await navigator.mediaDevices.getUserMedia(constraints);
|
||||
|
||||
if (videoElement && !cancelled) {
|
||||
videoElement.srcObject = stream;
|
||||
startCountdown();
|
||||
}
|
||||
} catch (e) {
|
||||
error = e instanceof Error ? e.message : 'Camera access denied';
|
||||
}
|
||||
}
|
||||
|
||||
function stopCamera() {
|
||||
if (countdownInterval) {
|
||||
clearInterval(countdownInterval);
|
||||
countdownInterval = null;
|
||||
}
|
||||
if (stream) {
|
||||
stream.getTracks().forEach((track) => track.stop());
|
||||
stream = null;
|
||||
}
|
||||
}
|
||||
|
||||
function startCountdown() {
|
||||
countdownInterval = setInterval(() => {
|
||||
if (cancelled) {
|
||||
stopCamera();
|
||||
return;
|
||||
}
|
||||
count--;
|
||||
if (count === 0) {
|
||||
if (countdownInterval) clearInterval(countdownInterval);
|
||||
capture();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function capture() {
|
||||
if (cancelled || !videoElement) {
|
||||
stopCamera();
|
||||
return;
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = videoElement.videoWidth;
|
||||
canvas.height = videoElement.videoHeight;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
stopCamera();
|
||||
oncancel();
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.drawImage(videoElement, 0, 0);
|
||||
|
||||
const base64 = canvas.toDataURL('image/jpeg', 0.85).split(',')[1];
|
||||
const mediaType = 'image/jpeg';
|
||||
|
||||
const thumbMaxSize = 800;
|
||||
const scale = Math.min(thumbMaxSize / canvas.width, thumbMaxSize / canvas.height, 1);
|
||||
const thumbCanvas = document.createElement('canvas');
|
||||
thumbCanvas.width = Math.round(canvas.width * scale);
|
||||
thumbCanvas.height = Math.round(canvas.height * scale);
|
||||
const thumbCtx = thumbCanvas.getContext('2d');
|
||||
if (thumbCtx) {
|
||||
thumbCtx.drawImage(canvas, 0, 0, thumbCanvas.width, thumbCanvas.height);
|
||||
}
|
||||
const thumbnailBase64 = thumbCanvas.toDataURL('image/jpeg', 0.7).split(',')[1];
|
||||
|
||||
stopCamera();
|
||||
oncomplete(base64, mediaType, thumbnailBase64);
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
cancelled = true;
|
||||
stopCamera();
|
||||
oncancel();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
startCamera();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
stopCamera();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="fixed inset-0 z-50 flex flex-col bg-black" data-camera-ui>
|
||||
{#if error}
|
||||
<div class="flex flex-1 flex-col items-center justify-center p-4">
|
||||
<p class="mb-4 text-center text-sm text-red-400">{error}</p>
|
||||
<button onclick={handleCancel} class="rounded bg-neutral-700 px-4 py-2 text-sm text-white">
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative flex-1">
|
||||
<video bind:this={videoElement} autoplay playsinline muted class="h-full w-full object-cover"
|
||||
></video>
|
||||
<div class="absolute inset-0 flex items-center justify-center">
|
||||
<span class="text-8xl font-bold text-white drop-shadow-lg">{count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="p-4 text-center">
|
||||
<button onclick={handleCancel} class="text-sm text-neutral-400">Cancel</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user