123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277 |
- 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);
- }
|