UNPKG

8.39 kBJavaScriptView Raw
1import { FFMessageType } from "./const.js";
2import { getMessageID } from "./utils.js";
3import { ERROR_TERMINATED, ERROR_NOT_LOADED } from "./errors.js";
4/**
5 * Provides APIs to interact with ffmpeg web worker.
6 *
7 * @example
8 * ```ts
9 * const ffmpeg = new FFmpeg();
10 * ```
11 */
12export class FFmpeg {
13 #worker = null;
14 /**
15 * #resolves and #rejects tracks Promise resolves and rejects to
16 * be called when we receive message from web worker.
17 */
18 #resolves = {};
19 #rejects = {};
20 #logEventCallbacks = [];
21 #progressEventCallbacks = [];
22 loaded = false;
23 /**
24 * register worker message event handlers.
25 */
26 #registerHandlers = () => {
27 if (this.#worker) {
28 this.#worker.onmessage = ({ data: { id, type, data }, }) => {
29 switch (type) {
30 case FFMessageType.LOAD:
31 this.loaded = true;
32 this.#resolves[id](data);
33 break;
34 case FFMessageType.MOUNT:
35 case FFMessageType.UNMOUNT:
36 case FFMessageType.EXEC:
37 case FFMessageType.WRITE_FILE:
38 case FFMessageType.READ_FILE:
39 case FFMessageType.DELETE_FILE:
40 case FFMessageType.RENAME:
41 case FFMessageType.CREATE_DIR:
42 case FFMessageType.LIST_DIR:
43 case FFMessageType.DELETE_DIR:
44 this.#resolves[id](data);
45 break;
46 case FFMessageType.LOG:
47 this.#logEventCallbacks.forEach((f) => f(data));
48 break;
49 case FFMessageType.PROGRESS:
50 this.#progressEventCallbacks.forEach((f) => f(data));
51 break;
52 case FFMessageType.ERROR:
53 this.#rejects[id](data);
54 break;
55 }
56 delete this.#resolves[id];
57 delete this.#rejects[id];
58 };
59 }
60 };
61 /**
62 * Generic function to send messages to web worker.
63 */
64 #send = ({ type, data }, trans = [], signal) => {
65 if (!this.#worker) {
66 return Promise.reject(ERROR_NOT_LOADED);
67 }
68 return new Promise((resolve, reject) => {
69 const id = getMessageID();
70 this.#worker && this.#worker.postMessage({ id, type, data }, trans);
71 this.#resolves[id] = resolve;
72 this.#rejects[id] = reject;
73 signal?.addEventListener("abort", () => {
74 reject(new DOMException(`Message # ${id} was aborted`, "AbortError"));
75 }, { once: true });
76 });
77 };
78 on(event, callback) {
79 if (event === "log") {
80 this.#logEventCallbacks.push(callback);
81 }
82 else if (event === "progress") {
83 this.#progressEventCallbacks.push(callback);
84 }
85 }
86 off(event, callback) {
87 if (event === "log") {
88 this.#logEventCallbacks = this.#logEventCallbacks.filter((f) => f !== callback);
89 }
90 else if (event === "progress") {
91 this.#progressEventCallbacks = this.#progressEventCallbacks.filter((f) => f !== callback);
92 }
93 }
94 /**
95 * Loads ffmpeg-core inside web worker. It is required to call this method first
96 * as it initializes WebAssembly and other essential variables.
97 *
98 * @category FFmpeg
99 * @returns `true` if ffmpeg core is loaded for the first time.
100 */
101 load = (config = {}, { signal } = {}) => {
102 if (!this.#worker) {
103 this.#worker = new Worker(new URL("./worker.js", import.meta.url), {
104 type: "module",
105 });
106 this.#registerHandlers();
107 }
108 return this.#send({
109 type: FFMessageType.LOAD,
110 data: config,
111 }, undefined, signal);
112 };
113 /**
114 * Execute ffmpeg command.
115 *
116 * @remarks
117 * To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
118 * by default.
119 *
120 * @example
121 * ```ts
122 * const ffmpeg = new FFmpeg();
123 * await ffmpeg.load();
124 * await ffmpeg.writeFile("video.avi", ...);
125 * // ffmpeg -i video.avi video.mp4
126 * await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
127 * const data = ffmpeg.readFile("video.mp4");
128 * ```
129 *
130 * @returns `0` if no error, `!= 0` if timeout (1) or error.
131 * @category FFmpeg
132 */
133 exec = (
134 /** ffmpeg command line args */
135 args,
136 /**
137 * milliseconds to wait before stopping the command execution.
138 *
139 * @defaultValue -1
140 */
141 timeout = -1, { signal } = {}) => this.#send({
142 type: FFMessageType.EXEC,
143 data: { args, timeout },
144 }, undefined, signal);
145 /**
146 * Terminate all ongoing API calls and terminate web worker.
147 * `FFmpeg.load()` must be called again before calling any other APIs.
148 *
149 * @category FFmpeg
150 */
151 terminate = () => {
152 const ids = Object.keys(this.#rejects);
153 // rejects all incomplete Promises.
154 for (const id of ids) {
155 this.#rejects[id](ERROR_TERMINATED);
156 delete this.#rejects[id];
157 delete this.#resolves[id];
158 }
159 if (this.#worker) {
160 this.#worker.terminate();
161 this.#worker = null;
162 this.loaded = false;
163 }
164 };
165 /**
166 * Write data to ffmpeg.wasm.
167 *
168 * @example
169 * ```ts
170 * const ffmpeg = new FFmpeg();
171 * await ffmpeg.load();
172 * await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
173 * await ffmpeg.writeFile("text.txt", "hello world");
174 * ```
175 *
176 * @category File System
177 */
178 writeFile = (path, data, { signal } = {}) => {
179 const trans = [];
180 if (data instanceof Uint8Array) {
181 trans.push(data.buffer);
182 }
183 return this.#send({
184 type: FFMessageType.WRITE_FILE,
185 data: { path, data },
186 }, trans, signal);
187 };
188 mount = (fsType, options, mountPoint) => {
189 const trans = [];
190 return this.#send({
191 type: FFMessageType.MOUNT,
192 data: { fsType, options, mountPoint },
193 }, trans);
194 };
195 unmount = (mountPoint) => {
196 const trans = [];
197 return this.#send({
198 type: FFMessageType.UNMOUNT,
199 data: { mountPoint },
200 }, trans);
201 };
202 /**
203 * Read data from ffmpeg.wasm.
204 *
205 * @example
206 * ```ts
207 * const ffmpeg = new FFmpeg();
208 * await ffmpeg.load();
209 * const data = await ffmpeg.readFile("video.mp4");
210 * ```
211 *
212 * @category File System
213 */
214 readFile = (path,
215 /**
216 * File content encoding, supports two encodings:
217 * - utf8: read file as text file, return data in string type.
218 * - binary: read file as binary file, return data in Uint8Array type.
219 *
220 * @defaultValue binary
221 */
222 encoding = "binary", { signal } = {}) => this.#send({
223 type: FFMessageType.READ_FILE,
224 data: { path, encoding },
225 }, undefined, signal);
226 /**
227 * Delete a file.
228 *
229 * @category File System
230 */
231 deleteFile = (path, { signal } = {}) => this.#send({
232 type: FFMessageType.DELETE_FILE,
233 data: { path },
234 }, undefined, signal);
235 /**
236 * Rename a file or directory.
237 *
238 * @category File System
239 */
240 rename = (oldPath, newPath, { signal } = {}) => this.#send({
241 type: FFMessageType.RENAME,
242 data: { oldPath, newPath },
243 }, undefined, signal);
244 /**
245 * Create a directory.
246 *
247 * @category File System
248 */
249 createDir = (path, { signal } = {}) => this.#send({
250 type: FFMessageType.CREATE_DIR,
251 data: { path },
252 }, undefined, signal);
253 /**
254 * List directory contents.
255 *
256 * @category File System
257 */
258 listDir = (path, { signal } = {}) => this.#send({
259 type: FFMessageType.LIST_DIR,
260 data: { path },
261 }, undefined, signal);
262 /**
263 * Delete an empty directory.
264 *
265 * @category File System
266 */
267 deleteDir = (path, { signal } = {}) => this.#send({
268 type: FFMessageType.DELETE_DIR,
269 data: { path },
270 }, undefined, signal);
271}