Skip to content

Fix occurences of MEDIA_TIME_NOT_FOUND on initial fallback#1758

Merged
peaBerberian merged 1 commit intodevfrom
fix/media-time-not-found-on-load
Oct 23, 2025
Merged

Fix occurences of MEDIA_TIME_NOT_FOUND on initial fallback#1758
peaBerberian merged 1 commit intodevfrom
fix/media-time-not-found-on-load

Conversation

@peaBerberian
Copy link
Copy Markdown
Collaborator

@peaBerberian peaBerberian commented 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 ([Proposal] Allow seeking through seekTo before the HTMLMediaElement is ready #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 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 (as this would also have saved us from this issue)
  3. if we don't know the playing position when the DRM fallback logic is triggered, we throw a MEDIA_TIME_NOT_FOUND error

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 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 also 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 peaBerberian force-pushed the fix/media-time-not-found-on-load branch 2 times, most recently from 22ba6e9 to 3d1766d Compare October 13, 2025 18:26
@peaBerberian peaBerberian added the Priority: 0 (Very high) This issue or PR has a very high priority. Efforts should be concentrated on it first. label Oct 13, 2025
@peaBerberian peaBerberian added this to the 4.5.0 milestone Oct 13, 2025
@peaBerberian peaBerberian force-pushed the fix/media-time-not-found-on-load branch 2 times, most recently from 61ff3c7 to c871d1e Compare October 13, 2025 20:00
@peaBerberian peaBerberian force-pushed the fix/media-time-not-found-on-load branch 2 times, most recently from 74d6b70 to 3c1cd2f Compare October 13, 2025 20:04
@canalplus canalplus deleted a comment from github-actions bot Oct 13, 2025
@canalplus canalplus deleted a comment from github-actions bot Oct 13, 2025
@peaBerberian peaBerberian force-pushed the fix/media-time-not-found-on-load branch from 3c1cd2f to 04bd366 Compare October 13, 2025 20:17
@peaBerberian peaBerberian force-pushed the fix/media-time-not-found-on-load branch from 04bd366 to b24f4eb Compare October 13, 2025 20:25
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.
@github-actions
Copy link
Copy Markdown

✅ Automated performance checks have passed on commit a585c0b96ff7ba1583def6175e6d1a11c45be468 with the base branch dev.

Details

Performance tests 1st run output

No significative change in performance for tests:

Name Mean Median
loading 20.25ms -> 20.48ms (-0.231ms, z: 1.64620) 28.05ms -> 28.05ms
seeking 16.52ms -> 17.20ms (-0.682ms, z: 0.77977) 10.95ms -> 10.95ms
audio-track-reload 26.61ms -> 26.78ms (-0.167ms, z: 3.31649) 38.85ms -> 39.00ms
cold loading multithread 46.71ms -> 45.88ms (0.826ms, z: 14.09525) 68.85ms -> 67.50ms
seeking multithread 29.73ms -> 39.03ms (-9.300ms, z: 0.06233) 10.20ms -> 10.20ms
audio-track-reload multithread 25.54ms -> 25.63ms (-0.093ms, z: 0.14066) 37.50ms -> 37.50ms
hot loading multithread 15.11ms -> 14.96ms (0.153ms, z: 4.71350) 21.90ms -> 21.75ms

@peaBerberian peaBerberian merged commit 4c198f1 into dev Oct 23, 2025
32 of 35 checks passed
@peaBerberian peaBerberian modified the milestones: 4.5.0, 4.4.1 Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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