Customizing license requests¶
Every license server has its own rules. Some want an authentication token that expires every few minutes, some wrap the license in JSON, and some expect a content ID in a format of their own. Modifiers let PRESTOplay meet those rules: they’re small callback functions that run at specific points of license acquisition, so you can adjust a request before it’s sent or a response before the player uses it.
Do you need modifiers?¶
Check whether a built-in DRM environment already covers your license server:
With DRMtoday, the DRMtoday environment handles authentication, request format, and FairPlay content IDs for you.
If your license server only needs fixed headers, such as an API key, use the header-based DRM environment and pass them in
customData.headers.
Use modifiers when your license server needs more than that, for example:
Your license server… |
Use |
|---|---|
needs a token that you fetch or refresh at request time |
|
expects the license challenge in a custom request body |
|
returns the license wrapped in JSON or another format |
|
needs headers on the FairPlay certificate request |
|
expects a different FairPlay content ID than the default |
|
needs a custom FairPlay key request payload |
How modifiers work¶
Modifiers belong to a DRM system in a
custom DRM environment. Pass them in
the modifiers object of
drm.DrmEnvironment#setDrmSystem:
const environment = new clpp.drm.DrmEnvironment('myDrm');
environment.setDrmSystem(clpp.drm.KeySystem.WIDEVINE, {
licenseUrl: 'https://license.example.com/widevine',
modifiers: {
licenseRequest: (config, request, sessionContext) => { /* ... */ },
licenseResponse: (config, response, sessionContext) => { /* ... */ },
},
});
clpp.drm.registerDrmEnvironment(environment);
Each DRM system takes its own set of modifiers, so you can handle Widevine,
PlayReady, and FairPlay differently. The available modifiers are defined in
drm.LicenseModifiers:
Modifier |
Runs |
Receives |
|---|---|---|
|
Before a license request is sent. |
|
|
After a license response arrives, before the CDM gets the license. |
|
|
Before a certificate request is sent. |
|
|
After a certificate response arrives. |
|
|
When the player extracts the FairPlay content ID from init data. |
Init data |
|
Before init data is passed to the CDM. |
Request and response modifiers change the request or response in place, for
example request.headers or response.data. They can also return a
promise, and the player waits for it before it continues. To pass data
between modifiers of the same media session, store it in
sessionContext.customData. See
SessionContext.
Add authentication to license requests¶
If your license server needs a short-lived token, fetch it in the
licenseRequest modifier and add it as a header. Because the modifier
returns a promise, the license request waits until the token is available:
environment.setDrmSystem(clpp.drm.KeySystem.WIDEVINE, {
licenseUrl: 'https://license.example.com/widevine',
modifiers: {
licenseRequest: async (config, request) => {
// Your own backend call that returns a fresh token.
const token = await fetchLicenseToken();
request.headers['Authorization'] = `Bearer ${token}`;
},
},
});
Send a custom request and unwrap the response¶
Some license servers don’t accept the raw license challenge from the CDM.
For example, a server might expect the challenge as Base64 in a JSON body and
return the license as Base64 in a JSON response. Use licenseRequest to build
the body and licenseResponse to extract the license:
const { toStandardBase64, fromBase64 } = clpp.utils.Uint8ArrayUtils;
const { toUtf8, fromUtf8 } = clpp.utils.strings;
environment.setDrmSystem(clpp.drm.KeySystem.WIDEVINE, {
licenseUrl: 'https://license.example.com/widevine',
modifiers: {
licenseRequest: (config, request) => {
const body = {
challenge: toStandardBase64(new Uint8Array(request.body)),
contentId: 'my-content-id',
};
request.headers['Content-Type'] = 'application/json';
request.body = toUtf8(JSON.stringify(body)).buffer;
},
licenseResponse: (config, response) => {
const { license } = JSON.parse(fromUtf8(response.data));
response.data = fromBase64(license).buffer;
},
},
});
Use a custom FairPlay content ID¶
For FairPlay, the player extracts a content ID from the init data and
sends it to the license server. By default, it uses the hostname part of the
HLS #EXT-X-KEY URI. For this tag, the content ID is d192ebad-5097:
#EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://d192ebad-5097",
KEYFORMAT="com.apple.streamingkeydelivery",KEYFORMATVERSIONS="1"
If your FairPlay Streaming (FPS) license server expects a different value,
return it from extractContentId. This example sends only d192ebad:
environment.setDrmSystem(clpp.drm.KeySystem.FAIRPLAY, {
licenseUrl: 'https://license.example.com/fairplay',
certificateUrl: 'https://license.example.com/fairplay.cer',
modifiers: {
extractContentId: (config, initData) => {
const { extractContentId } = clpp.utils.FairplayUtils;
const fullHostname = extractContentId(initData); // d192ebad-5097
return fullHostname.split('-')[0]; // d192ebad
},
},
});
Build a custom FairPlay key request¶
If changing the content ID isn’t enough, transformInitData gives you full
control over the payload the CDM uses to generate a key request. Use it when
your license server needs a content ID from an external source, such as your
own API, or a payload format of its own.
The following example builds the FairPlay payload with a content ID from your
own video configuration. It stores the key ID in sessionContext.customData,
so the licenseRequest and licenseResponse modifiers can use it to talk to
a license server with a JSON API:
// Your own data, e.g. fetched from your backend.
const videoConfig = {
pid: 'isdl83hfja',
authHeaders: {
Authorization: 'Basic aHR0cDovL2FjY2V...',
},
};
const environment = new clpp.drm.DrmEnvironment('myDrmEnv');
environment.setDrmSystem(clpp.drm.KeySystem.FAIRPLAY, {
licenseUrl: 'https://license.example.com/fairplay',
certificateUrl: 'https://license.example.com/fairplay.cer',
modifiers: {
transformInitData: ({
initData,
initDataType,
drmInfo: { serverCertificate },
sessionContext: { customData },
emeApi,
}) => {
if (initDataType === 'skd' &&
emeApi === clpp.drm.eme.EmeFactory.Apis.APPLE) {
const { extractContentId, createFairplayPayload } =
clpp.utils.FairplayUtils;
customData.keyId = extractContentId(initData);
initData = createFairplayPayload(
initData,
videoConfig.pid,
serverCertificate,
);
}
return initData;
},
licenseRequest: (config, request, { customData }) => {
const { toUint8Array } = clpp.utils.BufferUtils;
const { toStandardBase64 } = clpp.utils.Uint8ArrayUtils;
const { toUtf8 } = clpp.utils.strings;
const requestBody = {
getFairplayLicense: {
spcMessage: toStandardBase64(toUint8Array(request.body)),
releasePid: videoConfig.pid,
keyId: customData.keyId,
},
};
Object.assign(request.headers, videoConfig.authHeaders);
request.body = toUtf8(JSON.stringify(requestBody)).buffer;
},
licenseResponse: (config, response, { customData }) => {
const { fromUtf8 } = clpp.utils.strings;
const { fromBase64 } = clpp.utils.Uint8ArrayUtils;
let licenseResponse;
try {
licenseResponse = JSON.parse(fromUtf8(response.data));
} catch (e) {
throw new Error(
`Can't parse the license response for key ID ${customData.keyId}: ${e}`,
);
}
const { ckcResponse } = licenseResponse.getFairplayLicenseResponse;
response.data = fromBase64(ckcResponse).buffer;
},
},
});
clpp.drm.registerDrmEnvironment(environment);
The utilities in utils.FairplayUtils,
utils.strings, and
utils.BufferUtils help with this
processing. See drm.InitDataTransformer
and the EME initialization data spec.
Modifiers on Chromecast receivers¶
In the Chromecast receiver context, the license modifiers have limited functionality, based on the data the Cast Application Framework (CAF) SDK provides:
Request modifiers don’t have access to most of the request data, such as
licenseRequestType.Response modifiers have access only to the license data and headers, not the associated request.
licenseResponsecan be asynchronous, butlicenseRequestmust be synchronous.
See the CAF SDK reference for licenseRequestHandler, licenseHandler, NetworkRequestInfo, and NetworkResponseInfo.