From ef6fbd3e045772400a929dd6d6e29cbdbcf754c0 Mon Sep 17 00:00:00 2001 From: Reimar Date: Tue, 6 Jan 2026 10:36:18 +0100 Subject: [PATCH] Refactor into classes, ask user when leaving page --- public/assets/scripts/Notifier.js | 2 +- public/assets/scripts/ProgressBar.js | 13 ++++ public/assets/scripts/VideoCompressor.js | 85 ++++++++++++++++++++ public/assets/scripts/main.js | 99 +++++++----------------- public/index.html | 17 ++-- 5 files changed, 136 insertions(+), 80 deletions(-) create mode 100644 public/assets/scripts/ProgressBar.js create mode 100644 public/assets/scripts/VideoCompressor.js diff --git a/public/assets/scripts/Notifier.js b/public/assets/scripts/Notifier.js index 8d54d00..f859ec6 100644 --- a/public/assets/scripts/Notifier.js +++ b/public/assets/scripts/Notifier.js @@ -1,4 +1,4 @@ -class Notifier { +export class Notifier { constructor() { this.checkbox = document.getElementById("notify-checkbox"); diff --git a/public/assets/scripts/ProgressBar.js b/public/assets/scripts/ProgressBar.js new file mode 100644 index 0000000..63ae104 --- /dev/null +++ b/public/assets/scripts/ProgressBar.js @@ -0,0 +1,13 @@ +export class ProgressBar { + constructor() { + this.percentage = document.getElementById("progress-percentage"); + this.indicator = document.getElementById("progress-indicator"); + } + + setProgress(progress) { + const percent = (progress * 100).toFixed(1) + "%"; + + this.percentage.innerText = percent; + this.indicator.style.clipPath = `rect(0 ${percent} 100% 0)`; + } +} diff --git a/public/assets/scripts/VideoCompressor.js b/public/assets/scripts/VideoCompressor.js new file mode 100644 index 0000000..b67b29f --- /dev/null +++ b/public/assets/scripts/VideoCompressor.js @@ -0,0 +1,85 @@ +import { ERROR_TERMINATED } from "./ffmpeg/package/dist/esm/errors.js"; + +export class VideoCompressor { + constructor(progressBar) { + this.progressBar = progressBar; + this.ffmpeg = new FFmpegWASM.FFmpeg(); + this.inProgress = false; + } + + async compress(file, filesizeBytes, duration) { + this.progressBar.setProgress(0); + this.inProgress = true; + + const progressCallback = event => this.progressBar.setProgress(event.progress); + this.ffmpeg.on("progress", progressCallback); + + const filesizeKbit = filesizeBytes * 0.008; + + // Calculate target bitrate to create a video file with desired file size (https://trac.ffmpeg.org/wiki/Encode/H.264#twopass) + let bitrate = filesizeKbit / duration; + bitrate -= 128; // Subtract audio bitrate + bitrate -= (bitrate / 100) * 5; // Subtract 5% to compensate for overhead + bitrate = Math.floor(bitrate); + + if (bitrate <= 0) { + this.inProgress = false; + throw new Error("Selected file size is too low for this video"); + } + + console.debug("Target bitrate:", bitrate, "Video length:", duration); + + // Run compression + try { + await this.ffmpeg.load({ coreURL: "/assets/scripts/core/package/dist/umd/ffmpeg-core.js" }); + + await this.ffmpeg.writeFile(file.name, await this.readFromBlob(file)); + + await this.ffmpeg.exec(["-i", file.name, "-preset", "ultrafast", "-c:v", "libx264", "-b:v", bitrate + "k", "-c:a", "copy", "-b:a", "128k", "compressed.mp4"]); + } catch (e) { + // Ignore termination error + if (e.message === ERROR_TERMINATED.message) { + this.inProgress = false; + return null; + } + + throw e; + } + + const video = await this.ffmpeg.readFile("compressed.mp4"); + const url = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" })); + + // Clean up + this.ffmpeg.off("progress", progressCallback); + this.inProgress = false; + + return url; + } + + stop() { + this.ffmpeg.terminate(); + this.inProgress = false; + } + + // https://github.com/ffmpegwasm/ffmpeg.wasm/blob/main/packages/util/src/index.ts + readFromBlob(blob) { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + + fileReader.onload = () => { + const { result } = fileReader; + if (result instanceof ArrayBuffer) { + resolve(new Uint8Array(result)); + } else { + resolve(new Uint8Array()); + } + }; + + fileReader.onerror = (event) => { + reject(Error(`File could not be read! Code=${event?.target?.error?.code || -1}`)); + }; + + fileReader.readAsArrayBuffer(blob); + }); + } +} diff --git a/public/assets/scripts/main.js b/public/assets/scripts/main.js index 513191a..4277c16 100644 --- a/public/assets/scripts/main.js +++ b/public/assets/scripts/main.js @@ -1,8 +1,12 @@ -const ffmpeg = new FFmpegWASM.FFmpeg(); +import { Notifier } from "./Notifier.js"; +import { ProgressBar } from "./ProgressBar.js"; +import { VideoCompressor } from "./VideoCompressor.js"; const notifier = new Notifier(); +const progressBar = new ProgressBar(); +const videoCompressor = new VideoCompressor(progressBar); -async function compress() { +document.getElementById("compress").onclick = async () => { document.getElementById("uploaded-video").pause(); showSection("loading"); @@ -11,67 +15,42 @@ async function compress() { const file = document.getElementById("file-input").files[0]; const videoLength = document.getElementById("uploaded-video").duration; - let targetFilesize; // Stored in kBit + + let targetFilesize; switch (filesizeUnit) { - case "K": targetFilesize = filesize * 8; break; - case "M": targetFilesize = filesize * 8000; break; + case "kb": targetFilesize = filesize * 1000; break; + case "mb": targetFilesize = filesize * 1000000; break; } - ffmpeg.on("log", event => { - console.log("[ffmpeg]", event.type, event.message); - }); + try { + const url = await videoCompressor.compress(file, targetFilesize, videoLength); - updateProgress(0); - ffmpeg.on("progress", event => { - updateProgress(event.progress); - }); + if (!url) return; - await ffmpeg.load({ coreURL: "/assets/scripts/core/package/dist/umd/ffmpeg-core.js" }); + notifier.notifyFinished(); - await ffmpeg.writeFile(file.name, await readFromBlob(file)); + location.href = url; + } catch (e) { + alert(e.message); - // Calculate target bitrate to create a video file with desired file size (https://trac.ffmpeg.org/wiki/Encode/H.264#twopass) - let bitrate = targetFilesize / videoLength; - bitrate -= 128; // Subtract audio bitrate - bitrate -= (bitrate / 100) * 5; // Subtract 5% to compensate for overhead - bitrate = Math.floor(bitrate); - - if (bitrate <= 0) { showSection("file-picker"); - alert("Selected file size is too low for this video"); - - return; } +}; - console.debug("Target bitrate:", bitrate, "Video length:", videoLength); - - await ffmpeg.exec(["-i", file.name, "-preset", "ultrafast", "-c:v", "libx264", "-b:v", bitrate + "k", "-c:a", "copy", "-b:a", "128k", "compressed.mp4"]); - - const video = await ffmpeg.readFile("compressed.mp4"); - - notifier.notifyFinished(); - - location.href = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" })); -} - -function cancel() { - ffmpeg.terminate(); +document.getElementById("cancel").onclick = () => { + videoCompressor.stop(); showSection("file-picker"); -} - -function updateProgress(progress) { - const percent = (progress * 100).toFixed(1) + "%"; - - document.getElementById("progress-percentage").innerText = percent; - document.getElementById("progress-indicator").style.clipPath = `rect(0 ${percent} 100% 0)`; -} +}; function openFileSelector() { document.getElementById("file-input").click(); } -function selectFile() { +document.getElementById("file-drop-area").onclick = openFileSelector; +document.getElementById("change-file").onclick = openFileSelector; + +document.getElementById("file-input").oninput = () => { setTimeout(() => { const file = document.getElementById("file-input").files[0]; @@ -81,28 +60,8 @@ function selectFile() { hideElements("#file-drop-area", "#file-input-spacing"); showElements("#uploaded-video", "#change-file"); }, 200); -} - -// https://github.com/ffmpegwasm/ffmpeg.wasm/blob/main/packages/util/src/index.ts -function readFromBlob(blob) { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = () => { - const { result } = fileReader; - if (result instanceof ArrayBuffer) { - resolve(new Uint8Array(result)); - } else { - resolve(new Uint8Array()); - } - }; - fileReader.onerror = (event) => { - reject( - Error( - `File could not be read! Code=${event?.target?.error?.code || -1}` - ) - ); - }; - fileReader.readAsArrayBuffer(blob); - }); -} +}; +window.onbeforeunload = event => { + if (videoCompressor.inProgress) event.preventDefault(); +}; diff --git a/public/index.html b/public/index.html index b24085a..cd9ef45 100644 --- a/public/index.html +++ b/public/index.html @@ -4,9 +4,8 @@ Video Compressor - - + @@ -14,25 +13,25 @@

Compress video files to a specific file size, so you can upload them to your favorite social media and messaging apps!

-
+
Drag and drop a file here!
- +
- + - +