Compare commits

...

6 Commits

12 changed files with 564 additions and 291 deletions

View File

@ -13,4 +13,3 @@ mkdir -p public/assets/scripts
fetch ffmpeg 0.12.15
fetch core 0.12.10

View File

@ -1,4 +1,4 @@
class Notifier {
export class Notifier {
constructor() {
this.checkbox = document.getElementById("notify-checkbox");

View File

@ -0,0 +1,18 @@
export class ProgressBar {
constructor() {
this.percentage = document.getElementById("progress-percentage");
this.indicator = document.getElementById("progress-indicator");
}
setStatus(status) {
this.percentage.innerText = status;
this.indicator.style.clipPath = "rect(0 0 100% 0)";
}
setProgress(progress) {
const percent = (progress * 100).toFixed(1) + "%";
this.percentage.innerText = percent;
this.indicator.style.clipPath = `rect(0 ${percent} 100% 0)`;
}
}

View File

@ -0,0 +1,94 @@
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.setStatus("Initializing...");
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 resultFile = new File([video.buffer], this.generateFileName(file.name), { type: "video/mp4" });
// Clean up
this.ffmpeg.off("progress", progressCallback);
this.inProgress = false;
return resultFile;
}
stop() {
this.ffmpeg.terminate();
this.inProgress = false;
}
generateFileName(filename) {
let name = filename.replace(/\.\w+$/, "");
if (/\s/.test(name)) name += " compressed";
else name += "_compressed";
return name + ".mp4";
}
// 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);
});
}
}

View File

@ -1,78 +1,110 @@
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() {
// Reset file input cache after reload
document.getElementById("file-input").value = "";
async function compress(filesize, filesizeUnit) {
document.getElementById("uploaded-video").pause();
showSection("loading");
const filesize = document.getElementById("filesize").value;
const filesizeUnit = document.getElementById("filesize-unit").value;
const file = document.getElementById("file-input").files[0];
const videoLength = document.getElementById("uploaded-video").duration;
let targetFilesize; // Stored in kBit
switch (filesizeUnit) {
case "K": targetFilesize = filesize * 8; break;
case "M": targetFilesize = filesize * 8000; break;
if (!file) {
alert("Please select a file");
return;
}
ffmpeg.on("log", event => {
console.log("[ffmpeg]", event.type, event.message);
});
const videoLength = document.getElementById("uploaded-video").duration;
let pass = 1;
let targetFilesize;
switch (filesizeUnit) {
case "kb": targetFilesize = filesize * 1000; break;
case "mb": targetFilesize = filesize * 1000000; break;
}
updateProgress(0, pass);
ffmpeg.on("progress", event => {
updateProgress(event.progress, pass);
});
showSection("loading");
await ffmpeg.load({ coreURL: "/assets/scripts/core/package/dist/umd/ffmpeg-core.js" });
let result;
try {
result = await videoCompressor.compress(file, targetFilesize, videoLength);
} catch (e) {
alert(e.message);
await ffmpeg.writeFile(file.name, await readFromBlob(file));
showSection("file-picker");
// Use Two-Pass to create a video file with desired file size
// https://trac.ffmpeg.org/wiki/Encode/H.264#twopass
return;
}
const bitrate = Math.floor(targetFilesize / videoLength) - 128; // Subtract audio bitrate
console.debug("Target bitrate:", bitrate, "Video length:", videoLength);
const options = ["-i", file.name, "-preset", "ultrafast", "-c:v", "libx264", "-b:v", bitrate + "k"];
await ffmpeg.exec([...options, "-pass", "1", "-vsync", "cfr", "-f", "null", "/dev/null"]);
pass = 2;
await ffmpeg.exec([...options, "-pass", "2", "-c:a", "copy", "-b:a", "128k", "compressed.mp4"]);
const video = await ffmpeg.readFile("compressed.mp4");
if (!result) return;
notifier.notifyFinished();
location.href = URL.createObjectURL(new Blob([video.buffer], { type: "video/mp4" }));
const url = URL.createObjectURL(result);
document.getElementById("compressed-video").src = url;
document.getElementById("compressed-video").load();
document.getElementById("result-size").innerText = bytesToSizeString(result.size);
document.getElementById("download").onclick = () => {
const a = document.createElement("a");
a.href = url;
a.download = result.name;
a.click();
};
const shareData = {
text: "Compressed using " + window.origin,
files: [result],
};
if (navigator.share && (!navigator.canShare || navigator.canShare(shareData))) {
document.getElementById("share").style.display = "inline-block";
document.getElementById("share").onclick = () => navigator.share(shareData);
} else {
document.getElementById("share").style.display = "none";
}
showSection("result");
}
function cancel() {
ffmpeg.terminate();
document.getElementById("compress").onclick = async () => {
const filesize = document.getElementById("filesize").value;
const filesizeUnit = document.getElementById("filesize-unit").value;
await compress(filesize, filesizeUnit);
}
for (const preset of document.getElementsByClassName("preset")) {
preset.onclick = () => compress(parseInt(preset.dataset.size), preset.dataset.unit);
}
document.getElementById("cancel").onclick = () => {
videoCompressor.stop();
showSection("file-picker");
}
function updateProgress(progress, pass) {
const percent = (progress * 100).toFixed(1) + "%";
document.getElementById("progress-step-value").innerText = pass;
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;
for (const tabbable of document.querySelectorAll("[tabindex='0']")) {
tabbable.onkeydown = event => {
if (['Enter', 'Space'].includes(event.code))
tabbable.click();
}
}
document.getElementById("file-input").oninput = () => {
setTimeout(() => {
const file = document.getElementById("file-input").files[0];
@ -82,28 +114,19 @@ 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);
});
}
document.getElementById("back").onclick = () => {
showSection("file-picker");
};
window.onbeforeunload = event => {
if (videoCompressor.inProgress) event.preventDefault();
};
function bytesToSizeString(bytes) {
if (bytes >= 1000000000) return (bytes / 1000000000).toFixed(1) + " GB";
if (bytes >= 1000000) return (bytes / 1000000).toFixed(1) + " MB";
if (bytes >= 1000) return (bytes / 1000).toFixed(1) + " KB";
return bytes + " B";
}

View File

@ -1,206 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
body {
background-color: #F0F0F0;
font-family: "Open Sans", sans-serif;
text-align: center;
}
*::selection {
background-color: #00BCD4;
color: white;
}
h1 {
background-image: linear-gradient(to right, #4CAF50, #4CAF50 40%, #00BCD4 60%);
background-clip: text;
color: transparent;
font-size: 3rem;
margin-top: 3rem;
margin-bottom: 1rem;
}
p {
color: #9E9E9E;
font-size: 0.8rem;
}
* {
opacity: 1;
transition: opacity 300ms;
}
video {
width: 355px;
height: 200px;
border: 2px solid #212121;
background-color: black;
border-radius: 1rem;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2);
}
#file-input, #change-file {
display: none;
opacity: 0;
}
#file-input-spacing * {
visibility: hidden;
}
#file-drop-area {
max-width: 355px;
height: 198px;
border: 3px dotted #BDBDBD;
margin: 3rem auto 1rem auto;
border-radius: 1rem;
display: flex;
justify-content: center;
align-items: center;
transition: all 300ms;
cursor: pointer;
}
#file-drop-area span {
color: #9E9E9E;
}
#file-drop-area:hover, #file-drop-area:focus {
background-color: #E0E0E0;
outline: none;
}
#uploaded-video {
display: none;
opacity: 0;
margin: 3rem auto 1rem auto;
}
button.primary {
background-image: linear-gradient(to right, #4CAF50, #00BCD4);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
font-weight: bold;
transition: all ease-in 100ms;
cursor: pointer;
}
button.primary:hover {
filter: brightness(1.1);
}
button.primary:active {
filter: brightness(0.8);
}
button.primary:focus {
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
outline: 2px solid black;
}
button.simple {
border: 1px solid #9E9E9E;
border-radius: 0.25rem;
background-color: transparent;
transition: all 100ms;
cursor: pointer;
color: #9E9E9E;
margin: 1rem auto;
}
button.simple:hover {
color: black;
border-color: black;
}
button.simple:focus {
border-color: black;
outline: none;
}
input, select {
background-color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #BDBDBD;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
transition: all ease-in 100ms;
}
input:focus, select:focus {
outline: none;
border: 1px solid #00BCD4;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
}
input[type=checkbox] {
accent-color: #4CAF50;
}
label {
color: #757575;
}
#filesize {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
#filesize-unit {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid white;
}
#filesize-unit:focus {
border-left: 1px solid #00BCD4;
}
#compress {
margin-left: 1rem;
}
#loading-title {
color: #757575;
font-size: 2.5rem;
font-weight: bold;
margin-top: 4rem;
margin-bottom: 0;
}
#loading-description {
font-size: 1rem;
}
#progress-container {
width: 80vw;
height: 2rem;
max-width: 500px;
border-radius: 2rem;
margin: auto;
background-color: #E0E0E0;
position: relative;
}
#progress-indicator {
position: absolute;
inset: 0;
background-image: linear-gradient(to right, #4CAF50, #00BCD4);
border-radius: 2rem;
transition: clip-path 200ms;
}
#progress-percentage {
color: #757575;
font-weight: bold;
font-size: 1.2rem;
}
#progress-step {
font-weight: bold;
margin-top: 3rem;
margin-bottom: 0.5rem;
}

View File

@ -0,0 +1,98 @@
#file-input, #change-file {
display: none;
opacity: 0;
}
#file-input-spacing * {
visibility: hidden;
}
#file-drop-area {
max-width: 355px;
height: 198px;
border: 3px dotted #BDBDBD;
margin: 3rem auto 1rem auto;
border-radius: 1rem;
display: flex;
justify-content: center;
align-items: center;
transition: all 300ms;
cursor: pointer;
}
#file-drop-area span {
color: #9E9E9E;
}
#file-drop-area:hover, #file-drop-area:focus {
background-color: #E0E0E0;
outline: none;
}
#uploaded-video {
display: none;
opacity: 0;
margin: 3rem auto 1rem auto;
}
#filesize {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
width: 80px;
}
#filesize-unit {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
border-left: 1px solid white;
}
#filesize-unit:focus {
border-left: 1px solid #00BCD4;
}
#compress {
margin-left: 1rem;
}
#preset-divider {
margin-top: 2rem;
}
.preset {
display: inline-flex;
flex-direction: column;
justify-content: space-between;
gap: 0.5rem;
background-color: white;
padding: 0.5rem;
margin: 1rem 0.25rem auto 0.25rem;
border-radius: 0.5rem;
border: 1px solid #BDBDBD;
width: 100px;
height: 100px;
cursor: pointer;
position: relative;
bottom: 0;
transition: box-shadow, bottom 200ms;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
}
.preset:hover {
bottom: 3px;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.1);
}
.preset:focus {
border-color: black;
outline: 1px solid black;
}
.preset p {
margin: 0;
color: #757575;
}
.preset svg {
color: #BDBDBD;
}

View File

@ -0,0 +1,35 @@
#loading-title {
color: #757575;
font-size: 2.5rem;
font-weight: bold;
margin-top: 4rem;
margin-bottom: 0;
}
#loading-description {
font-size: 1rem;
}
#progress-container {
width: 80vw;
height: 2rem;
max-width: 500px;
border-radius: 2rem;
margin: 3rem auto auto;
background-color: #E0E0E0;
position: relative;
}
#progress-indicator {
position: absolute;
inset: 0;
background-image: linear-gradient(to right, #4CAF50, #00BCD4);
border-radius: 2rem;
transition: clip-path 200ms;
}
#progress-percentage {
color: #757575;
font-weight: bold;
font-size: 1.2rem;
}

View File

@ -0,0 +1,45 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
body {
background-color: #F0F0F0;
font-family: "Open Sans", sans-serif;
text-align: center;
}
*::selection {
background-color: #00BCD4;
color: white;
}
h1 {
color: #757575;
font-size: 3rem;
margin-top: 3rem;
margin-bottom: 1rem;
}
.gradient-text {
background-image: linear-gradient(to right, #4CAF50, #4CAF50 20%, #00BCD4 80%);
background-clip: text;
color: transparent;
}
p {
color: #9E9E9E;
font-size: 0.8rem;
}
* {
opacity: 1;
transition: opacity 300ms;
-webkit-tap-highlight-color: transparent;
}
video {
width: 355px;
height: 200px;
border: 2px solid #212121;
background-color: black;
border-radius: 1rem;
box-shadow: 0 5px 5px rgba(0, 0, 0, 0.2);
}

View File

@ -0,0 +1,25 @@
#result-section {
display: flex;
gap: 1rem;
}
#result-title {
color: #757575;
font-weight: bold;
font-size: 1.2rem;
margin-top: 2.5rem;
margin-bottom: 0;
}
#compressed-video {
margin-bottom: 1rem;
}
#download, #share {
width: 120px;
margin-top: 0.5rem;
}
#back {
margin-top: 2rem;
}

View File

@ -0,0 +1,78 @@
button {
cursor: pointer;
}
button.primary {
background-image: linear-gradient(to right, #4CAF50, #00BCD4);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
font-weight: bold;
transition: all ease-in 100ms;
height: 37px;
}
button.primary:hover {
filter: brightness(1.1);
}
button.primary:active {
filter: brightness(0.8);
}
button.primary:focus {
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
outline: 2px solid black;
}
button.simple {
border: 1px solid #9E9E9E;
border-radius: 0.25rem;
background-color: transparent;
transition: all 100ms;
color: #9E9E9E;
margin: 1rem auto;
padding: 0.2rem 0.5rem;
}
button.simple:hover {
color: black;
border-color: black;
}
button.simple:focus {
border-color: black;
outline: none;
}
input:not([type=checkbox]), select, button.secondary {
background-color: white;
padding: 0.5rem 1rem;
border-radius: 0.5rem;
border: 1px solid #BDBDBD;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
transition: all ease-in 100ms;
box-sizing: border-box;
height: 37px;
}
input:not([type=checkbox]):focus, select:focus, button.secondary:focus {
outline: none;
border: 1px solid #00BCD4;
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.2);
}
input[type=checkbox] {
transition: all ease-in 100ms;
accent-color: #4CAF50;
}
input[type=checkbox]:hover {
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.1);
}
label {
color: #757575;
}

View File

@ -2,44 +2,96 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Easily compress video files to a specific file size for uploading to various communication platforms that restrict upload size">
<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>
<link rel="stylesheet" href="/assets/style/main.css">
<script defer type="module" src="/assets/scripts/main.js"></script>
<link rel="stylesheet" href="/assets/styles/main.css">
<link rel="stylesheet" href="/assets/styles/ui.css">
<link rel="stylesheet" href="/assets/styles/file-picker-section.css">
<link rel="stylesheet" href="/assets/styles/loading-section.css">
<link rel="stylesheet" href="/assets/styles/result-section.css">
</head>
<body>
<h1>Video Compressor</h1>
<h1><span class="gradient-text">compact</span>.video</h1>
<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" 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">Compress</button>
<p id="preset-divider">
<s>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;</s>
&emsp; Or use a preset &emsp;
<s>&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;&emsp;</s>
</p>
<div class="preset" data-size="10" data-unit="mb" tabindex="0">
<p>Discord</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M524.5 133.8C524.3 133.5 524.1 133.2 523.7 133.1C485.6 115.6 445.3 103.1 404 96C403.6 95.9 403.2 96 402.9 96.1C402.6 96.2 402.3 96.5 402.1 96.9C396.6 106.8 391.6 117.1 387.2 127.5C342.6 120.7 297.3 120.7 252.8 127.5C248.3 117 243.3 106.8 237.7 96.9C237.5 96.6 237.2 96.3 236.9 96.1C236.6 95.9 236.2 95.9 235.8 95.9C194.5 103 154.2 115.5 116.1 133C115.8 133.1 115.5 133.4 115.3 133.7C39.1 247.5 18.2 358.6 28.4 468.2C28.4 468.5 28.5 468.7 28.6 469C28.7 469.3 28.9 469.4 29.1 469.6C73.5 502.5 123.1 527.6 175.9 543.8C176.3 543.9 176.7 543.9 177 543.8C177.3 543.7 177.7 543.4 177.9 543.1C189.2 527.7 199.3 511.3 207.9 494.3C208 494.1 208.1 493.8 208.1 493.5C208.1 493.2 208.1 493 208 492.7C207.9 492.4 207.8 492.2 207.6 492.1C207.4 492 207.2 491.8 206.9 491.7C191.1 485.6 175.7 478.3 161 469.8C160.7 469.6 160.5 469.4 160.3 469.2C160.1 469 160 468.6 160 468.3C160 468 160 467.7 160.2 467.4C160.4 467.1 160.5 466.9 160.8 466.7C163.9 464.4 167 462 169.9 459.6C170.2 459.4 170.5 459.2 170.8 459.2C171.1 459.2 171.5 459.2 171.8 459.3C268 503.2 372.2 503.2 467.3 459.3C467.6 459.2 468 459.1 468.3 459.1C468.6 459.1 469 459.3 469.2 459.5C472.1 461.9 475.2 464.4 478.3 466.7C478.5 466.9 478.7 467.1 478.9 467.4C479.1 467.7 479.1 468 479.1 468.3C479.1 468.6 479 468.9 478.8 469.2C478.6 469.5 478.4 469.7 478.2 469.8C463.5 478.4 448.2 485.7 432.3 491.6C432.1 491.7 431.8 491.8 431.6 492C431.4 492.2 431.3 492.4 431.2 492.7C431.1 493 431.1 493.2 431.1 493.5C431.1 493.8 431.2 494 431.3 494.3C440.1 511.3 450.1 527.6 461.3 543.1C461.5 543.4 461.9 543.7 462.2 543.8C462.5 543.9 463 543.9 463.3 543.8C516.2 527.6 565.9 502.5 610.4 469.6C610.6 469.4 610.8 469.2 610.9 469C611 468.8 611.1 468.5 611.1 468.2C623.4 341.4 590.6 231.3 524.2 133.7zM222.5 401.5C193.5 401.5 169.7 374.9 169.7 342.3C169.7 309.7 193.1 283.1 222.5 283.1C252.2 283.1 275.8 309.9 275.3 342.3C275.3 375 251.9 401.5 222.5 401.5zM417.9 401.5C388.9 401.5 365.1 374.9 365.1 342.3C365.1 309.7 388.5 283.1 417.9 283.1C447.6 283.1 471.2 309.9 470.7 342.3C470.7 375 447.5 401.5 417.9 401.5z"/></svg>
<p>10 MB</p>
</div>
<div class="preset" data-size="25" data-unit="mb" tabindex="0">
<p>Gmail</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="52 42 88 66" fill="currentColor" style="padding-left: 2rem; padding-right: 2rem;">
<path d="M58 108h14V74L52 59v43c0 3.32 2.69 6 6 6"/>
<path d="M120 108h14c3.32 0 6-2.69 6-6V59l-20 15"/>
<path d="M120 48v26l20-15v-8c0-7.42-8.47-11.65-14.4-7.2"/>
<path d="M72 74V48l24 18 24-18v26L96 92"/>
<path d="M52 51v8l20 15V48l-5.6-4.2c-5.94-4.45-14.4-.22-14.4 7.2"/>
</svg>
<p>25 MB</p>
</div>
<div class="preset" data-size="25" data-unit="mb" tabindex="0">
<p>Yahoo Mail</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M287.8 205.1L231.1 348.3L175.1 205.1L79 205.1L184.8 454.2L146.2 544L240.4 544L381.3 205.1L287.7 205.1zM393.2 340.9C361.1 340.9 335 367 335 399.1C335 431.2 361.1 457.3 393.2 457.3C425.3 457.3 451.4 431.2 451.4 399.1C451.4 367 425.3 340.9 393.2 340.9zM458.7 96L365.7 319.5L470.5 319.5L563.1 96L458.7 96z"/></svg>
<p>25 MB</p>
</div>
<div class="preset" data-size="100" data-unit="mb" tabindex="0">
<p>Messenger</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320.6 72C180.6 72 72 174.3 72 312.6C72 384.9 101.7 447.4 150.1 490.5C158.4 498 156.7 502.4 158.1 548.7C158.2 551.9 159.1 555.1 160.7 557.9C162.3 560.7 164.6 563.1 167.4 564.8C170.2 566.5 173.3 567.6 176.5 567.8C179.7 568 183 567.5 186 566.2C238.9 543 239.6 541.2 248.6 543.6C401.8 585.8 568 487.7 568 312.6C568 174.3 460.6 72 320.6 72zM469.8 257.1L396.8 372.7C394 377 390.4 380.8 386.2 383.7C382 386.6 377.1 388.5 372.1 389.5C367.1 390.5 361.8 390.3 356.8 389.1C351.8 387.9 347.1 385.7 343 382.7L284.9 339.2C282.3 337.3 279.1 336.2 275.9 336.2C272.7 336.2 269.5 337.3 266.9 339.2L188.5 398.6C178 406.5 164.3 394 171.4 382.9L244.4 267.3C247.2 263 250.8 259.2 255 256.3C259.2 253.4 264.1 251.5 269.1 250.5C274.1 249.5 279.4 249.7 284.4 250.9C289.4 252.1 294.1 254.3 298.3 257.3L356.4 300.8C359 302.7 362.2 303.8 365.4 303.8C368.6 303.8 371.8 302.7 374.4 300.8L452.8 241.4C463.2 233.4 476.9 245.9 469.9 257z"/></svg>
<p>100 MB</p>
</div>
<div class="preset" data-size="100" data-unit="mb" tabindex="0">
<p>Signal</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M320 64C306.7 64 293.7 65 280.9 67L284.6 90.7C296.1 88.9 308 88 320 88C332 88 343.9 88.9 355.4 90.7L359.1 67C346.3 65 333.3 64 320 64zM380.8 71.3L375.1 94.6C398.5 100.3 420.5 109.5 440.5 121.7L453 101.2C430.9 87.8 406.6 77.6 380.8 71.3zM457.2 132.9C476.3 146.9 493.2 163.8 507.3 183L526.7 168.8C511 147.6 492.4 129 471.3 113.5L457.1 132.8zM538.9 187.1L518.4 199.6C530.6 219.6 539.8 241.6 545.5 265L568.8 259.3C562.5 233.5 552.3 209.2 538.9 187.1zM549.4 284.6C551.2 296.1 552.1 308 552.1 320C552.1 332 551.2 343.9 549.4 355.4L573.1 359.1C575 346.4 576.1 333.3 576.1 320C576.1 306.7 575.1 293.7 573.1 280.9L549.4 284.6zM518.4 440.5L538.9 453C552.3 430.9 562.5 406.6 568.8 380.8L545.5 375.1C539.8 398.5 530.6 420.5 518.4 440.5zM526.6 471.3L507.2 457.1C493.2 476.2 476.3 493.1 457.1 507.2L471.3 526.6C492.4 511.1 511.1 492.5 526.5 471.4zM440.5 518.3C420.5 530.5 398.5 539.7 375.1 545.4L380.8 568.7C406.6 562.4 430.9 552.2 453 538.8L440.5 518.3zM359.1 573L355.4 549.3C343.9 551.1 332 552 320 552C308 552 296.1 551.1 284.6 549.3L280.9 573C293.6 574.9 306.7 576 320 576C333.3 576 346.3 575 359.1 573zM265 545.4C247.4 541.1 230.6 534.8 214.9 526.8L207.1 522.8L174.3 530.5L179.8 553.9L204.1 548.2C221.5 557.1 240 564 259.4 568.7L265.1 545.4zM159.4 558.6L154 535.3L112.3 545C101.9 547.4 92.6 538.1 95 527.7L104.7 486.1L81.3 480.6L71.6 522.2C65.2 550 90 574.8 117.8 568.4L159.4 558.7zM109.4 465.7L117.1 432.9L113.1 425.1C105.1 409.4 98.8 392.6 94.5 375L71.3 380.7C76 400.1 82.9 418.7 91.7 436L86 460.3L109.4 465.8zM67 359.1L90.7 355.4C88.9 343.9 88 332 88 320C88 308 88.9 296.1 90.7 284.6L67 280.9C65 293.7 64 306.7 64 320C64 333.3 65 346.3 67 359.1zM94.6 265C100.3 241.6 109.5 219.6 121.7 199.6L101.2 187.1C87.8 209.2 77.6 233.5 71.3 259.3L94.6 265zM113.5 168.8L132.9 183C146.9 163.9 163.8 147 183 132.9L168.7 113.5C147.6 129 129 147.6 113.5 168.7zM199.6 121.8C219.6 109.6 241.6 100.4 265 94.7L259.2 71.3C233.4 77.6 209.1 87.8 187 101.2L199.5 121.7zM320 528C434.9 528 528 434.9 528 320C528 205.1 434.9 112 320 112C205.1 112 112 205.1 112 320C112 356.4 121.4 390.7 137.8 420.5C139.4 423.4 139.9 426.7 139.2 429.9L117.6 522.4L210.1 500.8C213.3 500.1 216.6 500.6 219.5 502.2C249.3 518.7 283.5 528 320 528z"/></svg>
<p>100 MB</p>
</div>
<div class="preset" data-size="180" data-unit="mb" tabindex="0">
<p>WhatsApp</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><!--!Font Awesome Free v7.1.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2026 Fonticons, Inc.--><path fill="currentColor" d="M476.9 161.1C435 119.1 379.2 96 319.9 96C197.5 96 97.9 195.6 97.9 318C97.9 357.1 108.1 395.3 127.5 429L96 544L213.7 513.1C246.1 530.8 282.6 540.1 319.8 540.1L319.9 540.1C442.2 540.1 544 440.5 544 318.1C544 258.8 518.8 203.1 476.9 161.1zM319.9 502.7C286.7 502.7 254.2 493.8 225.9 477L219.2 473L149.4 491.3L168 423.2L163.6 416.2C145.1 386.8 135.4 352.9 135.4 318C135.4 216.3 218.2 133.5 320 133.5C369.3 133.5 415.6 152.7 450.4 187.6C485.2 222.5 506.6 268.8 506.5 318.1C506.5 419.9 421.6 502.7 319.9 502.7zM421.1 364.5C415.6 361.7 388.3 348.3 383.2 346.5C378.1 344.6 374.4 343.7 370.7 349.3C367 354.9 356.4 367.3 353.1 371.1C349.9 374.8 346.6 375.3 341.1 372.5C308.5 356.2 287.1 343.4 265.6 306.5C259.9 296.7 271.3 297.4 281.9 276.2C283.7 272.5 282.8 269.3 281.4 266.5C280 263.7 268.9 236.4 264.3 225.3C259.8 214.5 255.2 216 251.8 215.8C248.6 215.6 244.9 215.6 241.2 215.6C237.5 215.6 231.5 217 226.4 222.5C221.3 228.1 207 241.5 207 268.8C207 296.1 226.9 322.5 229.6 326.2C232.4 329.9 268.7 385.9 324.4 410C359.6 425.2 373.4 426.5 391 423.9C401.7 422.3 423.8 410.5 428.4 397.5C433 384.5 433 373.4 431.6 371.1C430.3 368.6 426.6 367.2 421.1 364.5z"/></svg>
<p>180 MB</p>
</div>
</section>
<section id="loading-section" style="opacity: 0; display: none;">
<h3 id="loading-title">Please wait...</h3>
<p id="loading-description">Your video is being compressed. This may take a while.</p>
<p id="progress-step">Step <span id="progress-step-value">1</span>/2</p>
<div id="progress-container">
<div id="progress-indicator"></div>
</div>
@ -48,7 +100,19 @@
<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 id="result-section" style="opacity: 0; display: none;">
<h4 id="result-title">Your video has been compressed!</h4>
<p>Result size: <span id="result-size"></span></p>
<video id="compressed-video" controls autoplay></video><br>
<button id="download" class="primary">Download</button><br>
<button id="share" class="secondary">Share video</button><br>
<button id="back" class="simple">Go back</button>
</section>
</body>
</html>