aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/Renderer/Renderer.svelte
blob: 3d661d8470d5172f4f09504eed7823bc61df0e46 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
<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>

<svelte:head>
	<title>{message} - Videotool Renderer</title>
</svelte:head>

<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>