Skip to content

v4.5.0

Latest

Choose a tag to compare

@peaBerberian peaBerberian released this 06 Jul 17:49
v4.5.0

Release v4.5.0 (2026-07-06)

Quick Links:
πŸ“– API documentation - ⏯ Demo - πŸŽ“ Migration guide from v3

πŸ” Overview

This new release adds several fixes and features:

  • The biggest update here is on the MULTI_THREAD experimental feature: applications can now rely on manifestLoader, segmentLoader and representationFilter callbacks in "multithreading mode".

    This is done by letting you build your own separate RxPlayer worker bundle, through a wrapper with a simple API exported from the rx-player/experimental/worker path. Once bundled you can then provide the resulting file/build to the RxPlayer's usual attachWorker API.

  • Moreover, it's now possible through the same worker bundle feature to add "transport" features that were previously unavailable in multithreading mode: SMOOTH, LOCAL_MANIFEST and METAPLAYLIST.

    This means that no feature should now be missing from the multithreading mode, aside from directfile which would not really profit from it anyway.

  • We also have done a big internal change now that we're planning to remove the "experimental" label from the MULTI_THREAD feature: a large part of the monothreaded and multithreaded codebase has been merged into one shared path.
    This should simplify maintenance and make future behavior differences between both modes less likely.

  • We added more information to MEDIA_TIME_BEFORE_MANIFEST and MEDIA_TIME_AFTER_MANIFEST errors

  • We fixed multiple Safari / native-HLS edge cases

  • We improved DASH thumbnail handling

  • We fixed a small memory leak linked to quality changes

  • and we brought several smaller API and playback behavior improvements.

Changelog

Features

  • MULTI_THREAD: Allow applications to define their own representationFilter, segmentLoader and manifestLoader callbacks worker-side [#1719]
  • MULTI_THREAD: Enable adding most transport features worker-side (DASH, DASH_WASM, SMOOTH, LOCAL_MANIFEST and METAPLAYLIST) to play them in "multithreading" mode [#1783]
  • MediaError with the code MEDIA_TIME_BEFORE_MANIFEST and MEDIA_TIME_AFTER_MANIFEST now have a timeInfo property to make explicit the exact boundaries and present position linked to the issue [#1838]

Bug fixes

  • Fix memory leak linked to quality changes [#1781]
  • DRM: When a license request times out, now send a KEY_LOAD_TIMEOUT error instead of KEY_LOAD_ERROR
  • DRM: Fix "close-session" handling (from onKeyExpiration, onKeyOutputRestricted and onKeyInternalError keySystems options) that could unnecessarily close other DRM sessions
  • On Safari when beginning at 0, seek explicitly to 0 because its native HLS player would otherwise start at live position [#1803]
  • Ignore unhandled runtime track types when advertising Periods to prevent infinite rebuffering when text tracks and/or video tracks are not available features [#1850]
  • Fix startAt.wallClockTime on Safari HLS live playback [#1799]
  • Fix rare scenario when an ended live audio content would not have its duration known until the end-of-stream [#1801]
  • TTML: Do not apply percentage line heights anymore as they are sometimes subtly broken [#1812]
  • Thumbnails: Fix shared thumbnail requests (through multiple HTMLElement for the same data) in MULTI_THREAD mode [#1830]
  • Thumbnails: Fix thumbnail request sometimes being wrongly cancelled [#1810]
  • API: Fix incomplete format in the initial audioRepresentationChange event
  • CMCD: fix the headers CMCD communicationType [#1809]
  • fix precision difference which triggered frequent content boundaries warnings [#1848]
  • Compat: On some older LG TV patch out some Dolby Vision-related ISOBMFF boxes when playing retro-compatible Dolby Vision contents [#1818]
  • Do not send a streamEvent twice after temporarily switching to the RELOADING state while playing it [#1828]
  • Compat/DRM: Preserve pssh boxes in encrypted initialization segments on some DStv set-top boxes to fix playback of encrypted playback on them [#1822]
  • Text: Continue playback if subtitle initialization fails instead of remaining stuck in LOADING [#1827]
  • directfile: fix some track events not being sent when quickly switching between directfile contents [#1845]
  • directfile/compat: fix issue on Safari with startAt.fromLivePosition with directfile content when duration is infinite [#1842]

Other improvements

  • Adaptive bitrate: React faster to sudden bandwidth drops when playback is close to starving [#1831]
  • Add getMaximumPosition and getMinimumPosition support when using directfile with HLS playlists on Safari [#1800]
  • CMCD: Avoid sending invalid CMCD values when playback rate is 0 or negative, and better escape string values [#1833]
  • Fix ordering of a PeriodChange/AdaptationChange couple that may have previously led to unnecessary duplicate events [#1764]
  • Text track id can only be string [#1785]
  • The reload API now only throws if no loadVideo call has been made before [#1767]
  • Use native base64-bytes conversion utils when they exist [#1786]
  • consider HTTP 429 as retryable requests [#1860]
  • Merge most of the multithreaded and monothreaded codebase to simplify maintenance [#1627]

MULTI_THREAD: Application-defined worker logic

Applications can define custom loading and filtering logic through loadVideo options such as manifestLoader, segmentLoader and representationFilter. Those can be used for multiple advanced needs: peer-to-peer integrations, alternative application-known URLs, pre-processing of loaded data, or filtering out media qualities based on specific constraints.

Yet in multithreading mode (under the MULTI_THREAD feature), those APIs weren't available until now.
This was because those callbacks would run in a WebWorker, yet be originally written outside of it (in your application's code).

Sending arbitrary JavaScript functions to a WebWorker is not something we can do reliably: the browser mostly gives us message passing, which means those function's code would need to be serialized, e.g. to their stringified content.
Serialization breaks common JavaScript assumptions such as accessing variables defined outside your function and some build-tool transformations (e.g. your build tool writing some helpers that would be accessed inside that function, or that could be unavailable in a WebWorker context).

RELEASE_NOTES_v4 5 0_regular_function_worker_boundary

Schema: The previous representationFilter API for multithreading mode removed several assumptions from regular JavaScript code, that made it very hard to implement.

We previously tried to work around that for representationFilter with a stringified function format. It made it explicit that this function would not be running under regular JavaScript assumptions, but it was not very pleasant to write nor to maintain and very easy to get wrong.

For example, to keep only video Representations up to 1080p, you previously had to provide the function as a string:

rxPlayer.loadVideo({
  url,
  transport: "dash",
  representationFilter: `function (representation, context) {
    if (context.trackType !== "video") {
      return true;
    }
    var width = representation.width;
    var height = representation.height;
    return width != null && height != null && width <= 1920 && height <= 1080;
  }`,
});

The format was voluntarily awkward (writing code inside a string) to indicate clearly to developers that this was not a normal closure carrying its environment with it, but more of a "worker callback JS subset".
Yet this meant losing syntax highlighting and type-checking in most setups, making escaping mistakes more likely and the code much harder to maintain.

With a partner application, we even ended up reviewing all changes to their representationFilter logic to make sure it complied with those rules, corresponded to their target JS (ES5-compatible) and did not just use untrusted input in that implementation.

All those issues were already known initially, which is why we only tried this string solution with the usually simpler representationFilter API - as a test.

Now we propose a new solution that should be both more convenient and safer for application developers. Instead of having to write weird not-exactly-JS stringified logic, they can just write the code like they usually do and then build the RxPlayer worker themselves:

RELEASE_NOTES_v4 5 0_two_application_bundles

Schema: this new API lets you define the RxPlayer worker yourself. In that situation you can add whatever code you want on top and then handle the bundling/transpiling yourself.

For example, you can create a separate worker bundle that "registers" a representationFilter:

import RxPlayerWorker from "rx-player/experimental/worker";
import { DASH } from "rx-player/experimental/worker/features";

RxPlayerWorker.addFeatures([DASH]);

const rxPlayerWorker = new RxPlayerWorker();

rxPlayerWorker.registerRepresentationFilter(
  "resolution-based-filter",
  (representationInfo, context) => {
    if (context.trackType === "video") {
      const { width, height } = representationInfo;
      return width != null && height != null && width <= 1920 && height <= 1080;
    }
    return true;
  },
);

The worker has to be bundled separately and can then just be communicated to the RxPlayer through the same attachWorker API that you would have used in other multithreading scenarios:

import RxPlayer from "rx-player";

const rxPlayer = new RxPlayer({ videoElement });
rxPlayer.attachWorker({
  // Here the URL to the bundled worker
  workerUrl,
});

rxPlayer.loadVideo({
  // You can now refer to your registered `representationFilter` through its
  // declared "workerId" identifier
  representationFilter: { workerId: "resolution-based-filter" },

  // ...
});

The same general mechanism also exists for segmentLoader and manifestLoader.

We also added APIs to communicate with the worker from the main application bundle. This makes it possible to update worker-side state without rebuilding a new worker each time.

bundles responsibilities

Schema: The responsibilities of the two bundles. Your application code continue to be in your usual application logic, but you can define callbacks and process custom runtime events in the RxPlayer worker to tweak the corresponding rx-player callbacks.

Note that all complexities around communication between the two bundles are already handled by the RxPlayer here, an application just has to produce and provide the separate worker bundle and call a few documented RxPlayer API. Complete examples can be found in the new Importable Worker documentation page.

MULTI_THREAD: More complete transport support

Another important improvement for the MULTI_THREAD feature is that most transport features can now be added worker-side: DASH, DASH_WASM, SMOOTH, LOCAL_MANIFEST and METAPLAYLIST.

RELEASE_NOTES_v4 5 0_multithread_transport_support

This means that you can now play e.g. Smooth streaming contents in multithreading mode. We didn't permit anything other than DASH/DASH_WASM before as we didn't want to provide a heavy default worker bundle containing rarely-used features.
Moreover we first had to make some code updates to ensure all of them can run in our worker.

For example, Smooth support needed a specific effort because its Manifest parser relied on DOM APIs that are not available in the same way from a WebWorker. To make it work, we had to transition to the same full-JS fast XML parser approach as our DASH Manifest parser.

It also means that you can remove features that you wouldn't need anyway from the worker bundle. For example we included DASH_WASM in all builds before because applications relying on the MULTI_THREAD feature could also have needed it. With the separate worker bundle you just select all the transports you want with none included by default.

There's however one missing transport here: directfile (through the DIRECTFILE feature). Yet porting that one made much less sense as most of its logic is performed by the browser anyway, thus it would probably not gain much by running in multithreading mode.

MULTI_THREAD: A more shared core path internally

Historically, multithreaded and monothreaded logic had distinct initialization logic and some separate concepts. This made sense when MULTI_THREAD was introduced as it was very experimental and weren't sure of how it would evolve, but it made the code harder to maintain and made behavior differences between both modes easier to introduce.

Now that it's pretty clear to us that multithreading mode will be a long-term API, we decided to do something about it: we merged most of the unique logic both modes had into only one code path.

There are some differences between the two (as one of those modes mostly runs in a WebWorker yet the other does not) but they have been kept minimal:

  • There's now a thin communication layer between the code running always in main-thread (decryption handling, API, HTML5 media monitoring...) and the code that would run in a WebWorker if we're in multithreading mode (manifest+segment loading/parsing, adaptive logic, buffer management...).

    That "communication layer" allows to exchange messages between both. There is a multithreaded flavor (which uses the postMessage API to transmit those messages) whereas our monothreaded implementation just uses a simple event listener logic for those same messages.

RELEASE_NOTES_v4 5 0_core_interface_shared_path

Schema: How the inner architecture of the RxPlayer was updated. Instead of two independent paths each importing and using blocks of the RxPlayer separately, we've now merged most of it into one path.

  • Also we had to do a few tweaks to ensure we didn't degrade the monothreaded mode: we e.g. might want to share some data structures in that mode whereas the multithreaded mode has to go through a serialization step and synchronization. We kept that difference for efficiency reasons, yet this could lead in the future to some behavior change and bugs (e.g. mutations being shared in one mode but not the other) if we're not careful about it.

    Thankfully, most of that complexity is centralized around a single structure (our Manifest concept), so we could here make it work without sacrificing code readability too much.

The end goal was to remove duplicated logic, reduce accidental differences between both modes, and make our tests more valuable: when both modes go through more of the same code, a test written for one mode is more likely to protect the other one too.

This should not change the public API in any way. Yet we still wanted to mention it as it is a large internal change and as it also explain why this release contains multiple small fixes around MULTI_THREAD, monothreaded Manifest updates and event ordering.

More explicit out-of-bounds errors

Some dynamic contents have a moving playable window.

You can think for example of live contents: the maximum position evolves over time as newer audio/video becomes available, yet often the minimum position also does as old data is removed server-side to make some space. Because of that, an application can sometimes ask for a position that is not available anymore, or not available yet.

The RxPlayer already reported those cases through MEDIA_TIME_BEFORE_MANIFEST and MEDIA_TIME_AFTER_MANIFEST errors.

RELEASE_NOTES_v4 5 0_dynamic_manifest_window

Schema: simple visual of the type of situations where a MEDIA_TIME_BEFORE_MANIFEST warning would be triggered. The position to play (e.g. requested through a seekTo call) is before the minimum boundary of the last-fetched Manifest.

There was something missing with those errors though: they did not communicate what the position we were at when we sent the error nor what the "safe" position to play would be. We initially assumed that the user could just rely on other APIs like getPosition() and getMinimumPosition() / getMaximumPosition() to obtain that information.
Yet there were some edge cases such as right when the content is loading, where API like getPosition might not yet reflect the position we are trying to load.

In v4.5.0, those errors thus now expose a timeInfo property with more context on the issue. This property indicates the relevant boundary and the position that led to the error.

This should make it easier for applications to decide how to recover:

rxPlayer.addEventListener("warning", (error) => {
  if (error.code === "MEDIA_TIME_BEFORE_MANIFEST") {
    const { position, minPosition } = error.timeInfo;

    console.warn(
      `Position ${position} is before the current Manifest start: ${minPosition}`,
    );
  } else if (error.code === "MEDIA_TIME_AFTER_MANIFEST") {
    const { position, maxPosition } = error.timeInfo;

    console.warn(
      `Position ${position} is after the current Manifest end: ${maxPosition}`,
    );
  }
});

This is mostly useful for live or dynamic contents where the application may want to display a specific message, retry at a corrected position, or adapt its own UI to the available content boundaries.

The reload API is now easier to integrate

The reload API allows to re-load from scratch the last content loaded through a loadVideo call, even if it failed on an error or was stopped since.

Previously, reload could throw when reloadAt.relative was used before the RxPlayer had been able to know the previous content position. This single case would generally force applications into an awkward try / catch logic just because a previous content did not reach the right state soon enough.

After looking at some application code, we decided to remove this as a reason to throw, instead we're now just ignoring that relative offset - as how it should be interpreted here is ambiguous.

For a very simple example (actual code we saw was much more complex), an application wanting to retry after a playback error, slightly after the last known position, previously had to go through a try-catch block:

retryButton.onclick = () => {
  try {
    rxPlayer.reload({ reloadAt: { relative: 5 } });
  } catch (err) {
    rxPlayer.reload();
  }
};

Starting with v4.5.0, that fallback is handled by the RxPlayer:

retryButton.onclick = () => {
  // If `relative` cannot be applied, we silently skip it
  rxPlayer.reload({ reloadAt: { relative: 5 } });
};

This has been done only to simplify application code and their expectations. Application developers can mostly assume that reload does not throw anymore.

There's actually one remaining case where it can, but this one does not make much sense from an application perspective: it still throws if reload is called before any loadVideo call has been made - as there's nothing to reload.

Thumbnail improvements

Thumbnail handling also gained a few small fixes.

  • cancelling one renderThumbnail request should no longer cancel the shared request pipeline still needed by another request for the same thumbnail;

  • requesting a thumbnail outside of the available thumbnail range should now propagate the expected NO_THUMBNAIL error instead of a more generic NOT_FOUND error;

  • shared thumbnail requests are now fixed in MULTI_THREAD mode too.

RELEASE_NOTES_v4 5 0_shared_thumbnail_request

Schema: multiple renderThumbnail calls can actually rely on the same thumbnail request behind the hood. We previously had a bug where if a previous call was aborted, further ones relying on the same request resource could also be aborted as a side-effect.

Safari and native HLS improvements

Applications often use our directfile transport to be able to play HLS contents on Safari.

In that scenario, the browser may expose timing information through its own media element APIs instead of through Manifest and segment data loaded and parsed by the RxPlayer. That can lead to subtle differences, especially for live contents or when starting from a position relative to the live edge.

RELEASE_NOTES_v4 5 0_safari_native_hls_timing

Schema: In the directfile transport, we obtain all timing information from the browser, which are often less precise than from the media itself (Manifest, segments) which we only have access to in other transports.

We fixed multiple cases around that:

  • the startAt.wallClockTime loadVideo option should now work better on Safari HLS live playlists;

  • startAt.fromLivePosition is fixed for directfile contents when the duration is infinite;

  • the getMinimumPosition and getMaximumPosition methods now better consider that Safari HLS case, which should make them more precise.

  • when explicitly asked to begin at 0, the RxPlayer now also explicitly seeks to 0, avoiding a Safari native-HLS behavior where playback could instead start at the live position.

CMCD updates

We also updated how we handled CMCD, a way of providing playback metrics to the CDN (through either a query string of HTTP headers).

Amongst the few fixes, two stand out:

  • we avoid sending some optional values when the playback rate is 0 or negative where the semantics of those values in the specification are unclear
  • We had an issue that basically broke communication of CMCD values through HTTP headers, that we've now fixed