|
@@ -0,0 +1,277 @@
|
|
|
+import { FFMessageType } from "./const.js";
|
|
|
+import { getMessageID } from "./utils.js";
|
|
|
+import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js";
|
|
|
+/**
|
|
|
+ * Provides APIs to interact with ffmpeg web worker.
|
|
|
+ *
|
|
|
+ * @example
|
|
|
+ * ```ts
|
|
|
+ * const ffmpeg = new FFmpeg();
|
|
|
+ * ```
|
|
|
+ */
|
|
|
+export class FFmpeg {
|
|
|
+ #worker = null;
|
|
|
+ /**
|
|
|
+ * #resolves and #rejects tracks Promise resolves and rejects to
|
|
|
+ * be called when we receive message from web worker.
|
|
|
+ */
|
|
|
+ #resolves = {};
|
|
|
+ #rejects = {};
|
|
|
+ #logEventCallbacks = [];
|
|
|
+ #progressEventCallbacks = [];
|
|
|
+ loaded = false;
|
|
|
+ /**
|
|
|
+ * register worker message event handlers.
|
|
|
+ */
|
|
|
+ #registerHandlers = () => {
|
|
|
+ if (this.#worker) {
|
|
|
+ this.#worker.onmessage = ({ data: { id, type, data }, }) => {
|
|
|
+ switch (type) {
|
|
|
+ case FFMessageType.LOAD:
|
|
|
+ this.loaded = true;
|
|
|
+ this.#resolves[id](data);
|
|
|
+ break;
|
|
|
+ case FFMessageType.MOUNT:
|
|
|
+ case FFMessageType.UNMOUNT:
|
|
|
+ case FFMessageType.EXEC:
|
|
|
+ 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));
|
|
|
+ break;
|
|
|
+ case FFMessageType.PROGRESS:
|
|
|
+ this.#progressEventCallbacks.forEach((f) => f(data));
|
|
|
+ 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 }, trans = [], signal) => {
|
|
|
+ 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 });
|
|
|
+ });
|
|
|
+ };
|
|
|
+ on(event, callback) {
|
|
|
+ if (event === "log") {
|
|
|
+ this.#logEventCallbacks.push(callback);
|
|
|
+ }
|
|
|
+ else if (event === "progress") {
|
|
|
+ this.#progressEventCallbacks.push(callback);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ off(event, callback) {
|
|
|
+ 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.
|
|
|
+ */
|
|
|
+ load = ({ classWorkerURL, ...config } = {}, { signal } = {}) => {
|
|
|
+ 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 worekr.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);
|
|
|
+ };
|
|
|
+ /**
|
|
|
+ * 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
|
|
|
+ */
|
|
|
+ exec = (
|
|
|
+ /** ffmpeg command line args */
|
|
|
+ args,
|
|
|
+ /**
|
|
|
+ * milliseconds to wait before stopping the command execution.
|
|
|
+ *
|
|
|
+ * @defaultValue -1
|
|
|
+ */
|
|
|
+ timeout = -1, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.EXEC,
|
|
|
+ data: { args, timeout },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * Terminate all ongoing API calls and terminate web worker.
|
|
|
+ * `FFmpeg.load()` must be called again before calling any other APIs.
|
|
|
+ *
|
|
|
+ * @category FFmpeg
|
|
|
+ */
|
|
|
+ terminate = () => {
|
|
|
+ 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
|
|
|
+ */
|
|
|
+ writeFile = (path, data, { signal } = {}) => {
|
|
|
+ const trans = [];
|
|
|
+ if (data instanceof Uint8Array) {
|
|
|
+ trans.push(data.buffer);
|
|
|
+ }
|
|
|
+ return this.#send({
|
|
|
+ type: FFMessageType.WRITE_FILE,
|
|
|
+ data: { path, data },
|
|
|
+ }, trans, signal);
|
|
|
+ };
|
|
|
+ mount = (fsType, options, mountPoint) => {
|
|
|
+ const trans = [];
|
|
|
+ return this.#send({
|
|
|
+ type: FFMessageType.MOUNT,
|
|
|
+ data: { fsType, options, mountPoint },
|
|
|
+ }, trans);
|
|
|
+ };
|
|
|
+ unmount = (mountPoint) => {
|
|
|
+ const trans = [];
|
|
|
+ return this.#send({
|
|
|
+ type: FFMessageType.UNMOUNT,
|
|
|
+ data: { mountPoint },
|
|
|
+ }, trans);
|
|
|
+ };
|
|
|
+ /**
|
|
|
+ * Read data from ffmpeg.wasm.
|
|
|
+ *
|
|
|
+ * @example
|
|
|
+ * ```ts
|
|
|
+ * const ffmpeg = new FFmpeg();
|
|
|
+ * await ffmpeg.load();
|
|
|
+ * const data = await ffmpeg.readFile("video.mp4");
|
|
|
+ * ```
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ readFile = (path,
|
|
|
+ /**
|
|
|
+ * 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 } = {}) => this.#send({
|
|
|
+ type: FFMessageType.READ_FILE,
|
|
|
+ data: { path, encoding },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * Delete a file.
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ deleteFile = (path, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.DELETE_FILE,
|
|
|
+ data: { path },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * Rename a file or directory.
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ rename = (oldPath, newPath, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.RENAME,
|
|
|
+ data: { oldPath, newPath },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * Create a directory.
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ createDir = (path, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.CREATE_DIR,
|
|
|
+ data: { path },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * List directory contents.
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ listDir = (path, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.LIST_DIR,
|
|
|
+ data: { path },
|
|
|
+ }, undefined, signal);
|
|
|
+ /**
|
|
|
+ * Delete an empty directory.
|
|
|
+ *
|
|
|
+ * @category File System
|
|
|
+ */
|
|
|
+ deleteDir = (path, { signal } = {}) => this.#send({
|
|
|
+ type: FFMessageType.DELETE_DIR,
|
|
|
+ data: { path },
|
|
|
+ }, undefined, signal);
|
|
|
+}
|