feat(ffmpeg): abort signal (#573)
* feat(ffmpeg): abort signal * with test
This commit is contained in:
parent
cf9cf11c6d
commit
efaae603d8
@ -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,
|
type: FFMessageType.LOAD,
|
||||||
data: config,
|
data: config,
|
||||||
}) as Promise<IsFirst>;
|
},
|
||||||
|
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,
|
type: FFMessageType.EXEC,
|
||||||
data: { args, timeout },
|
data: { args, timeout },
|
||||||
}) as Promise<number>;
|
},
|
||||||
|
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,
|
type: FFMessageType.READ_FILE,
|
||||||
data: { path, encoding },
|
data: { path, encoding },
|
||||||
}) as Promise<FileData>;
|
},
|
||||||
|
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,
|
||||||
|
{ signal }: FFMessageOptions = {}
|
||||||
|
): Promise<OK> =>
|
||||||
|
this.#send(
|
||||||
|
{
|
||||||
type: FFMessageType.DELETE_FILE,
|
type: FFMessageType.DELETE_FILE,
|
||||||
data: { path },
|
data: { path },
|
||||||
}) as Promise<OK>;
|
},
|
||||||
|
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,
|
||||||
|
newPath: string,
|
||||||
|
{ signal }: FFMessageOptions = {}
|
||||||
|
): Promise<OK> =>
|
||||||
|
this.#send(
|
||||||
|
{
|
||||||
type: FFMessageType.RENAME,
|
type: FFMessageType.RENAME,
|
||||||
data: { oldPath, newPath },
|
data: { oldPath, newPath },
|
||||||
}) as Promise<OK>;
|
},
|
||||||
|
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,
|
||||||
|
{ signal }: FFMessageOptions = {}
|
||||||
|
): Promise<OK> =>
|
||||||
|
this.#send(
|
||||||
|
{
|
||||||
type: FFMessageType.CREATE_DIR,
|
type: FFMessageType.CREATE_DIR,
|
||||||
data: { path },
|
data: { path },
|
||||||
}) as Promise<OK>;
|
},
|
||||||
|
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,
|
||||||
|
{ signal }: FFMessageOptions = {}
|
||||||
|
): Promise<FSNode[]> =>
|
||||||
|
this.#send(
|
||||||
|
{
|
||||||
type: FFMessageType.LIST_DIR,
|
type: FFMessageType.LIST_DIR,
|
||||||
data: { path },
|
data: { path },
|
||||||
}) as Promise<FSNode[]>;
|
},
|
||||||
|
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,
|
||||||
|
{ signal }: FFMessageOptions = {}
|
||||||
|
): Promise<OK> =>
|
||||||
|
this.#send(
|
||||||
|
{
|
||||||
type: FFMessageType.DELETE_DIR,
|
type: FFMessageType.DELETE_DIR,
|
||||||
data: { path },
|
data: { path },
|
||||||
}) as Promise<OK>;
|
},
|
||||||
|
undefined,
|
||||||
|
signal
|
||||||
|
) as Promise<OK>;
|
||||||
}
|
}
|
||||||
|
@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user