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); }); } }