diff options
feat: initial commit
Diffstat (limited to 'src/lib/Renderer')
-rw-r--r-- | src/lib/Renderer/Renderer.svelte | 146 |
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> |