diff --git a/packages/ffmpeg/src/classes.ts b/packages/ffmpeg/src/classes.ts index bb366d9..d9348b2 100644 --- a/packages/ffmpeg/src/classes.ts +++ b/packages/ffmpeg/src/classes.ts @@ -17,6 +17,10 @@ import { 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. * @@ -85,7 +89,8 @@ export class FFmpeg { */ #send = ( { type, data }: Message, - trans: Transferable[] = [] + trans: Transferable[] = [], + signal?: AbortSignal ): Promise => { if (!this.#worker) { return Promise.reject(ERROR_NOT_LOADED); @@ -96,6 +101,14 @@ export class FFmpeg { 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 } + ); }); }; @@ -148,9 +161,13 @@ export class FFmpeg { callback: LogEventCallback | ProgressEventCallback ) { if (event === "log") { - this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback); + this.#logEventCallbacks = this.#logEventCallbacks.filter( + (f) => f !== callback + ); } else if (event === "progress") { - this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback); + this.#progressEventCallbacks = this.#progressEventCallbacks.filter( + (f) => f !== callback + ); } } @@ -161,17 +178,24 @@ export class FFmpeg { * @category FFmpeg * @returns `true` if ffmpeg core is loaded for the first time. */ - public load = (config: FFMessageLoadConfig = {}): Promise => { + public load = ( + config: FFMessageLoadConfig = {}, + { signal }: FFMessageOptions = {} + ): Promise => { if (!this.#worker) { this.#worker = new Worker(new URL("./worker.js", import.meta.url), { type: "module", }); this.#registerHandlers(); } - return this.#send({ - type: FFMessageType.LOAD, - data: config, - }) as Promise; + return this.#send( + { + type: FFMessageType.LOAD, + data: config, + }, + undefined, + signal + ) as Promise; }; /** @@ -202,12 +226,17 @@ export class FFmpeg { * * @defaultValue -1 */ - timeout = -1 + timeout = -1, + { signal }: FFMessageOptions = {} ): Promise => - this.#send({ - type: FFMessageType.EXEC, - data: { args, timeout }, - }) as Promise; + this.#send( + { + type: FFMessageType.EXEC, + data: { args, timeout }, + }, + undefined, + signal + ) as Promise; /** * Terminate all ongoing API calls and terminate web worker. @@ -244,7 +273,11 @@ export class FFmpeg { * * @category File System */ - public writeFile = (path: string, data: FileData): Promise => { + public writeFile = ( + path: string, + data: FileData, + { signal }: FFMessageOptions = {} + ): Promise => { const trans: Transferable[] = []; if (data instanceof Uint8Array) { trans.push(data.buffer); @@ -254,7 +287,8 @@ export class FFmpeg { type: FFMessageType.WRITE_FILE, data: { path, data }, }, - trans + trans, + signal ) as Promise; }; @@ -279,65 +313,106 @@ export class FFmpeg { * * @defaultValue binary */ - encoding = "binary" + encoding = "binary", + { signal }: FFMessageOptions = {} ): Promise => - this.#send({ - type: FFMessageType.READ_FILE, - data: { path, encoding }, - }) as Promise; + this.#send( + { + type: FFMessageType.READ_FILE, + data: { path, encoding }, + }, + undefined, + signal + ) as Promise; /** * Delete a file. * * @category File System */ - public deleteFile = (path: string): Promise => - this.#send({ - type: FFMessageType.DELETE_FILE, - data: { path }, - }) as Promise; + 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): Promise => - this.#send({ - type: FFMessageType.RENAME, - data: { oldPath, newPath }, - }) as Promise; + 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): Promise => - this.#send({ - type: FFMessageType.CREATE_DIR, - data: { path }, - }) as Promise; + 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): Promise => - this.#send({ - type: FFMessageType.LIST_DIR, - data: { path }, - }) as Promise; + 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): Promise => - this.#send({ - type: FFMessageType.DELETE_DIR, - data: { path }, - }) as Promise; + public deleteDir = ( + path: string, + { signal }: FFMessageOptions = {} + ): Promise => + this.#send( + { + type: FFMessageType.DELETE_DIR, + data: { path }, + }, + undefined, + signal + ) as Promise; } diff --git a/tests/ffmpeg.test.js b/tests/ffmpeg.test.js index 762d71f..5e20eef 100644 --- a/tests/ffmpeg.test.js +++ b/tests/ffmpeg.test.js @@ -135,4 +135,18 @@ describe(genName("FFmpeg.exec()"), function () { const ret = await ffmpeg.exec(["-i", "video.mp4", "video.avi"], 1); expect(ret).to.equal(1); }); + + it("should abort", () => { + const controller = new AbortController(); + const { signal } = controller; + + const promise = ffmpeg.exec(["-i", "video.mp4", "video.avi"], undefined, { + signal, + }); + controller.abort(); + + return promise.catch((err) => { + expect(err.name).to.equal("AbortError"); + }); + }); });