From 8bd90c85dcd88803058315cc7cc8f431072a80ad Mon Sep 17 00:00:00 2001 From: ct Date: Sat, 21 Jun 2025 20:21:47 +0800 Subject: [PATCH] Update --- public/ffmpeg_packages/core-mt/.gitignore | 2 + public/ffmpeg_packages/core-mt/package.json | 46 ++ public/ffmpeg_packages/core/.gitignore | 2 + public/ffmpeg_packages/core/package.json | 42 ++ public/ffmpeg_packages/ffmpeg/.eslintignore | 1 + public/ffmpeg_packages/ffmpeg/.eslintrc.cjs | 13 + public/ffmpeg_packages/ffmpeg/.gitignore | 2 + public/ffmpeg_packages/ffmpeg/package.json | 68 +++ public/ffmpeg_packages/ffmpeg/src/classes.ts | 488 ++++++++++++++++++ public/ffmpeg_packages/ffmpeg/src/const.ts | 25 + public/ffmpeg_packages/ffmpeg/src/empty.mts | 7 + public/ffmpeg_packages/ffmpeg/src/errors.ts | 8 + public/ffmpeg_packages/ffmpeg/src/index.ts | 2 + public/ffmpeg_packages/ffmpeg/src/types.ts | 182 +++++++ public/ffmpeg_packages/ffmpeg/src/utils.ts | 7 + public/ffmpeg_packages/ffmpeg/src/worker.ts | 226 ++++++++ .../ffmpeg_packages/ffmpeg/tsconfig.esm.json | 10 + public/ffmpeg_packages/ffmpeg/tsconfig.json | 7 + .../ffmpeg_packages/ffmpeg/webpack.config.js | 19 + public/ffmpeg_packages/types/package.json | 32 ++ public/ffmpeg_packages/types/types/index.d.ts | 142 +++++ public/ffmpeg_packages/util/.eslintrc.cjs | 13 + public/ffmpeg_packages/util/.gitignore | 1 + public/ffmpeg_packages/util/package.json | 57 ++ public/ffmpeg_packages/util/src/const.ts | 1 + public/ffmpeg_packages/util/src/errors.ts | 6 + public/ffmpeg_packages/util/src/index.ts | 180 +++++++ public/ffmpeg_packages/util/src/types.ts | 9 + public/ffmpeg_packages/util/tests/.eslintrc | 6 + .../ffmpeg_packages/util/tests/constants.js | 19 + .../util/tests/ffmpeg.test.html | 21 + .../ffmpeg_packages/util/tests/ffmpeg.test.js | 69 +++ public/ffmpeg_packages/util/tsconfig.cjs.json | 8 + public/ffmpeg_packages/util/tsconfig.esm.json | 9 + public/ffmpeg_packages/util/tsconfig.json | 6 + .../ffmpeg_packages/util/webpack.config.cjs | 12 + .../editor/partials/canvas/video-export.jsx | 5 +- 37 files changed, 1752 insertions(+), 1 deletion(-) create mode 100755 public/ffmpeg_packages/core-mt/.gitignore create mode 100755 public/ffmpeg_packages/core-mt/package.json create mode 100755 public/ffmpeg_packages/core/.gitignore create mode 100755 public/ffmpeg_packages/core/package.json create mode 100755 public/ffmpeg_packages/ffmpeg/.eslintignore create mode 100755 public/ffmpeg_packages/ffmpeg/.eslintrc.cjs create mode 100755 public/ffmpeg_packages/ffmpeg/.gitignore create mode 100755 public/ffmpeg_packages/ffmpeg/package.json create mode 100755 public/ffmpeg_packages/ffmpeg/src/classes.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/const.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/empty.mts create mode 100755 public/ffmpeg_packages/ffmpeg/src/errors.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/index.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/types.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/utils.ts create mode 100755 public/ffmpeg_packages/ffmpeg/src/worker.ts create mode 100755 public/ffmpeg_packages/ffmpeg/tsconfig.esm.json create mode 100755 public/ffmpeg_packages/ffmpeg/tsconfig.json create mode 100755 public/ffmpeg_packages/ffmpeg/webpack.config.js create mode 100755 public/ffmpeg_packages/types/package.json create mode 100755 public/ffmpeg_packages/types/types/index.d.ts create mode 100755 public/ffmpeg_packages/util/.eslintrc.cjs create mode 100755 public/ffmpeg_packages/util/.gitignore create mode 100755 public/ffmpeg_packages/util/package.json create mode 100755 public/ffmpeg_packages/util/src/const.ts create mode 100755 public/ffmpeg_packages/util/src/errors.ts create mode 100755 public/ffmpeg_packages/util/src/index.ts create mode 100755 public/ffmpeg_packages/util/src/types.ts create mode 100755 public/ffmpeg_packages/util/tests/.eslintrc create mode 100755 public/ffmpeg_packages/util/tests/constants.js create mode 100755 public/ffmpeg_packages/util/tests/ffmpeg.test.html create mode 100755 public/ffmpeg_packages/util/tests/ffmpeg.test.js create mode 100755 public/ffmpeg_packages/util/tsconfig.cjs.json create mode 100755 public/ffmpeg_packages/util/tsconfig.esm.json create mode 100755 public/ffmpeg_packages/util/tsconfig.json create mode 100755 public/ffmpeg_packages/util/webpack.config.cjs diff --git a/public/ffmpeg_packages/core-mt/.gitignore b/public/ffmpeg_packages/core-mt/.gitignore new file mode 100755 index 0000000..6bc1442 --- /dev/null +++ b/public/ffmpeg_packages/core-mt/.gitignore @@ -0,0 +1,2 @@ +dist/ +types/ diff --git a/public/ffmpeg_packages/core-mt/package.json b/public/ffmpeg_packages/core-mt/package.json new file mode 100755 index 0000000..4d70370 --- /dev/null +++ b/public/ffmpeg_packages/core-mt/package.json @@ -0,0 +1,46 @@ +{ + "name": "@ffmpeg/core-mt", + "version": "0.12.10", + "description": "FFmpeg WebAssembly version (multi thread)", + "main": "./dist/umd/ffmpeg-core.js", + "exports": { + ".": { + "import": "./dist/esm/ffmpeg-core.js", + "require": "./dist/umd/ffmpeg-core.js" + }, + "./wasm": { + "import": "./dist/esm/ffmpeg-core.wasm", + "require": "./dist/umd/ffmpeg-core.wasm" + }, + "./worker": { + "import": "./dist/esm/ffmpeg-core.worker.js", + "require": "./dist/umd/ffmpeg-core.worker.js" + } + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git" + }, + "keywords": [ + "ffmpeg", + "WebAssembly", + "video", + "audio", + "transcode" + ], + "author": "Jerome Wu ", + "license": "GPL-2.0-or-later", + "bugs": { + "url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues" + }, + "engines": { + "node": ">=16.x" + }, + "homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme", + "publishConfig": { + "access": "public" + } +} diff --git a/public/ffmpeg_packages/core/.gitignore b/public/ffmpeg_packages/core/.gitignore new file mode 100755 index 0000000..6bc1442 --- /dev/null +++ b/public/ffmpeg_packages/core/.gitignore @@ -0,0 +1,2 @@ +dist/ +types/ diff --git a/public/ffmpeg_packages/core/package.json b/public/ffmpeg_packages/core/package.json new file mode 100755 index 0000000..96ad5a9 --- /dev/null +++ b/public/ffmpeg_packages/core/package.json @@ -0,0 +1,42 @@ +{ + "name": "@ffmpeg/core", + "version": "0.12.10", + "description": "FFmpeg WebAssembly version (single thread)", + "main": "./dist/umd/ffmpeg-core.js", + "exports": { + ".": { + "import": "./dist/esm/ffmpeg-core.js", + "require": "./dist/umd/ffmpeg-core.js" + }, + "./wasm": { + "import": "./dist/esm/ffmpeg-core.wasm", + "require": "./dist/umd/ffmpeg-core.wasm" + } + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git" + }, + "keywords": [ + "ffmpeg", + "WebAssembly", + "video", + "audio", + "transcode" + ], + "author": "Jerome Wu ", + "license": "GPL-2.0-or-later", + "bugs": { + "url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues" + }, + "engines": { + "node": ">=16.x" + }, + "homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme", + "publishConfig": { + "access": "public" + } +} diff --git a/public/ffmpeg_packages/ffmpeg/.eslintignore b/public/ffmpeg_packages/ffmpeg/.eslintignore new file mode 100755 index 0000000..cf64a52 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/.eslintignore @@ -0,0 +1 @@ +.eslintrc.cjs diff --git a/public/ffmpeg_packages/ffmpeg/.eslintrc.cjs b/public/ffmpeg_packages/ffmpeg/.eslintrc.cjs new file mode 100755 index 0000000..ba04d87 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/.eslintrc.cjs @@ -0,0 +1,13 @@ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json", "./src/worker/tsconfig.json"], + }, + plugins: ["@typescript-eslint"], +}; diff --git a/public/ffmpeg_packages/ffmpeg/.gitignore b/public/ffmpeg_packages/ffmpeg/.gitignore new file mode 100755 index 0000000..5c53e32 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/.gitignore @@ -0,0 +1,2 @@ +dist/ +docs/ diff --git a/public/ffmpeg_packages/ffmpeg/package.json b/public/ffmpeg_packages/ffmpeg/package.json new file mode 100755 index 0000000..bf68fe1 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/package.json @@ -0,0 +1,68 @@ +{ + "name": "@ffmpeg/ffmpeg", + "version": "0.12.15", + "description": "FFmpeg WebAssembly version for browser", + "main": "./dist/umd/ffmpeg.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "node": "./dist/esm/empty.mjs", + "default": { + "import": "./dist/esm/index.js", + "require": "./dist/umd/ffmpeg.js" + } + }, + "./worker": { + "types": "./dist/esm/worker.d.ts", + "default": "./dist/esm/worker.js" + } + }, + "scripts": { + "dev": "webpack -w --mode development", + "lint": "eslint src", + "clean": "rimraf dist", + "build:esm": "tsc -p tsconfig.esm.json", + "build:umd": "webpack", + "build": "npm run clean && npm run build:esm && npm run build:umd", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist", + "types/ffmpeg.d.ts" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git" + }, + "keywords": [ + "ffmpeg", + "WebAssembly", + "video", + "audio", + "transcode" + ], + "author": "Jerome Wu ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues" + }, + "engines": { + "node": ">=18.x" + }, + "homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "eslint": "^8.45.0", + "rimraf": "^5.0.1", + "typescript": "^5.1.6", + "webpack-cli": "^5.1.4" + }, + "dependencies": { + "@ffmpeg/types": "^0.12.4" + } +} diff --git a/public/ffmpeg_packages/ffmpeg/src/classes.ts b/public/ffmpeg_packages/ffmpeg/src/classes.ts new file mode 100755 index 0000000..ff40e83 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/classes.ts @@ -0,0 +1,488 @@ +import { FFMessageType } from "./const.js"; +import { + CallbackData, + Callbacks, + FSNode, + FFMessageEventCallback, + FFMessageLoadConfig, + OK, + IsFirst, + LogEvent, + Message, + ProgressEvent, + LogEventCallback, + ProgressEventCallback, + FileData, + FFFSType, + FFFSMountOptions, + FFFSPath, +} from "./types.js"; +import { getMessageID } from "./utils.js"; +import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js"; + +type FFMessageOptions = { + signal?: AbortSignal; +}; + +/** + * Provides APIs to interact with ffmpeg web worker. + * + * @example + * ```ts + * const ffmpeg = new FFmpeg(); + * ``` + */ +export class FFmpeg { + #worker: Worker | null = null; + /** + * #resolves and #rejects tracks Promise resolves and rejects to + * be called when we receive message from web worker. + */ + #resolves: Callbacks = {}; + #rejects: Callbacks = {}; + + #logEventCallbacks: LogEventCallback[] = []; + #progressEventCallbacks: ProgressEventCallback[] = []; + + public loaded = false; + + /** + * register worker message event handlers. + */ + #registerHandlers = () => { + if (this.#worker) { + this.#worker.onmessage = ({ + data: { id, type, data }, + }: FFMessageEventCallback) => { + switch (type) { + case FFMessageType.LOAD: + this.loaded = true; + this.#resolves[id](data); + break; + case FFMessageType.MOUNT: + case FFMessageType.UNMOUNT: + case FFMessageType.EXEC: + case FFMessageType.FFPROBE: + case FFMessageType.WRITE_FILE: + case FFMessageType.READ_FILE: + case FFMessageType.DELETE_FILE: + case FFMessageType.RENAME: + case FFMessageType.CREATE_DIR: + case FFMessageType.LIST_DIR: + case FFMessageType.DELETE_DIR: + this.#resolves[id](data); + break; + case FFMessageType.LOG: + this.#logEventCallbacks.forEach((f) => f(data as LogEvent)); + break; + case FFMessageType.PROGRESS: + this.#progressEventCallbacks.forEach((f) => + f(data as ProgressEvent) + ); + break; + case FFMessageType.ERROR: + this.#rejects[id](data); + break; + } + delete this.#resolves[id]; + delete this.#rejects[id]; + }; + } + }; + + /** + * Generic function to send messages to web worker. + */ + #send = ( + { type, data }: Message, + trans: Transferable[] = [], + signal?: AbortSignal + ): Promise => { + if (!this.#worker) { + return Promise.reject(ERROR_NOT_LOADED); + } + + return new Promise((resolve, reject) => { + const id = getMessageID(); + this.#worker && this.#worker.postMessage({ id, type, data }, trans); + this.#resolves[id] = resolve; + this.#rejects[id] = reject; + + signal?.addEventListener( + "abort", + () => { + reject(new DOMException(`Message # ${id} was aborted`, "AbortError")); + }, + { once: true } + ); + }); + }; + + /** + * Listen to log or prgress events from `ffmpeg.exec()`. + * + * @example + * ```ts + * ffmpeg.on("log", ({ type, message }) => { + * // ... + * }) + * ``` + * + * @example + * ```ts + * ffmpeg.on("progress", ({ progress, time }) => { + * // ... + * }) + * ``` + * + * @remarks + * - log includes output to stdout and stderr. + * - The progress events are accurate only when the length of + * input and output video/audio file are the same. + * + * @category FFmpeg + */ + public on(event: "log", callback: LogEventCallback): void; + public on(event: "progress", callback: ProgressEventCallback): void; + public on( + event: "log" | "progress", + callback: LogEventCallback | ProgressEventCallback + ) { + if (event === "log") { + this.#logEventCallbacks.push(callback as LogEventCallback); + } else if (event === "progress") { + this.#progressEventCallbacks.push(callback as ProgressEventCallback); + } + } + + /** + * Unlisten to log or progress events from `ffmpeg.exec()`. + * + * @category FFmpeg + */ + public off(event: "log", callback: LogEventCallback): void; + public off(event: "progress", callback: ProgressEventCallback): void; + public off( + event: "log" | "progress", + callback: LogEventCallback | ProgressEventCallback + ) { + if (event === "log") { + this.#logEventCallbacks = this.#logEventCallbacks.filter( + (f) => f !== callback + ); + } else if (event === "progress") { + this.#progressEventCallbacks = this.#progressEventCallbacks.filter( + (f) => f !== callback + ); + } + } + + /** + * Loads ffmpeg-core inside web worker. It is required to call this method first + * as it initializes WebAssembly and other essential variables. + * + * @category FFmpeg + * @returns `true` if ffmpeg core is loaded for the first time. + */ + public load = ( + { classWorkerURL, ...config }: FFMessageLoadConfig = {}, + { signal }: FFMessageOptions = {} + ): Promise => { + if (!this.#worker) { + this.#worker = classWorkerURL ? + new Worker(new URL(classWorkerURL, import.meta.url), { + type: "module", + }) : + // We need to duplicated the code here to enable webpack + // to bundle worker.js here. + new Worker(new URL("./worker.js", import.meta.url), { + type: "module", + }); + this.#registerHandlers(); + } + return this.#send( + { + type: FFMessageType.LOAD, + data: config, + }, + undefined, + signal + ) as Promise; + }; + + /** + * Execute ffmpeg command. + * + * @remarks + * To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args + * by default. + * + * @example + * ```ts + * const ffmpeg = new FFmpeg(); + * await ffmpeg.load(); + * await ffmpeg.writeFile("video.avi", ...); + * // ffmpeg -i video.avi video.mp4 + * await ffmpeg.exec(["-i", "video.avi", "video.mp4"]); + * const data = ffmpeg.readFile("video.mp4"); + * ``` + * + * @returns `0` if no error, `!= 0` if timeout (1) or error. + * @category FFmpeg + */ + public exec = ( + /** ffmpeg command line args */ + args: string[], + /** + * milliseconds to wait before stopping the command execution. + * + * @defaultValue -1 + */ + timeout = -1, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.EXEC, + data: { args, timeout }, + }, + undefined, + signal + ) as Promise; + + /** + * Execute ffprobe command. + * + * @example + * ```ts + * const ffmpeg = new FFmpeg(); + * await ffmpeg.load(); + * await ffmpeg.writeFile("video.avi", ...); + * // Getting duration of a video in seconds: ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 video.avi -o output.txt + * await ffmpeg.ffprobe(["-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", "video.avi", "-o", "output.txt"]); + * const data = ffmpeg.readFile("output.txt"); + * ``` + * + * @returns `0` if no error, `!= 0` if timeout (1) or error. + * @category FFmpeg + */ + public ffprobe = ( + /** ffprobe command line args */ + args: string[], + /** + * milliseconds to wait before stopping the command execution. + * + * @defaultValue -1 + */ + timeout = -1, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.FFPROBE, + data: { args, timeout }, + }, + undefined, + signal + ) as Promise; + + /** + * Terminate all ongoing API calls and terminate web worker. + * `FFmpeg.load()` must be called again before calling any other APIs. + * + * @category FFmpeg + */ + public terminate = (): void => { + const ids = Object.keys(this.#rejects); + // rejects all incomplete Promises. + for (const id of ids) { + this.#rejects[id](ERROR_TERMINATED); + delete this.#rejects[id]; + delete this.#resolves[id]; + } + + if (this.#worker) { + this.#worker.terminate(); + this.#worker = null; + this.loaded = false; + } + }; + + /** + * Write data to ffmpeg.wasm. + * + * @example + * ```ts + * const ffmpeg = new FFmpeg(); + * await ffmpeg.load(); + * await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi")); + * await ffmpeg.writeFile("text.txt", "hello world"); + * ``` + * + * @category File System + */ + public writeFile = ( + path: string, + data: FileData, + { signal }: FFMessageOptions = {} + ): Promise => { + const trans: Transferable[] = []; + if (data instanceof Uint8Array) { + trans.push(data.buffer); + } + return this.#send( + { + type: FFMessageType.WRITE_FILE, + data: { path, data }, + }, + trans, + signal + ) as Promise; + }; + + public mount = (fsType: FFFSType, options: FFFSMountOptions, mountPoint: FFFSPath, ): Promise => { + const trans: Transferable[] = []; + return this.#send( + { + type: FFMessageType.MOUNT, + data: { fsType, options, mountPoint }, + }, + trans + ) as Promise; + }; + + public unmount = (mountPoint: FFFSPath): Promise => { + const trans: Transferable[] = []; + return this.#send( + { + type: FFMessageType.UNMOUNT, + data: { mountPoint }, + }, + trans + ) as Promise; + }; + + /** + * Read data from ffmpeg.wasm. + * + * @example + * ```ts + * const ffmpeg = new FFmpeg(); + * await ffmpeg.load(); + * const data = await ffmpeg.readFile("video.mp4"); + * ``` + * + * @category File System + */ + public readFile = ( + path: string, + /** + * File content encoding, supports two encodings: + * - utf8: read file as text file, return data in string type. + * - binary: read file as binary file, return data in Uint8Array type. + * + * @defaultValue binary + */ + encoding = "binary", + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.READ_FILE, + data: { path, encoding }, + }, + undefined, + signal + ) as Promise; + + /** + * Delete a file. + * + * @category File System + */ + public deleteFile = ( + path: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.DELETE_FILE, + data: { path }, + }, + undefined, + signal + ) as Promise; + + /** + * Rename a file or directory. + * + * @category File System + */ + public rename = ( + oldPath: string, + newPath: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.RENAME, + data: { oldPath, newPath }, + }, + undefined, + signal + ) as Promise; + + /** + * Create a directory. + * + * @category File System + */ + public createDir = ( + path: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.CREATE_DIR, + data: { path }, + }, + undefined, + signal + ) as Promise; + + /** + * List directory contents. + * + * @category File System + */ + public listDir = ( + path: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.LIST_DIR, + data: { path }, + }, + undefined, + signal + ) as Promise; + + /** + * Delete an empty directory. + * + * @category File System + */ + public deleteDir = ( + path: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.DELETE_DIR, + data: { path }, + }, + undefined, + signal + ) as Promise; +} diff --git a/public/ffmpeg_packages/ffmpeg/src/const.ts b/public/ffmpeg_packages/ffmpeg/src/const.ts new file mode 100755 index 0000000..9406513 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/const.ts @@ -0,0 +1,25 @@ +export const MIME_TYPE_JAVASCRIPT = "text/javascript"; +export const MIME_TYPE_WASM = "application/wasm"; + +export const CORE_VERSION = "0.12.10"; +export const CORE_URL = `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`; + +export enum FFMessageType { + LOAD = "LOAD", + EXEC = "EXEC", + FFPROBE = "FFPROBE", + WRITE_FILE = "WRITE_FILE", + READ_FILE = "READ_FILE", + DELETE_FILE = "DELETE_FILE", + RENAME = "RENAME", + CREATE_DIR = "CREATE_DIR", + LIST_DIR = "LIST_DIR", + DELETE_DIR = "DELETE_DIR", + ERROR = "ERROR", + + DOWNLOAD = "DOWNLOAD", + PROGRESS = "PROGRESS", + LOG = "LOG", + MOUNT = "MOUNT", + UNMOUNT = "UNMOUNT", +} diff --git a/public/ffmpeg_packages/ffmpeg/src/empty.mts b/public/ffmpeg_packages/ffmpeg/src/empty.mts new file mode 100755 index 0000000..4819fb9 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/empty.mts @@ -0,0 +1,7 @@ +// File to be imported in node enviroments + +export class FFmpeg { + constructor() { + throw new Error("ffmpeg.wasm does not support nodejs"); + } +} \ No newline at end of file diff --git a/public/ffmpeg_packages/ffmpeg/src/errors.ts b/public/ffmpeg_packages/ffmpeg/src/errors.ts new file mode 100755 index 0000000..d7246c7 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/errors.ts @@ -0,0 +1,8 @@ +export const ERROR_UNKNOWN_MESSAGE_TYPE = new Error("unknown message type"); +export const ERROR_NOT_LOADED = new Error( + "ffmpeg is not loaded, call `await ffmpeg.load()` first" +); +export const ERROR_TERMINATED = new Error("called FFmpeg.terminate()"); +export const ERROR_IMPORT_FAILURE = new Error( + "failed to import ffmpeg-core.js" +); diff --git a/public/ffmpeg_packages/ffmpeg/src/index.ts b/public/ffmpeg_packages/ffmpeg/src/index.ts new file mode 100755 index 0000000..4c8e184 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/index.ts @@ -0,0 +1,2 @@ +export * from "./classes.js"; +export * from "./types.js"; diff --git a/public/ffmpeg_packages/ffmpeg/src/types.ts b/public/ffmpeg_packages/ffmpeg/src/types.ts new file mode 100755 index 0000000..6082a6d --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/types.ts @@ -0,0 +1,182 @@ +export type FFFSPath = string; + +/** + * ffmpeg-core loading configuration. + */ +export interface FFMessageLoadConfig { + /** + * `ffmpeg-core.js` URL. + * + * @defaultValue `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.js`; + */ + coreURL?: string; + /** + * `ffmpeg-core.wasm` URL. + * + * @defaultValue `https://unpkg.com/@ffmpeg/core@${CORE_VERSION}/dist/umd/ffmpeg-core.wasm`; + */ + wasmURL?: string; + /** + * `ffmpeg-core.worker.js` URL. This worker is spawned when using multithread version of ffmpeg-core. + * + * @ref: https://ffmpegwasm.netlify.app/docs/overview#architecture + * @defaultValue `https://unpkg.com/@ffmpeg/core-mt@${CORE_VERSION}/dist/umd/ffmpeg-core.worker.js`; + */ + workerURL?: string; + /** + * `ffmpeg.worker.js` URL. This worker is spawned when FFmpeg.load() is called, it is an essential worker and usually you don't need to update this config. + * + * @ref: https://ffmpegwasm.netlify.app/docs/overview#architecture + * @defaultValue `./worker.js` + */ + classWorkerURL?: string; +} + +export interface FFMessageExecData { + args: string[]; + timeout?: number; +} + +export interface FFMessageWriteFileData { + path: FFFSPath; + data: FileData; +} + +export interface FFMessageReadFileData { + path: FFFSPath; + encoding: string; +} + +export interface FFMessageDeleteFileData { + path: FFFSPath; +} + +export interface FFMessageRenameData { + oldPath: FFFSPath; + newPath: FFFSPath; +} + +export interface FFMessageCreateDirData { + path: FFFSPath; +} + +export interface FFMessageListDirData { + path: FFFSPath; +} + +/** + * @remarks + * Only deletes empty directory. + */ +export interface FFMessageDeleteDirData { + path: FFFSPath; +} + +export enum FFFSType { + MEMFS = "MEMFS", + NODEFS = "NODEFS", + NODERAWFS = "NODERAWFS", + IDBFS = "IDBFS", + WORKERFS = "WORKERFS", + PROXYFS = "PROXYFS", +} + +export type WorkerFSFileEntry = + | File; + +export interface WorkerFSBlobEntry { + name: string; + data: Blob; +} + +export interface WorkerFSMountData { + blobs?: WorkerFSBlobEntry[]; + files?: WorkerFSFileEntry[]; +} + +export type FFFSMountOptions = + | WorkerFSMountData; + +export interface FFMessageMountData { + fsType: FFFSType; + options: FFFSMountOptions; + mountPoint: FFFSPath; +} + +export interface FFMessageUnmountData { + mountPoint: FFFSPath; +} + +export type FFMessageData = + | FFMessageLoadConfig + | FFMessageExecData + | FFMessageWriteFileData + | FFMessageReadFileData + | FFMessageDeleteFileData + | FFMessageRenameData + | FFMessageCreateDirData + | FFMessageListDirData + | FFMessageDeleteDirData + | FFMessageMountData + | FFMessageUnmountData; + +export interface Message { + type: string; + data?: FFMessageData; +} + +export interface FFMessage extends Message { + id: number; +} + +export interface FFMessageEvent extends MessageEvent { + data: FFMessage; +} + +export interface LogEvent { + type: string; + message: string; +} + +export interface ProgressEvent { + progress: number; + time: number; +} + +export type ExitCode = number; +export type ErrorMessage = string; +export type FileData = Uint8Array | string; +export type IsFirst = boolean; +export type OK = boolean; + +export interface FSNode { + name: string; + isDir: boolean; +} + +export type CallbackData = + | FileData + | ExitCode + | ErrorMessage + | LogEvent + | ProgressEvent + | IsFirst + | OK // eslint-disable-line + | Error + | FSNode[] + | undefined; + +export interface Callbacks { + [id: number | string]: (data: CallbackData) => void; +} + +export type LogEventCallback = (event: LogEvent) => void; +export type ProgressEventCallback = (event: ProgressEvent) => void; + +export interface FFMessageEventCallback { + data: { + id: number; + type: string; + data: CallbackData; + }; +} diff --git a/public/ffmpeg_packages/ffmpeg/src/utils.ts b/public/ffmpeg_packages/ffmpeg/src/utils.ts new file mode 100755 index 0000000..9281556 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/utils.ts @@ -0,0 +1,7 @@ +/** + * Generate an unique message ID. + */ +export const getMessageID = (() => { + let messageID = 0; + return () => messageID++; +})(); diff --git a/public/ffmpeg_packages/ffmpeg/src/worker.ts b/public/ffmpeg_packages/ffmpeg/src/worker.ts new file mode 100755 index 0000000..cf8b6c5 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/src/worker.ts @@ -0,0 +1,226 @@ +/// +/// +/// + +import type { FFmpegCoreModule, FFmpegCoreModuleFactory } from "@ffmpeg/types"; +import type { + FFMessageEvent, + FFMessageLoadConfig, + FFMessageExecData, + FFMessageWriteFileData, + FFMessageReadFileData, + FFMessageDeleteFileData, + FFMessageRenameData, + FFMessageCreateDirData, + FFMessageListDirData, + FFMessageDeleteDirData, + FFMessageMountData, + FFMessageUnmountData, + CallbackData, + IsFirst, + OK, + ExitCode, + FSNode, + FileData, +} from "./types"; +import { CORE_URL, FFMessageType } from "./const.js"; +import { + ERROR_UNKNOWN_MESSAGE_TYPE, + ERROR_NOT_LOADED, + ERROR_IMPORT_FAILURE, +} from "./errors.js"; + +declare global { + interface WorkerGlobalScope { + createFFmpegCore: FFmpegCoreModuleFactory; + } +} + +interface ImportedFFmpegCoreModuleFactory { + default: FFmpegCoreModuleFactory; +} + +let ffmpeg: FFmpegCoreModule; + +const load = async ({ + coreURL: _coreURL, + wasmURL: _wasmURL, + workerURL: _workerURL, +}: FFMessageLoadConfig): Promise => { + const first = !ffmpeg; + + try { + if (!_coreURL) _coreURL = CORE_URL; + // when web worker type is `classic`. + importScripts(_coreURL); + } catch { + if (!_coreURL || _coreURL === CORE_URL) _coreURL = CORE_URL.replace('/umd/', '/esm/'); + // when web worker type is `module`. + (self as WorkerGlobalScope).createFFmpegCore = ( + (await import( + /* @vite-ignore */ _coreURL + )) as ImportedFFmpegCoreModuleFactory + ).default; + + if (!(self as WorkerGlobalScope).createFFmpegCore) { + throw ERROR_IMPORT_FAILURE; + } + } + + const coreURL = _coreURL; + const wasmURL = _wasmURL ? _wasmURL : _coreURL.replace(/.js$/g, ".wasm"); + const workerURL = _workerURL + ? _workerURL + : _coreURL.replace(/.js$/g, ".worker.js"); + + ffmpeg = await (self as WorkerGlobalScope).createFFmpegCore({ + // Fix `Overload resolution failed.` when using multi-threaded ffmpeg-core. + // Encoded wasmURL and workerURL in the URL as a hack to fix locateFile issue. + mainScriptUrlOrBlob: `${coreURL}#${btoa( + JSON.stringify({ wasmURL, workerURL }) + )}`, + }); + ffmpeg.setLogger((data) => + self.postMessage({ type: FFMessageType.LOG, data }) + ); + ffmpeg.setProgress((data) => + self.postMessage({ + type: FFMessageType.PROGRESS, + data, + }) + ); + return first; +}; + +const exec = ({ args, timeout = -1 }: FFMessageExecData): ExitCode => { + ffmpeg.setTimeout(timeout); + ffmpeg.exec(...args); + const ret = ffmpeg.ret; + ffmpeg.reset(); + return ret; +}; + +const ffprobe = ({ args, timeout = -1 }: FFMessageExecData): ExitCode => { + ffmpeg.setTimeout(timeout); + ffmpeg.ffprobe(...args); + const ret = ffmpeg.ret; + ffmpeg.reset(); + return ret; +}; + +const writeFile = ({ path, data }: FFMessageWriteFileData): OK => { + ffmpeg.FS.writeFile(path, data); + return true; +}; + +const readFile = ({ path, encoding }: FFMessageReadFileData): FileData => + ffmpeg.FS.readFile(path, { encoding }); + +// TODO: check if deletion works. +const deleteFile = ({ path }: FFMessageDeleteFileData): OK => { + ffmpeg.FS.unlink(path); + return true; +}; + +const rename = ({ oldPath, newPath }: FFMessageRenameData): OK => { + ffmpeg.FS.rename(oldPath, newPath); + return true; +}; + +// TODO: check if creation works. +const createDir = ({ path }: FFMessageCreateDirData): OK => { + ffmpeg.FS.mkdir(path); + return true; +}; + +const listDir = ({ path }: FFMessageListDirData): FSNode[] => { + const names = ffmpeg.FS.readdir(path); + const nodes: FSNode[] = []; + for (const name of names) { + const stat = ffmpeg.FS.stat(`${path}/${name}`); + const isDir = ffmpeg.FS.isDir(stat.mode); + nodes.push({ name, isDir }); + } + return nodes; +}; + +// TODO: check if deletion works. +const deleteDir = ({ path }: FFMessageDeleteDirData): OK => { + ffmpeg.FS.rmdir(path); + return true; +}; + +const mount = ({ fsType, options, mountPoint }: FFMessageMountData): OK => { + const str = fsType as keyof typeof ffmpeg.FS.filesystems; + const fs = ffmpeg.FS.filesystems[str]; + if (!fs) return false; + ffmpeg.FS.mount(fs, options, mountPoint); + return true; +}; + +const unmount = ({ mountPoint }: FFMessageUnmountData): OK => { + ffmpeg.FS.unmount(mountPoint); + return true; +}; + +self.onmessage = async ({ + data: { id, type, data: _data }, +}: FFMessageEvent): Promise => { + const trans = []; + let data: CallbackData; + try { + if (type !== FFMessageType.LOAD && !ffmpeg) throw ERROR_NOT_LOADED; // eslint-disable-line + + switch (type) { + case FFMessageType.LOAD: + data = await load(_data as FFMessageLoadConfig); + break; + case FFMessageType.EXEC: + data = exec(_data as FFMessageExecData); + break; + case FFMessageType.FFPROBE: + data = ffprobe(_data as FFMessageExecData); + break; + case FFMessageType.WRITE_FILE: + data = writeFile(_data as FFMessageWriteFileData); + break; + case FFMessageType.READ_FILE: + data = readFile(_data as FFMessageReadFileData); + break; + case FFMessageType.DELETE_FILE: + data = deleteFile(_data as FFMessageDeleteFileData); + break; + case FFMessageType.RENAME: + data = rename(_data as FFMessageRenameData); + break; + case FFMessageType.CREATE_DIR: + data = createDir(_data as FFMessageCreateDirData); + break; + case FFMessageType.LIST_DIR: + data = listDir(_data as FFMessageListDirData); + break; + case FFMessageType.DELETE_DIR: + data = deleteDir(_data as FFMessageDeleteDirData); + break; + case FFMessageType.MOUNT: + data = mount(_data as FFMessageMountData); + break; + case FFMessageType.UNMOUNT: + data = unmount(_data as FFMessageUnmountData); + break; + default: + throw ERROR_UNKNOWN_MESSAGE_TYPE; + } + } catch (e) { + self.postMessage({ + id, + type: FFMessageType.ERROR, + data: (e as Error).toString(), + }); + return; + } + if (data instanceof Uint8Array) { + trans.push(data.buffer); + } + self.postMessage({ id, type, data }, trans); +}; diff --git a/public/ffmpeg_packages/ffmpeg/tsconfig.esm.json b/public/ffmpeg_packages/ffmpeg/tsconfig.esm.json new file mode 100755 index 0000000..5a3cf61 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/tsconfig.esm.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "src", + "declaration": true, + "outDir": "./dist/esm", + "target": "esnext", + "moduleResolution": "nodenext" + } +} diff --git a/public/ffmpeg_packages/ffmpeg/tsconfig.json b/public/ffmpeg_packages/ffmpeg/tsconfig.json new file mode 100755 index 0000000..6013564 --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "esnext", + "rootDir": "src" + } +} diff --git a/public/ffmpeg_packages/ffmpeg/webpack.config.js b/public/ffmpeg_packages/ffmpeg/webpack.config.js new file mode 100755 index 0000000..094425c --- /dev/null +++ b/public/ffmpeg_packages/ffmpeg/webpack.config.js @@ -0,0 +1,19 @@ +const path = require("path"); + +module.exports = { + mode: "production", + devtool: "source-map", + entry: "./dist/esm/index.js", + resolve: { + extensions: [".js"], + }, + output: { + path: path.resolve(__dirname, "dist/umd"), + filename: "ffmpeg.js", + library: "FFmpegWASM", + libraryTarget: "umd", + }, + stats: { + warnings:false + } +}; diff --git a/public/ffmpeg_packages/types/package.json b/public/ffmpeg_packages/types/package.json new file mode 100755 index 0000000..99a4231 --- /dev/null +++ b/public/ffmpeg_packages/types/package.json @@ -0,0 +1,32 @@ +{ + "name": "@ffmpeg/types", + "version": "0.12.4", + "description": "ffmpeg.wasm types", + "types": "types", + "files": [ + "types" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git" + }, + "keywords": [ + "ffmpeg", + "WebAssembly", + "video", + "audio", + "transcode" + ], + "author": "Jerome Wu ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues" + }, + "engines": { + "node": ">=16.x" + }, + "homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme", + "publishConfig": { + "access": "public" + } +} diff --git a/public/ffmpeg_packages/types/types/index.d.ts b/public/ffmpeg_packages/types/types/index.d.ts new file mode 100755 index 0000000..8795007 --- /dev/null +++ b/public/ffmpeg_packages/types/types/index.d.ts @@ -0,0 +1,142 @@ +// TODO: Add lint and test. + +export type Pointer = number; + +export type StringPointer = Pointer; +export type StringArrayPointer = Pointer; +export type DateString = string; + +/** + * Options for readFile. + * + * @see [Emscripten File System API](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.readFile) + * @category File System + */ +export interface ReadFileOptions { + /** encoding of the file, must be `binary` or `utf8` */ + encdoing: string; +} + +/** + * Describes attributes of a node. (a.k.a file, directory) + * + * @see [Emscripten File System API](https://emscripten.org/docs/api_reference/Filesystem-API.html#FS.stat) + * @category File System + */ +export interface Stat { + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + atime: DateString; + mtime: DateString; + ctime: DateString; + blksize: number; + blocks: number; +} + +export interface FSFilesystemWORKERFS {} + +export interface FSFilesystemMEMFS {} + +export interface FSFilesystems { + WORKERFS: FSFilesystemWORKERFS; + MEMFS: FSFilesystemMEMFS; +} + +export type FSFilesystem = FSFilesystemWORKERFS | FSFilesystemMEMFS; + +export interface OptionReadFile { + encoding: string; +} + +export interface WorkerFSMountConfig { + blobs?: { + name: string; + data: Blob; + }[]; + files?: File[]; +} + +/** + * Functions to interact with Emscripten FS library. + * + * @see [Emscripten File System API](https://emscripten.org/docs/api_reference/Filesystem-API.html) + * @category File System + */ +export interface FS { + mkdir: (path: string) => void; + rmdir: (path: string) => void; + rename: (oldPath: string, newPath: string) => void; + writeFile: (path: string, data: Uint8Array | string) => void; + readFile: (path: string, opts: OptionReadFile) => Uint8Array | string; + readdir: (path: string) => string[]; + unlink: (path: string) => void; + stat: (path: string) => Stat; + /** mode is a numeric notation of permission, @see [Numeric Notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) */ + isFile: (mode: number) => boolean; + /** mode is a numeric notation of permission, @see [Numeric Notation](https://en.wikipedia.org/wiki/File-system_permissions#Numeric_notation) */ + isDir: (mode: number) => boolean; + mount: ( + fileSystemType: FSFilesystem, + data: WorkerFSMountConfig, + path: string + ) => void; + unmount: (path: string) => void; + filesystems: FSFilesystems; +} + +/** + * Arguments passed to setLogger callback function. + */ +export interface Log { + /** file descriptor of the log, must be `stdout` or `stderr` */ + type: string; + message: string; +} + +/** + * Arguments passed to setProgress callback function. + */ +export interface Progress { + /** progress of the operation, interval = [0, 1] */ + progress: number; + /** time of transcoded media in microseconds, ex: if a video is 10 seconds long, when time is 1000000 means 1 second of the video is transcoded already. */ + time: number; +} + +/** + * FFmpeg core module, an object to interact with ffmpeg. + */ +export interface FFmpegCoreModule { + /** default arguments prepend when running exec() */ + DEFAULT_ARGS: string[]; + FS: FS; + NULL: Pointer; + SIZE_I32: number; + + /** return code of the ffmpeg exec, error when ret != 0 */ + ret: number; + timeout: number; + mainScriptUrlOrBlob: string; + + exec: (...args: string[]) => number; + ffprobe: (...args: string[]) => number; + reset: () => void; + setLogger: (logger: (log: Log) => void) => void; + setTimeout: (timeout: number) => void; + setProgress: (handler: (progress: Progress) => void) => void; + + locateFile: (path: string, prefix: string) => string; +} + +/** + * Factory of FFmpegCoreModule. + */ +export type FFmpegCoreModuleFactory = ( + moduleOverrides?: Partial +) => Promise; diff --git a/public/ffmpeg_packages/util/.eslintrc.cjs b/public/ffmpeg_packages/util/.eslintrc.cjs new file mode 100755 index 0000000..318425f --- /dev/null +++ b/public/ffmpeg_packages/util/.eslintrc.cjs @@ -0,0 +1,13 @@ +module.exports = { + extends: [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "plugin:@typescript-eslint/recommended-requiring-type-checking", + ], + parser: "@typescript-eslint/parser", + parserOptions: { + tsconfigRootDir: __dirname, + project: ["./tsconfig.json"], + }, + plugins: ["@typescript-eslint"], +}; diff --git a/public/ffmpeg_packages/util/.gitignore b/public/ffmpeg_packages/util/.gitignore new file mode 100755 index 0000000..849ddff --- /dev/null +++ b/public/ffmpeg_packages/util/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/public/ffmpeg_packages/util/package.json b/public/ffmpeg_packages/util/package.json new file mode 100755 index 0000000..78aa9f7 --- /dev/null +++ b/public/ffmpeg_packages/util/package.json @@ -0,0 +1,57 @@ +{ + "name": "@ffmpeg/util", + "version": "0.12.2", + "description": "browser utils for @ffmpeg/*", + "main": "./dist/cjs/index.js", + "type": "module", + "types": "./dist/cjs/index.d.ts", + "exports": { + ".": { + "types": "./dist/cjs/index.d.ts", + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + }, + "scripts": { + "dev": "tsc -p tsconfig-esm.json --watch", + "lint": "eslint src", + "clean": "rimraf dist", + "build:esm": "tsc -p tsconfig.esm.json", + "build:umd": "tsc -p tsconfig.cjs.json && webpack", + "build": "npm run clean && npm run build:esm && npm run build:umd", + "prepublishOnly": "npm run build" + }, + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/ffmpegwasm/ffmpeg.wasm.git" + }, + "keywords": [ + "ffmpeg", + "video", + "audio", + "transcode" + ], + "author": "Jerome Wu ", + "license": "MIT", + "bugs": { + "url": "https://github.com/ffmpegwasm/ffmpeg.wasm/issues" + }, + "engines": { + "node": ">=18.x" + }, + "homepage": "https://github.com/ffmpegwasm/ffmpeg.wasm#readme", + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^6.1.0", + "@typescript-eslint/parser": "^6.1.0", + "eslint": "^8.45.0", + "rimraf": "^5.0.1", + "typescript": "^5.1.6", + "webpack-cli": "^5.1.4" + } +} diff --git a/public/ffmpeg_packages/util/src/const.ts b/public/ffmpeg_packages/util/src/const.ts new file mode 100755 index 0000000..5d0cd85 --- /dev/null +++ b/public/ffmpeg_packages/util/src/const.ts @@ -0,0 +1 @@ +export const HeaderContentLength = "Content-Length"; diff --git a/public/ffmpeg_packages/util/src/errors.ts b/public/ffmpeg_packages/util/src/errors.ts new file mode 100755 index 0000000..ba395bf --- /dev/null +++ b/public/ffmpeg_packages/util/src/errors.ts @@ -0,0 +1,6 @@ +export const ERROR_RESPONSE_BODY_READER = new Error( + "failed to get response body reader" +); +export const ERROR_INCOMPLETED_DOWNLOAD = new Error( + "failed to complete download" +); diff --git a/public/ffmpeg_packages/util/src/index.ts b/public/ffmpeg_packages/util/src/index.ts new file mode 100755 index 0000000..0f0756e --- /dev/null +++ b/public/ffmpeg_packages/util/src/index.ts @@ -0,0 +1,180 @@ +import { + ERROR_RESPONSE_BODY_READER, + ERROR_INCOMPLETED_DOWNLOAD, +} from "./errors.js"; +import { HeaderContentLength } from "./const.js"; +import { ProgressCallback } from "./types.js"; + +const readFromBlobOrFile = (blob: Blob | File): Promise => + 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); + }); + +/** + * An util function to fetch data from url string, base64, URL, File or Blob format. + * + * Examples: + * ```ts + * // URL + * await fetchFile("http://localhost:3000/video.mp4"); + * // base64 + * await fetchFile("data:;base64,wL2dvYWwgbW9yZ..."); + * // URL + * await fetchFile(new URL("video.mp4", import.meta.url)); + * // File + * fileInput.addEventListener('change', (e) => { + * await fetchFile(e.target.files[0]); + * }); + * // Blob + * const blob = new Blob(...); + * await fetchFile(blob); + * ``` + */ +export const fetchFile = async ( + file?: string | File | Blob +): Promise => { + let data: ArrayBuffer | number[]; + + if (typeof file === "string") { + /* From base64 format */ + if (/data:_data\/([a-zA-Z]*);base64,([^"]*)/.test(file)) { + data = atob(file.split(",")[1]) + .split("") + .map((c) => c.charCodeAt(0)); + /* From remote server/URL */ + } else { + data = await (await fetch(file)).arrayBuffer(); + } + } else if (file instanceof URL) { + data = await (await fetch(file)).arrayBuffer(); + } else if (file instanceof File || file instanceof Blob) { + data = await readFromBlobOrFile(file); + } else { + return new Uint8Array(); + } + + return new Uint8Array(data); +}; + +/** + * importScript dynamically import a script, useful when you + * want to use different versions of ffmpeg.wasm based on environment. + * + * Example: + * + * ```ts + * await importScript("http://localhost:3000/ffmpeg.js"); + * ``` + */ +export const importScript = async (url: string): Promise => + new Promise((resolve) => { + const script = document.createElement("script"); + const eventHandler = () => { + script.removeEventListener("load", eventHandler); + resolve(); + }; + script.src = url; + script.type = "text/javascript"; + script.addEventListener("load", eventHandler); + document.getElementsByTagName("head")[0].appendChild(script); + }); + +/** + * Download content of a URL with progress. + * + * Progress only works when Content-Length is provided by the server. + * + */ +export const downloadWithProgress = async ( + url: string | URL, + cb?: ProgressCallback +): Promise => { + const resp = await fetch(url); + let buf; + + try { + // Set total to -1 to indicate that there is not Content-Type Header. + const total = parseInt(resp.headers.get(HeaderContentLength) || "-1"); + + const reader = resp.body?.getReader(); + if (!reader) throw ERROR_RESPONSE_BODY_READER; + + const chunks = []; + let received = 0; + for (;;) { + const { done, value } = await reader.read(); + const delta = value ? value.length : 0; + + if (done) { + if (total != -1 && total !== received) throw ERROR_INCOMPLETED_DOWNLOAD; + cb && cb({ url, total, received, delta, done }); + break; + } + + chunks.push(value); + received += delta; + cb && cb({ url, total, received, delta, done }); + } + + const data = new Uint8Array(received); + let position = 0; + for (const chunk of chunks) { + data.set(chunk, position); + position += chunk.length; + } + + buf = data.buffer; + } catch (e) { + console.log(`failed to send download progress event: `, e); + // Fetch arrayBuffer directly when it is not possible to get progress. + buf = await resp.arrayBuffer(); + cb && + cb({ + url, + total: buf.byteLength, + received: buf.byteLength, + delta: 0, + done: true, + }); + } + + return buf; +}; + +/** + * toBlobURL fetches data from an URL and return a blob URL. + * + * Example: + * + * ```ts + * await toBlobURL("http://localhost:3000/ffmpeg.js", "text/javascript"); + * ``` + */ +export const toBlobURL = async ( + url: string, + mimeType: string, + progress = false, + cb?: ProgressCallback +): Promise => { + const buf = progress + ? await downloadWithProgress(url, cb) + : await (await fetch(url)).arrayBuffer(); + const blob = new Blob([buf], { type: mimeType }); + return URL.createObjectURL(blob); +}; diff --git a/public/ffmpeg_packages/util/src/types.ts b/public/ffmpeg_packages/util/src/types.ts new file mode 100755 index 0000000..10c9308 --- /dev/null +++ b/public/ffmpeg_packages/util/src/types.ts @@ -0,0 +1,9 @@ +export interface DownloadProgressEvent { + url: string | URL; + total: number; + received: number; + delta: number; + done: boolean; +} + +export type ProgressCallback = (event: DownloadProgressEvent) => void; diff --git a/public/ffmpeg_packages/util/tests/.eslintrc b/public/ffmpeg_packages/util/tests/.eslintrc new file mode 100755 index 0000000..e56b933 --- /dev/null +++ b/public/ffmpeg_packages/util/tests/.eslintrc @@ -0,0 +1,6 @@ +{ + "rules": { + "no-undef": 0, + "camelcase": 0 + } +} diff --git a/public/ffmpeg_packages/util/tests/constants.js b/public/ffmpeg_packages/util/tests/constants.js new file mode 100755 index 0000000..6060ebc --- /dev/null +++ b/public/ffmpeg_packages/util/tests/constants.js @@ -0,0 +1,19 @@ +const TIMEOUT = 60000; +const IS_BROWSER = typeof window !== 'undefined' && typeof window.document !== 'undefined'; +const OPTIONS = { + corePath: IS_BROWSER ? 'http://localhost:3000/node_modules/@ffmpeg/core/dist/ffmpeg-core.js' : '@ffmpeg/core', +}; +const FLAME_MP4_LENGTH = 100374; +const META_FLAME_MP4_LENGTH = 100408; +const META_FLAME_MP4_LENGTH_NO_SPACE = 100404; + +if (typeof module !== 'undefined') { + module.exports = { + TIMEOUT, + IS_BROWSER, + OPTIONS, + FLAME_MP4_LENGTH, + META_FLAME_MP4_LENGTH, + META_FLAME_MP4_LENGTH_NO_SPACE, + }; +} diff --git a/public/ffmpeg_packages/util/tests/ffmpeg.test.html b/public/ffmpeg_packages/util/tests/ffmpeg.test.html new file mode 100755 index 0000000..f173422 --- /dev/null +++ b/public/ffmpeg_packages/util/tests/ffmpeg.test.html @@ -0,0 +1,21 @@ + + + + + FFmpeg Unit Test + + + +
+ + + + + + + + + diff --git a/public/ffmpeg_packages/util/tests/ffmpeg.test.js b/public/ffmpeg_packages/util/tests/ffmpeg.test.js new file mode 100755 index 0000000..6678521 --- /dev/null +++ b/public/ffmpeg_packages/util/tests/ffmpeg.test.js @@ -0,0 +1,69 @@ +const { createFFmpeg } = FFmpeg; + +describe('FS()', () => { + const ffmpeg = createFFmpeg(OPTIONS); + before(async function cb() { + this.timeout(0); + await ffmpeg.load(); + }); + + it('should throw error when readdir for invalid path ', () => { + expect(() => ffmpeg.FS('readdir', '/invalid')).to.throw(/readdir/); + }); + it('should throw error when readFile for invalid path ', () => { + expect(() => ffmpeg.FS('readFile', '/invalid')).to.throw(/readFile/); + }); + it('should throw an default error ', () => { + expect(() => ffmpeg.FS('unlink', '/invalid')).to.throw(/Oops/); + }); +}); + +describe('load()', () => { + it('should throw error when corePath is not a string', async () => { + const ffmpeg = createFFmpeg({ ...OPTIONS, corePath: null }); + + try { + await ffmpeg.load(); + } catch (e) { + expect(e).to.be.an('Error'); + } + }); + it('should throw error when not called before FS() and run()', () => { + const ffmpeg = createFFmpeg(OPTIONS); + expect(() => ffmpeg.FS('readdir', 'dummy')).to.throw(); + expect(() => ffmpeg.run('-h')).to.throw(); + }); + + it('should throw error when running load() more than once', async () => { + const ffmpeg = createFFmpeg(OPTIONS); + await ffmpeg.load(); + try { + await ffmpeg.load(); + } catch (e) { + expect(e).to.be.an('Error'); + } + }).timeout(TIMEOUT); +}); + +describe('isLoaded()', () => { + it('should return true when loaded', async () => { + const ffmpeg = createFFmpeg(OPTIONS); + await ffmpeg.load(); + expect(ffmpeg.isLoaded()).to.equal(true); + }).timeout(TIMEOUT); +}); + +describe('run()', () => { + it('should not allow to run two command at the same time', async () => { + const ffmpeg = createFFmpeg(OPTIONS); + await ffmpeg.load(); + ffmpeg.run('-h'); + setTimeout(() => { + try { + ffmpeg.run('-h'); + } catch (e) { + expect(e).to.be.an(Error); + } + }, 500); + }).timeout(TIMEOUT); +}); diff --git a/public/ffmpeg_packages/util/tsconfig.cjs.json b/public/ffmpeg_packages/util/tsconfig.cjs.json new file mode 100755 index 0000000..407c338 --- /dev/null +++ b/public/ffmpeg_packages/util/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "dist/cjs", + "target": "es2015" + } +} diff --git a/public/ffmpeg_packages/util/tsconfig.esm.json b/public/ffmpeg_packages/util/tsconfig.esm.json new file mode 100755 index 0000000..77244f4 --- /dev/null +++ b/public/ffmpeg_packages/util/tsconfig.esm.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "esnext", + "outDir": "dist/esm", + "target": "esnext", + "moduleResolution": "nodenext" + } +} diff --git a/public/ffmpeg_packages/util/tsconfig.json b/public/ffmpeg_packages/util/tsconfig.json new file mode 100755 index 0000000..9607e81 --- /dev/null +++ b/public/ffmpeg_packages/util/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "declaration": true + } +} diff --git a/public/ffmpeg_packages/util/webpack.config.cjs b/public/ffmpeg_packages/util/webpack.config.cjs new file mode 100755 index 0000000..1e504df --- /dev/null +++ b/public/ffmpeg_packages/util/webpack.config.cjs @@ -0,0 +1,12 @@ +const path = require("path"); + +module.exports = { + mode: "production", + entry: "./dist/cjs/index.js", + output: { + library: "FFmpegUtil", + libraryTarget: "umd", + filename: "index.js", + path: path.resolve(__dirname, "dist", "umd"), + }, +}; diff --git a/resources/js/modules/editor/partials/canvas/video-export.jsx b/resources/js/modules/editor/partials/canvas/video-export.jsx index cdac215..dca8b59 100644 --- a/resources/js/modules/editor/partials/canvas/video-export.jsx +++ b/resources/js/modules/editor/partials/canvas/video-export.jsx @@ -391,7 +391,8 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { showConsoleLogs && console.log('FFmpeg Log:', message); }); - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.15/dist/esm'; + //const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.10/dist/esm'; + const baseURL = window.location.origin + '/ffmpeg_packages/core/dist/esm'; const coreURL = `${baseURL}/ffmpeg-core.js`; const wasmURL = `${baseURL}/ffmpeg-core.wasm`; @@ -504,6 +505,8 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => { showConsoleLogs && console.log('Generated FFmpeg arguments:', args); + showConsoleLogs && console.log(generateFFmpegCommand(true, true)); + setExportProgress(70); try {