Tutorial: Playing contents with DRMs
Because different applications and different devices can work completely differently when it comes to DRM, and because it is a complex feature, we have a large API allowing to manage it.
This tutorial page is specifically there to help you navigate through this API.
We will begin from the simplest of use cases to dive into the more complex ones.
We recommend you to read the quick start tutorial first if you haven’t, to have a general grasp on how to basically run a content.
Playing a simple encrypted content
To be able to play a simple encrypted content, we will need at least two parameters:
type
: the name of the “key system” you want to use.getLicense
: the license-fetching logic
This chapter will explain both and provide examples on how to load a video with both of these properties.
The key system
The key system, also known as “DRM name”, will designate which Content Decryption Module (or CDM) to use. You might have heard of “Widevine”, “PlayReady” or “FairPlay”, that’s the name what we want to know: which system you want to use.
Which of them you want to use depend on several factors, among which:
- what the content allows
- what the content right holder wants
- what you/your company wants
- what the browser can do
In the RxPlayer’s API, we more especially expect the whole “reverse domain name”
for that key systems (e.g. com.widevine.alpha
or com.microsoft.playready
).
We also have shortcuts for Widevine or PlayReady, where you can just tell us
respectively widevine
or playready
as the key system and we will try
several corresponding reverse domain names.
In any case, you can ask for several key systems, even including ones that are not available in the current browser. Those will be detected and automatically filtered out.
rxPlayer.loadVideo({
// ...
keySystems: [
{
type: "com.widevine.alpha",
// ...
},
{
type: "com.microsoft.playready",
// ...
},
],
});
The license-fetching logic
The second needed argument is a callback allowing to fetch the content license.
An encrypted content will need one or several keys to be able to decrypt a content. Those keys are contained in one or several license files.
Those files usually need to be downloaded from a license server.
As that logic sometimes depends on your application (i.e. you might want to add authentification to that request to know which user made that request), the RxPlayer team made the choice to let you write your logic entirely.
This logic takes the form of a callback named getLicense
.
This function is in fact triggered everytime a message is sent by the Content Decryption Module (what is sometimes known as “Widevine” or “PlayReady”), which is usually a request to fetch or renew the license.
It gets two arguments when called:
- message (
Uint8Array
): The “message” - messageType (
string
): String describing the type of message received. There is only 4 possible message types, all defined in the w3c specification.
In most cases, this function is triggered for license requests.
You’re encouraged to read what the messageType
can be, but don’t be scared by
it, you’ll most likely never need to check it.
What you will most likely need to do, is simply sending the first argument,
message
, to the license server to fetch the license. That message generally
contains information about the license you want to fetch.
You will then need to return a Promise, which resolves with the license in an
ArrayBuffer or Uint8Array form.
If you don’t want to communicate a license based on this message, you can just
return null
or a Promise resolving with null
.
Here is an example of a valid and simple getLicense implementation:
function getLicense(challenge) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", LICENSE_SERVER_URL, true);
xhr.onerror = (err) => {
reject(err);
};
xhr.onload = (evt) => {
if (xhr.status >= 200 && xhr.status < 300) {
const license = evt.target.response;
resolve(license);
} else {
const error = new Error(
"getLicense's request finished with a " + `${xhr.status} HTTP error`
);
reject(error);
}
};
xhr.responseType = "arraybuffer";
xhr.send(challenge);
});
}
Example with both properties
Now that all that has been explained here’s an example to play a simple encrypted DASH content with either PlayReady or Widevine.
// We will use the same logic for both PlayReady and Widevine
function getLicense(challenge) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", LICENSE_SERVER_URL, true);
xhr.onerror = (err) => {
reject(err);
};
xhr.onload = (evt) => {
if (xhr.status >= 200 && xhr.status < 300) {
const license = evt.target.response;
resolve(license);
} else {
const error = new Error(
"getLicense's request finished with a " + `${xhr.status} HTTP error`
);
reject(error);
}
};
xhr.responseType = "arraybuffer";
xhr.send(challenge);
});
}
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
},
{
type: "playready",
getLicense,
},
],
});
This code is sufficient for a majority of encrypted contents.
More control over the license-fetching logic
There’s a lot of things that can go wrong during the license request:
- The user could be temporarly disconnected
- The license server might be down
- The license server might refuse to deliver a license based on your rights
- The license server might refuse to deliver a license based on your CDM capabilities
- And like any request a lot of other errors can happen
From this, you could want to have a different behavior based on what happened:
- When a user is temporarly disconnected, you could chose to retry indefinitely (the RxPlayer retry after a delay to not overload the client or the server).
- When the license server is down, you might want to fail directly.
- When the license server refuse to deliver a license based on your rights, you might want to throw an explicit error message that you will be able to display.
- If there’s a problem with your CDM capabilities, you might want to just fallback to another media quality with a different license.
All of this is possible with more advanced APIs that we will see in this chapter.
getLicenseConfig
getLicenseConfig
is an object allowing to configure two parameters:
-
retry
, which will set the maximum number of retry. When setting1
, for example, we will try two times the request: A first original time and one retry.You can decide to by default retry indefinitely by setting it to
Infinity
(yes, that’s a valid number in JS and some other languages). Don’t worry, you will still be able to retry less time on some other events (explained in thegetLicense error configuration
chapter). -
timeout
, which is the maximum time in milliseconds the RxPlayer will wait until it considers agetLicense
call to have failed. By default it is set to10000
(10 seconds). You can set it to-1
to disable any timeout.
For example, for infinite retry and no timeout, you can set:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
getLicenseConfig: {
retry: Infinity,
timeout: -1,
},
},
// ...
],
});
getLicense error configuration
getLicenseConfig
handle general configurations about every getLicense
calls,
but you can also have more specific configuration when a specific license
request fails.
This is done thanks to the rejected Promise returned by getLicense
.
You can reject an error (or just an object), with the following properties:
-
noRetry
: when set totrue
, thegetLicense
call will not be retried. -
message
: a custom message string we will communicate through a warning or error event (depending if we will retry or not the call) -
fallbackOnLastTry
: When set totrue
and if we are doing or last try or retry (to be sure you can setnoRetry
to true), we will try to fallback to another quality, which might have a different license.This is only useful for contents which have a different license depending on the quality (for example having different rights for 4k video content than for 480p video content). It is also only useful if the license server can refuse to deliver a license for a particular quality but accept for another quality.
Here is an example showcasing all of those properties:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense(challenge) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open("POST", LICENSE_SERVER_URL, true);
xhr.onerror = (err) => {
// Keep retrying on XHR errors.
// Instanciating an Error like that automatically set the
// message attribute to this Error's message. That way, the
// linked "error" or "warning" event sent by the RxPlayer
// will have the same message.
const error = new Error("Request error: " + err.toString());
reject(err);
};
xhr.onload = (evt) => {
if (xhr.status >= 200 && xhr.status < 300) {
const license = evt.target.response;
resolve(license);
} else if (xhr.status >= 500 && xhr.status < 600) {
// Directly fails + fallbacks on a server error
const error = new Error(
"The license server had a problem and" +
` responded with ${xhr.status} HTTP ` +
"error. We will now fallback to another" +
"quality."
);
error.noRetry = true;
error.fallbackOnLastTry = true;
reject(error);
} else {
// else continue to retry
const error = new Error(
"getLicense's request finished with a " +
`${xhr.status} HTTP error`
);
reject(error);
}
};
xhr.responseType = "arraybuffer";
xhr.send(challenge);
});
},
getLicenseConfig: {
retry: Infinity,
timeout: -1,
},
},
// ...
],
});
Contents with multiple keys
Why and how
One of the issues arising with DRM is that not all devices, Operating systems or web browsers can provide a high level of guarantee that a content will be protected against unauthorized usages (such as illegal copy). In other words, some devices, OS or browsers might provide more guarantees than other.
That’s the main reason why there’s sometime a compromise to have between accessibility of a content (the number of the people able to view it) and this guarantee.
To be able to provide the best of both worlds, a content right holder might ask for a higher protection guarantee for higher video qualities.
For example, it might ask that a 4k video content of a given film should be much harder to “pirate” than the 240p version of the same film. In return, the 240p version can be watched by a lot more people on a lot of different devices.
To achieve that, one of the solution on the content-side is to have different decryption keys depending on the quality chosen. There’s then two main strategies:
-
Every keys are in the same license. A player will thus do only one license request for the whole content and the keys inside will be individually refused or accepted.
-
There is one or several keys per licenses, in several licenses. That way, a player might ask a different license when switching the current quality.
There’s pros and cons to both, but let’s not go too far into that!
Let’s start from the principle that our content is under one of those two cases here and let’s find out what we have to do to handle it.
The strategy adopted by the RxPlayer
When playing a content, the RxPlayer by default stops and throws an error as
soon as either a key is refused or as the license fetching logic (the
getLicense
function) fails (after enough retries).
When playing a content with multiple keys, you might instead not care that much if a key is refused or if the license-fetching logic fails. What you can just do is to remove the quality for which we could not obtain a key and to instead fallback on another, decipherable, quality.
That’s exactly what the RxPlayer does, when the right options are set:
-
when it detects a quality to be un-decipherable, it first emit a
decipherabilityUpdate
event through its API, to signal to an application which qualities have been blacklisted. -
it automatically removes from the current media buffer the data linked to the un-decipherable quality and put it in a black list: we will not load this quality for the current content anymore.
-
it switches to another, hopefully decipherable, quality.
Let’s now talk about the API.
fallbackOnLastTry
This option was already explained in a previous chapter. Basically, it is a
boolean you can set to true
when rejecting an error from the getLicense
callback. When set and if it was the last getLicense
try or retry, the
RxPlayer will stop to play every quality sharing the same “protection
initialization data”.
What that last part really means is a little complex, but it generally means
that every qualities sharing the same license file will be considered as
un-decipherable.
For more information and an example on how to use it, you can go back on the concerned previous part of this tutorial.
Please note that this option does not concern every errors linked to a refused
key. It only concerns issues when the license server refuse to deliver a
license.
On most cases you will also need the API documented in the next part,
fallbackOn
.
fallbackOn
Where fallbackOnLastTry
really is about the failure of a license request, the
fallbackOn
is about the refused keys themselves.
As an example, the Content Decryption Module in the browser might decide that your current device cannot provide a high enough guarantee that the content cannot be copied. It might thus refuse to use one of the decryption key found in a license, especially the one needed for the higher content qualities.
The fallbackOn
object allows to fallback when this happens.
There is two possible sub-properties in it:
keyInternalError
: fallback when the corresponding key has the status"internal-error"
. We found that most widevine implementation use this error when a key is refused.keyOutputRestricted
: fallback when the corresponding key has the status"output-restricted"
. This is the proper status for a key refused due to output restrictions.
Both are booleans, and for the moment we recommend to set both to true in most cases.
For people on embedded devices with specific key systems, you can look a little more into what MediaKeyStatus is set when a key is refused, and just set one of both.
Here is an example:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
fallbackOn: {
keyInternalError: true,
keyOutputRestricted: true,
},
},
],
});
decipherabilityUpdate event
When the RxPlayer detects a quality to be un-decipherable (which can only
happens when one of the properties explained here is set), it sends a
decipherabilityUpdate
event.
This event allows an application to know that some key or license could not be used by the RxPlayer.
The application could then infer that other contents from the same right holders
will have the same issues.
In that case, an optimization is possible by using the representationFilter
API which is part of the transportOptions
loadVideo
option, documented
here. By using this API, we
can filter out un-decipherable quality to avoid downloading them in the first
place.
Server certificate
The “server Certificate” is a certificate allowing to encrypt messages coming from the Content Decryption module to the license server. They can be required by some key system as a supplementary security mechanism.
Thankfully, an application is not obligated to set one, even if one is needed.
If not set, the Content Decryption Module will download it itself by using the
same route than a license request (the getLicense
callback will be called).
This means however, that we have to perform two round-trips to the license server instead of one:
- one to fetch the “server certificate”.
- the other to fetch the license.
To avoid the first round-trip, it is possible for an application to directly
indicate what the serverCertificate
is when calling loadVideo.
This is done through the serverCertificate
property, in keySystems
:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
serverCertificate,
},
],
});
The serverCertificate
has to either be in an ArrayBuffer
form or a
TypedArray
(i.e. Uint8Array
, Uint16Array
etc.)
Persistent licenses
A persistent license allows to store a license for it to be available even when a user quits the current page or restarts its computer. It can be used even if the user is offline.
After loading a persistent license, it is automatically stored on the browser’s
side, but the RxPlayer still has to store an ID to be able to retrieve the right
session when reloading the same content later.
Because of that, persistent-license management comes in two part in the RxPlayer
API (as usual here, those should be set in keySystems
):
-
You’ll have to set the
persistentLicense
boolean totrue
-
You’ll have to provide a license storage mechanism and set it as the
licenseStorage
property.
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
persistentLicense: true,
licenseStorage,
},
],
});
licenseStorage property
The licenseStorage
property is an object allowing the RxPlayer to load and
saved stored IDs.
It needs to contain two functions:
save
: Which sould store the argument given. The argument will be an array of Objects.load
: Called without any argument, it has to return what was given to the lastsave
call. Any return value which is not an Array will be ignored (example: whensave
has never been called).
This API can very simply be implemented with the localStorage browser API:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
persistentLicense: true,
licenseStorage: {
save(data) {
localStorage.setItem("RxPlayer-licenseStorage", JSON.stringify(data));
},
load() {
const item = localStorage.getItem("RxPlayer-licenseStorage");
return item === null ? [] : JSON.parse(item);
},
},
},
],
});
Do not be scared about security implications, the data saved is not secret and does not help to identify a user.
You can also use every storage API at your disposition (some embedded devices might have their own).
As a nice bonus, you can note that the data given is perfectly “serializable”
through the JSON.stringify browser API. This means that, as the example shown
above, you can call JSON.stringify
on that data and retrieve it through a
JSON.parse
call.
This is very useful for storage APIs which cannot store JavaScript objects.
Playback issues related to DRMs
The configuration example which finishes the last chapter should work in most cases, but you might encounter very specific issues on some devices.
Issues with fallbacking with the Edge browser and PlayReady
We sometimes encountered a bug which makes the player loads indefinitely when
fallbacking from an undecipherable quality, if done through the
fallbackOnLastTry
option. This was only constated on the Edge browser and
appears to be a browser or CDM bug.
Sadly, no work-around has been found for now for this issue. We’re currently
trying to create a reproducible scenario and document that issue so it can
hopefully be fixed in the future. In the meantime, you’re encouraged either to
use Widevine (only on Chromium-based Edge) or to not make use of the
fallBackOnLastTry
option on that browser.
The Player do not download any segment when playing encrypted contents
This is probably due to an issue we encountered several time on embedded devices.
Basically, this behavior is due to a deadlock, where the RxPlayer is waiting for the CDM logic to be initialized to download segments but the CDM logic wait for the opposite: it will only initialize itself once segments have been downloaded.
The RxPlayer is waiting for the CDM initialization for a very specific usage: playing a mix of unencrypted and encrypted data. We detected that on some Chrome versions we could not play encrypted data if we first played unencrypted data without the CDM logic in place.
Fortunately, this usage is for very specific cases and you most likely won’t need it (or even if you will, you most likely will not encounter that problem).
You can completely remove that deadlock with a property called
disableMediaKeysAttachmentLock
. Like other properties introduced here, you
should put it in the keySystems
object of loadVideo
, like such:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
disableMediaKeysAttachmentLock: true,
},
{
type: "playready",
getLicense,
disableMediaKeysAttachmentLock: true,
},
],
});
After two or several loadVideo calls the RxPlayer refuses to play
There’s a chance that you’re encountering another issue we found on embedded devices.
By default, the RxPlayer maintains a cache containing the last loaded licenses. This allows to quickly switch to already-played contents, an important improvement when playing live contents for example. Rest assured, our cache size is not infinite, and as such it should work on most devices.
However, we found that on some devices, this logic can be problematic, and it will just refuse to add a license at a given point.
You can add a property which will flush that cache anytime the content changes,
called closeSessionsOnStop
.
Like other properties introduced here, you should put it in the keySystems
object of loadVideo
, like such:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
closeSessionsOnStop: true,
},
{
type: "playready",
getLicense,
closeSessionsOnStop: true,
},
],
});