189 lines
6.3 KiB
Svelte
189 lines
6.3 KiB
Svelte
<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> |