test-card/src/routes/(pages)/camera/+page.svelte

268 lines
5.9 KiB
Svelte

<script lang="ts">
import { run } from 'svelte/legacy';
import { onDestroy, onMount } from 'svelte';
import { browser } from '$app/environment';
import debug from 'debug';
import { i18n } from '$lib/i18n';
const dbg = debug('app:camera');
let video: HTMLVideoElement = $state();
let devices: MediaDeviceInfo[] = $state([]);
let currentDevice: string | undefined = $state();
let requestResolution: [number, number] | 'auto' = $state('auto');
let requestFramerate: number | 'auto' = $state('auto');
let deviceInfo: {
resolution?: string;
frameRate?: number;
} = $state({});
let snapshot: string | undefined = $state();
let flipped = $state(false);
run(() => {
dbg('devices %O', devices);
});
run(() => {
dbg('currentDevice %s', currentDevice);
});
onMount(() => {
refreshDevices();
video.addEventListener('playing', () => {
if (browser && video?.srcObject instanceof MediaStream) {
deviceInfo = {
resolution: `${video.videoWidth}x${video.videoHeight}`,
frameRate: video?.srcObject?.getVideoTracks()[0]?.getSettings().frameRate
};
}
});
});
onDestroy(() => {
if (browser && video?.srcObject instanceof MediaStream) {
video.srcObject.getTracks().forEach((t) => t.stop());
}
});
async function refreshDevices() {
devices = (await navigator.mediaDevices.enumerateDevices()).filter(
(d) => d.kind === 'videoinput'
);
if (!currentDevice) {
currentDevice = devices[0]?.deviceId;
}
}
run(() => {
if (currentDevice) {
navigator.mediaDevices
.getUserMedia({
video: {
deviceId: currentDevice,
width: requestResolution === 'auto' ? undefined : requestResolution[0],
height: requestResolution === 'auto' ? undefined : requestResolution[1],
frameRate: requestFramerate === 'auto' ? undefined : requestFramerate
}
})
.then((stream) => {
video.srcObject = stream;
refreshDevices();
});
}
});
async function takeSnapshot() {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
const ctx = canvas.getContext('2d');
if (!ctx) return;
if (flipped) {
ctx.scale(-1, 1);
ctx.translate(-canvas.width, 0);
}
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
snapshot = canvas.toDataURL('image/png');
}
</script>
<h2><i class="ti ti-camera"></i> {$i18n.t('Camera test')}</h2>
<div class="controls">
<label>
{$i18n.t('Device')}
<select bind:value={currentDevice} disabled={!devices.length}>
{#each devices as device}
<option value={device.deviceId}>{device.label || '???'}</option>
{:else}
<option>{$i18n.t('No camera found')}</option>
{/each}
</select>
</label>
<button onclick={refreshDevices}>
<i class="ti ti-refresh"></i>
{$i18n.t('Refresh')}
</button>
<div class="separator"></div>
<label>
{$i18n.t('Resolution')}
<select bind:value={requestResolution}>
<option value="auto">Auto</option>
<option value={[4096, 2160]}>4096x2160</option>
<option value={[3840, 2160]}>3840x2160</option>
<option value={[1920, 1080]}>1920x1080</option>
<option value={[1280, 720]}>1280x720</option>
<option value={[640, 480]}>640x480</option>
<option value={[320, 240]}>320x240</option>
</select>
</label>
<label>
{$i18n.t('Frame rate')}
<select bind:value={requestFramerate}>
<option value="auto">Auto</option>
<option value={120}>120 fps</option>
<option value={60}>60 fps</option>
<option value={30}>30 fps</option>
<option value={15}>15 fps</option>
<option value={10}>10 fps</option>
<option value={5}>5 fps</option>
</select>
</label>
</div>
<div class="display" class:snapshot={Boolean(snapshot)}>
<!-- svelte-ignore a11y_media_has_caption -->
<video class:flipped bind:this={video} autoplay class:unloaded={!currentDevice}></video>
{#if snapshot}
<!-- svelte-ignore a11y_missing_attribute -->
<!--suppress HtmlRequiredAltAttribute -->
<img src={snapshot} />
<button onclick={() => (snapshot = undefined)}><i class="ti ti-x"></i></button>
{/if}
</div>
<footer>
{#if !currentDevice}
<span class="subdued">{$i18n.t('No camera selected')}</span>
{:else}
<ul>
{#key currentDevice}
<li>
{$i18n.t('Resolution')}: <strong>{deviceInfo.resolution || '???'}</strong>
</li>
<li>
{$i18n.t('Frame rate')}: <strong>{deviceInfo.frameRate || '???'}</strong>
</li>
{/key}
</ul>
<div class="controls">
<button onclick={takeSnapshot}>
<i class="ti ti-camera"></i>
{$i18n.t('Take picture')}
</button>
<button onclick={() => (flipped = !flipped)}>
<i class="ti ti-flip-vertical"></i>
{#if flipped}
{$i18n.t('Unflip image')}
{:else}
{$i18n.t('Flip image')}
{/if}
</button>
</div>
{/if}
</footer>
<style>
.controls {
display: flex;
align-items: end;
justify-content: stretch;
gap: 1em;
& label:first-child {
flex-grow: 1;
min-width: 0;
}
}
select {
background: black;
color: white;
padding: 0.25em 0.5em;
border-radius: 0.25em;
border: 1px solid white;
}
label {
display: flex;
flex-direction: column;
gap: 0.2em;
font-size: 0.8em;
& select {
font-size: initial;
}
}
.display {
position: relative;
display: flex;
flex-direction: column;
min-height: 0;
flex-grow: 1;
justify-content: center;
& img {
object-fit: contain;
}
& button {
position: absolute;
top: 1em;
right: 1em;
}
&.snapshot {
& video {
display: none;
}
}
}
video {
min-width: 0;
min-height: 0;
max-width: 100%;
max-height: 100%;
margin: 1em 0;
&.unloaded {
background: repeating-linear-gradient(45deg, gray, gray 20px, darkgray 20px, darkgray 40px);
flex-grow: 1;
}
&.flipped {
transform: scaleX(-1);
}
}
footer {
display: flex;
justify-content: space-between;
align-items: center;
}
ul {
list-style: none;
margin: 0;
padding: 0;
display: inline-flex;
gap: 1rem;
}
.subdued {
opacity: 0.8;
}
</style>