aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/Renderer/Renderer.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/Renderer/Renderer.svelte')
-rw-r--r--src/lib/Renderer/Renderer.svelte146
1 files changed, 146 insertions, 0 deletions
diff --git a/src/lib/Renderer/Renderer.svelte b/src/lib/Renderer/Renderer.svelte
new file mode 100644
index 0000000..72c270b
--- /dev/null
+++ b/src/lib/Renderer/Renderer.svelte
@@ -0,0 +1,146 @@
+<script lang="ts">
+ import { onDestroy } from 'svelte';
+ import type { VideoConstructor } from '../Player/Video';
+ import { FFmpeg, type LogEvent } from '@ffmpeg/ffmpeg';
+ import { fetchFile, toBlobURL } from '@ffmpeg/util';
+
+ let {
+ Video: VideoImplementation
+ }: {
+ Video: VideoConstructor;
+ } = $props();
+ let canvas = $state(null as null | HTMLCanvasElement);
+
+ let frame = $state(0);
+ let frameCount = $state(0);
+ let active = false;
+
+ let lastAnimationFrame = 0;
+ let startedAt = 0;
+ let message = $state('Waiting');
+ let videoUrl = $state(null as null | string);
+
+ const ffmpegBaseURL = 'https://unpkg.com/@ffmpeg/core-mt@0.12.6/dist/esm';
+
+ const start = async (format = 'mp4') => {
+ active = true;
+ startedAt = performance.now();
+
+ const ffmpeg = new FFmpeg();
+ message = 'Loading ffmpeg-core.js';
+ ffmpeg.on('log', ({ message: msg }: LogEvent) => {
+ message = msg;
+ console.log(message);
+ });
+ await ffmpeg.load({
+ coreURL: await toBlobURL(`${ffmpegBaseURL}/ffmpeg-core.js`, 'text/javascript'),
+ wasmURL: await toBlobURL(`${ffmpegBaseURL}/ffmpeg-core.wasm`, 'application/wasm'),
+ workerURL: await toBlobURL(`${ffmpegBaseURL}/ffmpeg-core.worker.js`, 'text/javascript')
+ });
+
+ message = 'Making directory';
+ await ffmpeg.createDir('frames');
+ message = 'Preparing Canvas';
+ const c = canvas!;
+ const video = new VideoImplementation(c);
+ video['_isInit'] = true;
+ await video.init();
+ video['_isInit'] = false;
+ frameCount = video.length;
+ message = 'Rendering first few frames...';
+ for (frame = 0; frame <= frameCount; frame++) {
+ if (!active) return;
+ await video.renderFrame({
+ frames: frame,
+ milliseconds: (frame / video.fps) * 1000,
+ seconds: frame / video.fps
+ });
+ // TODO: see if we can pipe this into an active ffmpeg instead of writing a file then running a command after
+ const file = await fetchFile(c.toDataURL());
+ await ffmpeg.writeFile('frames/f' + frame.toString().padStart(10, '0') + '.png', file);
+ if (performance.now() > lastAnimationFrame + 33) {
+ await new Promise((rs) => requestAnimationFrame(rs));
+ lastAnimationFrame = performance.now();
+ message = `Rendering to pngs - ${frame}/${frameCount} frames | running for ${(
+ Math.floor(lastAnimationFrame - startedAt) / 1000
+ ).toFixed(3)} seconds`;
+ }
+ }
+
+ message = 'Start transcoding';
+ await ffmpeg.exec([
+ '-framerate',
+ video.fps.toString(),
+ '-pattern_type',
+ 'sequence',
+ '-start_number',
+ '0',
+ '-pattern_type',
+ 'glob',
+ '-i',
+ 'frames/f*.png',
+ // 'frames/f%04d.png',
+ 'middle.' + format
+ ]);
+ message = 'Disposing pngs';
+ for (const i of await ffmpeg.listDir('frames'))
+ if (!i.isDir) await ffmpeg.deleteFile('frames/' + i.name);
+ await ffmpeg.deleteDir('frames/');
+ if (video.audioUrl) {
+ message = 'Fetching Audio';
+ await ffmpeg.writeFile('audio.' + video.audioUrl[0], await fetchFile(video.audioUrl[1]));
+ message = 'Merging Audio';
+ await ffmpeg.exec([
+ '-i',
+ 'middle.' + format,
+ '-r',
+ video.fps.toString(),
+ '-i',
+ 'audio.' + video.audioUrl[0],
+ '-c:v',
+ 'copy',
+ '-map',
+ '0:v:0',
+ '-map',
+ '1:a:0',
+ '-frames:v',
+ video.length.toString(),
+ 'output.' + format
+ ]);
+ message = 'Removing videoless file';
+ await ffmpeg.deleteFile('middle.' + format);
+ } else await ffmpeg.rename('middle.' + format, 'output.' + format);
+ message = 'Reading File';
+ const data = await ffmpeg.readFile('output.' + format);
+ console.log('done');
+ videoUrl = URL.createObjectURL(
+ // @ts-ignore bufferlike is good enuf
+ new Blob([(data as Uint8Array).buffer], { type: 'video/' + format })
+ );
+ message = 'Disposing ffmpeg state';
+ await ffmpeg.deleteFile('output.' + format);
+ location.href = videoUrl;
+ };
+ $effect(() => {
+ if (canvas && VideoImplementation) start();
+ });
+ onDestroy(() => (active = false));
+</script>
+
+<div class="flex flex-col h-screen w-screen">
+ {#if videoUrl}
+ <!-- svelte-ignore a11y_media_has_caption -->
+ <video src={videoUrl} controls class="max-w-screen max-h-screen flex-1"></video>
+ {:else}
+ <div class="flex-1 relative">
+ <div class="absolute top-0 left-0 w-full h-full flex items-center justify-center">
+ <canvas class="pointer-events-none bg-black" bind:this={canvas}>
+ Your browser doesn't support the canvas API.
+ </canvas>
+ </div>
+ </div>
+ {/if}
+ <p class="p-4">
+ {message}
+ </p>
+</div>