classes.js 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import { FFMessageType } from "./const.js";
  2. import { getMessageID } from "./utils.js";
  3. import { 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. */
  12. export 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 = ({ classWorkerURL, ...config } = {}, { signal } = {}) => {
  102. if (!this.#worker) {
  103. this.#worker = classWorkerURL ?
  104. new Worker(new URL(classWorkerURL, import.meta.url), {
  105. type: "module",
  106. }) :
  107. // We need to duplicated the code here to enable webpack
  108. // to bundle worekr.js here.
  109. new Worker(new URL("./worker.js", import.meta.url), {
  110. type: "module",
  111. });
  112. this.#registerHandlers();
  113. }
  114. return this.#send({
  115. type: FFMessageType.LOAD,
  116. data: config,
  117. }, undefined, signal);
  118. };
  119. /**
  120. * Execute ffmpeg command.
  121. *
  122. * @remarks
  123. * To avoid common I/O issues, ["-nostdin", "-y"] are prepended to the args
  124. * by default.
  125. *
  126. * @example
  127. * ```ts
  128. * const ffmpeg = new FFmpeg();
  129. * await ffmpeg.load();
  130. * await ffmpeg.writeFile("video.avi", ...);
  131. * // ffmpeg -i video.avi video.mp4
  132. * await ffmpeg.exec(["-i", "video.avi", "video.mp4"]);
  133. * const data = ffmpeg.readFile("video.mp4");
  134. * ```
  135. *
  136. * @returns `0` if no error, `!= 0` if timeout (1) or error.
  137. * @category FFmpeg
  138. */
  139. exec = (
  140. /** ffmpeg command line args */
  141. args,
  142. /**
  143. * milliseconds to wait before stopping the command execution.
  144. *
  145. * @defaultValue -1
  146. */
  147. timeout = -1, { signal } = {}) => this.#send({
  148. type: FFMessageType.EXEC,
  149. data: { args, timeout },
  150. }, undefined, signal);
  151. /**
  152. * Terminate all ongoing API calls and terminate web worker.
  153. * `FFmpeg.load()` must be called again before calling any other APIs.
  154. *
  155. * @category FFmpeg
  156. */
  157. terminate = () => {
  158. const ids = Object.keys(this.#rejects);
  159. // rejects all incomplete Promises.
  160. for (const id of ids) {
  161. this.#rejects[id](ERROR_TERMINATED);
  162. delete this.#rejects[id];
  163. delete this.#resolves[id];
  164. }
  165. if (this.#worker) {
  166. this.#worker.terminate();
  167. this.#worker = null;
  168. this.loaded = false;
  169. }
  170. };
  171. /**
  172. * Write data to ffmpeg.wasm.
  173. *
  174. * @example
  175. * ```ts
  176. * const ffmpeg = new FFmpeg();
  177. * await ffmpeg.load();
  178. * await ffmpeg.writeFile("video.avi", await fetchFile("../video.avi"));
  179. * await ffmpeg.writeFile("text.txt", "hello world");
  180. * ```
  181. *
  182. * @category File System
  183. */
  184. writeFile = (path, data, { signal } = {}) => {
  185. const trans = [];
  186. if (data instanceof Uint8Array) {
  187. trans.push(data.buffer);
  188. }
  189. return this.#send({
  190. type: FFMessageType.WRITE_FILE,
  191. data: { path, data },
  192. }, trans, signal);
  193. };
  194. mount = (fsType, options, mountPoint) => {
  195. const trans = [];
  196. return this.#send({
  197. type: FFMessageType.MOUNT,
  198. data: { fsType, options, mountPoint },
  199. }, trans);
  200. };
  201. unmount = (mountPoint) => {
  202. const trans = [];
  203. return this.#send({
  204. type: FFMessageType.UNMOUNT,
  205. data: { mountPoint },
  206. }, trans);
  207. };
  208. /**
  209. * Read data from ffmpeg.wasm.
  210. *
  211. * @example
  212. * ```ts
  213. * const ffmpeg = new FFmpeg();
  214. * await ffmpeg.load();
  215. * const data = await ffmpeg.readFile("video.mp4");
  216. * ```
  217. *
  218. * @category File System
  219. */
  220. readFile = (path,
  221. /**
  222. * File content encoding, supports two encodings:
  223. * - utf8: read file as text file, return data in string type.
  224. * - binary: read file as binary file, return data in Uint8Array type.
  225. *
  226. * @defaultValue binary
  227. */
  228. encoding = "binary", { signal } = {}) => this.#send({
  229. type: FFMessageType.READ_FILE,
  230. data: { path, encoding },
  231. }, undefined, signal);
  232. /**
  233. * Delete a file.
  234. *
  235. * @category File System
  236. */
  237. deleteFile = (path, { signal } = {}) => this.#send({
  238. type: FFMessageType.DELETE_FILE,
  239. data: { path },
  240. }, undefined, signal);
  241. /**
  242. * Rename a file or directory.
  243. *
  244. * @category File System
  245. */
  246. rename = (oldPath, newPath, { signal } = {}) => this.#send({
  247. type: FFMessageType.RENAME,
  248. data: { oldPath, newPath },
  249. }, undefined, signal);
  250. /**
  251. * Create a directory.
  252. *
  253. * @category File System
  254. */
  255. createDir = (path, { signal } = {}) => this.#send({
  256. type: FFMessageType.CREATE_DIR,
  257. data: { path },
  258. }, undefined, signal);
  259. /**
  260. * List directory contents.
  261. *
  262. * @category File System
  263. */
  264. listDir = (path, { signal } = {}) => this.#send({
  265. type: FFMessageType.LIST_DIR,
  266. data: { path },
  267. }, undefined, signal);
  268. /**
  269. * Delete an empty directory.
  270. *
  271. * @category File System
  272. */
  273. deleteDir = (path, { signal } = {}) => this.#send({
  274. type: FFMessageType.DELETE_DIR,
  275. data: { path },
  276. }, undefined, signal);
  277. }