<script lang="ts"> import { onMount } from "svelte"; import { hashBuffer } from "$lib/hash"; import { dateDiff } from "$lib/date"; import { getKeyVault, loggedIn } from "$lib/auth"; import { encrypt, uint8ToHex } from "$lib/crypto"; import { lang } from "$lib/lang"; type StatusT = "pending" | "active" | "error"; let status = $state<StatusT>("pending"); let errors = $state<string[]>([]); let now = $state(new Date()); let recordingStart = $state<Date | null>(null); let recordingURL = $state<string | null>(null); // svelte-ignore non_reactive_update let video: HTMLVideoElement, finalVideo: HTMLVideoElement; let recorder: MediaRecorder | null = null; onMount(() => { if (typeof window !== 'object') return; if (!loggedIn()) { window.location.href = '/login'; } }); onMount(() => { if (typeof window !== 'object') return; const interval = setInterval(() => { now = new Date(); }, 100); return () => clearInterval(interval); }); onMount(async () => { if (typeof window !== 'object') return; if (!navigator || !navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { return; } const WIDTH = 320; const HEIGHT = 480; const FPS = 24; const stream = await navigator.mediaDevices.getUserMedia({ video: { width: WIDTH, height: HEIGHT, frameRate: FPS, facingMode: 'user' }, audio: true, }); video.srcObject = stream; recorder = new MediaRecorder(stream, { mimeType: 'video/mp4; codecs="avc1.42001E"' }); recorder.addEventListener('dataavailable', async (e) => { const inputBlob = new Blob([e.data], { type: 'video/mp4' }); const convertRes = await fetch('/api/convert', { method: 'POST', body: inputBlob, }); if (!convertRes.ok) return; const convertBlob = await convertRes.blob(); recordingURL = URL.createObjectURL(convertBlob); setTimeout(() => finalVideo.scrollIntoView(), 150); }) }); const start = () => { recordingStart = new Date(); recorder?.start(); }; const stop = () => { recordingStart = null; recorder?.stop(); }; const submit = async () => { if (!recordingURL) return; const blob = await (await fetch(recordingURL)).blob(); const buffer = await blob.arrayBuffer(); console.log('buffer', buffer); const vault = getKeyVault()!; const encrypted = await encrypt(vault, new Uint8Array(buffer)); console.log('encrypted', encrypted); const res = await fetch(`/api/today?pubkey=${uint8ToHex(vault.publicKey)}`, { method: 'POST', body: encrypted, headers: { 'Content-Type': 'application/octet-stream', }, }); if (res.ok) { window.location.href = '/log'; } }; </script> <div class="min-rh-screen"> <div class="rh-screen p-4 min-h-0 grid place-items-center"> <video autoplay muted bind:this={video} class="opacity-0 h-full w-full min-h-0 max-w-[70vh] object-cover rounded-[40px] bg-[#303030] fade-in" style="animation-delay: 500ms;" > <track kind="captions" /> </video> <div class="absolute top-0 left-0 right-0 p-6 m-auto w-fit scale-in" style="animation-delay: 50ms;" > <div class="rounded-3xl bg-white px-3 py-1 font-serif text-center w-fit"> <h1 class="font-bold text-xl">{now.toLocaleDateString(lang(), { dateStyle: 'long' })}</h1> <h2 class="text-sm">{now.toLocaleTimeString(lang())}</h2> </div> {#if !!recordingStart} <div class="h-1"></div> <div class="rounded-3xl bg-white px-3 py-1 font-serif flex gap-1 items-center w-fit m-auto scale-in"> <svg class="blink" xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 0 24 24" width="24px" fill="red"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zm0-5C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"/></svg> <div> <span class="text-md"> <!-- <span class="font-bold">Recording</span> --> <span>{dateDiff(now, recordingStart).toLocaleTimeString(lang(), { minute: 'numeric', second: 'numeric' })}</span> </span> </div> </div> {/if} </div> <div class="absolute bottom-0 left-0 right-0 p-6 m-auto w-fit fade-in" style="animation-delay: 50ms;" > <button class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer ${recordingStart ? 'bg-[#e14c2f]' : 'bg-[#2596be]'}`} style="transition-duration: 250ms;" onclick={() => recordingStart ? stop() : start()} > {recordingStart ? 'Stop' : 'Start'} </button> </div> </div> {#if recordingURL} <div class="h-8"></div> <div class="p-4 min-h-0 grid place-items-center"> <video controls bind:this={finalVideo} src={recordingURL} class="opacity-0 h-full w-full max-w-[50vh] min-h-0 w-full object-cover rounded-[40px] bg-[#303030] fade-in" style="aspect-ratio: 320 / 480;" > <track kind="captions" /> </video> <div class="h-2"></div> <button class={`font-serif px-5 py-3 rounded-3xl text-white font-bold text-2xl cursor-pointer bg-[#2596be]`} style="transition-duration: 250ms;" onclick={submit} > Submit </button> </div> {/if} </div> <!-- <canvas class="hidden" bind:this={renderCanvas}></canvas> <canvas class="hidden" bind:this={captureCanvas}></canvas> <video class="hidden" bind:this={sourceVideo}> <track kind="captions" /> </video> --> <style> </style>