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
	 Antoine BERNIER
						Antoine BERNIER