diff --git a/src/core/api/public_api.ts b/src/core/api/public_api.ts index e365acd26b..368fc74124 100644 --- a/src/core/api/public_api.ts +++ b/src/core/api/public_api.ts @@ -36,6 +36,7 @@ import { IErrorType, MediaError, } from "../../errors"; +import WorkerInitializationError from "../../errors/worker_initialization_error"; import features, { addFeatures, IFeature, @@ -54,7 +55,11 @@ import Manifest, { ManifestMetadataFormat, createRepresentationFilterFromFnString, } from "../../manifest"; -import { MainThreadMessageType } from "../../multithread_types"; +import { + IWorkerMessage, + MainThreadMessageType, + WorkerMessageType, +} from "../../multithread_types"; import { IAudioRepresentation, IAudioRepresentationsSwitchingMode, @@ -435,10 +440,18 @@ class Player extends EventEmitter { */ public attachWorker( workerSettings : IWorkerSettings - ) : void { - if (!hasWebassembly) { - log.warn("API: Cannot rely on a WebWorker: WebAssembly unavailable"); - } else { + ) : Promise { + return new Promise((res, rej) => { + if (typeof Worker !== "function") { + log.warn("API: Cannot rely on a WebWorker: Worker API unavailable"); + return rej(new WorkerInitializationError("INCOMPATIBLE_ERROR", + "Worker unavailable")); + } + if (!hasWebassembly) { + log.warn("API: Cannot rely on a WebWorker: WebAssembly unavailable"); + return rej(new WorkerInitializationError("INCOMPATIBLE_ERROR", + "WebAssembly unavailable")); + } if (typeof workerSettings.workerUrl === "string") { this._priv_worker = new Worker(workerSettings.workerUrl); } else { @@ -448,10 +461,40 @@ class Player extends EventEmitter { } this._priv_worker.onerror = (evt: ErrorEvent) => { - this._priv_worker = null; - log.error("Unexpected worker error", + if (this._priv_worker !== null) { + this._priv_worker.terminate(); + this._priv_worker = null; + } + log.error("API: Unexpected worker error", evt.error instanceof Error ? evt.error : undefined); + rej(new WorkerInitializationError("UNKNOWN_ERROR", + "Unexpected Worker \"error\" event")); + }; + const handleInitMessages = (msg: MessageEvent) => { + const msgData = msg.data as unknown as IWorkerMessage; + if (msgData.type === WorkerMessageType.InitError) { + log.warn("API: Processing InitError worker message: detaching worker"); + if (this._priv_worker !== null) { + this._priv_worker.removeEventListener("message", handleInitMessages); + this._priv_worker.terminate(); + this._priv_worker = null; + } + rej( + new WorkerInitializationError( + "SETUP_ERROR", + "Worker parser initialization failed: " + msgData.value.errorMessage + ) + ); + } else if (msgData.type === WorkerMessageType.InitSuccess) { + log.info("API: InitSuccess received from worker."); + if (this._priv_worker !== null) { + this._priv_worker.removeEventListener("message", handleInitMessages); + } + res(); + } }; + this._priv_worker.addEventListener("message", handleInitMessages); + log.debug("---> Sending To Worker:", MainThreadMessageType.Init); this._priv_worker.postMessage({ type: MainThreadMessageType.Init, @@ -478,7 +521,7 @@ class Player extends EventEmitter { }, }); }, this._destroyCanceller.signal); - } + }); } /** diff --git a/src/core/init/multithread/main_thread/multi_thread_content_initializer.ts b/src/core/init/multithread/main_thread/multi_thread_content_initializer.ts index 7baf3b4966..31a4b1a599 100644 --- a/src/core/init/multithread/main_thread/multi_thread_content_initializer.ts +++ b/src/core/init/multithread/main_thread/multi_thread_content_initializer.ts @@ -1031,6 +1031,11 @@ export default class MultiThreadContentInitializer extends ContentInitializer { break; } + case WorkerMessageType.InitSuccess: + case WorkerMessageType.InitError: + // Should already be handled by the API + break; + default: assertUnreachable(msgData); } diff --git a/src/core/init/multithread/worker/worker_portal.ts b/src/core/init/multithread/worker/worker_portal.ts index e739f6fd3e..f13f44dd48 100644 --- a/src/core/init/multithread/worker/worker_portal.ts +++ b/src/core/init/multithread/worker/worker_portal.ts @@ -108,12 +108,20 @@ export default function initializeWorkerPortal() { const diffWorker = Date.now() - performance.now(); mainThreadTimestampDiff.setValueIfChanged(diffWorker - diffMain); updateLoggerLevel(msg.value.logLevel, msg.value.sendBackLogs); - dashWasmParser.initialize({ wasmUrl: msg.value.dashWasmUrl }).catch((err) => { - const error = err instanceof Error ? - err.toString() : - "Unknown Error"; - log.error("Worker: Could not initialize DASH_WASM parser", error); - }); + dashWasmParser.initialize({ wasmUrl: msg.value.dashWasmUrl }).then( + () => { + sendMessage({ type: WorkerMessageType.InitSuccess, + value: null }); + }, (err) => { + const error = err instanceof Error ? + err.toString() : + "Unknown Error"; + log.error("Worker: Could not initialize DASH_WASM parser", error); + sendMessage({ type: WorkerMessageType.InitError, + value: { errorMessage: error, + kind: "dashWasmInitialization" } }); + + }); if (!msg.value.hasVideo || msg.value.hasMseInWorker) { contentPreparer.disposeCurrentContent(); diff --git a/src/errors/worker_initialization_error.ts b/src/errors/worker_initialization_error.ts new file mode 100644 index 0000000000..252f0a0e3b --- /dev/null +++ b/src/errors/worker_initialization_error.ts @@ -0,0 +1,36 @@ +import errorMessage from "./error_message"; + +type IWorkerInitializationErrorCode = "UNKNOWN_ERROR" | + "SETUP_ERROR" | + "INCOMPATIBLE_ERROR"; + +/** + * Error linked to the WebWorker initialization. + * + * @class WorkerInitializationError + * @extends Error + */ +export default class WorkerInitializationError extends Error { + public readonly name : "WorkerInitializationError"; + public readonly type : "WORKER_INITIALIZATION_ERROR"; + public readonly message : string; + public readonly code : IWorkerInitializationErrorCode; + + /** + * @param {string} code + * @param {string} message + */ + constructor( + code : IWorkerInitializationErrorCode, + message : string + ) { + super(); + // @see https://stackoverflow.com/questions/41102060/typescript-extending-error-class + Object.setPrototypeOf(this, WorkerInitializationError.prototype); + + this.name = "WorkerInitializationError"; + this.type = "WORKER_INITIALIZATION_ERROR"; + this.code = code; + this.message = errorMessage(this.code, message); + } +} diff --git a/src/multithread_types.ts b/src/multithread_types.ts index b3c1881f28..4d6f97fa9c 100644 --- a/src/multithread_types.ts +++ b/src/multithread_types.ts @@ -573,6 +573,44 @@ export type ISentError = ISerializedNetworkError | ISerializedEncryptedMediaError | ISerializedOtherError; +/** + * Message sent by the WebWorker when its initialization, started implicitely + * as soon as the `new Worker` call was made for it, has finished and succeeded. + * + * Once that message has been received, you can ensure that no + * `IInitErrorWorkerMessage` will ever be received for the same worker. + * + * Note that receiving this message is not a requirement before preparing and + * loading a content, both initialization and content loading can be started in + * parallel. + */ +export interface IInitSuccessWorkerMessage { + type: WorkerMessageType.InitSuccess; + value: null; +} + +/** + * Message sent by the WebWorker when its initialization, started implicitely + * as soon as the `new Worker` call was made for it, has finished and failed. + * + * Once that message has been received, you can ensure that no + * `IInitErrorWorkerMessage` will ever be received for the same worker. + * + * Note that you may received this message while preparing and/or loading a + * content, both initialization and content loading can be started in + * parallel. + * As such, this message may be coupled with a content error. + */ +export interface IInitErrorWorkerMessage { + type: WorkerMessageType.InitError; + value: { + /** A string describing the error encountered. */ + errorMessage: string; + + kind: "dashWasmInitialization"; + }; +} + export interface INeedsBufferFlushWorkerMessage { type: WorkerMessageType.NeedsBufferFlush; contentId: string; @@ -883,6 +921,8 @@ export const enum WorkerMessageType { EndOfStream = "end-of-stream", Error = "error", InbandEvent = "inband-event", + InitError = "init-error", + InitSuccess = "init-success", InterruptEndOfStream = "stop-end-of-stream", InterruptMediaSourceDurationUpdate = "stop-media-source-duration", LockedStream = "locked-stream", @@ -921,6 +961,8 @@ export type IWorkerMessage = IAbortBufferWorkerMessage | IEndOfStreamWorkerMessage | IErrorWorkerMessage | IInbandEventWorkerMessage | + IInitSuccessWorkerMessage | + IInitErrorWorkerMessage | IInterruptMediaSourceDurationWorkerMessage | ILockedStreamWorkerMessage | ILogMessageWorkerMessage |