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() {
|
constructor() {
|
||||||
this.checkbox = document.getElementById("notify-checkbox");
|
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 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();
|
document.getElementById("uploaded-video").pause();
|
||||||
showSection("loading");
|
showSection("loading");
|
||||||
|
|
||||||
@ -11,67 +15,42 @@ async function compress() {
|
|||||||
const file = document.getElementById("file-input").files[0];
|
const file = document.getElementById("file-input").files[0];
|
||||||
|
|
||||||
const videoLength = document.getElementById("uploaded-video").duration;
|
const videoLength = document.getElementById("uploaded-video").duration;
|
||||||
let targetFilesize; // Stored in kBit
|
|
||||||
|
let targetFilesize;
|
||||||
switch (filesizeUnit) {
|
switch (filesizeUnit) {
|
||||||
case "K": targetFilesize = filesize * 8; break;
|
case "kb": targetFilesize = filesize * 1000; break;
|
||||||
case "M": targetFilesize = filesize * 8000; break;
|
case "mb": targetFilesize = filesize * 1000000; break;
|
||||||
}
|
}
|
||||||
|
|
||||||
ffmpeg.on("log", event => {
|
try {
|
||||||
console.log("[ffmpeg]", event.type, event.message);
|
const url = await videoCompressor.compress(file, targetFilesize, videoLength);
|
||||||
});
|
|
||||||
|
|
||||||
updateProgress(0);
|
if (!url) return;
|
||||||
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");
|
|
||||||
|
|
||||||
notifier.notifyFinished();
|
notifier.notifyFinished();
|
||||||
|
|
||||||
location.href = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" }));
|
location.href = url;
|
||||||
}
|
} catch (e) {
|
||||||
|
alert(e.message);
|
||||||
function cancel() {
|
|
||||||
ffmpeg.terminate();
|
|
||||||
|
|
||||||
showSection("file-picker");
|
showSection("file-picker");
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function updateProgress(progress) {
|
document.getElementById("cancel").onclick = () => {
|
||||||
const percent = (progress * 100).toFixed(1) + "%";
|
videoCompressor.stop();
|
||||||
|
|
||||||
document.getElementById("progress-percentage").innerText = percent;
|
showSection("file-picker");
|
||||||
document.getElementById("progress-indicator").style.clipPath = `rect(0 ${percent} 100% 0)`;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
function openFileSelector() {
|
function openFileSelector() {
|
||||||
document.getElementById("file-input").click();
|
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(() => {
|
setTimeout(() => {
|
||||||
const file = document.getElementById("file-input").files[0];
|
const file = document.getElementById("file-input").files[0];
|
||||||
|
|
||||||
@ -81,28 +60,8 @@ function selectFile() {
|
|||||||
hideElements("#file-drop-area", "#file-input-spacing");
|
hideElements("#file-drop-area", "#file-input-spacing");
|
||||||
showElements("#uploaded-video", "#change-file");
|
showElements("#uploaded-video", "#change-file");
|
||||||
}, 200);
|
}, 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">
|
<meta charset="UTF-8">
|
||||||
<title>Video Compressor</title>
|
<title>Video Compressor</title>
|
||||||
<script defer src="/assets/scripts/ffmpeg/package/dist/umd/ffmpeg.js"></script>
|
<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/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">
|
<link rel="stylesheet" href="/assets/style/main.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<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>
|
<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">
|
<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>
|
<span>Drag and drop a file here!</span>
|
||||||
</div>
|
</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">
|
<div id="file-input-spacing" style="margin-top: -1rem">
|
||||||
<button class="simple">.</button>
|
<button class="simple">.</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<video id="uploaded-video" controls autoplay></video>
|
<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"><!--
|
<input id="filesize" type="number" value="10" size="3" placeholder="#" style="margin-top: 1rem"><!--
|
||||||
|
|
||||||
--><select id="filesize-unit">
|
--><select id="filesize-unit">
|
||||||
<option value="K">KB</option>
|
<option value="kb">KB</option>
|
||||||
<option value="M" selected>MB</option>
|
<option value="mb" selected>MB</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<button id="compress" onclick="compress()" class="primary">Go!</button>
|
<button id="compress" class="primary">Go!</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="loading-section" style="opacity: 0; display: none;">
|
<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>
|
<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>
|
</section>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user