Release v4.4.0 (2025-09-25)
Quick Links:
π API documentation
-
β― Demo
-
π Migration guide from v3
- π Overview
- π Changelog
onVideoTracksNotPlayable
/onAudioTracksNotPlayable
- new
disableAudioTrack
API - new method:
getWallClockOffset
seekTo
now callable whileLOADING
/RELOADING
- Key system information on most
EncryptedMediaError
- Firefox and Chrome now handling PlayReady SL3000 on Windows
- More structured logs
- New hidden experimental API: Dummy Media Element
- Improved MediaCapabilitiesProber Experimental API
- Auditable RxPlayer releases
π Overview
The v4.4.0 is here, with many new features and improvements:
-
new
onAudioTracksNotPlayable
andonVideoTracksNotPlayable
loadVideo
options to allow playback even if no audio or video tracks are supported on the current content. -
A new
disableAudioTrack
API, to complement the existingdisableVideoTrack
anddisableTextTrack
APIs and give you control over all media track types -
seekTo
can now be relied on even when stillLOADING
/RELOADING
, to correct a future playback position. -
A new
getWallClockOffset
API to facilitate conversions between the RxPlayer'sposition
and the "wallClockTime"
that is generally more useful for live contents. -
EncryptedMediaError
errors, which are those linked to content decryption, now will also communicate context when possible, such as the corresponding key system name and configuration. -
Add
MediaError
with the"NO_AUDIO_VIDEO_TRACKS"
code for when neither audio nor video is available or when both are disabled -
"local"
Manifests (experimental feature to load contents that are downloaded locally) can now announce forced narrative subtitles tracks, through the newforcedSubtitles
property -
The experimental
MediaCapabilitiesProber
tool had its API updated to be more flexible (see below) -
WebVTT embedded in an mp4 segment are now properly handled.
-
Firefox and Chrome recently added PlayReady SL3000 support under most Windows 11 devices. It is now properly handled.
-
Thumbnails (from our new thumbnails API brought in v4.3.0) now have a better caching mechanism.
-
RxPlayer logs have been rewritten to be more easily read and exploited by people relying on the RxPlayer logs for debugging
-
many bug fixes and other improvements
Changelog
Features
- Add
onAudioTracksNotPlayable
andonVideoTracksNotPlayable
loadVideo
options to control whether to continue playback is one of those components is not compatible to the current device [#1624] - Add
getWallClockOffset
API to obtain a "live position"'s offset [#1601] - add
disableAudioTrack
API [#1715] - DRM: Add
keySystem
andkeySystemConfiguration
to mostEncryptedMediaError
so an application can determine which key system caused an issue [#1690] - Update API of the experimental
mediaCapabilitiesProber
API so it's more flexible to use [#1472] - Implement inband WebVTT (vtte/vttc) [#1639]
- Add
"NO_AUDIO_VIDEO_TRACKS"
error for when neither audio nor video is available or when both are explicitly disabled [#1624] - Allow seeking through
seekTo
even whileLOADING
orRELOADING
the content [#1607] - local: local-manifests now can have a
forcedSubtitles
property in anadaptations
object, to indicate that a text track actually represents "forced narrative" subtitles [#1722]
Bug fixes
- Subtitles: Fix some subtitle missing on multiple period assets [#1708]
- DRM: renew the mediaKeySystemAccess on Edge and Firefox when using a PlayReady keySystem to work-around frequent DRM issues. [#1694]
- DASH: Fix some rare occurence of infinite rebuffering on multi-Period when seeking exactly at the end of a Period. [#1738]
- DRM: On Firefox check extensively PlayReady DRMs support before using them to work-around recent firefox issue with PlayReady integration [#1691]
- DRM: Fix persistent session loading when content has no key id [#1713]
representation
object returned by some API had incorrect shape for optional attributes [#1720]- Directfile: set autoplay attribute on directfile contents, to work-around safari-specific issues [#1711]
- Remove unnecessary duration logs when reaching the end of some VoD contents [#1744]
Other improvements
- Add hidden experimental API "Dummy media API" to facilitate tests and debugging of the RxPlayer behavior without having to support the content in the current environment [#1478]
- Thumbnails: Cache pending thumbnail request [#1718]
- For Dynamic contents, let the initial position go outside the range of the Manifest and let the application correct if if needed based on
MEDIA_TIME_BEFORE_MANIFEST
orMEDIA_TIME_AFTER_MANIFEST
events [#1607] - Update log syntax so users can follow them more easily [#1717]
- Add to hidden RxPlayer config most compat switches [#1514]
- Prevent unnecessary resources usage by inactive worker [#1696]
- Improve on freeze resolution, especially for encrypted contents [#1705]
- Detect that
fetch
/AbortController
is native to provide a better experience for application relying on (broken) polyfills instead [#1698] - enable debug logs as soon as
__RX_PLAYER_DEBUG_MODE__
is set [#1626] - rely on "provenance statements" when publishing our builds [#1742]
onVideoTracksNotPlayable
/ onAudioTracksNotPlayable
One of the most significant additions in this release is the introduction of two new loadVideo
options:
Reason: error or reduced experience?
Until now, when playing a content whose audio was not supported on the device, the RxPlayer would stop playback and throw a MANIFEST_INCOMPATIBLE_CODECS_ERROR
error.
Technically, we could have just continued only playing video and no audio in that scenario. We only stop playback on an error because we inferred that most applications didn't want the possibility of those kind of reduced experiences.
And the same rules apply if video is unsupported yet audio is, or when one of them is non-decipherable (in which case you could by default see the NO_PLAYABLE_REPRESENTATION
fatal error) but the other is.
novideoshort.mp4
Video: When you play content whose audio or video is not currently supported (here HEVC video on my device), you're left with an error. Here is how it displays in our demo page.
But failing directly is not what everybody want: we had multiple feature requests to allow playback without audio or video if one of them was unsupported (either due to codec issues or due to the impossibility to decipher it).
Two new options
We thus brought two new options in this release to give you granular control over how the player behaves in such scenarios:
player.loadVideo({
transport: "dash",
url: "https://example.com/content.mpd",
autoPlay: true,
onAudioTracksNotPlayable: "continue", // Play without audio instead of failing
onVideoTracksNotPlayable: "error", // Still fail if video tracks are incompatible
});
Both options accept either "continue"
or "error"
:
-
"error"
: The player will throw the corresponding error and stop, maintaining the default behavior. -
"continue"
: The player will continue playback without the problematic media type.
You can thus set either of those to "continue"
now to continue playback anyway:
novideo1.mp4
Video: Demonstration of how the onVideoTracksNotPlayable
in "continue"
mode could work. Here in our demo page we end up playing the content without the video (but with audio playing).
If the video does not play, it is available here: https://github.com/user-attachments/assets/8595d290-62cd-4f45-8d7f-7ff74bcba598
Note that if such a "fallback" happens during playback (for example: during a DASH Period transition), it may lead to a small RELOADING
step.
New event: noPlayableTrack
When setting either of those two new options to continue
, you might want to be alerted when and if such "fallback" scenario happens.
For exactly this, we also added the noPlayableTrack
event which will be triggered just before the fallback is actually enforced.
rxPlayer.addEventListener("noPlayableTrack", (evt) => {
console.log(
"No compatible track for the media type",
evt.trackType /* "audio" or "video" */,
"for period",
evt.period.id
);
});
Worst case: no compatible audio and no compatible video
This new API also brings a new possibility to the RxPlayer: we might be disabling both audio and video (e.g. if both are set to continue
and fallbacked or if one is disabled while the other is fallbacked from).
In that scenario, we'll still stop playback with an "error"
event. However we added a new kind of error just for that kind of scenario: "NO_AUDIO_VIDEO_TRACKS"
:
rxPlayer.addEventListener("error", (error) => {
if (error.code === "NO_AUDIO_VIDEO_TRACKS") {
console.error("No audio nor video track was enabled on the content");
}
});
new disableAudioTrack
API
Building on the enhanced compatibility options, we've also added a new disableAudioTrack
API.
It basically does what its name imply and complements the existing disableVideoTrack
and disableTextTrack
APIs.
// Disable audio track for audio-description or silent playback scenarios
player.disableAudioTrack();
// Re-enable audio later
const audioTracks = player.getAvailableAudioTracks();
if (audioTracks.length > 0) {
player.setAudioTrack(audioTracks[0].id);
}
This API is arguably much less useful than disabling text tracks or even the video track, but could still make sense in some advanced applications or especially when debugging issues, where it could allow to easily only check video playback and not audio).
The new "NO_AUDIO_VIDEO_TRACKS"
error also added in this release may result from a call to this API if no video was enabled at that point.
new method: getWallClockOffset
The RxPlayer has multiple API to get the current playback position:
-
getPosition
to obtain the current actual position we're playing on the media element (what we also call the "media time"). -
getWallClockTime
which tries to give the corresponding broadcast time as a unix timestamp. This one is here mostly to help applications display a "time" for live contents to their users.
There were time however where applications wanted to convert a "wall-clock time" to a "media time" and vice versa.
Especially, most other API rely on media time only. If an application wanted to compare the "wall-clock time" to our concept of "minimum position" or "maximum position" it had no real easy way to do it.
New getWallClockOffset
API
To improve on that situation, the new getWallClockOffset
API just returns you the difference between the "wall-clock time" and the "media time" that is used by almost all other RxPlayer API.
const wallClockPosition = player.getWallClockTime();
const wallClockOffset = player.getWallClockOffset();
const minimumPosition = player.getMinimumPosition();
if (minimumPosition !== null && wallClockPosition - wallClockOffset < minimumPosition) {
console.warn("We're playing before the currently minimum seekable position");
}
seekTo
now callable while LOADING
/ RELOADING
The RxPlayer has a central concept of "player states" which complexifies our API a little bit.
For example, when either STOPPED
, LOADING
or RELOADING
, most API won't work as expected: you cannot call play()
or pause()
, you cannot call seekTo()
to seek on the content, you cannot update the "playback rate" etc.
To help with this, we added in v3.31.0
(June 2023) the isContentLoaded
API that is just a shortcut to check that the state is different from those three.
Yet we were still not happy with this situation: even if there's technical reasons for those limitations, we would prefer it to be transparent to applications.
First step: handle seekTo
complexity on our side
As a first step toward removing some complexity from our API, we made seekTo
now callable and functional during the LOADING
and RELOADING
steps.
seekto.mp4
Video: In our demo page, I seek to a position at 60s while the content is still loading. We can see that it does seek to that position ultimately.
It will still throw when the state is STOPPED
however - as it makes no sense to seek when there's no content loaded nor loading on the RxPlayer.
There is however for now a small caveat: the wallClockTime
option from seekTo
is not always usable under the LOADING
state for now, as it relies on Manifest information - and we may not have fetched the Manifest yet at that point. Ultimately, we also want to handle that last issue but note that for now this is still a limitation.
It's still not possible to rely on play()
, pause()
, setPlaybackRate()
etc. on those states yet though. Those will hopefully be future improvements, to make the API even easier to use.
Key system information on most EncryptedMediaError
DRM-related errors now provide much more context to help with debugging and implementing fallback strategies. When an EncryptedMediaError
occurs, you'll now in most cases also receive additional information about the key system and its configuration.
This enhancement is particularly valuable for applications that want to implement DRM fallback mechanisms or provide more detailed error reporting:
player.addEventListener("error", (error) => {
if (error.type === "ENCRYPTED_MEDIA_ERROR") {
const { keySystem, keySystemConfiguration } = error;
console.log("Error with DRM Configuration:", { keySystem, keySystemConfiguration });
if (keySystem?.includes("playready")) {
console.log("PlayReady-specific error, trying Widevine fallback");
// Implement fallback logic
}
}
});
The two additional properties are both optional, they are:
keySystem
: The key system identifier (e.g., "com.widevine.alpha", "com.microsoft.playready")keySystemConfiguration
: TheMediaKeySystemConfiguration
object that was being used
Firefox and Chrome now handling PlayReady SL3000 on Windows
Earlier this year, Firefox on Windows 11 may be able to rely on PlayReady SL3000. Also, right now, Chrome is doing the same thing (they are both using a common Windows-provided API behind the hood).
Most notably, this means hardware DRM support which is one of the main requirements from content right holders to play high video resolutions and video content with a high dynamic range. This means that we'll be able to provide to many customers high quality encrypted media on their desktop PCs with the browser of their choice.
Generally such changes should go smoothly and transparently with the RxPlayer, the application should just have to ask for "playready"
or a "com.microsoft.playready.recommendation"
type
property in keySystems
and we would automatically enable it if available.
However there have been some issues when testing the Firefox implementation on the RxPlayer (some of which we also saw on Edge Windows). This means that previous RxPlayer versions may not be able to profit from it.
We added some mechanisms in this release to handle the potential issues we found while testing this, we're now successfully providing high quality media to most Windows 11 user - on Edge, Firefox and very soon Chrome (chrome's implementation can already be tested).
More structured logs
In the RxPlayer team, we heavily rely on logs for debugging complex issues. If you ever set the RxPlayer.LogLevel
static property, you saw how much data those logs output.
There is a lot of room for improvements though: logs are still hard to understand - even for us - without cross-referencing with the code again.
To give an example, here is a log you might have encountered in the console until now:
RS: future discontinuity encountered video 1163.04 1163.051770833
Many things are not clear here:
-
What's
RS:
exactly? (even us we often don't remember without grepping the code) -
What are those floating numbers at the end about?
Compare this with the new format:
Stream: future discontinuity encountered bufferType="video" discontinuityStart=1163.04 discontinuityEnd=1163.051770833`
So now:
Stream:
doesn't seem much better at first, but it is a clear larger area of the code for us (in thesrc/core/stream
directory)- All values that were previously floating are now clearly identified (as
bufferType
,discontinuityStart
anddiscontinuityEnd
) so you can at least a good idea of what's happening - especially for us - without having to re-check everything.
And the same kind of format is now done for ALL our logs.
The end goal is to make the logs even easy-to-understand for applications if they want to check first if some issue they're having is due to a content or application issue (as opposed to an RxPlayer issue).
New hidden experimental API: Dummy Media Element
We also added in this release a "virtual" media element implementation that can be used for testing RxPlayer behavior without requiring an actual HTML media element.
We now rely on this new API in new advanced tests and in on our demo page, to be able to check how the RxPlayer would play a content even when testing on a device without the right codec and/or DRM support:
dummy2.mp4
Video: How the dummy media element is exposed in our demo page. Here we use it to be able to "play" an encrypted content without actually needing to perform a license request.
Of course it doesn't actually decode but the RxPlayer thinks it does - useful to debug or check the RxPlayer's behavior. Not all media containers (e.g. .mp4
, .webm
, .mkv
etc.) are handled for now.
What this actually does is to mock the HTML5 video, MSE and EME API so the RxPlayer believes it is running on a regular implementation (in reality, no video nor audio is actually playing - the RxPlayer does not "see" that though). This has been a huge effort, but it pays off with the possibility to write complex integration tests for situations that are not easily reproducible in our tests environments.
Because this is not a stable API though, we didn't really want to encourage applications to rely on it, which is why it is for now left undocumented and thus not officially part of our API.
Improved MediaCapabilitiesProber Experimental API
The "experimental" (meaning: its API is not "stable") MediaCapabilitiesProber
API has been reworked for better flexibility regarding DRM capabilities polling: The getCompatibleDRMConfigurations
method is replaced by checkDrmConfiguration
, which now checks a single configuration per call rather than multiple configurations at once.
It greatly facilitates the API and allows you to try different ways of checking multiple DRM configurations at once:
// /!\ Older approach not compatible anymore.
// Old approach (no longer available)
const configs = await mediaCapabilitiesProber.getCompatibleDRMConfigurations([
widevineConfig,
playreadyConfig,
]).then(([widevineResult, playReadyResult]) => {
if (widevineResult.compatibleConfiguration) {
console.warn("Widevine has a compatible configuration:", widevineResult.compatibleConfiguration);
// ...
}
if (playReadyResult.compatibleConfiguration) {
console.warn("PlayReady has a compatible configuration:", playReadyResult.compatibleConfiguration);
// ...
}
});
// New approach
// Checking all at once: just combine them with `allSettled`
const results = await Promise.allSettled([
mediaCapabilitiesProber.checkDrmConfiguration("com.widevine.alpha", widevineConfig),
mediaCapabilitiesProber.checkDrmConfiguration(
"com.microsoft.playready.recommendation",
playreadyConfig
),
]).then(([widevineResult, playReadyResult]) => {
// NOTE: here `widevineResult` and `playReadyResult` correspond to the return value of `allSettled`
if (widevineResult.status === "fulfilled") {
console.warn("Widevine has a compatible configuration:", widevineResult.value);
// ...
}
if (playReadyResult.status === "fulfilled") {
console.warn("PlayReady has a compatible configuration:", playReadyResult.value);
// ...
}
});
// Or check just the first compatible one: combine them with allSettled
const firstWorkingConfiguration = await Promise.race([
mediaCapabilitiesProber.checkDrmConfiguration("com.widevine.alpha", widevineConfig),
mediaCapabilitiesProber.checkDrmConfiguration(
"com.microsoft.playready.recommendation",
playreadyConfig
),
]);
// ....
// etc.
This new API is documented here.
Auditable RxPlayer releases
We saw multiple so-called "supply chain attacks" in the npm ecosystem the last few weeks. You may have heard for example about the chalk
and "shai-hulud" ones.
Both of those had the attacker rely on the maintainer's npm account (either through phishing or by stealing an npm token) to instead locally publish their own infected copy of the same dependency.
Schema: Basically how those attacks worked. The compromised releases were built by the attacker on his/her own computer then published to npm directly, without having the new source code on the repository.
To better protect applications relying on the RxPlayer against those types of attacks we decided to raise the security needed to produce our builds. From now on, RxPlayer releases will most notably not be built by us locally, but by the CI associated to our GitHub repository.
This mean that you can now inspect the steps and source coded relied on for any new releases.
Schema: How it works from now on with the RxPlayer. We only create a tag (that we sign) with git and push it to this repository. In reaction, our CI builds a version from that tag and publish it to npm directly. We've also linked npm to the repo (and CI process) so we can be sure new versions come from this repository only.
For example, this release has been built and published here. It relies on other scripts from the RxPlayer so it's not so simple to read however, but you can ensure that what has been built is associated to the corresponding commit and (gpg-signed) release tag.
By linking this to npm, it also comes with a nice green checkmark which can link to the corresponding code and build pipeline.
Screenshot: how the new version looks like on npm with its green checkmark.
Clicking on the "view more details" link on npm for that version links to the following information:
Screenshot: Information on the provenance attestation of this specific v4.4.0
release. It links to the source commit (github.com/canalplus/rx-player@f7fdb30) linked to this version, our CI workflow that generated the build (https://github.com/canalplus/rx-player/actions/runs/18015878748/workflow) and the public ledger associated to it (https://search.sigstore.dev/?logIndex=561444039).
Note that we also now rely on the same mechanism for our dev
and canal
-tagged pre-releases.