feat(ffmpeg): abort signal (#573)

* feat(ffmpeg): abort signal

* with test
This commit is contained in:
Antoine BERNIER 2023-10-09 15:51:08 +02:00 committed by GitHub
parent cf9cf11c6d
commit efaae603d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 134 additions and 45 deletions

View File

@ -17,6 +17,10 @@ import {
import { getMessageID } from "./utils.js"; import { getMessageID } from "./utils.js";
import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js"; import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js";
type FFMessageOptions = {
signal?: AbortSignal;
};
/** /**
* Provides APIs to interact with ffmpeg web worker. * Provides APIs to interact with ffmpeg web worker.
* *
@ -85,7 +89,8 @@ export class FFmpeg {
*/ */
#send = ( #send = (
{ type, data }: Message, { type, data }: Message,
trans: Transferable[] = [] trans: Transferable[] = [],
signal?: AbortSignal
): Promise<CallbackData> => { ): Promise<CallbackData> => {
if (!this.#worker) { if (!this.#worker) {
return Promise.reject(ERROR_NOT_LOADED); return Promise.reject(ERROR_NOT_LOADED);
@ -96,6 +101,14 @@ export class FFmpeg {
this.#worker && this.#worker.postMessage({ id, type, data }, trans); this.#worker && this.#worker.postMessage({ id, type, data }, trans);
this.#resolves[id] = resolve; this.#resolves[id] = resolve;
this.#rejects[id] = reject; 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 callback: LogEventCallback | ProgressEventCallback
) { ) {
if (event === "log") { if (event === "log") {
this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback); this.#logEventCallbacks = this.#logEventCallbacks.filter(
(f) => f !== callback
);
} else if (event === "progress") { } 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 * @category FFmpeg
* @returns `true` if ffmpeg core is loaded for the first time. * @returns `true` if ffmpeg core is loaded for the first time.
*/ */
public load = (config: FFMessageLoadConfig = {}): Promise<IsFirst> => { public load = (
config: FFMessageLoadConfig = {},
{ signal }: FFMessageOptions = {}
): Promise<IsFirst> => {
if (!this.#worker) { if (!this.#worker) {
this.#worker = new Worker(new URL("./worker.js", import.meta.url), { this.#worker = new Worker(new URL("./worker.js", import.meta.url), {
type: "module", type: "module",
}); });
this.#registerHandlers(); this.#registerHandlers();
} }
return this.#send({ return this.#send(
type: FFMessageType.LOAD, {
data: config, type: FFMessageType.LOAD,
}) as Promise<IsFirst>; data: config,
},
undefined,
signal
) as Promise<IsFirst>;
}; };
/** /**
@ -202,12 +226,17 @@ export class FFmpeg {
* *
* @defaultValue -1 * @defaultValue -1
*/ */
timeout = -1 timeout = -1,
{ signal }: FFMessageOptions = {}
): Promise<number> => ): Promise<number> =>
this.#send({ this.#send(
type: FFMessageType.EXEC, {
data: { args, timeout }, type: FFMessageType.EXEC,
}) as Promise<number>; data: { args, timeout },
},
undefined,
signal
) as Promise<number>;
/** /**
* Terminate all ongoing API calls and terminate web worker. * Terminate all ongoing API calls and terminate web worker.
@ -244,7 +273,11 @@ export class FFmpeg {
* *
* @category File System * @category File System
*/ */
public writeFile = (path: string, data: FileData): Promise<OK> => { public writeFile = (
path: string,
data: FileData,
{ signal }: FFMessageOptions = {}
): Promise<OK> => {
const trans: Transferable[] = []; const trans: Transferable[] = [];
if (data instanceof Uint8Array) { if (data instanceof Uint8Array) {
trans.push(data.buffer); trans.push(data.buffer);
@ -254,7 +287,8 @@ export class FFmpeg {
type: FFMessageType.WRITE_FILE, type: FFMessageType.WRITE_FILE,
data: { path, data }, data: { path, data },
}, },
trans trans,
signal
) as Promise<OK>; ) as Promise<OK>;
}; };
@ -279,65 +313,106 @@ export class FFmpeg {
* *
* @defaultValue binary * @defaultValue binary
*/ */
encoding = "binary" encoding = "binary",
{ signal }: FFMessageOptions = {}
): Promise<FileData> => ): Promise<FileData> =>
this.#send({ this.#send(
type: FFMessageType.READ_FILE, {
data: { path, encoding }, type: FFMessageType.READ_FILE,
}) as Promise<FileData>; data: { path, encoding },
},
undefined,
signal
) as Promise<FileData>;
/** /**
* Delete a file. * Delete a file.
* *
* @category File System * @category File System
*/ */
public deleteFile = (path: string): Promise<OK> => public deleteFile = (
this.#send({ path: string,
type: FFMessageType.DELETE_FILE, { signal }: FFMessageOptions = {}
data: { path }, ): Promise<OK> =>
}) as Promise<OK>; this.#send(
{
type: FFMessageType.DELETE_FILE,
data: { path },
},
undefined,
signal
) as Promise<OK>;
/** /**
* Rename a file or directory. * Rename a file or directory.
* *
* @category File System * @category File System
*/ */
public rename = (oldPath: string, newPath: string): Promise<OK> => public rename = (
this.#send({ oldPath: string,
type: FFMessageType.RENAME, newPath: string,
data: { oldPath, newPath }, { signal }: FFMessageOptions = {}
}) as Promise<OK>; ): Promise<OK> =>
this.#send(
{
type: FFMessageType.RENAME,
data: { oldPath, newPath },
},
undefined,
signal
) as Promise<OK>;
/** /**
* Create a directory. * Create a directory.
* *
* @category File System * @category File System
*/ */
public createDir = (path: string): Promise<OK> => public createDir = (
this.#send({ path: string,
type: FFMessageType.CREATE_DIR, { signal }: FFMessageOptions = {}
data: { path }, ): Promise<OK> =>
}) as Promise<OK>; this.#send(
{
type: FFMessageType.CREATE_DIR,
data: { path },
},
undefined,
signal
) as Promise<OK>;
/** /**
* List directory contents. * List directory contents.
* *
* @category File System * @category File System
*/ */
public listDir = (path: string): Promise<FSNode[]> => public listDir = (
this.#send({ path: string,
type: FFMessageType.LIST_DIR, { signal }: FFMessageOptions = {}
data: { path }, ): Promise<FSNode[]> =>
}) as Promise<FSNode[]>; this.#send(
{
type: FFMessageType.LIST_DIR,
data: { path },
},
undefined,
signal
) as Promise<FSNode[]>;
/** /**
* Delete an empty directory. * Delete an empty directory.
* *
* @category File System * @category File System
*/ */
public deleteDir = (path: string): Promise<OK> => public deleteDir = (
this.#send({ path: string,
type: FFMessageType.DELETE_DIR, { signal }: FFMessageOptions = {}
data: { path }, ): Promise<OK> =>
}) as Promise<OK>; this.#send(
{
type: FFMessageType.DELETE_DIR,
data: { path },
},
undefined,
signal
) as Promise<OK>;
} }

View File

@ -135,4 +135,18 @@ describe(genName("FFmpeg.exec()"), function () {
const ret = await ffmpeg.exec(["-i", "video.mp4", "video.avi"], 1); const ret = await ffmpeg.exec(["-i", "video.mp4", "video.avi"], 1);
expect(ret).to.equal(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");
});
});
}); });