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

licenseRequest

expects the license challenge in a custom request body

licenseRequest

returns the license wrapped in JSON or another format

licenseResponse

needs headers on the FairPlay certificate request

certificateRequest

expects a different FairPlay content ID than the default

extractContentId

needs a custom FairPlay key request payload

transformInitData

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

licenseRequest

Before a license request is sent.

net.Request

licenseResponse

After a license response arrives, before the CDM gets the license.

net.Response

certificateRequest

Before a certificate request is sent.

net.Request

certificateResponse

After a certificate response arrives.

net.Response

extractContentId

When the player extracts the FairPlay content ID from init data.

Init data

transformInitData

Before init data is passed to the CDM.

InitDataTransformerParams

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
    },
  },
});

See drm.ContentIdExtractor.

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.

  • licenseResponse can be asynchronous, but licenseRequest must be synchronous.

See the CAF SDK reference for licenseRequestHandler, licenseHandler, NetworkRequestInfo, and NetworkResponseInfo.

Next topic: Tracks and languages
Previous topic: Persistent DRM licenses