* Fixing <#1513197912660906035> on my own plugin. * #1163: Update src/equicordplugins/clipUpload.desktop/ffmpeg.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * #1163: Update src/equicordplugins/clipUpload.desktop/upload.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * #1163: Update src/equicordplugins/clipUpload.desktop/upload.ts * chore(clipUpload): document unavoidable path exposure Added documentation to in the main process and in the renderer. This clarifies that exposing the absolute filesystem path is necessary because Discord's is a renderer-only module and requires an absolute path to function. Co-authored-by: Gemini <198982749+gemini-code-assist@users.noreply.github.com> * Conflict Fix * [autofix.ci] apply automated fixes * Fix Missing upper-bound size check on renderer-supplied bytes --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Gemini <198982749+gemini-code-assist@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
91 lines
8 KiB
TypeScript
91 lines
8 KiB
TypeScript
/*
|
|
* Vencord, a Discord client mod
|
|
* Copyright (c) 2026 Vendicated and contributors
|
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
*/
|
|
|
|
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
|
import { Logger } from "@utils/Logger";
|
|
|
|
const logger = new Logger("ClipUpload");
|
|
|
|
let ffmpeg: FFmpeg | null = null;
|
|
let ffmpegLoading: Promise<FFmpeg> | null = null;
|
|
let conversionCounter = 0;
|
|
|
|
async function loadFFmpeg() {
|
|
if (ffmpeg?.loaded) return ffmpeg;
|
|
if (ffmpegLoading) return ffmpegLoading;
|
|
|
|
ffmpegLoading = (async () => {
|
|
const instance = new FFmpeg();
|
|
const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
|
|
const classWorkerRaw = "/// <reference no-default-lib=\"true\" />\n/// <reference lib=\"esnext\" />\n/// <reference lib=\"webworker\" />\nconst MIME_TYPE_JAVASCRIPT = \"text/javascript\";\nconst MIME_TYPE_WASM = \"application/wasm\";\nconst CORE_VERSION = \"0.12.6\";\nconst CORE_URL = `https://cdn.jsdelivr.net/npm/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`;\nvar FFMessageType;\n(function(FFMessageType) {\n FFMessageType[\"LOAD\"] = \"LOAD\";\n FFMessageType[\"EXEC\"] = \"EXEC\";\n FFMessageType[\"WRITE_FILE\"] = \"WRITE_FILE\";\n FFMessageType[\"READ_FILE\"] = \"READ_FILE\";\n FFMessageType[\"DELETE_FILE\"] = \"DELETE_FILE\";\n FFMessageType[\"RENAME\"] = \"RENAME\";\n FFMessageType[\"CREATE_DIR\"] = \"CREATE_DIR\";\n FFMessageType[\"LIST_DIR\"] = \"LIST_DIR\";\n FFMessageType[\"DELETE_DIR\"] = \"DELETE_DIR\";\n FFMessageType[\"ERROR\"] = \"ERROR\";\n FFMessageType[\"DOWNLOAD\"] = \"DOWNLOAD\";\n FFMessageType[\"PROGRESS\"] = \"PROGRESS\";\n FFMessageType[\"LOG\"] = \"LOG\";\n FFMessageType[\"MOUNT\"] = \"MOUNT\";\n FFMessageType[\"UNMOUNT\"] = \"UNMOUNT\";\n})(FFMessageType || (FFMessageType = {}));\n\n\nconst ERROR_UNKNOWN_MESSAGE_TYPE = new Error(\"unknown message type\");\nconst ERROR_NOT_LOADED = new Error(\"ffmpeg is not loaded, call `await ffmpeg.load()` first\");\nconst ERROR_TERMINATED = new Error(\"called FFmpeg.terminate()\");\nconst ERROR_IMPORT_FAILURE = new Error(\"failed to import ffmpeg-core.js\");\n\nlet ffmpeg;\nconst load = async ({ coreURL: _coreURL, wasmURL: _wasmURL, workerURL: _workerURL, }) => {\n const first = !ffmpeg;\n try {\n if (!_coreURL)\n _coreURL = CORE_URL;\n importScripts(_coreURL);\n }\n catch {\n if (!_coreURL)\n _coreURL = CORE_URL.replace('/umd/', '/esm/');\n self.createFFmpegCore = (await import(\n /* webpackIgnore: true */ /* @vite-ignore */ _coreURL)).default;\n if (!self.createFFmpegCore) {\n throw ERROR_IMPORT_FAILURE;\n }\n }\n const coreURL = _coreURL;\n const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, \".wasm\");\n const workerURL = _workerURL\n ? _workerURL\n : _coreURL.replace(/.js$/g, \".worker.js\");\n ffmpeg = await self.createFFmpegCore({\n mainScriptUrlOrBlob: `${coreURL}#${btoa(JSON.stringify({ wasmURL, workerURL }))}`,\n });\n ffmpeg.setLogger((data) => self.postMessage({ type: FFMessageType.LOG, data }));\n ffmpeg.setProgress((data) => self.postMessage({\n type: FFMessageType.PROGRESS,\n data,\n }));\n return first;\n};\nconst exec = ({ args, timeout = -1 }) => {\n ffmpeg.setTimeout(timeout);\n ffmpeg.exec(...args);\n const ret = ffmpeg.ret;\n ffmpeg.reset();\n return ret;\n};\nconst writeFile = ({ path, data }) => {\n ffmpeg.FS.writeFile(path, data);\n return true;\n};\nconst readFile = ({ path, encoding }) => ffmpeg.FS.readFile(path, { encoding });\nconst deleteFile = ({ path }) => {\n ffmpeg.FS.unlink(path);\n return true;\n};\nconst rename = ({ oldPath, newPath }) => {\n ffmpeg.FS.rename(oldPath, newPath);\n return true;\n};\nconst createDir = ({ path }) => {\n ffmpeg.FS.mkdir(path);\n return true;\n};\nconst listDir = ({ path }) => {\n const names = ffmpeg.FS.readdir(path);\n const nodes = [];\n for (const name of names) {\n const stat = ffmpeg.FS.stat(`${path}/${name}`);\n const isDir = ffmpeg.FS.isDir(stat.mode);\n nodes.push({ name, isDir });\n }\n return nodes;\n};\nconst deleteDir = ({ path }) => {\n ffmpeg.FS.rmdir(path);\n return true;\n};\nconst mount = ({ fsType, options, mountPoint }) => {\n const str = fsType;\n const fs = ffmpeg.FS.filesystems[str];\n if (!fs)\n return false;\n ffmpeg.FS.mount(fs, options, mountPoint);\n return true;\n};\nconst unmount = ({ mountPoint }) => {\n ffmpeg.FS.unmount(mountPoint);\n return true;\n};\nself.onmessage = async ({ data: { id, type, data: _data }, }) => {\n const trans = [];\n let data;\n try {\n if (type !== FFMessageType.LOAD && !ffmpeg)\n throw ERROR_NOT_LOADED;\n switch (type) {\n case FFMessageType.LOAD:\n data = await load(_data);\n break;\n case FFMessageType.EXEC:\n data = exec(_data);\n break;\n case FFMessageType.WRITE_FILE:\n data = writeFile(_data);\n break;\n case FFMessageType.READ_FILE:\n data = readFile(_data);\n break;\n case FFMessageType.DELETE_FILE:\n data = deleteFile(_data);\n break;\n case FFMessageType.RENAME:\n data = rename(_data);\n break;\n case FFMessageType.CREATE_DIR:\n data = createDir(_data);\n break;\n case FFMessageType.LIST_DIR:\n data = listDir(_data);\n break;\n case FFMessageType.DELETE_DIR:\n data = deleteDir(_data);\n break;\n case FFMessageType.MOUNT:\n data = mount(_data);\n break;\n case FFMessageType.UNMOUNT:\n data = unmount(_data);\n break;\n default:\n throw ERROR_UNKNOWN_MESSAGE_TYPE;\n }\n }\n catch (e) {\n self.postMessage({\n id,\n type: FFMessageType.ERROR,\n data: e.toString(),\n });\n return;\n }\n if (data instanceof Uint8Array) {\n trans.push(data.buffer);\n }\n self.postMessage({ id, type, data }, trans);\n};\n\n";
|
|
const classWorkerBlob = new Blob([(new TextEncoder()).encode(classWorkerRaw)], { type: "text/javascript" });
|
|
const classWorkerURL = URL.createObjectURL(classWorkerBlob);
|
|
|
|
try {
|
|
await instance.load({
|
|
coreURL: `${baseURL}/ffmpeg-core.js`,
|
|
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
|
|
workerURL: `${baseURL}/ffmpeg-core.worker.js`,
|
|
classWorkerURL
|
|
});
|
|
|
|
ffmpeg = instance;
|
|
logger.info("FFmpeg loaded.");
|
|
return instance;
|
|
} catch (error) {
|
|
instance.terminate();
|
|
ffmpeg = null;
|
|
throw error;
|
|
} finally {
|
|
URL.revokeObjectURL(classWorkerURL);
|
|
ffmpegLoading = null;
|
|
}
|
|
})();
|
|
|
|
return ffmpegLoading;
|
|
}
|
|
|
|
function getInputName(fileName: string, id: number) {
|
|
return `input_${id}${fileName.match(/\.[a-z0-9]+$/i)?.[0].toLowerCase() ?? ".video"}`;
|
|
}
|
|
|
|
export async function convertClipToMp4(file: File, fileName: string) {
|
|
const id = conversionCounter++;
|
|
const inputName = getInputName(file.name, id);
|
|
const outputName = `output_${id}.mp4`;
|
|
const ff = await loadFFmpeg();
|
|
|
|
try {
|
|
await ff.writeFile(inputName, new Uint8Array(await file.arrayBuffer()));
|
|
|
|
const exitCode = await ff.exec([
|
|
"-i", inputName,
|
|
"-map", "0:v:0",
|
|
"-map", "0:a:0?",
|
|
"-c:v", "libx264",
|
|
"-preset", "veryfast",
|
|
"-profile:v", "high",
|
|
"-level:v", "4.0",
|
|
"-pix_fmt", "yuv420p",
|
|
"-c:a", "aac",
|
|
"-b:a", "128k",
|
|
"-movflags", "+faststart",
|
|
outputName
|
|
]);
|
|
|
|
if (exitCode !== 0) throw new Error("Couldn't convert the selected file.");
|
|
|
|
const data = await ff.readFile(outputName);
|
|
if (typeof data === "string") throw new Error("Couldn't read the converted file.");
|
|
|
|
return new File([new Uint8Array(data)], fileName, { type: "video/mp4" });
|
|
} finally {
|
|
await Promise.all([
|
|
ff.deleteFile(inputName).catch(() => undefined),
|
|
ff.deleteFile(outputName).catch(() => undefined)
|
|
]);
|
|
}
|
|
}
|