classes.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  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 = (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. }