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:
-
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.
onKeyInternalError
, onKeyOutputRestricted
and onKeyExpiration
Where fallbackOnLastTry
really is about the failure of a license request, those
options 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.
Those options then allows to fallback when this happens.
-
onKeyInternalError
: Behavior to set when the corresponding key has the status"internal-error"
. We found that most widevine implementation use this error when a key is refused.You can set it to
"fallback"
so the RxPlayer switch to other, decipherable, Representations when this status is received. -
onKeyOutputRestricted
: Behavior to set when the corresponding key has the status"output-restricted"
. This is the proper status for a key refused due to output restrictions.You can set it to
"fallback"
so the RxPlayer switch to other, decipherable, Representations when this status is received. -
onKeyOutputRestricted
: Behavior to set when the corresponding key has the status"expired"
.You can set it to
"fallback"
so the RxPlayer switch to other, decipherable, Representations when this status is received.
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.
Here is an example which would be adapted to most cases:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
onKeyOutputRestricted: "fallback",
onKeyInternalError: "fallback",
},
],
});
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, you'll have to provide a storage mechanism and set it as a
persistentLicenseConfig
property of loadVideo
's keySystems
option:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
persistentLicenseConfig,
},
],
});
persistentLicenseConfig property
The persistentLicenseConfig
property is an object allowing the RxPlayer to
load and saved stored session identifiers, to be able to retrieve them later.
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).
If the ability to retrieve persistent-licenses from older RxPlayer version is
not important to you, you can also add a disableRetroCompatibility
property
set to true
, this will unlock supplementary optimizations on contents.
This API can very simply be implemented with the localStorage browser API:
rxPlayer.loadVideo({
url: MANIFEST_URL,
transport: "dash",
keySystems: [
{
type: "widevine",
getLicense,
persistentLicenseConfig: {
save(data) {
localStorage.setItem("RxPlayer-persistent-storage", JSON.stringify(data));
},
load() {
const item = localStorage.getItem("RxPlayer-persistent-storage");
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.