Playing protected content with Nagra DRM - Web
Overview
Digital Rights Management (DRM) solutions often require specific data exchanges with a license server, usually in proprietary formats. Rather than integrating multiple license providers directly into the core of our player, we leverage flexible configuration options through the player configuration. This guide explains how to integrate the Bitmovin Web Player SDK with Nagra Secure Services Platform (SSP) for DRM license acquisition and secure session management.
Use Cases
This guide provides the necessary Bitmovin player and source configurations (including DRM settings) to support the following use cases:
- Playback of DRM-Protected content using Nagra’s secure session management.
- Continuous Playback and Secure Session Management including DRM license renewal.
- Secure Session Termination after playback is complete or when the session is no longer needed.
Pre-requisites
Before you begin, please ensure you have the following information and resources required for playing back Nagra-protected DRM streams:
- DASH/HLS_MANIFEST_URL : URL of the DASH MPD of HLS multivariant URL for the content to be played.
- NAGRA_WIDEVINE_LICENSE_SERVER_URL : Nagra Widevine license server endpoint for initial license acquisition.
- NAGRA_LICENSE_RENEWAL_SERVER_URL : Nagra SSP endpoint for license renewals during secure playback session.
- NAGRA_CONTENT_TOKEN_URL : Nagra endpoint for fetching content token.
- NAGRA_SSM_SETUP_URL : Nagra SSP endpoint for setting a secure session.
- NAGRA_SSM_TEARDOWN_URL : Nagra SSP endpoint for secure session teardown.
- NAGRA_TENANT_ID : Nagra tenant Id.
- NAGRA_CONTENT_ID : Nagra content Id specific for the content to be played.
- BITMOVIN_PLAYER_KEY : You can get in touch with your Nagra or Bitmovin contact for this. You can also sign up for a trial account.
Configuration
1. Application preparation for DRM content playback and Secure Session Management
Note:
The steps below describe interactions between your application and Nagra’s backend (SSP or OpenTV).
These are outside the scope of Bitmovin’s integration but are included here because they are required to obtain certain authorization tokens needed to successfully request DRM licenses from the Nagra backend.
Please refer to Nagra’s own documentation portal or contact your Nagra representatives for detailed information about these application-to-backend interactions.
- Authorization
Your application signs in to aSIGNON_URL
endpoint usingSIGNON_USERNAME
,SIGNON_PASSWORD
and any required device/user-agent metadata. Upon successful sign-on, you obtain a login token. - Content Token Retrieval
Using theNAGRA_CONTENT_TOKEN_URL
endpoint and yourNAGRA_CONTENT_ID
, your application requests aCONTENT_TOKEN
from the Nagra backend. - Secure Session Setup
Your application initiates a secure session with the Nagra SSP backend by sending theCONTENT_TOKEN
toNAGRA_SSM_SETUP_URL
. Upon successful setup, you receive anSSM_TOKEN
. - Secure Session Teardown
Once playback finishes or you switch to another piece of content, your application terminates the secure session by callingNAGRA_SSM_TEARDOWN_URL
. If you plan to play a different piece of DRM-protected content, you must first repeat the Content Token Retrieval and Secure Session Setup steps to obtain a new content token andSSM_TOKEN
.
2. Bitmovin Player Configuration for Widevine Playback
This section covers how to configure the Bitmovin Player for playback of Nagra protected Widevine DRM content, we will do two things here.
- Add a NetworkConfig.preprocessHttpRequest callback to inject CONTENT_TOKEN and SSM_TOKEN into the license and certificate requests.
- Use SourceConfig and WidevineModularDRMConfig to specify license server URLs and custom prepareMessage / prepareLicense callbacks for request/response formatting
2.1 Configure NetworkConfig.preprocessHttpRequest (code snippet below)
The preprocessHttpRequest callback allows you to modify DRM-related requests before the Bitmovin Player sends them out.
- Add the CONTENT_TOKEN and SSM_TOKEN as custom HTTP header("nv-authorizations") in the Widevine DRM request.
- Then set HTTP headers in Widevine request depending on type of the request.
- Update NAGRA_LICENSE_RENEWAL_SERVER_URL as the request URL for Widevine renewal requests.
const playerConfig = {
key: "<BITMOVIN_PLAYER_KEY>",
............
............
network: {
preprocessHttpRequest: (type, request) => {
if (type === bitmovin.player.HttpRequestType.DRM_LICENSE_WIDEVINE) {
if (isInitialLicenseReceived == false) {
// initial license workflow
if (isWidevineCertificateRequest === false) {
// Widevine license request is formatted as JSON string in prepareMessage
// so if license request body is string type, then it is a license request
request.headers["nv-authorizations"] = contentToken + ',' + ssmToken;
request.headers["accept"] = "application/json";
request.headers["content-type"] = "application/json";
} else {
// on browsers, Widevine certificate request precedes license request
// the certificate request is not formatted as string in prepareMessage
// so if request body is not of type string, then it is WV certificate request
request.headers["nv-authorizations"] = contentToken + ',' + ssmToken;
request.headers["accept"] = "application/octet-stream";
request.headers["content-type"] = "application/octet-stream";
}
} else {
// Renewal license workflow
request.url = RENEWAL_LICENSE_SERVER_URL;
request.headers["nv-authorizations"] = ssmToken;
request.headers["accept"] = "application/json";
request.headers["content-type"] = "application/json";
}
}
return Promise.resolve(request);
},
},
};
2.2 Configure SourceConfig including Widevine DRM Config (code snippet below)
Next we need to specify the Widevine license server URL via LA_URL and implement the prepareMessage and prepareLicense callbacks, which handle request/response formatting for Nagra’s DRM services.
- Add either the DASH/HLS Manifest as the content URL.
- Add NAGRA_WIDEVINE_LICENSE_SERVER_URL as LA_URL initial license acquisition.
- Add prepareMessage configuration to format the Widevine license request message as expected by Nagra Widevine server.
- Add prepareLicense configuration to re-format the Widevine license response coming from Nagra Widevine server into format required by Bitmovin player.
const prepareMessage = (keyMessage) => {
const message = keyMessage.message;
if (message.byteLength > 2) {
isWidevineCertificateRequest = false;
console.log("WV License Request, message length=" + message.byteLength);
return JSON.stringify({
challenge: btoa(String.fromCharCode(...new Uint8Array(keyMessage.message)))
});
} else {
isWidevineCertificateRequest = true;
console.log("WV Certificate Request, message length=" + message.byteLength);
}
return keyMessage.message;
};
const prepareLicense = (licenseObj) => {
const license = { license: licenseObj.license };
if (isWidevineCertificateRequest == true) {
console.log("Returning Widevine certificate request without any processing");
return license;
} else if (isInitialLicenseReceived) {
try {
const responseStr = String.fromCharCode(
...new Uint8Array(licenseObj.license)
);
let responseObj = JSON.parse(responseStr);
if (responseObj.sessionToken && responseObj.license) {
ssmToken = responseObj.sessionToken;
const str = window.atob(responseObj.license);
const bufView = new Uint8Array(new ArrayBuffer(str.length));
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
license.license = bufView;
}
} catch (e) {
console.log("prepareLicense not able to parse binary response.");
}
} else {
try {
const drmObj = JSON.parse(
String.fromCharCode.apply(null, licenseObj.license)
);
if (drmObj && drmObj.status && drmObj.license) {
if (drmObj.status === "OK") {
const str = window.atob(drmObj.license);
const bufView = new Uint8Array(new ArrayBuffer(str.length));
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
license.license = bufView;
}
}
} catch (e) {
console.log("prepareLicense not able to parse json response.");
}
}
return license;
};
const source = {
dash: DASH_MANIFEST_URL,
drm: {
immediateLicenseRequest: true,
widevine: {
LA_URL: NAGRA_WIDEVINE_LICENSE_SERVER_URL,
prepareMessage: prepareMessage,
prepareLicense: prepareLicense,
},
},
}
3. Playback Sequence
-
The application prepares its configuration and required tokens, then loads the source into the Bitmovin Player using the load() API.
-
The Bitmovin Player downloads the playlist and segments, identifying that the content is protected with Widevine.
-
The Bitmovin Player initiates the Widevine license acquisition process, which proceeds as follows:
- Certificate Request (Desktop Chrome/Edge Only): On desktop Chrome/Edge, the Widevine CDM first requests a Widevine certificate. This step does not occur on TV devices (e.g., LG WebOS, Samsung Tizen). The WidevineModularDRMConfig.prepareMessage handler checks if the request is for a certificate or a license. Certificate requests/responses are not pre-processed or reformatted.
- License Request: Next, the Widevine CDM in the browser creates a Widevine challenge/request. The WidevineModularDRMConfig.prepareMessage callback reformats this request into a JSON-encoded, base64 string as required by the Nagra DRM server.
- Request Preprocessing: The Bitmovin Player invokes the NetworkConfig.preprocessHttpRequest callback registered by the application. If this callback is triggered before playback starts, it indicates an initial license request. Here, the application sets the necessary HTTP headers in the request.
- Response Handling: The Bitmovin Player sends the request to the Nagra DRM server and receives a response. The WidevineModularDRMConfig.prepareLicense callback reformats this response into a format compatible with the Bitmovin Player. The response also includes an updated SSM_TOKEN, which is saved for use in subsequent license renewal requests.
- Playback Start: After the initial license acquisition succeeds, playback of the content begins.
- Failure Handling: If license acquisition fails, the player throws a fatal error with a detailed message.
-
Once playback begins successfully, DRM license renewal occurs periodically at set intervals. The renewal sequence is as follows:
- The Widevine CDM in the browser detects the need for a license renewal and initiates a renewal request.
- The license renewal request follows the same process as the initial request, with two key differences:
- The SSP endpoint for the renewal request is different from the initial license request, so the NetworkConfig.preprocessHttpRequest callback updates the HTTP request URL accordingly.
- An updated SSM token is returned by the Nagra DRM server in the initial license response’s HTTP header, which the application must store and include in subsequent renewal requests.
-
When playback ends or the user switches to new content, the application should tear down the existing SSM session. Terminating the SSM session is handled directly with the Nagra SSP backend (outside the Bitmovin Player). This step is crucial because failing to tear down previous sessions may cause the Nagra SSP backend to deny new session requests, preventing further playback.
note:
The licence renewal interval is determined by Nagra. Please consult your Nagra documentation or representative for more details on this.
Complete Example of Nagra Widevine Content Playback.
Below is a minimal, end-to-end HTML page showing how to use the Bitmovin Player with Nagra Widevine. Replace all placeholder variables with valid values provided by Nagra or your internal configuration.
<!DOCTYPE html>
<html lang="en">
<head>
<title>Bitmovin Player Demo</title>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="text/html; charset=utf-8" />
<script src="https://cdn.jsdelivr.net/npm/bitmovin-player@8/bitmovinplayer.js"></script>
</head>
<body>
<div>
<div id="player"></div>
</div>
<script type="text/javascript">
let player;
const WIDEVINE_LICENSE_URL =
"<NAGRA_WIDEVINE_LICENSE_SERVER_URL>";
const RENEWAL_LICENSE_URL =
"<NAGRA_LICENSE_RENEWAL_SERVER_URL>";
const SSM_SETUP_URL =
"<NAGRA_SSM_SETUP_URL>";
const SSM_TEARDOWN_URL =
"<NAGRA_SSM_TEARDOWN_URL>";
const CONTENT_TOKEN_URL =
"<NAGRA_CONTENT_TOKEN_URL>";
const TENANT_ID = "<NAGRA_TENANT_ID>";
const DASH_MANIFEST_URL = "<DASH_MANIFEST_URL>";
const CONTENT_ID = "<NAGRA_CONTENT_ID>";
let isSecureSessionCreated = false;
let isWidevineCertificateRequest = false;
let isInitialLicenseReceived = false;
let loginToken = "";
let contentToken = ""
let ssmToken = "";
const conf = {
key: "<BITMOVIN_PLAYER_KEY>",
logs: {
level: "debug",
},
playback: {
autoplay: true,
muted: true,
},
network: {
preprocessHttpRequest: (type, request) => {
if (type === bitmovin.player.HttpRequestType.DRM_LICENSE_WIDEVINE) {
if (isInitialLicenseReceived == false) {
// initial license workflow
if (isWidevineCertificateRequest === false) {
// Widevine license request is formatted as JSON string in prepareMessage
// so if license request body is string type, then it is a license request
request.headers["nv-authorizations"] = contentToken + ',' + ssmToken;
request.headers["accept"] = "application/json";
request.headers["content-type"] = "application/json";
} else {
// on browsers, Widevine certificate request precedes license request
// the certificate request is not formatted as string in prepareMessage
// so if request body is not of type string, then it is WV certificate request
request.headers["nv-authorizations"] = contentToken + ',' + ssmToken;
request.headers["accept"] = "application/octet-stream";
request.headers["content-type"] = "application/octet-stream";
}
} else {
// Renewal license workflow
request.url = RENEWAL_LICENSE_SERVER_URL;
request.headers["nv-authorizations"] = ssmToken;
request.headers["accept"] = "application/json";
request.headers["content-type"] = "application/json";
}
}
return Promise.resolve(request);
},
},
};
const prepareMessage = (keyMessage) => {
console.log('prepareMessage: entry');
const message = keyMessage.message;
if (message.byteLength > 2) {
isWidevineCertificateRequest = false;
console.log("WV License Request, message length=" + message.byteLength);
return JSON.stringify({
challenge: btoa(String.fromCharCode(...new Uint8Array(keyMessage.message)))});
} else {
isWidevineCertificateRequest = true;
console.log("WV Certificate Request, message length=" + message.byteLength);
}
return keyMessage.message;
};
const prepareLicense = (licenseObj) => {
const license = { license: licenseObj.license };
if (isWidevineCertificateRequest == true) {
console.log("Returning Widevine certificate request without any processing");
return license;
} else if (isInitiaLicenseReceived) {
try {
const responseStr = String.fromCharCode(
...new Uint8Array(licenseObj.license)
);
let responseObj = JSON.parse(responseStr);
if (responseObj.sessionToken && responseObj.license) {
ssmToken = responseObj.sessionToken;
const str = window.atob(responseObj.license);
const bufView = new Uint8Array(new ArrayBuffer(str.length));
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
license.license = bufView;
console.log("prepareLicense DRM license success if");
}
} catch (e) {
console.log("prepareLicense not able to parse binary response.");
}
} else {
try {
const drmObj = JSON.parse(
String.fromCharCode.apply(null, licenseObj.license)
);
if (drmObj && drmObj.status && drmObj.license) {
if (drmObj.status === "OK") {
const str = window.atob(drmObj.license);
const bufView = new Uint8Array(new ArrayBuffer(str.length));
for (let i = 0; i < str.length; i++) {
bufView[i] = str.charCodeAt(i);
}
license.license = bufView;
console.log("prepareLicense DRM license success else");
}
}
} catch (e) {
console.log("prepareLicense not able to parse json response.");
}
isPrepareLicenseEventReceived = true;
}
return license;
};
const source = {
dash: DASH_MANIFEST_URL,
drm: {
immediateLicenseRequest: true,
widevine: {
LA_URL: WIDEVINE_LICENSE_URL,
prepareMessage: prepareMessage,
prepareLicense: prepareLicense,
},
},
}
const resetPlaybackState = () => {
isSecureSessionCreated = false;
isWidevineCertificateRequest = false;
isInitiaLicenseReceived = false;
contentToken = ""
ssmToken = "";
};
const loadSource = async (source) => {
if (isSecureSessionCreated && ssmToken != "") {
tearDownSecureSession();
}
resetPlaybackState();
contentToken = await getContentToken(CONTENT_ID);
if (contentToken) {
ssmToken = await setupSecureSession(contentToken);
if (ssmToken) {
console.log("ssmSession Token fetch suceeded.");
} else {
console.log("ssmSession Token fetch failed.");
return false;
}
} else {
console.log("Content Token fetch failed.");
return false;
}
console.log("source updated to ", source);
player.load(source);
};
const onPlaying = (event) => {
isInitiaLicenseReceived = true;
};
const initPlayer = async () => {
player = new bitmovin.player.Player(document.getElementById("player"), conf);
// player events
player.on(bitmovin.player.PlayerEvent.Playing, onPlaying);
await doSignOn();
loadSource(source);
};
/*
* The code below is not related to Bitmovin player integration
* This is added as reference for application's communication with
* Nagra backend for login/authentication and fetching CONTENT_TOKEN,
* SSM_TOKEN etc. which are required for DRM request authorization and
* secure session management. Customers/Integrators must refer to
* Nagra documentation portal for best practices around this topic
*/
const doSignOn = async () => {
const SIGNON_URL =
"<NAGRA_SIGNON_URL>";
const USN = "<NAGRA_SIGNON_USERNAME>";
const PWD = "<NAGRA_SIGNON_PASSWORD>";
const SIGNON_PARAMS = {
username: USN,
password: PWD,
deviceInformation: {
securePlayer: { streamings: [], codecs: [], DRMs: [] },
device: {
hardware: {
manufacturer: "Mozilla/5.0 (Macintosh",
model:
" Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
type: "Mac OS",
},
OS: { type: "Mac OS", version: "10.15.7" },
CPU: { cores: 7, neon: 1000000000 },
screen: { density: 160, width: 1920, height: 1080 },
GPU: { cores: "0", frequency: "0.000000" },
},
},
};
const response = await fetch(SIGNON_URL, {
headers: {
accept: "*/*",
"content-type": "application/json",
"nv-tenant-id": TENANT_ID,
},
body: JSON.stringify(SIGNON_PARAMS),
method: "POST",
mode: "cors",
});
if ([200, 201].includes(response.status)) {
const res = await response.json();
loginToken = res.access_token;
return res.access_token;
} else {
document.querySelector("#message").textContent = "Signon failed.";
return false;
}
};
const setupSecureSession = async (token) => {
const response = await fetch(SSM_SETUP_URL, {
headers: {
accept: "application/json",
"Nv-Authorizations": token,
},
body: "{}",
method: "POST",
mode: "cors",
});
if (response.status === 200) {
console.log('Successfully setup SSM session');
isSecureSessionCreated = true;
const res = await response.json();
return res.sessionToken;
} else {
console.log('Error in setup of SSM session');
return false;
}
};
const teardownSecureSession = async () => {
const reponse = await fetch(SSM_TEARDOWN_URL, {
headers: {
accept: "application/json",
"content-type": "application/json; charset=UTF-8",
"nv-authorizations": ssmToken,
},
body: "{}",
method: "POST",
mode: "cors",
});
if (response.status === 200) {
console.log('Successfully teared down SSM session');
} else {
console.log('Error while tearing down SSM session');
}
isSecureSessionCreated = false;
};
const getContentToken = async (contentId) => {
const response = await fetch(CONTENT_TOKEN_URL + contentId, {
headers: {
authorization: `Bearer ${loginToken}`,
"content-type": "application/json",
"nv-tenant-id": TENANT_ID,
},
body: null,
method: "POST",
mode: "cors",
});
if (response.status === 200) {
const res = await response.json();
return res.content_token;
} else {
return false;
}
};
/*
* App <--> Nagra backend interaction code ends here.
*/
initPlayer();
</script>
</body>
</html>
Please replace the following placeholders in the code:
- DASH_MANIFEST_URL: The URL to the DASH manifest (mpd) file.
- NAGRA_WIDEVINE_LICENSE_SERVER_URL: To be provided by Nagra.
- NAGRA_LICENSE_RENEWAL_SERVER_URL: To be provided by Nagra.
- NAGRA_CONTENT_TOKEN_URL: To be provided by Nagra.
- NAGRA_SSM_SETUP_URL: To be provided by Nagra.
- NAGRA_SSM_TEARDOWN_URL: To be provided by Nagra.
- NAGRA_TENANT_ID: To be provided by Nagra.
- NAGRA_CONTENT_ID: To be provided by Nagra.
- NAGRA_SIGNON_URL: To be provided by Nagra.
- NAGRA_SIGNON_USERNAME: To be provided by Nagra.
- NAGRA_SIGNON_PASSWORD: To be provided by Nagra.
- BITMOVIN_PLAYER_KEY: To be provided by Nagra or Bitmovin or use a trial key.
And that's it. If you have any issues implementing this please reach out to your Bitmovin and Nagra representatives.
For similar examples across other platforms also using Nagra security please see the links below:
Playing protected content with Nagra DRM - ios
Updated about 3 hours ago