feat: full studio build -- light theme, canvas thumbnails, i18n (fa/en)

This commit is contained in:
Soroush.Asadi
2026-05-24 17:37:21 +03:30
parent d962483359
commit c61f587767
295 changed files with 29797 additions and 265 deletions
+165
View File
@@ -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",
});
}
}
};