Open
Conversation
fdc67fd to
fac1335
Compare
c77ca40 to
6315c98
Compare
4da14d4 to
7c35c69
Compare
6315c98 to
6e6f754
Compare
7c35c69 to
3a336a5
Compare
6e6f754 to
8c04a87
Compare
8c04a87 to
5d9b410
Compare
3a336a5 to
f2c0726
Compare
1f9e5d2 to
964c957
Compare
1e534f4 to
84ba4d8
Compare
peaBerberian
commented
Aug 29, 2025
| @@ -262,10 +262,12 @@ function getKeySystemsOption( | |||
|
|
|||
Collaborator
Author
There was a problem hiding this comment.
NOTE: The demo is here modified to be able to test quickly the feature, this would probably be scrapped before merging anything
84ba4d8 to
54ee64e
Compare
3c69b17 to
ff00f45
Compare
54ee64e to
9bc8a61
Compare
ff00f45 to
4e86bd2
Compare
9bc8a61 to
3774328
Compare
4e86bd2 to
f20b8d2
Compare
3ce6633 to
01848d3
Compare
f20b8d2 to
2ae626e
Compare
01848d3 to
f4c2d81
Compare
peaBerberian
added a commit
that referenced
this pull request
Oct 13, 2025
We've recently seen `MEDIA_TIME_NOT_FOUND` errors in a Canal+ application. It's one of the errors that are part of our API that should actually be never sent to an application - unless there's an RxPlayer bug somewhere (it is documented as such in our API documentation). The issue --------- Turns out they were encountering a very specific race condition after a chain of events: 1. The application relied on a multi-threaded RxPlayer and played an encrypted content with some non-decipherable qualities. They also have all conditions met to allow our cache of `MediaKeySession` (which allows to reuse already-loaded decryption keys). 2. They then loaded another content, with a completely different media position that does not map to anything in the previous content (it shouldn't matter, but it will be important later) 3. The application switches again to the first content, without calling `stop` in between (which is OK and even more performant). In that situation, the following happen just after the third step: 4. We start to initialize everything for that last content. Since recently (#1607), that initialization step includes the initial polling of media metrics such as the position, the playbackRate etc. Before that work, this polling was done in a later step. 5. We stop the previous content. We do this after initializing the next content on purpose. "Stopping" a content (setting `mediaElement.src = ""`, typically) is a synchronous/blocking operations in JS that can actually take a lot of time - like hundred of ms on the lower end devices. To improve performance, we thus "initialize" the next content before stopping the previous one, as the former includes some parallelizable operations (network requests, `postMessage` to our Worker etc.) that can still take place while the "stop operation" is blocking the JS main thread. 6. The RxPlayer core (running in a WebWorker here) initialize everything, fetches the Manifest etc. Here the content is already known and its decryption keys are still "cached" locally, so we directly know that some qualities in the Manifest are not decipherable and decide to "fallback" from the higher qualities. 7. The fallback mechanism reads the last polled media metrics. If we reached that logic too fast, we will actually read the initially-polled metrics (3 steps earlier). This should be OK, but because we last polled the media metrics _BEFORE_ stopping the previous content, the playback position in those metrics is actually the last position reached in the previous content. 8. The RxPlayer checks that wanted position, see that it doesn't make sense in the current content, and triggers the `MEDIA_TIME_NOT_FOUND` error. The fix ------- The crux of this issue is just that we poll media metrics before stopping the previous content, so there was a lot of ways this issue could be easily fixed. The fix I ended up choosing is very far from being the most straightforward one though :D. It's a solution I already PoCed with my [preload work](#1646) which I found elegant in terms of code architecture. Basically the `MediaElementPlaybackObserver` (the class doing the polling) can now be "headless" initially: without a media element. In that case, default media metrics are considered (position `0`, paused etc.) When the media element is considered "ready", it can be attached to it, in which case "real" polling will be performed. It wasn't done with this issue in mind, but I found that it also made sense there: before stopping the previous content, we want to start our polling logic but are not yet ready to attach the media element as it is still technically playing the previous content. Once the previous content is stopped, we can now begin to actually link the media element to it to enable actual polling. This makes it far from a hotfix (there's a lot of lines) but I like this solution.
peaBerberian
added a commit
that referenced
this pull request
Oct 13, 2025
We've recently seen `MEDIA_TIME_NOT_FOUND` errors in a Canal+ application. It's one of the errors that are part of our API that should actually be never sent to an application - unless there's an RxPlayer bug somewhere (it is documented as such in our API documentation). The issue --------- Turns out they were encountering a very specific race condition after a chain of events: 1. The application relied on a multi-threaded RxPlayer and played an encrypted content with some non-decipherable qualities. They also have all conditions met to allow our cache of `MediaKeySession` (which allows to reuse already-loaded decryption keys). 2. They then loaded another content, with a completely different media position that does not map to anything in the previous content (it shouldn't matter, but it will be important later) 3. The application switches again to the first content, without calling `stop` in between (which is OK and even more performant). In that situation, the following happen just after the third step: 4. We start to initialize everything for that last content. Since recently (#1607), that initialization step includes the initial polling of media metrics such as the position, the playbackRate etc. Before that work, this polling was done in a later step. 5. We stop the previous content. We do this after initializing the next content on purpose. "Stopping" a content (setting `mediaElement.src = ""`, typically) is a synchronous/blocking operations in JS that can actually take a lot of time - like hundred of ms on lower-end devices. To improve performance, we thus "initialize" the next content before stopping the previous one, as the former includes some parallelizable operations (network requests, `postMessage` to our Worker etc.) that can still take place while the "stop operation" is blocking the JS main thread. 6. The RxPlayer core (running in a WebWorker here) initialize everything, fetches the Manifest etc. Here the content is already known and its decryption keys are still "cached" locally, so we directly know that some qualities in the Manifest are not decipherable and decide to "fallback" from the higher qualities. 7. The fallback mechanism reads the last polled media metrics. If we reached that logic too fast, we will actually read the initially-polled metrics (3 steps earlier). This should be OK, but because we last polled the media metrics _BEFORE_ stopping the previous content **AND** no metrics has been emitted since then, the playback position in those metrics is actually the last position reached in the previous content. 8. The RxPlayer checks that wanted position, see that it doesn't make sense in the current content, and triggers the `MEDIA_TIME_NOT_FOUND` error. The fixes --------- The crux of this issue is both that: 1. we poll media metrics before stopping the previous content and, 2. we do not emit new metrics immediately when the initial position to seek to is known The fix I ended up choosing is very far from being the most straightforward one though :D. It's a solution I already PoCed with my [preload work](#1646) which I found elegant in terms of code architecture. Basically the `PlaybackObserver` (the class doing the polling) can now be "headless" initially: without a media element. In that case, default media metrics are considered (position `0`, paused etc.) When the media element is considered "ready", it can be attached to it, in which case "real" polling will be performed. This ensure that no polling of media metrics linked to a previous content is going on. It wasn't done with this issue in mind, but I found that it also made sense there: before stopping the previous content, we want to start our metrics-polling module (the `PlaybackObserver`) but are not yet ready to attach the media element as it is still technically playing the previous content. Once the previous content is stopped, we can now begin to actually link the media element to it to enable actual polling. Moreover, I now decide to emit those metrics right when we know what the initial position to seek to will be (as those metrics include this data point as a "wanted position). This makes sure that the RxPlayer core always has the more up-to-date information. Lastly, the already-merged #1755 fix should also have fixed that issue - as the initial position would have been known at some point by the RxPlayer Core anyway. This makes it far from a hotfix (there's a lot of lines) but I like this solution.
peaBerberian
added a commit
that referenced
this pull request
Oct 13, 2025
We've recently seen `MEDIA_TIME_NOT_FOUND` errors in a Canal+ application. It's one of the errors that are part of our API that should actually be never sent to an application - unless there's an RxPlayer bug somewhere (it is documented as such in our API documentation). The issue --------- Turns out they were encountering a very specific race condition after a chain of events: 1. The application relied on a multi-threaded RxPlayer and played an encrypted content with some non-decipherable qualities. They also have all conditions met to allow our cache of `MediaKeySession` (which allows to reuse already-loaded decryption keys). 2. They then loaded another content, with a completely different media position that does not map to anything in the previous content (it shouldn't matter, but it will be important later) 3. The application switches again to the first content, without calling `stop` in between (which is OK and even more performant). In that situation, the following happen just after the third step: 4. We start to initialize everything for that last content. Since recently (#1607), that initialization step includes the initial polling of media metrics such as the position, the playbackRate etc. Before that work, this polling was done in a later step. 5. We stop the previous content. We do this after initializing the next content on purpose. "Stopping" a content (setting `mediaElement.src = ""`, typically) is a synchronous/blocking operations in JS that can actually take a lot of time - like hundred of ms on lower-end devices. To improve performance, we thus "initialize" the next content before stopping the previous one, as the former includes some parallelizable operations (network requests, `postMessage` to our Worker etc.) that can still take place while the "stop operation" is blocking the JS main thread. 6. The RxPlayer core (running in a WebWorker here) initialize everything, fetches the Manifest etc. Here the content is already known and its decryption keys are still "cached" locally, so we directly know that some qualities in the Manifest are not decipherable and decide to "fallback" from the higher qualities. 7. The fallback mechanism reads the last polled media metrics. If we reached that logic too fast, we will actually read the initially-polled metrics (3 steps earlier). This should be OK, but because we last polled the media metrics _BEFORE_ stopping the previous content **AND** no metrics has been emitted since then, the playback position in those metrics is actually the last position reached in the previous content. 8. The RxPlayer checks that wanted position, see that it doesn't make sense in the current content, and triggers the `MEDIA_TIME_NOT_FOUND` error. The fixes --------- The crux of this issue is both that: 1. we poll media metrics before stopping the previous content and, 2. we do not emit new metrics immediately when the initial position to seek to is known The fix I ended up choosing is very far from being the most straightforward one though :D. It's a solution I already PoCed with my [preload work](#1646) which I found elegant in terms of code architecture. Basically the `PlaybackObserver` (the class doing the polling) can now be "headless" initially: without a media element. In that case, default media metrics are considered (position `0`, paused etc.) When the media element is considered "ready", it can be attached to it, in which case "real" polling will be performed. This ensure that no polling of media metrics linked to a previous content is going on. It wasn't done with this issue in mind, but I found that it also made sense there: before stopping the previous content, we want to start our metrics-polling module (the `PlaybackObserver`) but are not yet ready to attach the media element as it is still technically playing the previous content. Once the previous content is stopped, we can now begin to actually link the media element to it to enable actual polling. Moreover, I now decide to emit those metrics right when we know what the initial position to seek to will be (as those metrics include this data point as a "wanted position). This makes sure that the RxPlayer core always has the more up-to-date information. Lastly, the already-merged #1755 fix should also have fixed that issue - as the initial position would have been known at some point by the RxPlayer Core anyway. This makes it far from a hotfix (there's a lot of lines) but I like this solution.
peaBerberian
added a commit
that referenced
this pull request
Oct 13, 2025
We've recently seen `MEDIA_TIME_NOT_FOUND` errors in a Canal+ application. It's one of the errors that are part of our API that should actually be never sent to an application - unless there's an RxPlayer bug somewhere (it is documented as such in our API documentation). The issue --------- Turns out they were encountering a very specific race condition after a chain of events: 1. The application relied on a multi-threaded RxPlayer and played an encrypted content with some non-decipherable qualities. They also have all conditions met to allow our cache of `MediaKeySession` (which allows to reuse already-loaded decryption keys). 2. They then loaded another content, with a completely different media position that does not map to anything in the previous content (it shouldn't matter, but it will be important later) 3. The application switches again to the first content, without calling `stop` in between (which is OK and even more performant). In that situation, the following happen just after the third step: 4. We start to initialize everything for that last content. Since recently (#1607), that initialization step includes the initial polling of media metrics such as the position, the playbackRate etc. Before that work, this polling was done in a later step. 5. We stop the previous content. We do this after initializing the next content on purpose. "Stopping" a content (setting `mediaElement.src = ""`, typically) is a synchronous/blocking operations in JS that can actually take a lot of time - like hundred of ms on lower-end devices. To improve performance, we thus "initialize" the next content before stopping the previous one, as the former includes some parallelizable operations (network requests, `postMessage` to our Worker etc.) that can still take place while the "stop operation" is blocking the JS main thread. 6. The RxPlayer core (running in a WebWorker here) initialize everything, fetches the Manifest etc. Here the content is already known and its decryption keys are still "cached" locally, so we directly know that some qualities in the Manifest are not decipherable and decide to "fallback" from the higher qualities. 7. The fallback mechanism reads the last polled media metrics. If we reached that logic too fast, we will actually read the initially-polled metrics (3 steps earlier). This should be OK, but because we last polled the media metrics _BEFORE_ stopping the previous content **AND** no metrics has been emitted since then, the playback position in those metrics is actually the last position reached in the previous content. 8. The RxPlayer checks that wanted position, see that it doesn't make sense in the current content, and triggers the `MEDIA_TIME_NOT_FOUND` error. The fixes --------- The crux of this issue is both that: 1. we poll media metrics before stopping the previous content and, 2. we do not emit new metrics immediately when the initial position to seek to is known The fix I ended up choosing is very far from being the most straightforward one though :D. It's a solution I already PoCed with my [preload work](#1646) which I found elegant in terms of code architecture. Basically the `PlaybackObserver` (the class doing the polling) can now be "headless" initially: without a media element. In that case, default media metrics are considered (position `0`, paused etc.) When the media element is considered "ready", it can be attached to it, in which case "real" polling will be performed. This ensure that no polling of media metrics linked to a previous content is going on. It wasn't done with this issue in mind, but I found that it also made sense there: before stopping the previous content, we want to start our metrics-polling module (the `PlaybackObserver`) but are not yet ready to attach the media element as it is still technically playing the previous content. Once the previous content is stopped, we can now begin to actually link the media element to it to enable actual polling. Moreover, I now decide to emit those metrics right when we know what the initial position to seek to will be (as those metrics include this data point as a "wanted position). This makes sure that the RxPlayer core always has the more up-to-date information. Lastly, the already-merged #1755 fix should also have fixed that issue - as the initial position would have been known at some point by the RxPlayer Core anyway. This makes it far from a hotfix (there's a lot of lines) but I like this solution.
peaBerberian
added a commit
that referenced
this pull request
Dec 5, 2025
We've recently seen `MEDIA_TIME_NOT_FOUND` errors in a Canal+ application. It's one of the errors that are part of our API that should actually be never sent to an application - unless there's an RxPlayer bug somewhere (it is documented as such in our API documentation). The issue --------- Turns out they were encountering a very specific race condition after a chain of events: 1. The application relied on a multi-threaded RxPlayer and played an encrypted content with some non-decipherable qualities. They also have all conditions met to allow our cache of `MediaKeySession` (which allows to reuse already-loaded decryption keys). 2. They then loaded another content, with a completely different media position that does not map to anything in the previous content (it shouldn't matter, but it will be important later) 3. The application switches again to the first content, without calling `stop` in between (which is OK and even more performant). In that situation, the following happen just after the third step: 4. We start to initialize everything for that last content. Since recently (#1607), that initialization step includes the initial polling of media metrics such as the position, the playbackRate etc. Before that work, this polling was done in a later step. 5. We stop the previous content. We do this after initializing the next content on purpose. "Stopping" a content (setting `mediaElement.src = ""`, typically) is a synchronous/blocking operations in JS that can actually take a lot of time - like hundred of ms on lower-end devices. To improve performance, we thus "initialize" the next content before stopping the previous one, as the former includes some parallelizable operations (network requests, `postMessage` to our Worker etc.) that can still take place while the "stop operation" is blocking the JS main thread. 6. The RxPlayer core (running in a WebWorker here) initialize everything, fetches the Manifest etc. Here the content is already known and its decryption keys are still "cached" locally, so we directly know that some qualities in the Manifest are not decipherable and decide to "fallback" from the higher qualities. 7. The fallback mechanism reads the last polled media metrics. If we reached that logic too fast, we will actually read the initially-polled metrics (3 steps earlier). This should be OK, but because we last polled the media metrics _BEFORE_ stopping the previous content **AND** no metrics has been emitted since then, the playback position in those metrics is actually the last position reached in the previous content. 8. The RxPlayer checks that wanted position, see that it doesn't make sense in the current content, and triggers the `MEDIA_TIME_NOT_FOUND` error. The fixes --------- The crux of this issue is both that: 1. we poll media metrics before stopping the previous content and, 2. we do not emit new metrics immediately when the initial position to seek to is known The fix I ended up choosing is very far from being the most straightforward one though :D. It's a solution I already PoCed with my [preload work](#1646) which I found elegant in terms of code architecture. Basically the `PlaybackObserver` (the class doing the polling) can now be "headless" initially: without a media element. In that case, default media metrics are considered (position `0`, paused etc.) When the media element is considered "ready", it can be attached to it, in which case "real" polling will be performed. This ensure that no polling of media metrics linked to a previous content is going on. It wasn't done with this issue in mind, but I found that it also made sense there: before stopping the previous content, we want to start our metrics-polling module (the `PlaybackObserver`) but are not yet ready to attach the media element as it is still technically playing the previous content. Once the previous content is stopped, we can now begin to actually link the media element to it to enable actual polling. Moreover, I now decide to emit those metrics right when we know what the initial position to seek to will be (as those metrics include this data point as a "wanted position). This makes sure that the RxPlayer core always has the more up-to-date information. Lastly, the already-merged #1755 fix should also have fixed that issue - as the initial position would have been known at some point by the RxPlayer Core anyway. This makes it far from a hotfix (there's a lot of lines) but I like this solution.
a42734c to
29372ac
Compare
f55a387 to
a938e33
Compare
d4be192 to
9ad6758
Compare
a938e33 to
3e4b05f
Compare
Back in our `v4.4.0` we brought the `DummyMediaElement` feature allowing to run the RxPlayer's logic even on environments where actual decoding and/or decryption is not possible (incompatible codecs, DRMs, unavailable media API etc.). Our main use case for this are testing purposes. While wrapping up the release, we noted that we still relied on the actual `MediaSource` API for checking codec support, removing a key use case of the feature: testing the RxPlayer on a content even if the current env does not support any of its codec. So we quickly implemented an exception in the corresponding code branch. It works and should catch all cases, but it was nonetheless not satisfactory for long-term maintainance. --- To make it more explicit, I propose here to pass from the `API` to the `Init` the `MediaSource` class that should be used to play the content. In a `DummyMediaElement`'s case, the `MediaSource` implementation linked to it will be provided instead of the favored `MediaSource` implementation found on the device. A remaining issue is that as the class is not transmitted to `core` (not serializable), `core` will still need to rely on the device's favored `MediaSource` implem. I thought that it wasn't an issue as a `DummyMediaElement` is not compatible for now with "MSE-in-worker" which is the only scenario where communicating its linked `MediaSource` would be needed.
3e4b05f to
e023f3d
Compare
0142e34 to
1fd9df3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Based on #1645
After first drafts more than a year ago, I at last began to see a path in how it could be implemented without losing any feature and without sacrificing the RxPlayer's code too much, though this PR is still just a POC and a work-in-progress.
The real final implementation would probably be separated into multiple PRs.
tl;dr
This PR shows a possible (and functional) RxPlayer architecture for a performant preload API, which allows to pre-buffer a content in-memory while potentially another content is playing on the media element. Potential usages are to make transitions between ad breaks/content (pre-roll, mid-roll, post-roll) and/or sequences of multiple contents (e.g. multiple episodes of a TV serie) smoother.
Here's what it looks like (I quickly added another button on the demo page to preload a content):
12.mp4
Video: We begin by loading a content (after simulating a low-ish network bandwidth). We see that it takes some time before that content can play. We then choose to pre-load a content - we can see segments being loaded but not yet pushed / played. Then we start the preload. On my computer - this
startPreloadstep is almost instantaneous. In some embedded devices, it has been observed to still take some time.Overview
This PR describes a proposal architecture to allow "content preloading", which is an advanced feature in some MSE players where a future content is loaded before being actually played to lower the time it will take once the user want to play that content.
It can be used as a mechanism to improve loading performance when we're very confident that the user will play a content yet we don't want to play it on the media element yet.
It may also be used to accelerate the transition between contents that will be played sequentially on the same media element (advertisement cuts, switching between episodes of a TV serie etc.).
As a supplementary goal, it may both be used explicitely by an application through an RxPlayer
APIor implicitely through an adaptive streaming technology mechanism that could profit from it (e.g. "interstitial" systems, MPD chaining etc.).Preloading high level behavior
After playing with multiple strategies, I for now chose to implement "preloading" by loading that content's media segments directly in JS memory (instead of pushing them to the lower-level browser's buffers). When/if the application then wants to actually load the content, we will push all of that stored data on the actual browser's buffers linked to the
HTMLMediaElement(which is itself linked to the current RxPlayer instance).An alternative solution that was studied was to preload and load all contents on the same
HTMLMediaElement, only at an offseted time for each preload. Though it could lead to better performance (i.e. a simple seek to switch content is generally much faster than initializing all MSE API and then feeding it data), I decided to abandon that solution for now because of fears of compatibility issues.I also postpone some API calls, such as encryption-related ones, for when the content is actually played to increase device compatibility. This even though those encryption-related exchanges may themselves take time and even though they also can lead to multiple licence requests at once when the content is finally played.
RxPlayer API for preloading
Starting/Stopping a preload
An application can ask for a content to be preloaded through a
preloadVideoAPI, which will return acontentIdidentifier.That API can be called multiple times for various contents, to preload all them simultaneously, though it should be noted that there may be memory, performance, and bandwidth-related issues if pre-loading too much contents at once. This last point is for now considered as a problem left for the application, though we may provide improvements to help them with those points in the future as those are not subjects they're usually exposed to.
An application can then actually load a preloaded content at any time by calling the
startPreloadAPI, which will stop the previous loaded content if one, but none of the potential other preloads.To stop preloading content, a
stopPreloadAPI was added. Also aclearPreloadAPI was added to allow an application to stop all preloads it has started.Track and quality selection
All track-related API (
setAudioTrack,getAvailableVideoTracksetc.) and representations-related API (lockVideoRepresentationetc.) now also accept an optionalcontentIdin which case they apply to the preload concerned.Preload events
When preloading a content, an application might also want to subscribe to some of the same events than it does when loading a content. For example
newAvailablePeriodsto set the initial choosen tracks orerrorto be notified if the preload fails.Because of this, I duplicated those concerned events with a
preload-prefix (reusing the same one would have either conflicted with those or broke the API):"preload-newAvailablePeriods""preload-brokenRepresentationsLock""preload-trackUpdate""preload-representationListUpdate""preload-error""preload-warning"Those events also signal the
contentIdto signal which preload concerned (and the actual payload is communicated through apayloadfield).Internal changes done for that new behavior
One of the key design strategy I followed for content preloading was that that rarely-used feature should not greatly decrease the readability of the RxPlayer's architecture.
With that in mind from the perspective of the RxPlayer's inner code, the main differences between a content that is pre-loading and a content that is actually loading is that:
HTMLMediaElement(e.g. the<video>tag) yet, though it will have one if we ever play it.MediaSource(the main object in the MSE group of API, which allows to feed media data programatically to anHTMLMediaElement) is not yet "ready" to play contents,SourceBuffers (MSE media buffers) cannot yet be added.Working around not having the
HTMLMediaElementwhen pre-loadingAs the
HTMLMediaElementis only playing the content when actually loading, not pre-loading, I decided the following modifications:For a few modules, they now can function with AND without an
HTMLMediaElementyet. In the cases where anHTMLMediaElementhasn't been provided to it , they behave in a not-yet-ready state as if anHTMLMediaElementwill be provided to it in the future coincidentally with actual playback.For example,
ITextDisplayerimplementations will store subtitles but not display them on screen synchronized with the media element until... a media element has actually been provided through itsattachMediaElementmethod (which may be called when/if the application finally decide to actually load that preload).Likewise, a
MediaElementPlaybackObserverwill just advertise about the default initial position until an actualHTMLMediaElementhas been provided here also though anattachMediaElementmethod.For most other cases, I removed direct usage of the
HTMLMediaElementto prefer relying on theMediaElementPlaybackObserverinstead, which is already intrinsically linked to theHTMLMediaElement.I then added to the
MediaElementPlaybackObservera method calledonMediaElementAttachmentwhich allows to register a callback that will be called once theHTMLMediaElementhas been attached to it (or called immediately and synchronously if it is already the case). This is what those other modules can rely on to declare logic relying on the media element.Working around not having
SourceBufferobjectsSourceBufferare very important here because preloading is all about loading in advance media segments and because under the RxPlayer's "normal" (i.e. "loading", not "preloading") behavior, media segments were immediately pushed to theirSourceBufferafter loading them (with the RxPlayer preferably not keeping an in-memory reference to that huge chunk of data).To be more exact, the RxPlayer most often do not use
SourceBufferobjects directly, it uses multiple abstractions to reach them :the
SourceBufferInterfaceabstraction which provides a more sensible API (promise-based methods instead of the older-school event-based APISourceBufferobjects have, queue system abstracted instead of exposed to the application) yet which stays roughly at the same functional level than aSourceBuffer: it serves the same purpose - adding and removing media data from lower-levels buffers - and do no more.At a higher level,
SourceBufferInterfaceare then used by theAudioVideoSegmentSinkabstraction.They implement multiple features to facilitate the usage of those buffers: an
inventoryof the currently-pushed segments' metadata, it has an history system of recent operations, pending operations that have not yet finished can be inspected and other higher-level niceties.It is then
AudioVideoSegmentSinkinstances that are used by most of the RxPlayer's modules.Making the
AudioVideoSegmentSinkwork without aSourceBufferInterfaceThe solution I proposed here is to make the
AudioVideoSegmentSinktemporarily work without aSourceBufferInterfaceand in that scenario to instead store all asked operations (segment pushing, data remove etc.) in-memory.From outside code, the corresponding operation's Promises will just be left opened, indicating that those operations have not yet been performed succesfully yet.
When it is finally possible to obtain a
SourceBuffer/SourceBufferInterface, theAudioVideoSegmentSink'slinkToMediaSourcemethod is called, which has the effect to directly communicate all operations pending in memory to thatSourceBufferInterface. It is at that point that those operations' Promises are bound to the actualSourceBufferoperation.All that happens completely transparently so that other modules continue to interact with the
AudioVideoSegmentSinkwithout even knowing that it is currently in the process of "transferring" data from memory to aSourceBuffer.Working around not having a ready
MediaSourceUnder normal ("loading") conditions, we create a
MediaSourceobject almost immediately after theloadVideoAPI is called, attach it to theHTMLMediaElementand then wait for thatMediaSource'sreadyStateproperty to switch to"open"and for theMediaKeysto be linked to theHTMLMediaElement. All this so we can then load segments on theSourceBufferand start playing.We do not want to link a
MediaSourcenor aMediaKeysto theHTMLMediaElementwhen preloading though.To work-around this, the
Corepart of the RxPlayer now separates the concept of being "ready" to fetch+push segments and the concept of having the media element actually available for playback: theCorewill start by creating aSegmentSinksStorewithout aMediaSource.Note that for code simplification purposes, this for now happen for all contents - not just the preloaded ones. We thus always begin by creating
AudioVideoSegmentSinkstoring segments in memory before actually switching to the real deal when ready.Generally when actually "loading" (and not "preloading"), segment fetching should take longer than the
MediaSourceandMediaKeysattachment logic in which case there shouldn't really be an observable in-JS storing of segments, not that this would be a problem anyway. It might even end up improving performance on devices where eitherMediaSource,MediaKeysattachment or both took a long time.This is implemented through a supplementary message from
main_threadtocore:MediaElementReady.Memory concerns
Considering we already have memory-related concerns on many devices (especially some smart TV and set-top boxes), enabling a preload feature on them seems for now a little ambitious: instead of loading a single content and having trouble doing so, we may here not only load a content but also pre-load the future one at the same time.
This is something I had in mind when doing the implementation, and the end goal is to allow the preloading feature to be enabled even on those devices. The key factor we may want to consider here is how easy it will be to integrate that notion with the current preloading implementation. I saw multiple possible compatible ways (e.g. synchronizing options like
maxVideoBufferSizeso it also consider preloads could seem logical) but did not do anything in that sense yet to keep the initial implementation """"simple"""".Can this work be reused for a content downloading feature
As I know there is some long term need for this, both at Canal+ and from outsiders, and as it may seem similar to a downloading concept (as when preloading, contents are also loaded for later), I thought that I had to add this chapter to this documentation.
The answer is: I don't think so.
For downloads, the solution found by other implementations we've seen have many key differences:
they favor the highest compatible media quality without network bandwidth considerations, where content preloading starts from the idea that playback will soon start and thus act like our regular playback logic on that point
for downloaded contents, playback will most often start much later, in a different browsing session and thus in a different RxPlayer instance. In contrast with "preloading" where preloading and actually playing are usually close in time to each other and are always on the same RxPlayer instance.
The current preloading solution proposed is basically about handling a "not-yet-playing" mode, where the media element can then be "hot-swapped" (well, "hot-inserted") to enable actual playback on the current RxPlayer instance.
This also means that all modules handling playback-related issues are in-place (and there's a LOT of them) because they might be imminently-needed. This is just added complexity in a context where the content will not be actually played in that same session.
Because of those, I actually think that having a separate code path/architecture for content downloading than for content loading and preloading would be easier to follow and maintain (though we know some people do not believe us on this :p!). Most advanced features worked-around here (
SegmentSinkinventories,HTMLMediaElementusage,PlaybackObserveretc.) are not needed at all for content downloading because they are linked to content playback.The main logic of content downloading seem actually easier to implement than what we're doing here, the complexities we had at the time of our first attempts (at implementing content downloading) were mostly linked to device compatibility with some advanced features (e.g. available long-term storage API on some devices, persistent licence implementation) and all the necessary code around the inevitable downloading-specific issues (no space left on storage, async suppression of downloaded contents that may be interrupted by the user closing the page etc.).
Adding those orthogonal complexities linked to content downloading on top of the preloading API would complexify the "regular" code for in my opinion no real gain. Re-using in a separate location loading-related modules (
segmentFetcher,manifestFetcher), selecting Representations through an util and a well-defined API, and then loading and storing all of their segments sequentially (code which would be written in that downloading-specific directory) seems much easier to implement.