Refactor into classes, ask user when leaving page
This commit is contained in:
parent
4feab31ef0
commit
ef6fbd3e04
@ -1,4 +1,4 @@
|
||||
class Notifier {
|
||||
export class Notifier {
|
||||
constructor() {
|
||||
this.checkbox = document.getElementById("notify-checkbox");
|
||||
|
||||
|
||||
13
public/assets/scripts/ProgressBar.js
Normal file
13
public/assets/scripts/ProgressBar.js
Normal file
@ -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)`;
|
||||
}
|
||||
}
|
||||
85
public/assets/scripts/VideoCompressor.js
Normal file
85
public/assets/scripts/VideoCompressor.js
Normal file
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
});
|
||||
|
||||
await ffmpeg.load({ coreURL: "/assets/scripts/core/package/dist/umd/ffmpeg-core.js" });
|
||||
|
||||
await ffmpeg.writeFile(file.name, await readFromBlob(file));
|
||||
|
||||
// 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");
|
||||
if (!url) return;
|
||||
|
||||
notifier.notifyFinished();
|
||||
|
||||
location.href = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" }));
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
ffmpeg.terminate();
|
||||
location.href = url;
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
|
||||
showSection("file-picker");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function updateProgress(progress) {
|
||||
const percent = (progress * 100).toFixed(1) + "%";
|
||||
document.getElementById("cancel").onclick = () => {
|
||||
videoCompressor.stop();
|
||||
|
||||
document.getElementById("progress-percentage").innerText = percent;
|
||||
document.getElementById("progress-indicator").style.clipPath = `rect(0 ${percent} 100% 0)`;
|
||||
}
|
||||
showSection("file-picker");
|
||||
};
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
@ -4,9 +4,8 @@
|
||||
<meta charset="UTF-8">
|
||||
<title>Video Compressor</title>
|
||||
<script defer src="/assets/scripts/ffmpeg/package/dist/umd/ffmpeg.js"></script>
|
||||
<script defer src="/assets/scripts/Notifier.js"></script>
|
||||
<script defer src="/assets/scripts/ui.js"></script>
|
||||
<script defer src="/assets/scripts/main.js"></script>
|
||||
<script defer type="module" src="/assets/scripts/main.js"></script>
|
||||
<link rel="stylesheet" href="/assets/style/main.css">
|
||||
</head>
|
||||
<body>
|
||||
@ -14,25 +13,25 @@
|
||||
<p>Compress video files to a specific file size, so you can upload them to your favorite social media and messaging apps!</p>
|
||||
|
||||
<section id="file-picker-section">
|
||||
<div id="file-drop-area" onclick="openFileSelector()" onkeydown="['Enter', 'Space'].includes(event.code) && openFileSelector()" tabindex="0">
|
||||
<div id="file-drop-area" onkeydown="['Enter', 'Space'].includes(event.code) && openFileSelector()" tabindex="0">
|
||||
<span>Drag and drop a file here!</span>
|
||||
</div>
|
||||
<input id="file-input" oninput="selectFile()" type="file" accept="video/*" tabindex="-1">
|
||||
<input id="file-input" type="file" accept="video/*" tabindex="-1">
|
||||
<div id="file-input-spacing" style="margin-top: -1rem">
|
||||
<button class="simple">.</button>
|
||||
</div>
|
||||
|
||||
<video id="uploaded-video" controls autoplay></video>
|
||||
<button id="change-file" onclick="openFileSelector()" class="simple">Change video</button>
|
||||
<button id="change-file" class="simple">Change video</button>
|
||||
|
||||
<input id="filesize" type="number" value="10" size="3" placeholder="#" style="margin-top: 1rem"><!--
|
||||
|
||||
--><select id="filesize-unit">
|
||||
<option value="K">KB</option>
|
||||
<option value="M" selected>MB</option>
|
||||
<option value="kb">KB</option>
|
||||
<option value="mb" selected>MB</option>
|
||||
</select>
|
||||
|
||||
<button id="compress" onclick="compress()" class="primary">Go!</button>
|
||||
<button id="compress" class="primary">Go!</button>
|
||||
</section>
|
||||
|
||||
<section id="loading-section" style="opacity: 0; display: none;">
|
||||
@ -47,7 +46,7 @@
|
||||
|
||||
<p><label><input id="notify-checkbox" type="checkbox"> Notify on finish</label></p>
|
||||
|
||||
<button id="cancel" onclick="cancel()" class="simple">Cancel</button>
|
||||
<button id="cancel" class="simple">Cancel</button>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user