feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { fetchFile, toBlobURL } from "@ffmpeg/util";
|
||||
|
||||
import { scaleCropToVideo } from "@/lib/trimmer-utils";
|
||||
import type { CropBox, ExportFormat, VideoDimensions } from "@/lib/trimmer-types";
|
||||
|
||||
const CORE_VERSION = "0.12.6";
|
||||
const CORE_BASE = `https://cdn.jsdelivr.net/npm/@ffmpeg/core@${CORE_VERSION}/dist/umd`;
|
||||
|
||||
let ffmpeg: FFmpeg | null = null;
|
||||
let loadPromise: Promise<FFmpeg> | null = null;
|
||||
|
||||
interface ProcessPayload {
|
||||
fileBuffer: ArrayBuffer;
|
||||
fileName: string;
|
||||
trimStart: number;
|
||||
trimEnd: number;
|
||||
cropBox: CropBox;
|
||||
displaySize: VideoDimensions;
|
||||
videoSize: VideoDimensions;
|
||||
exportFormat: ExportFormat;
|
||||
}
|
||||
|
||||
async function loadFfmpegInWorker(): Promise<FFmpeg> {
|
||||
if (ffmpeg?.loaded) return ffmpeg;
|
||||
if (loadPromise) return loadPromise;
|
||||
|
||||
loadPromise = (async () => {
|
||||
const instance = new FFmpeg();
|
||||
instance.on("log", ({ message }) => {
|
||||
self.postMessage({ type: "log", message });
|
||||
});
|
||||
|
||||
await instance.load({
|
||||
coreURL: await toBlobURL(
|
||||
`${CORE_BASE}/ffmpeg-core.js`,
|
||||
"text/javascript"
|
||||
),
|
||||
wasmURL: await toBlobURL(
|
||||
`${CORE_BASE}/ffmpeg-core.wasm`,
|
||||
"application/wasm"
|
||||
),
|
||||
});
|
||||
|
||||
ffmpeg = instance;
|
||||
return instance;
|
||||
})();
|
||||
|
||||
return loadPromise;
|
||||
}
|
||||
|
||||
async function processVideo(payload: ProcessPayload): Promise<{
|
||||
buffer: ArrayBuffer;
|
||||
mime: string;
|
||||
}> {
|
||||
const {
|
||||
fileBuffer,
|
||||
fileName,
|
||||
trimStart,
|
||||
trimEnd,
|
||||
cropBox,
|
||||
displaySize,
|
||||
videoSize,
|
||||
exportFormat,
|
||||
} = payload;
|
||||
|
||||
const ff = await loadFfmpegInWorker();
|
||||
const file = new File([fileBuffer], fileName, { type: "video/mp4" });
|
||||
const inputName =
|
||||
"input" +
|
||||
(file.name.includes(".")
|
||||
? file.name.slice(file.name.lastIndexOf("."))
|
||||
: ".mp4");
|
||||
const outputName = exportFormat === "mp4" ? "output.mp4" : "output.webm";
|
||||
const crop = scaleCropToVideo(cropBox, displaySize, videoSize);
|
||||
|
||||
ff.on("progress", ({ progress }) => {
|
||||
self.postMessage({
|
||||
type: "progress",
|
||||
percent: Math.min(100, Math.round(progress * 100)),
|
||||
});
|
||||
});
|
||||
|
||||
await ff.writeFile(inputName, await fetchFile(file));
|
||||
await ff.deleteFile(outputName).catch(() => undefined);
|
||||
|
||||
const args = [
|
||||
"-i",
|
||||
inputName,
|
||||
"-ss",
|
||||
trimStart.toFixed(3),
|
||||
"-to",
|
||||
trimEnd.toFixed(3),
|
||||
"-vf",
|
||||
`crop=${crop.w}:${crop.h}:${crop.x}:${crop.y}`,
|
||||
];
|
||||
|
||||
if (exportFormat === "mp4") {
|
||||
args.push(
|
||||
"-c:v",
|
||||
"libx264",
|
||||
"-preset",
|
||||
"fast",
|
||||
"-c:a",
|
||||
"aac",
|
||||
"-movflags",
|
||||
"+faststart"
|
||||
);
|
||||
} else {
|
||||
args.push("-c:v", "libvpx-vp9", "-c:a", "libopus", "-b:v", "1M");
|
||||
}
|
||||
|
||||
args.push(outputName);
|
||||
|
||||
await ff.exec(args);
|
||||
|
||||
const data = await ff.readFile(outputName);
|
||||
const bytes =
|
||||
data instanceof Uint8Array
|
||||
? data
|
||||
: new TextEncoder().encode(String(data));
|
||||
|
||||
await ff.deleteFile(inputName).catch(() => undefined);
|
||||
await ff.deleteFile(outputName).catch(() => undefined);
|
||||
|
||||
self.postMessage({ type: "progress", percent: 100 });
|
||||
|
||||
const mime = exportFormat === "mp4" ? "video/mp4" : "video/webm";
|
||||
const copy = new Uint8Array(bytes);
|
||||
return { buffer: copy.buffer, mime };
|
||||
}
|
||||
|
||||
self.onmessage = async (event: MessageEvent) => {
|
||||
const data = event.data as { type: string } & ProcessPayload;
|
||||
|
||||
if (data.type === "init") {
|
||||
try {
|
||||
await loadFfmpegInWorker();
|
||||
self.postMessage({ type: "ready" });
|
||||
} catch {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
message: "Failed to load FFmpeg in worker",
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === "process") {
|
||||
try {
|
||||
const { buffer, mime } = await processVideo(data);
|
||||
(self as DedicatedWorkerGlobalScope).postMessage(
|
||||
{ type: "complete", buffer, mime },
|
||||
[buffer]
|
||||
);
|
||||
} catch {
|
||||
self.postMessage({
|
||||
type: "error",
|
||||
message: "Video processing failed",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user