Skip to content

Releases: canalplus/rx-player

v4.2.0

17 Oct 13:17
419d66a
Compare
Choose a tag to compare

Release v4.2.0 (2024-10-17)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

🔍 Overview

The v4.2.0 is out, with both new features and multiple fixes:

  • "cmcd" (for "common media client data") v1 is now (fully) supported. It is a standardized system to allow the communication of current playback conditions through http(s) requests.

  • When checking which media codec is supported on the device, we now also consider encryption as a factor.

    This is following new Google Chrome releases supporting only HEVC when unencrypted.

  • We added a filterPlayableRepresentations option to our audio and video tracks getters.

    This lets the application know about which media has been filtered out (e.g. 4k content due to it not being decipherable, or hevc content for not being decodable).

  • We added a contentProtections property to Representations returned by the API to let applications know which Representations are encrypted and what their key id are.

  • We added the checkManifestIntegrity option, allowing to retry Manifest requests in cases where the CDN returned corrupted data.

    This was added after such behaviors was actually seen, in the name of resilience even when the issue is in another component of the media streaming process.

  • We worked around a Safari bug leading to infinite loading when playing encrypted HLS contents (through the directfile transport) and having a system language different than the playlist's default language.

  • We added the LogFormat static property to produce more informative logs.

  • Many other fixes, including some for the experimental MULTI_THREAD feature, some for the DASH_WASM feature, for a short-lived firefox issue, for legacy bundles...

Changelog

Features

  • Add cmcd object to loadVideo options to enable CMCD (Common Media Client Data) [#1461, #1518]
  • Add checkManifestIntegrity loadVideo option as a temporary work-around when you suspect your packager or CDN to sometimes deliver corrupted data [#1471]
  • Add contentProtections to the representations of the tracks API to know if they're considered encrypted [#1505]
  • Add filterPlayableRepresentations property to audio and video tracks API to get information on ALL representations, even those that won't be played [#1501]
  • Add LogFormat static property to the RxPlayer to try improving on bug reports [#1469]
  • Experimentally re-export config in v4 (only intended for debugging matters) [#1510]

Bug fixes

  • Detect cases where an encrypted media's codec is not supported specifically when the media is encrypted and prevent the playback of such contents [#1484]
  • Work-around the "hulu issue" seen on firefox 129 and 130 (1911283 and 1912238 on bugzilla) which also impacted the RxPlayer [#1495, #1498]
  • Fix rare cases where the active Period would not be advertised by the RxPlayer [#1502]
  • Actually trigger a BUFFER_FULL_ERROR when QuotaExceededError mitigations after appendBuffer MSE calls don't work #1546
  • Fix issues when handling a QuotaExceededError after an appendBuffer MSE call [#1546, #1559]
  • Directfile/Compat: Fix startAt.fromLastPosition handling on Safari when playing directfile contents [#1548]
  • DRM/Compat: Re-create MediaKeys for each content on Philips' NETTV, and KSTB40XX set-top boxes [#1515]
  • DRM/Compat: fix content not starting on Safari because key are never considered usable for a track [#1479, #1512]
  • DASH_WASM: fix Label element never being parsed [#1541, #1540]
  • Fix RxPlayer not being exposed in release bundles [#1542]
  • Consider stpp.ttml codec for text format [#1557]
  • Prevent very rare cases of infinite rebuffering after getting errors from calling the SourceBuffer.prototype.appendBuffer and SourceBuffer.prototype.remove MSE API [#1560, #1561]
  • MULTI_THREAD: Fix rare CancellationError error happening when reloading while a reload is pending. [#1528]
  • MULTI_THREAD: fix wrong Period considered as current in multi-Period DASH contents with the multi-thread feature [#1527]
  • MULTI_THREAD: Fix rare occurrences of infinite loading on constrained devices [#1556]

Other improvements

  • DASH: provide a more precize calculation for the timeshift buffer depth [#1483]
  • Handle hev1 codec and hvc1 codecs as part of the same family of codecs when trying to check for compatibility between the two [#1499]
  • Better handle QuotaExceededError issue after appendBuffer MSE calls when wantedBufferAhead is set to Infinity [#1546]
  • Code: Forbid the direct usage of MSE and HTML5 media TypeScript type in profit of our own compatible ones to facilitate testing and the addition of platform-specific differences [#1397].
  • Demo: Remove standalone demo as we never relied on it [#1473]
  • Scripts: Automatize official releases and CHANGELOG.md updating through a script [#1524]

CMCD v1 support

CMCD, for "Common Media Client Data", is a standard allowing to communicate various playback-related metrics to the CDN when requesting resources.

The idea is that a back-end may then be able to exploit those metrics with the goal of improving both the streaming experience (e.g. by putting more bandwidth to customers that need it the most or by preparing segments that will be requested in the future) and the monitoring aspect (by being notified about buffer-related information, following the user's QoE).

The information CMCD v1 relies on can be here communicated either through HTTP(S) request headers or a query string in the URL and is opt-in: it is only enabled if the new cmcd loadVideo option is communicated.

cmcd
Screenshot: request for an initialization segment with CMCD enabled as query string parameters.

Many players and CDN support CMCD today but the RxPlayer did not until now - mainly because we prioritized other work in the past.
We've now added a CMCD implementation so that application that wants to rely on it are now able to do so.

DRM: Detection of codecs unsupported when encrypted

Recently Google Chrome added support for HEVC (also known as "H.265"), though only if it was either unencrypted or if the current device had what we call "hardware DRM" available (which most desktop PCs do not have).

This was problematic because when considering what the RxPlayer was able to play it only checked if the codec was supported. It did not also take into consideration DRM matters.

We've now worked to not only consider whether a codec should theoretically be supported by a device but also consider, when the corresponding content is encrypted, if it is also supported when encrypted.
This improvement most notably now lead to the RxPlayer correctly avoiding encrypted H.265 video content on devices where hardware DRM is not available.

DRM: More information on Representations in the API

We sometimes work closely with applications using the RxPlayer, at least those at Canal+, to understand what API they might miss.

We noticed that our audio and video tracks API missed some key information that the application may find useful.

1. New filterPlayableRepresentations option

Previously, we filtered non-"playable" Representations (qualities) from our audio video tracks methods (getVideoTrack, getAvailableVideoTracks...) and events (videoTrackChange, availableVideoTracksChange...).

What this means is that video and audio qualities that were either in an unsupported codec or that were non-decipherable would not be communicated by those API, as they cannot be played anyway.

However, an application may want to know which qualities were present in the content yet are not supported, for example for monitoring and debugging use cases.

To allow this usage, the RxPlayer can now optionally take a filterPlayableRepresentations property through those following API:

Read more

v3.33.4

17 Oct 13:09
f9be414
Compare
Choose a tag to compare

Release v3.33.4 (2024-10-16)

Quick Links:
📖 API documentation - Demo

🔍 Overview

The v3.33.4 is a "legacy" release (now that the v4 is the current major version) mostly containing some of the bug fixes and improvements initially made for the upcoming v4.2.0 release that should be done just after this one.

📑 Changelog

Bug fixes

  • Compat: Fix autoPlay on Tizen when the content starts on a discontinuity [#1500]
  • Work-around the "hulu issue" seen on firefox 129 and 130 (1911283 and 1912238 on
    bugzilla) which also impacted the RxPlayer [#1544]
  • Fix rare cases where the active Period would not be advertised by the RxPlayer [#1534]
  • DRM/Compat: Re-create MediaKeys for each content on Philips' NETTV and KSTB40xx
    set-top boxes [#1519, #1538]
  • Fix some issues that may arise on BUFFER_FULL situations [#1566]
  • Directfile/Compat: Fix startAt.fromLastPosition handling on Safari when playing
    directfile contents [#1574]
  • Consider stpp.ttml codec for text format [#1567]
  • DRM/Compat: Re-create MediaKeys for each content on Philips' NETTV, and KSTB40XX
    set-top boxes [#1519, #1538]
  • DRM/Compat: fix content not starting on Safari because key are never considered usable
    [#1535]
  • DASH_WASM: fix Label element never being parsed [#1543]

Other improvements

  • DASH: provide a more precize calculation for the timeshift buffer depth [#1492]
  • Handle hev1 codec and hvc1 codecs as part of the same family of codecs when trying
    to check for compatibility between the two [#1536]
  • Better handle QuotaExceededError issue after appendBuffer MSE calls when
    wantedBufferAhead is set to Infinity [#1566]
  • Bring prettier to the v3 [#1545]

v4.1.0

08 Jul 17:23
74b603a
Compare
Choose a tag to compare

Release v4.1.0 (2024-07-08)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

🔍 Overview

We're now releasing the v4.1.0.
This release adds multiple fixes and improvements:

  • it improves our MULTI_THREAD experimental feature allowing to run most of the RxPlayer logic in another thread - which we're now using on the great majority of devices at Canal+.

  • This release also adds support for DASH Content Protection References, which can greatly reduce the size and thus improve parsing time of Multi-Period Manifest with complex DRM configurations.

  • After an issue report, we noticed that our DASH URL resolution algorithm did not handle all cases. We thus rewrote it so it completely respect the corresponding standard (the RFC 3986).

  • For the Edge browser, after having multiple PlayReady-specific issues, we now perform much more checks before validating the fact that PlayReady SL3000/SL2000 is available on the device. This helped to fix multiple Edge issues we were having.

  • Many other smaller fixes, including on the maxVideoBufferSize option which was not always relying on good estimates, on better handling contents with mixed encryption and multiple fixes of minor compatibility issues (mainly on the PlayStation 4 and 5, and on Safari)

📑 Changelog

Features

  • DASH: Implement ContentProtection references [#1439]

Bug fixes

  • DASH: support absolute path in URL resolution with RFC 3986 implementation [#1443, #1440]
  • DASH: fix cases of blinking subtitles [#1416, #1424]
  • Fix precision issues of the maxVideoBufferSize API [#1421]
  • DASH: Prevent multiple loading of the same segment for some DASH low-latency contents [#1422]
  • DRM/Compat: on Edge test comprehensively KeySystems before considering them as usable [#1434]
  • DRM/DASH: Ignore 0x0 key id found in DASH initialization segments are they are often linked to unencrypted data. [#1466, #1458]
  • DRM/Compat: On the PlayStation 5, reload directly when a decryption key become unusable to prevent fatal errors [#1399]
  • MULTI_THREAD: Perform several actions so that our MULTI_THREAD experimental feature now works on older browser and on the Playstation 4 [#1401, #1402]
  • Directfile/Compat: On safari on iOS no longer stay stuck in buffering when autoPlay is set to false or not set and the video element has the attribute "playsinline" [#1408, #1390]
  • Directfile/compat: On safari mobile in directfile mode, do not stay in an infinite LOADING state if the duration is set to NaN (rare issue in a normally-unsupported multiple RxPlayer-per-media-element scenario) [#1393]
  • Fix RxPlay error messages not properly displaying in Chrome's inspector since Chrome 126 [#1474]

Other improvements

  • Signal an error if multiple active RxPlayer are linked to the same media element [#1394]
  • Undetermined audio and text track language now have a normalized property equal to "und" for better ISO 639-3 compatibility [#1428]
  • MULTI_THREAD: The experimental MULTI_THREAD feature does not need a dashWasmUrl anymore nor compatibility to WebAssembly [#1384]
  • MULTI_THREAD: The DEBUG_ELEMENT feature now allows to display all debug information even under the "multithreading" mode [#1438]
  • Generate TypeScript declaration maps [#1412]
  • Do not rely on the performance.now API if not available [#1402]
  • DRM: Refactor MediaKeys attachment logic to simplify device support updates [#1357]
  • tests: use exponential backoff to speed up integration tests [#1389]
  • code: Rely on the TypeScript type keyword at type imports to be sure they have no code impact on our final build [#1365]
  • code: Reorganize core RxPlayer code into a src/main_thread and src/core respectively for main thread and worker code in a "multithread" mode [#1365]
  • code: Rely on the prettier and rustfmt formatting tools in the codebase [#1387]
  • build: remove dependency to webpack [#1435, #1425, #1420]
  • tests: migrate all tests to the vitest framework to simplify and unify test-related dependencies and test writing [#1444, #1445]

DASH ContentProtection References

ContentProtection metadata leading to huge MPD

DASH MPD can get quite huge on complex contents with multiple Period elements and various decryption keys depending on the Period and Representation.

One of the bigger part of the MPD in those scenario is the <ContentProtection> element, which contains metadata related to content decryption. This element can get very big as it contains Base64-encoded binary data for various client-side key systems.

protscreen1
Screenshot: For encrypted contents, we can see on this screenshot that encryption-related metadata (in <ContentProtection> elements) - especially base64-encoded data - are one of the main culprit for an MPD large size.
Moreover, note that this example only advertise metadata for PlayReady and Widevine. So this example is even lighter than most actual encrypted contents we play in production.

Each DASH AdaptationSet or Representation linked to that metadata is then supposed to have this element, contributing to the MPD's size. And when the same encryption metadata repeats, for example in another Period, that same huge element has to be repeated there - leading to a very large MPD.

Thankfully, newer iterations of the DASH specification provide a solution to greatly reduce the size of those kind of MPD: ContentProtection references.

ContentProtection references

With ContentProtection referecences we can only declare once (in the MPD) encryption metadata linked to multiple AdaptationSet or Representation and then refer to it through an identifier each time it is needed.

protscreen2
Screenshot: The same MPD than in the first screenshot, but this time making use of ContentProtection references. Here the actual metadata could be defined on top of the MPD, and only refered to through a ref attribute as pictured. You can see that the exact same information would take less space here.

This allows to greatly reduce the size of multi-Period MPD with encrypted media whose encryption key repeats multiple times in the stream - which happens often.

This feature is directly enabled, with nothing to do on the application-side.

MULTI_THREAD feature improvements

The MULTI_THREAD feature

In the v4.0.0, we added the MULTI_THREAD feature which allows to run most of the RxPlayer's logic in a Worker, and thus in another thread.

Doing this has multiple advantages, performance-related ones, yet this isolation also has a considerable effect on the quality of our adaptive algorithms: on some low-end devices where we before observed either a lower quality or frequent transitions between multiple qualities, we're now able to better maintain a higher quality.

Removing the need to add our WebAssembly parser

The main issue with adding the MULTI_THREAD mode was its complex setup, among which the need to add our WebAssembly MPD parser, which implies WebAssembly support and thus preventing its usage on many "old" (year ~2019 and less) devices.

We've since noticed some work done on efficient JavaScript-only XML parsing whose performance was impressive. As the need for optimal XML parsing in a Worker environment was the main reason why we relied on our WebAssembly parser, we wondered if we couldn't take inspiration from this work to much simplify the MULTI_THREAD setup and increase support.

This is now done and performance has been impressive enough that we now consider that this can be the default MPD parser directly included in the worker file provided to the RxPlayer's attachWorker method.

Without this supplementary step, the feature becomes much simpler to profit from:

import RxPlayer from "rx-player/minimal";
import { MULTI_THREAD } from "rx-player/experimental/features";

// To simplify this example, we'll directly import an "embedded" version of the
// supplementary code loaded by the `MULTI_THREAD` feature.
// We could also load it on demand through an URL
import { EMBEDDED_WORKER } from "rx-player/experimental/features/embeds";

RxPlayer.addFeatures([MULTI_THREAD]);
const player = new RxPlayer(/* your usual op...
Read more

v3.33.3

08 Jul 17:02
Compare
Choose a tag to compare

Release v3.33.3 (2024-07-08)

Quick Links:
📖 API documentation - Demo

🔍 Overview

The v3.33.3 is a "legacy" release (now that the v4 is the current major version) mostly containing some of the bug fixes and improvements initially made for the upcoming v4.1.0 release that should be done just after this one.

📑 Changelog

Bug fixes

  • DASH: support absolute path in URL resolution with RFC 3986 implementation [#1446]
  • DASH: fix cases of blinking subtitles [#1447]
  • Fix precision issues of the maxVideoBufferSize API [#1448]
  • DASH: Prevent multiple loading of the same segment for some DASH low-latency contents [#1449]
  • Await some delay before re-attempting to push a segment following an error [#1411]
  • DRM/Compat: on Edge test comprehensively KeySystems before considering them as usable [#1450]
  • DRM/DASH: Ignore 0x0 key id found in DASH initialization segments are they are often linked to unencrypted data. [#1466, #1458]
  • DRM/Compat: On the PlayStation 5, reload directly when a decryption key become unusable to prevent fatal errors [#1451]
  • Directfile/Compat: On safari on iOS no longer stay stuck in buffering when autoPlay is set to false or not set and the video element has the attribute "playsinline" [#1406, #1404, #1390]
  • Directfile/compat: On safari mobile in directfile mode, do not stay in an infinite LOADING state if the duration is set to NaN (rare issue in a normally-unsupported multiple RxPlayer-per-media-element scenario) [#1452]
  • Fix RxPlay error messages not properly displaying in Chrome's inspector since Chrome 126 [#1474]

Other improvements

  • Signal an error if multiple active RxPlayer are linked to the same media element [#1453]
  • Undetermined audio and text track language now have a normalized property equal to "und" for better ISO 639-3 compatibility [#1454]

v4.0.0

21 Feb 15:47
Compare
Choose a tag to compare

Release v4.0.0 (2024-02-21)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

🔍 Overview

It's finally time for the official v4.0.0 release with the v4 now becoming our default focus and the default major version when installing the RxPlayer through package managers relying on the npm registry (npm / yarn / pnpm etc.).

If you relied on the 4.0.0-rc.2 before, this release is a quasi-exact copy of that version, with only a minor fix for the representationListUpdate event - which was previously never triggered.

Because previous v4 beta and release candidate release notes already listed the new features available in v4 in comparison to v3, this release note will only summarize v4 features we consider to be the most important. They already all have been presented in one of the previous release notes.

To migrate from a v3 RxPlayer to a v4 one, you can rely on our migration guide, which lists every API that changed in that major version.

📑 Changelog

We decided to compile the full v4 changelog into one (instead of splitting and associating it to the various beta and release candidates they have been initially available in).

This lead to an enormous changelog for the v4. To avoid polluting this release note we will just redirect you to our CHANGELOG.md file, here.

About the v3

We will mainly only add bug fixes and small improvements from now on to the now legacy v3.x.x versions so it stays stable and usable for people not having time yet to do the switch to the v4.

As such, it should still work as expected, but new features probably won't be added to it, unless you provide the contribution and test cases for it yourself through pull requests.

However, we do recommend you to switch to v4 instead, and not hesitate to open an issue if you find an API change to be unclear or undesirable for your usage.

A more flexible track API

Note: this feature was already presented in the v4.0.0-beta.0 release note.

One of the focus of this new major release was to improve the RxPlayer API on DASH multi-Period contents - which are contents with various set of AdaptationSets (tracks) and Representations (qualities) depending on the time period.

The RxPlayer's previous track API (e.g. setAudioTrack and getAvailableAudioTracks) only allowed to get the list and update the track for the currently-playing Period.

// Example setting the first english audio track for the current Period if found
const availableAudioTracks = rxPlayer.getAvailableAudioTracks();
const englishAudioTrack = availableAudioTracks.find((track) => {
  return track.language === "eng";
});

if (englishAudioTrack !== undefined) {
  rxPlayer.setAudioTrack(englishAudioTrack.id);
}

Now, using the track API this way still works with the same result, but it is also possible to get and set the available tracks for any Period on the content.

// Get the list of Periods currently considered by the RxPlayer:
const availablePeriods = rxPlayer.getAvailablePeriods();

// Get the list of available audio tracks for a given period
const tracks = rxPlayer.getAvailableAudioTracks(availablePeriods[0].id);

// Set an audio track for that Period
rxPlayer.setAudioTrack({
  trackId: tracks[0].id,
  periodId: availablePeriods[0].id,
});

The new tracks API also let you to choose a behavior when switching from an old to any new track (e.g.: reloading, switching in place with a potential rebuffering or seamlessly) through a new switchingMode property and also allow to rewind a little (and let you set by how much) in cases where you want to give back some context (for example when switching the audio track to another language).

Last but not least, it is also possible to restrict the Representations (a.k.a. qualities) played under that new track, this will be described in the next chapter.

Some of those features have been described in our "Selecting a Track" tutorial. To have complete informations, you can also refer to our API documentation

Improved Representation selection

Note: this feature was already presented in the v4.0.0-beta.0 release note.

Previous RxPlayer versions only allowed to specify allowed Representation(s) (i.e. qualities) by using bitrate-oriented API.
For example you could call setVideoBitrate, setMaxVideoBitrate and setMinVideoBitrate to either choose a Representation (the first one) or to reduce the ranges of Representations to choose from (the latter two).

In real-life, you might instead want to select Representation(s) based on other criterias. In some more complex use cases, you might only want to allow Representations with a specific codec property. Both of those were not always possible with the previous API.

We chose to remediate to those issues in the v4 by providing a new API for Representation selection: the "Representation locking" family of API.
For example, the lockVideoRepresentations method allows to select which Representation for the current video track are allowed to play, the regular RxPlayer's adaptive logic then picking its choice between them, as usual. To lock a single Representation in place, you can just communicate a single Representation's id to that method:

// Example only playing the Representation with the lowest height in the
// current video track

const videoTrack = rxPlayer.getVideoTrack();
if (videoTrack !== null && videoTrack !== undefined) {
  const lowestHeight = videoTrack.representations.sort((a, b) => {
    // Put `undefined` heights at the end of the resulting array
    if (a.height === undefined) {
          return 1; // Put `a` after `b`
    } else if (b.height === undefined) {
      return -1; // Put `b` after `a`
    }
    // Sort ascending
    return a.height - b.height; // Put the higher height after
  })[0]; // Select the lowest one
  if (lowestHeight !== undefined) {
    // Only play the lowest anounced height
    rxPlayer.lockVideoRepresentations([lowestHeight.id]);
  }
}

There is a lot more to know on this API, see the lockVideoRepresentations / lockAudioRepresentations documentation page to see all that is can do.

We rely on this new API to display a better quality selection in our demo page for example:

new-bitrate-choice
Screenshot: our new demo page now allows a user to select a video quality based on its height and/or the wanted bitrate, thanks to this new API.

We also chose to remove the previous bitrate-related API to simplify the general API of the RxPlayer, considering that its behavior can be completely replaced by the new "Representation locking" methods.

Information on how to make the switch is present in its own page in our migration guide

The new MULTI_THREAD experimental feature

Note: this feature was already presented in the v4.0.0-rc.1 release note.

This major release also brings the possibility of running most of the RxPlayer main logic in a WebWorker, letting your application to run concurrently with it. This has potentially large positive impacts on performance and adaptive streaming stability (e.g. keeping a stable high video quality).

This new behavior is totally optional and has to be enabled through specific APIs.
The RxPlayer is also able to automatically detect when multithreading is not possible (very old devices), to go back in the regular monothreading mode instead.

Running the RxPlayer without a WebWorker (the default):

+-------------------------------------------------------------------------------+
| Main thread (also running the UI)                                             |
|                                ...
Read more

v3.33.2

21 Feb 15:33
Compare
Choose a tag to compare

Release v3.33.2 (2024-02-21)

Quick Links:
📖 API documentation - Demo

NOTE: we skipped the v3.33.1 release due to a minor TypeScript typing issue seen when testing that release. This is now fixed.

🔍 Overview

This release only brings minor bug fixes on top of the v3.33.0, none being regressions (those issues have been here since the corresponding features have been introduced).

This reassure us that the v3 is stable enough for it to be reliable for people not having the time yet to make the switch to a v4, as we plan to release the official v4.0.0 just after this release.

The v3 major releases should still be maintained and receive bug fixes for some time if we find them, but our main focus will now go to the v4 releases. Note that all future v3 releases will have the legacy-v3 tag on npm.

📑 Changelog

Bug fixes

  • dash: Don't unnecessarily reload external <UTCTiming> resources at each refresh if it failed for the first request of the Manifest [#1370]
  • dash: The DASH_WASM feature do not rely on WebAssembly's sign-extension operators anymore as that is poorly supported on older Samsung and LG TVs [#1372]

Other improvements

  • build: automatically install Rust and WASM toolchain locally if unavailable when building the RxPlayer WebAssembly file
  • doc: Update our documentation generator and fix all invalid anchors in it
  • npm: prevent the publishing of unnecessary files on the npm registry [#1377, #1378]

v4.0.0-rc.2

07 Feb 17:44
Compare
Choose a tag to compare
v4.0.0-rc.2 Pre-release
Pre-release

Release v4.0.0-rc.2 (2024-02-07)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

Overview

This new release candidate mainly fixes issues we've seen with the WebAssembly parser for DASH MPD, that is used for both the DASH_WASM feature (which lost its "experimental" label at v4.0.0-rc.1) and the MULTI_THREAD experimental feature.

Another minor fix is on the handling of the <UTCTiming> element in a DASH MPD. If it pointed to an URL and if the resource behind it could not be fetched when first loading the MPD due to issues with the request(s) (there may be several attempts), then the RxPlayer would keep re-loading the resource each time the MPD was refreshed, instead of just stopping doing it once it loaded successfully once - which is what the RxPlayer does now.

None of these issues are regressions, and thus some also concern the v3.33.0 (the DASH_WASM and <UTCTiming> ones). As those all are very minor issues (DASH_WASM being still experimental in v3.33.0 - with an error triggering a fallback on the JS-based parser anyway - and the <UTCTiming> issue being rare and not preventing smooth playback) they will only be released as a future v3.33.1 release once the 4.0.0 is released.

Changelog

Features

  • MULTI_THREAD: attachWorker now returns a Promise to indicate when WebWorker attachment failed [#1374]

Bug fixes

  • dash: Don't unnecessarily reload external <UTCTiming> resources at each refresh if it failed for the first request of the Manifest [#1370]
  • dash: The DASH_WASM feature do not rely on WebAssembly's sign-extension operators anymore as that is poorly supported on older Samsung and LG TVs [#1372]
  • MULTI_THREAD: properly categorize forced subtitles in multithread scenarios

Other improvements

  • build: automatically install Rust and WASM toolchain locally if unavailable when building the RxPlayer [#1373]
  • doc: Update our documentation generator and fix all invalid anchors in it
  • npm: prevent the publishing of unnecessary files on the npm registry [#1377, #1378]

DASH_WASM and MULTI_THREAD fix

After testing the MULTI_THREAD feature on a large array of smart TVs, we noticed that some old Samsung and LG TVs had issues instantiating our WebAssembly MPD parser (on which both the MULTI_THREAD and DASH_WASM features rely).
It turned out that those TVs had support for WebAssembly (if they did not, the RxPlayer would automatically have disabled "multithread" mode), yet to a very old version of it which did not have some of its early features present in the WebAssembly file produced by the Rust compiler (the compiler we're using) by default.

This was not known to us as we always assumed that the compiler targeted by default the earliest available version of WebAssembly.

We ended up doing supplementary transformations on our WebAssembly file to remove the reliance on those newer features. The new mpd-parser.wasm file delivered with this version (as well as exported through "rx-player/experimental/features/embeds") should now be compatible with those devices.

attachWorker now returns a Promise

The aforementioned DASH_WASM and MULTI_THREAD issue put a light into some scenarios we were not enough prepared for: how to handle cases where the WebWorker and/or WebAssembly module relied on when using the MULTI_THREAD feature fail to initialize.
This should hopefully be very rare now, yet may still happen if e.g. any of those resources are behind an URL that is not accessible or if browser security settings prevents the RxPlayer from creating or relying on a WebWorker.

To better handle those cases for now, we decided that the attachWorker method now returns a Promise. That promise will either resolve if the Worker was initialized with success and reject if it did not.

Like before, you may still call loadVideo synchronously after the attachWorker call has been made (you don't have to await this Promise) but it is now advised to await that Promise in scenarios where you're both relying on the MULTI_THREAD feature and on one of the corresponding monothreaded feature (either DASH or DASH_WASM) - in which case it is the role of the RxPlayer to choose between one or the other.

For example: the following code:

import RxPlayer from "rx-player/minimal";
import { DASH } from "rx-player/features";
import { MULTI_THREAD } from "rx-player/experimental/features";
import {
    EMBEDDED_WORKER,
    EMBEDDED_DASH_WASM,
} from "rx-player/experimental/features/embeds";

RxPlayer.addFeatures([
  // Will allow to play DASH contents in main thread
  DASH,

  // Will allow to play DASH contents in multithread scenarios
  MULTI_THREAD,
]);

const player = new RxPlayer(/* your usual RxPlayer options */);

try {
  await player.attachWorker({
    workerUrl: EMBEDDED_WORKER,
    dashWasmUrl: EMBEDDED_DASH_WASM,
  })
  console.log("Worker succesfully attached!");
} catch (err) {
  console.warn("An error arised while initializing the Worker", err);
}

player.loadVideo({ /* your usual loadVideo options */ });

Will only load the content in "multithread" mode if all of the following are true:

  1. Your browser supports the feature (which is already checked synchronously when attachWorker is called)
  2. Your content is compatible with multithread mode
  3. The Worker initialization went without issue (this is the case where awaiting attachWorker has an effect)

And in other cases, it will load in monothreaded mode, as the DASH feature is also added.

If you do not await attachWorker before calling loadVideo here and the Worker initialization fails, the RxPlayer might have already begun to load the content in "multithread" mode. In that case it might fail to do so and trigger an error for that content (we're also currently exploring ways of making the RxPlayer automatically reload in monothreaded mode in this exact last scenario but it isn't done for now).

Also note that if Worker initialization fails, the RxPlayer won't try to rely on it anymore for the next loadVideo and reload calls. If you want to retry Worker initialization in that unlikely scenario, you could call attachWorker again.

Project repository updates

As we're now very confident of the stability of the v4 pre-releases, we began making changes to the RxPlayer repository:

  1. The default branch seen on GitHub and choosen as a default target branch for Pull Requests is now dev, which corresponds to the latest merged developments on the v4. Note that it is currently further than the v4.0.0-rc.2 as we're already merging improvements for future v4 releases.

    Another branch, stable corresponds to the last released v4 version (so here v4.0.0-rc.2) plus optional hotfixes. dev is based on stable.

    The old master branch keeps refering to the v3 but has been renamed to legacy-v3. next does not exist anymore, any new developments for the v3 now targets legacy-v3.

  2. The default demo page exposed both by the GitHub link and in our README.md file now leads to the v4.0.0-rc.2 demo.

    The last v3.33.0 demo page can still be accessed here and the list of demo pages per version can be found here.

  3. Likewise, the default documentation pages exposed in our README.md file now leads to the v4.0.0-rc.2 documentation.

    The last v3.33.0 documentation page can still be accessed here and the list of documentation pages per version can be found here.

  4. We filtered files that were actually published on npm when we published a new version. For example, you now shouldn't be pulling the RxPlayer source files anymore when doing so.

v4.0.0-rc.1

24 Jan 16:54
Compare
Choose a tag to compare
v4.0.0-rc.1 Pre-release
Pre-release

Release v4.0.0-rc.1 (2024-01-24)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

🔍 Overview

After more than three years since its early drafts and one year of open beta releases, it is finally time for the first release candidate of the v4.0.0 of the RxPlayer.

The main ideas behind this new v4 major version are:

  • To provide a more flexible and powerful track and quality selection API, especially more adapted to multi-Period DASH contents.

    For example, it is now not only possible to set a particular quality on a content, you can also allow a subset of multiple qualities from which the RxPlayer will adaptively choose from, based on any of their characteristics exposed by the API.

    You can also indicate what to do if the buffer already contains media data in another quality (reload? Keep it in the buffer? Remove it?).

    Likewise, track switching becomes very configurable, allowing you to directly switch a track of any Period, again with a strategy if another track was previously present in the buffer. The RxPlayer also allow to rewind a little (and let you set by how much), in cases where you want to give back some context (for example when switching the audio track to another language).

    The main API documentation about quality selection can be found here.
    For track switching, you can refer to the API documentation of the concerned API, see for example the setAudioTrack API documentation.

  • To allow a more sane default behavior and more resilience in the RxPlayer.

    For example, we are now automatically reloading if something has been detected to have gone horribly wrong, such as when encountering some exceptional device-related decryption issues or when obtaining a completely different Manifest after updating it.

    Moreover, if the chosen track is linked to Representations (qualities) which are all impossible to decrypt, the RxPlayer will now automatically fallback to a supported track instead (after sending events to indicate that it does so).

    We were previously limited on how we could do those things to stay compatible to old APIs.

  • To facilitate the implementation of ambitious improvements.

    For example the v4.0.0-rc.1 brings the new MULTI_THREAD experimental feature, allowing to run the RxPlayer's main logic in a WebWorker for better adaptive streaming stability and performances.
    This is a huge work we're right now using on a multitude of devices at Canal+, intended to improve the quality of experience, especially on devices with limited resources.

Depending on return we have on our large-scale tests (basically, if no important issue is found with it), this release candidate may become an official v4.0.0 relatively fastly (in the coming weeks).

If you're still relying on a v3.x.x release, we recommend looking up our migration guide to make the switch more easily.

📑 Changelog

We decided to compile the full v4.0.0 changelog for this release (instead of comparing it to our latest beta release), which makes it too big for this release note!
So we're only linking you to it here: https://github.com/canalplus/rx-player/blob/bf79b9164f8d530a94e338725f795ec5a5c7b278/CHANGELOG.md

What about the v3?

The v3.33.0 release of the RxPlayer, which was published just before this one, is planned to be our final default release on npm in the v3 major version (unless there is a major issue with it).

Once the official v4.0.0 stable release is published, v4 releases will take its place as a default when installing the package through package managers such as yarn and npm.

Moreover, most incoming features and improvements will be planned for v4.x.x releases first from now on.

v3.x.x releases will still be maintained for some time (we were for example talking about providing at least 1 year of support), but it will mostly be only bug fixes and improvements simple enough to be ported to it. Of course, outside contributions are welcomed if you want to provide improvements on it yourself.

If you want to make the switch but you're afraid of relying on a release candidate right now, we recommend maintaining a branch which will depend on this
v4.0.0-rc.1 release in your project and putting it in production once the v4.0.0 is actually released.

v4 summary

The v4.0.0-rc.1 inherits from both the v4.0.0-beta.3 (and consequently any prior v4 release) and from the just-released v3.33.0 (and consequently all previous v3 releases).

The rest of this release note is going to focus ONLY on features brought in this particular release (which means we're not relisting features already announced in previous v4 and v3 release notes).

You can look at the v4 beta release notes for more information on some earlier v4 features:

And on the the v3.33.0 release note for the just-released features which are also in this one.

The new MULTI_THREAD experimental feature

This release brings the possibility of running most of the RxPlayer main logic in a WebWorker, letting your application to run concurrently with it. This has potentially large positive impacts on performance and adaptive streaming stability (e.g. keeping a stable high video quality).

This new behavior is totally optional and has to be enabled through specific APIs.
The RxPlayer is also able to automatically detect when multithreading is not possible (very old devices), to go back in the regular monothreading mode instead.

Running the RxPlayer without a WebWorker (the default):

+-------------------------------------------------------------------------------+
| Main thread (also running the UI)                                             |
|                                                                               |
| +------------------+     +----------------------+    +----------------------+ |
| |    Application   | --> |  RxPlayer Main [1]   | -> |   RxPlayer Core [2]  | |
| +------------------+     +----------------------+    +----------------------+ |
+-------------------------------------------------------------------------------+

Running with a WebWorker:

+----------------------------------------------------+
| Main thread (also running the UI)                  |
|                                                    |
| +------------------+      +----------------------+ |
| |    Application   | ---> |  RxPlayer Main [1]   | |
| +------------------+      +----------------------+ |
+--------------------------------------|-------------+
                                       | (messages)
+--------------------------------------|-------------+
| WebWorker                            V             |
|                           +----------------------+ |
|                           |  RxPlayer Core [2]   | |
|                           +----------------------+ |
+--------------...
Read more

v3.33.0

24 Jan 16:39
Compare
Choose a tag to compare

Release v3.33.0 (2024-01-24)

🔍 Overview

The v3.33.0 is now available on npm.

It is planned to be the last main v3 release now that the official v4.0.0 is around the corner and became our main focus (a v4.0.0-rc1 will be published just after this one), yet is still contains its share of improvements and bug fixes:

  • DASH: To better support retro-compatible Dolby Vision content, the scte214:supplementalCodecs is now handled. Concerned contents will be advertised under a Dolby Vision codec for compatible devices, or the retro-compatible codec for those who aren't
  • A new getLivePosition method and fromLivePosition startAt option have been added, mainly for advanced ad-switching usages.
  • subtitles: the tts:lineHeight TTML attribute was not properly applied, it is now fixed
  • subtitles: A rare occurence of blinking subtitles could arise, especially on low-latency contents. This also has been fixed.
  • Add the possibility of setting another keySystems option on the reload API.
  • Several fixes on issues that could arise when doing API calls after reaching the last playable position of a content.
  • DEBUG_ELEMENT: Undecipherable and unsupported codecs are now announced in the debug element
  • Multiple other minor fixes and improvements linked to ad-switching, checking of codec support, and low-memory handling.

📑 Changelog

Features

  • Add getLivePosition RxPlayer method [#1300]
  • Add startAt.fromLivePosition loadVideo option [#1300]
  • Add the possibility to set a new keySystems option on the reload API [#1308]

Bug fixes

  • Fix subtitles "blinking" in some specific conditions, especially with some DASH low-latency contents [#1314]
  • DASH: Fix Period overlap resolution logic for when the first Period is removed [#1311]
  • TTML: Fix handling of the tts:lineHeight attribute [#1320]
  • Fix import of the LOCAL_MANIFEST experimental feature
  • Avoid very rarely skipping segments which initially were too big to be pushed due to memory limitations [#1323]
  • Fix issue arising when using track APIs at the exact last possible position of a Period with no consecutive Period [#1337]
  • Starting at the end (through a startAt loadVideo option) or reloading at the end led to the restart of the content [#1338]
  • DRM/Safari: also perform Safari DRM work-arounds when the page is launched from the dock [#1351, #1356]

Other improvements

  • DASH: rely on SCTE214 supplementalCodecs instead of codecs if it's supported to better support backward compatible Dolby Vision contents [#1307]
  • DASH: Provide better support of the availabilityTimeOffset attribute [#1300]
  • DEBUG_ELEMENT: Add unsupported and undecipherable bitrates to the debug element [#1321]
  • DEBUG_ELEMENT: update buffer graph maximum size so it becomes more readable for lengthy contents [#1316]
  • DEBUG_ELEMENT: always synchronize inventory of segments before rendering it [#1317]
  • Remove remaining RxPlayer dependency removing possibility of some application-side bundling errors [#1312]
  • Add exception to text Garbage collection logic to avoid unnecessarily reload text segments frequently [#1325]
  • Avoid logging too much the buffer's content when our debugging UI or the demo is used [#1341]
  • Demo: Fix reporting of live position in demo page [#1313]

About it being the last "main v3 release"

As we've reached a point where we plan to release the first official v4.0.0 release, we also plan to make v4 releases our main focus when new features and improvements are added. As such, a v4.0.0-rc1 release should be released just after this release. If our large-scale tests go well, it will become the first v4.0.0 release.

This do not mean that the v3 will become unsupported. It should still receive bug fixes yet:

  1. It will not be the version installed by default through npm / yarn, which will be the v4 instead once the official v4.0.0 version is released.
    The idea will be to set the npm legacy-v3 tag for newer v3 releases, so an rx-player v3 may be installed through something like: npm install rx-player@legacy-v3.

  2. When planning new improvements, we now will implement it on the v4 first, and only backport it to the v3 if it is simple enough with few risks to break.

  3. As more and more applications will rely on the v4 (we already "convinced" many at Canal+ to do so), we will progressively become more aware of issues when they concern the v4 and less about v3 issues.

DASH supplementalCodecs handling

We recently had some issues trying to play Dolby Vision content on LG TVs where the device would not play at all, due to a discrepancy between the video codec announced by the RxPlayer (which did not make mention of Dolby Vision), and the Dolby Vision data actually pushed to the video buffer.

This was because the media stream we've tested make usage of a smart trick: as the Dolby Vision video data was backward-compatible to regular HDR for devices not supporting Dolby Vision, two codecs were actually announced in the content's MPD (the DASH's Manifest file, which list available tracks and qualities):

  • The much more compatible HDR codec, as a codecs attribute (e.g. in a <Representation> or <AdaptationSet> element in the MPD) like expected
  • The Dolby Vision codec, as a scte214:supplementalCodecs property, that we did not process until now

repsupcodec
Screenshot: Content of a DASH MPD with both a codecs property and a supplementalCodecs property in the <Representation> element

The RxPlayer previously only relied on the codecs property (so here not the Dolby Vision video codec) when interacting with lower-level media API.
Though the HDR codec was compatible to the video data, it was actually expected by some Dolby Vision-capable device that the Dolby Vision codec should have been relied on when creating video buffers.

However, if we did rely on the Dolby Vision codec instead through those API, devices which do not handle Dolby Vision video content would not have been able to play the content as just "regular" HDR video data, for them it would have been Dolby Vision, which they do not know how to decode.

So we chose to put a little more intelligence in the RxPlayer, when there is a scte214:supplementalCodecs property (let's call the corresponding codec property the "supplemental codec"):

  • If the supplemental codec is supported by the current device (we can know that through the MediaSource.isTypeSupported API, we consider it to be the actual "codec" of the corresponding video data.
  • If it is not supported, we rely on the codecs instead, just like we did before

suppcodec
Flowchart: How the RxPlayer will decide whether to rely on a scte214:supplementalCodecs or a codecs attribute in a DASH MPD.

This also means that API communicating about video codecs, like the getVideoTrack method, may now actually indicate the "supplemental codec" through their codec property, where it always corresponded in DASH to the corresponding codecs attribute instead).
This should in most case not lead to any change in your application as this is mainly a lower-level detail about a Representation (a.k.a. media quality).

getLivePosition method and startAt.fromLivePosition option

Some applications at Canal+ relying on the RxPlayer make use of "ad-switching" technologies.
Basically, the idea is to replace ads present in a source stream by targeted ads.

There are multiple techniques to implement this, yet for DASH they generally rely on its concept of Periods.
In the MPD (DASH's Manifest file), each of those ads would be defined in a different <Period> element, which is a time-delimited division of the content with its own media tracks and qualities.

multiperiods
Schema: Exemple of a schema representing a DASH multi-Period live content, with an ad break where each Period corresponds to a single Ad.

Another effect of ad-switching is that the server may know in advance which ad should be served to a user.

Let's for example consider a user which plays a live content. That user now encounters an ad-switched ad break at 11:04 AM, which will end 5 minutes later, at 11:09 AM.
Though it is still 11:04, the server already knows which ads it's going to serve to that user from 11:04 to 11:09 (those ads are targeted and already scheduled), so it may want to already announce them in the MPD even though most of those ads are m...

Read more

v4.0.0-beta.3

19 Oct 18:15
c9dd85f
Compare
Choose a tag to compare
v4.0.0-beta.3 Pre-release
Pre-release

Release v4.0.0-beta.3 (2023-10-19)

Quick Links:
📖 API documentation - Demo - 🎓 Migration guide from v3

🔍 Overview

The v4.0.0-beta.3 release is now here. As usual, it is based on the last official v3 release (here the just released v3.32.1) as well as previous v4 beta releases.

The v4.0.0-beta.3 release should be the last v4 "beta" release, meaning that the next v4-linked release will probably be our first release candidate for a first official v4.0.0 release.

We already tested the code behind this beta.3 release extensively in production conditions on most environments we target at Canal+ (which helped us detect and fix an adaptive-linked issue only seen on older Edge browser versions), so we're becoming confident that this version will work on every supported devices and for a large panel of usages.

As we wanted to ensure that the v4 API is final (or very close to final), we reviewed every RxPlayer v4 API on current usages as well as future planned usages to ensure that API stability is easy to guarantee. This led to some changes, described in this release note.

📑 Changelog

Changes

  • The MediaError's trackInfo property is now an array renamed as tracksInfo and similar MediaError are grouped in one [#1264]
  • The manifestUpdateUrl loadVideo option has been removed as it was unused [#1276]
  • The /dist directory in the project has been removed [#1270]

Bug fixes

  • Fix adaptive logic on some legacy Edge browsers [#1302]

Other improvements

  • newAvailablePeriods is now sent lazily at the time new Periods are considered to improve performance [#1265]
  • Implement better error messages by not repeating the Error Type in it [#1290]
  • All import path to the RxPlayer now depend on the same RxPlayer modular build (and not just the minimal, as before) [#1301]

newAvailablePeriods event now sent lazily

The newAvailablePeriods event is a new RxPlayer event central to the v4 API indicating that new "Periods" from the current content begin to be considered by the RxPlayer.
As each Period brings with it its own set of audio/text/video tracks and qualities, it is the main event to listen to when you want to choose an initial track or quality.

hierarmpd
Screenshot: screenshot describing the Period, AdaptationSet and Representation elements of an MPD, which are advertised in the v4 API beginning with a newAvailablePeriods event.

In previous beta and alpha releases, the RxPlayer sent this event just after parsing the content's Manifest (and after refreshing it), by communicating directly about all Periods seen in that Manifest.

We became afraid that on very large Manifest with a large amount of Periods and track choices, the current design for this event could lead to performance issues: as soon as the Manifest was parsed (and before the content started to play), the RxPlayer would send a newAvailablePeriods event - perhaps for the hundreds of Periods seen in that Manifest.
The application (the software using the RxPlayer library) would then iterate through all of those Periods, as well as its inner tracks and qualities, to make its initial choice - and all that before the content is finally able to play.

supergraph
Graph: Timeline of situations leading to the newAvailablePeriods event and corresponding player actions. It then continues as the setting of the audio, video and text tracks by the application has to be done for all Periods advertised through the newAvailablePeriods event.

This seems risky performance-wise as well as unnecessary. When the content starts to play, the only track and quality choices we probably want to set are just the one linked to the Period we're initially playing. For live contents for example, we don't need to set the audio track for the program that was aired 15 minutes ago.
The only time where we might want to set it, is if the user ever seeks back to that program (a situation which might never occur).

The RxPlayer now "lazily" sends newAvailablePeriods events, meaning that it will only communicate about "Period(s)" that are either playing or will be played soon by the RxPlayer. For example when playing a live content, it will probably only send this event for the currently-playing program, not the one playing before or after it.

supergraph2
Graph: Updated timeline now that newAvailablePeriods only transports information on the Period(s) that matter for now. Here we can see that the application for example only set the tracks for a single Period.

If the user seeks to another Period or if playback position reaches a new Period, a new newAvailablePeriods will then be sent for it.

Normally, this doesn't break the previous v4 API so you shouldn't need to change anything if you relied on a previous v4 version. Still, as it is a considerable change, you could need to check if this doesn't break assumptions you previously had on your side.

Changes concerning Errors

Error message changes

Historically, the message property of RxPlayer errors had the peculiar following format:

<Error Type> (<Error Code>) <Error Message>

For example, for an EncryptedMediaError with an INCOMPATIBLE_KEYSYSTEMS code, we could have:

EncryptedMediaError (INCOMPATIBLE_KEYSYSTEMS) Some description message

Packing so much information into the message was unconventional, repeated information already available elsewhere and led to an ugly reporting in debuggging tools.
For example, error logged in most inspectors and debuggers are already prepended by the Error Type, leading in the previous example to the following log:

EncryptedMediaError: EncryptedMediaError (INCOMPATIBLE_KEYSYSTEMS) Some description message

Although error messages are not part of our API (so we could have changed its format at any time) we were still reluctant to do so as applications might have relied on that format to extract information programatically. Now that the official v4 is around the corner, we finally updated its format to a more legible:

<Error Code>: <Error Message>

As such the previous error message would be:

INCOMPATIBLE_KEYSYSTEMS: Some description message

And it would be logged by inspectors as:

EncryptedMediaError: INCOMPATIBLE_KEYSYSTEMS: Some description message

trackInfo renamed as tracksInfo

We made another small change to RxPlayer errors, this time on the recently-added trackInfo property that could be set on some MediaError, to add precizion on the particular track concerned.

We noticed that many of them were sent in bulk, for example when the Manifest is found to contain multiple tracks in unsupported codecs, we would send a warning event dispatching a MediaError with an MANIFEST_INCOMPATIBLE_CODECS_ERROR for each of the unsupported track (and in multi-Period contents, there could be hundreds of them).

Now we decided to group such errors together when they are happening at the same time, and to allow the setting of multiple tracks' information in a tracksInfo array property (with an s, instead of the previous trackInfo). The MediaError documentation has been updated.

Changes concerning the builds

In previous RxPlayer versions, we exposed several builds of the RxPlayer in the npm repository:

  1. The "legacy" build, which is a bundled single JS file, which was used when importing the RxPlayer "normally" through an import RxPlayer from "rx-player" line in ES6-style (or const RxPlayer = require("rx-player") ni CommonJS style).

    We relied on the Webpack bundler to produce that build.

  2. The "modular" build, targeted by most other imports (such as the minimal build: import RxPlayer from "rx-player/minimal") which uses multiple files, allowing for "tree-shaking" on the application-side.

    Here we mostly rely on TypeScript and on our own scripts.

Having this double way of exporting the RxPlayer led to some complexity on our side and we weren't too comfortable of having too much complexity in such an important area for a library.

We'r...

Read more