Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make attachWorker return a promise #1374

Merged
merged 1 commit into from
Feb 5, 2024
Merged

Conversation

peaBerberian
Copy link
Collaborator

Last week, we've seen an issue on some Samsung and LG TVs when relying on the RxPlayer new experimental MULTI_THREAD feature due to specific opcodes found in our WebAssembly files which were not compatible with some of those TVs' browser.

Though we isolated and fixed the issue in #1372, it might be better to also find a longer term solution to rollback the MULTI_THREAD feature when an issue is detected with it preventing us from playing.

This could be done in several ways, from throwing errors, to new events, to just return a rejecting Promise in the attachWorker method.

I chose to go with the latter of those solutions now because it appears logical API-wise and implementation-wise to have that method return a Promise which resolves only if we're able to communicate with a WebWorker (and reject either if the browser does not support it, if a security policy prevent from running the worker, if the request for the worker's code fail or if the code evualation itself fails).

I've also added a specialized error just for that API to give more context about what failed (missing feature? etc.).

I was afraid that relying on this new Promise to indicate an issue at WebAssembly-compilation-time for our MPD parser would bother us in the future if we ever add other WebAssembly modules (e.g. a smooth parser), which could also independently fail (should we reject the Promise when either compilation fails? Even if we could theoretically still play DASH contents? How would we mix this way with a potentially lazy-loading of features where we wouldn't be compiling right away? and so on...), but after exposing all the potential future paths I could see this MULTI_THREAD feature taking, I was able to find an adapted solution still compatible with returning a Promise on the attachWorker API.

I also tried to automatically fallback from a "multithread mode" to the regular monothread one inside the RxPlayer but doing this was complex. So for now, if attachWorker fails, the RxPlayer will remove the worker from its state (new loadVideo calls won't depend on it) but it is the responsibility of the application to reload if a content was loaded in "multithread mode" was loaded in the meantime.

If an application doesn't want to handle that supplementary complexity, it can just await the Promise returned by attachWorker before loading the first content (and catching eventual errors). As the RxPlayer automatically removes the worker if its initialization fails, this will lead to automatically fallback on main thread. At the cost of some time compared to load and initialize the worker parallely.

@peaBerberian peaBerberian added Priority: 0 (Very high) This issue or PR has a very high priority. Efforts should be concentrated on it first. MultiThread Concerns specifically the multithreaded mode of the RxPlayer labels Feb 5, 2024
@peaBerberian peaBerberian changed the title Make attachWorker return a promise by default Make attachWorker return a promise Feb 5, 2024
Copy link
Collaborator

@Florent-Bouisset Florent-Bouisset left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am wondering about the relevance of having attachWorker a public method.
An application should always call the following attachWorker with the same arguments if it intents to play in multithread mode.

this.player.attachWorker({
    workerUrl: EMBEDDED_WORKER,
    dashWasmUrl: EMBEDDED_DASH_WASM,
});

Maybe we could include this step into the addFeature(MULTI_THREAD) or in the loadVideo(), to make the API simpler to use.
However doing this change would not let the opportunity for the application to
get the attachWorker promise result. And so, the automatic fallback on main thread you were talking about seems to be required if we decide to do this change.

Comment on lines 445 to 455
if (typeof Worker !== "function") {
log.warn("API: Cannot rely on a WebWorker: Worker API unavailable");
return rej(new WorkerInitializationError("INCOMPATIBLE_ERROR",
"Worker unavailable"));
} else if (!hasWebassembly) {
log.warn("API: Cannot rely on a WebWorker: WebAssembly unavailable");
return rej(new WorkerInitializationError("INCOMPATIBLE_ERROR",
"WebAssembly unavailable"));
} else {
const blobUrl = URL.createObjectURL(workerSettings.workerUrl);
this._priv_worker = new Worker(blobUrl);
URL.revokeObjectURL(blobUrl);
}

this._priv_worker.onerror = (evt: ErrorEvent) => {
this._priv_worker = null;
log.error("Unexpected worker error",
evt.error instanceof Error ? evt.error : undefined);
};
log.debug("---> Sending To Worker:", MainThreadMessageType.Init);
this._priv_worker.postMessage({
type: MainThreadMessageType.Init,
value: {
dashWasmUrl: workerSettings.dashWasmUrl,
logLevel: log.getLevel(),
sendBackLogs: isDebugModeEnabled(),
date: Date.now(),
timestamp: getMonotonicTimeStamp(),
hasVideo: this.videoElement?.nodeName.toLowerCase() === "video",
hasMseInWorker,
},
});
log.addEventListener("onLogLevelChange", (logLevel) => {
if (this._priv_worker === null) {
return;
if (typeof workerSettings.workerUrl === "string") {
this._priv_worker = new Worker(workerSettings.workerUrl);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there can be one less nesting level here because the if and else if act as early return

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

@peaBerberian
Copy link
Collaborator Author

I am wondering about the relevance of having attachWorker a public method.
An application should always call the following attachWorker with the same arguments if it intents to play in multithread mode.

An application only has to do it once per instance. And it allows to have a Worker per instance, which I thought mapped nicely.

Doing it at loadVideo would necessitate to do it everytime we loaded a video? With the WASM compilation re-started everytime?

Doing it on addFeatures would mean that all classes will need to attach its worker (or rely on the same but in that case we need to add like an RxPlayerId to determine which message would be for which player) at some point (at the first loadVideo call?) with no possibility of having only some of them relying on a worker (though I don't know if this is a real use case).

Overall, I find the attachWorker method easier to reason about

Last week, we've seen an issue on some Samsung and LG TVs when relying
on the RxPlayer new experimental `MULTI_THREAD` feature due to specific
opcodes found in our WebAssembly files which were not compatible with
some of those TVs' browser.

Though we isolated and fixed the issue in #1372, it might be better to
also find a longer term solution to rollback the `MULTI_THREAD` feature
when an issue is detected with it preventing us from playing.

This could be done in several ways, from throwing errors, to new events,
to just return a rejecting Promise in the `attachWorker` method.

I chose to go with the latter of those solutions now because it appears
logical API-wise and implementation-wise to have that method return a
Promise which resolves only if we're able to communicate with a
WebWorker (and reject either if the browser does not support it, if a
security policy prevent from running the worker, if the request for the
worker's code fail or if the code evualation itself fails).

I've also added a specialized error just for that API to give more
context about what failed (missing feature? etc.).

I was afraid that relying on this new Promise to indicate an issue at
WebAssembly-compilation-time for our MPD parser would bother us in the
future if we ever add other WebAssembly modules (e.g. a smooth parser),
which could also independently fail (should we reject the Promise when
either compilation fails? Even if we could theoretically still play DASH
contents? How would we mix this way with a potentially lazy-loading of
features where we wouldn't be compiling right away? and so on...), but
after exposing all the potential future paths I could see this
`MULTI_THREAD` feature taking, I was able to find an adapted solution
still compatible with returning a Promise on the `attachWorker` API.

I also tried to automatically fallback from a "multithread mode" to the
regular monothread one inside the RxPlayer but doing this was complex.
So for now, if `attachWorker` fails, the RxPlayer will remove the worker
from its state (new `loadVideo` calls won't depend on it) but it is the
responsibility of the application to reload if a content was loaded in
"multithread mode" was loaded in the meantime.

If an application doesn't want to handle that supplementary complexity,
it can just await the Promise returned by `attachWorker` before loading
the first content (and catching eventual errors). As the RxPlayer
automatically removes the worker if its initialization fails, this
will lead to automatically fallback on main thread. At the cost of some
time compared to load and initialize the worker parallely.
Copy link
Collaborator

@Florent-Bouisset Florent-Bouisset left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case where the video is played with mode: main (i.e: not worker)
and the parser is DASH_WASM, if we failed to use WASM , do we fallback on the standard parser?

@Florent-Bouisset
Copy link
Collaborator

Doing it on addFeatures would mean that all classes will need to attach its worker (or rely on the same but in that case we need to add like an RxPlayerId to determine which message would be for which player) at some point (at the first loadVideo call?) with no possibility of having only some of them relying on a worker (though I don't know if this is a real use case).

You are right, it need to be done one time by instance of the class. What about doing it in the constructor new RxPlayer() ?

@peaBerberian
Copy link
Collaborator Author

In the case where the video is played with mode: main (i.e: not worker) and the parser is DASH_WASM, if we failed to use WASM , do we fallback on the standard parser?

If both were added as feature, yes.

This is how it works:

  • DASH_WASM in main thread fails to compile -> fallback to JS parser if it exists (if it doesn't -> PIPELINE_PARSE_ERROR)
  • DASH_WASM in worker fails to compile -> fallback to JS parser in worker which doesn't exist -> PIPELINE_PARSE_ERROR

So the only difference in multithread mode today is that we will also reject the Promise of attachWorker there. This weird difference and the fact that we may change the behavior if other features are added to MULTI_THREAD are why I go in a tangent of being not sure if this is the right long term solution in that PR's description.
Though longer term, we could add things like FEATURE_ERROR warnings in general without breaking this API.

@peaBerberian
Copy link
Collaborator Author

You are right, it need to be done one time by instance of the class. What about doing it in the constructor new RxPlayer() ?

This would mean that the feature has to be added before the construction and that you could not lazily add a worker, which I thought was too restrictive.

@peaBerberian
Copy link
Collaborator Author

peaBerberian commented Feb 5, 2024

All other RxPlayer features today don't have to be added before construction.

They just need to be added before you rely on them (e.g. you could only add a HTML_TTML_PARSER when the next content has ttml subtitles).

Even with DASH_WASM, where the addFeatures call, the DASH_WASM.initialize call, and the RxPlayer construction can be made in any order - with different meanings for the three (addFeatures to add the glue js code to all RxPlayer classes, initialize to fetch the WASM file, constructor to create a new RxPlayer instance).

@Florent-Bouisset
Copy link
Collaborator

All other RxPlayer features today don't have to be added before construction.

They just need to be added before you rely on them (e.g. you could only add a HTML_TTML_PARSER when the next content has ttml subtitles).

Even with DASH_WASM, where the addFeatures call, the DASH_WASM.initialize call, and the RxPlayer construction can be made in any order - with different meanings for the three (addFeatures to add the glue js code to all RxPlayer classes, initialize to fetch the WASM file, constructor to create a new RxPlayer instance).

Ok thanks, I understand better why it's done that way, changing that was not the scope of that PR anyway :)
I'm ok for merging it.
Having an automatic fallback in the future would be appreciated by the API users I guess.

@peaBerberian peaBerberian mentioned this pull request Feb 5, 2024
@peaBerberian peaBerberian merged commit cf0cd1b into stable Feb 5, 2024
4 checks passed
@peaBerberian peaBerberian added this to the 4.0.0-rc.2 milestone Feb 5, 2024
@peaBerberian peaBerberian deleted the feat/attach-worker-prom branch February 7, 2024 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
MultiThread Concerns specifically the multithreaded mode of the RxPlayer Priority: 0 (Very high) This issue or PR has a very high priority. Efforts should be concentrated on it first.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants