DRM Protection

DRM Protection

This chapter explains how to perform secure playback using different DRM options with the PRESTOplay SDK for Android.

DRM Systems and Security Levels

DRM systems supported by the SDK are classified into three security levels. The DrmUtils class provides static methods to query the available DRM systems and their security level on a given device.

SecurityLevel.SECURE_MEDIA_PATH

Equals to L1 in Widevine. The Hardware must provide a secure Bootloader, a secure trust zone, and the key shall never be exposed in clear. The samples are provided encrypted to the rendering core and never exposed in clear.

SecurityLevel.ROOT_OF_TRUST

Equals to L2 in Widevine. The Hardware must provide a secure trust zone, and the key shall never be exposed in clear. A custom Android Bootloader could be provided by the Hardware manufacturer if the required security is met. The samples are provided encrypted to the rendering core and never exposed in clear.

SecurityLevel.SOFTWARE

Equals to L3 in Widevine. The key may be exposed in clear on the CPU, the Hardware does not need to protect the content. The Decryption happens in software, and the samples are passed to the rendering core in clear.

DRMtoday Integration

The PRESTOplay SDK for Android comes with a ready to use integration with castLabs’ DRMtoday services.

Our SDK supports both OMA-DRM on all platforms and versions as well as Widevine Modular from API 18 onwards. PlayReady is supported, but only on devices where the DRM system is implemented.

The OMA-DRM module is fully implemented by castLabs in C++ code and distributed as a native shared library within an Android AAR library. The OMA-DRM agent is protected with Arxan’s EnsureIT and Arxan’s TransformIT. If you want to use OMA-DRM, you will need to load and register the OMA plugin with the SDK (see OMA Integration).

The Widevine Modular implementation is provided by the Android operating system. It is only available on some devices from API 18, and all devices from API 19.

We recommend that Widevine is used always if possible. Widevine offers hardware protection on the video samples and rendering, and can be used together with the output protection configuration from DRMtoday.

OMA-DRM only offers software level protection and should not be used on HD content.

Using DRMtoday

The easiest way to integrate with DRMtoday is by using the provided DrmTodayConfiguration class.

Instances of the DrmTodayConfiguration can be passed to the PlayerController either as an Intent parameter or as a member of the PlayerConfig, and all DRM related configuration happens through this object.

New instances of the DrmTodayConfiguration class must be created through its Builder. The builders constructor takes all the mandatory parameters. Additional parameters can then be set using the methods provided by the builder.

The following parameters are mandatory:

Environment

The URL to the DRMtoday environment. The possible values are DrmTodayConfiguration.DRMTODAY_TEST, DrmTodayConfiguration.DRMTODAY_STAGING, or DrmTodayConfiguration.DRMTODAY_PRODUCTION.

UserID

The DRMtoday user id. Please refer to the DRMtoday documentation for details about this value.

SessionID

The DRMtoday session id. Please refer to the DRMtoday documentation for details about this value.

Merchant

Your DRMtoday merchant name.

AssetID

The DRMtoday asset id of the content. You may wish to further classify the content by also specifying a variant ID using the variantId(String) method on the builder instance.

Drm

Use the values of the Drm enum to specify which DRM system should be used for playback. You can use Drm.BestAvailable to let the player pick the best DRM system available on the current device. This usually picks Widevine first, then PlayReady, and then OMA as a fall back solution. Please note that the auto selection also considers the security level of the DRM implementation. For example, on an Amazon FireTV, PlayReady offers hardware level protection while Widevine is only implemented as a software based solution. In such case, Playready will be selected since it offers the higher security level.

The PRESTOplay SDK for Android also provides Clearkey integration with DRMtoday. To use it, just set it as the desired system and fill in the other DRMtoday params as usual.

If you are explicitly selecting a DRM system here, please note that you will need to ensure that the system is supported on the current device and that it offers enough security for your content. The SDK provides utility methods in the DrmUtils class that you can use to get information about supported DRM systems and their security levels on the current device.

Using the Builder, you can create an instance of the DRMtoday configuration object as follows:

DrmTodayConfiguration cfg = new DrmTodayConfiguration.Builder(
        DrmTodayConfiguration.DRMTODAY_STAGING,
        "purchase", // The user ID
        "sessionId", // The session ID
        "six", // The merchant
        "demo", // The asset ID
        Drm.BestAvailable) // Auto-select the DRM system
        .get();

You can then pass this instance either to the constructor of your PlaybackState or, if you are using Intent parameters and Bundles to start playback (see Starting Playback from an Intent), you can pass it to the bundle using:

intent.putExtra(SdkConsts.INTENT_DRM_CONFIGURATION, cfg);

Upfront Authentication

If you are using DRMtoday upfront authentication, please make sure that you are using the builders authToken(String) method to set the token.

Offline Key Storage

Some DRM systems allow you to store the license key on the device to allow offline playback or to ensure faster startup times when no additional request needs to be performed to obtain the key. To enable basic support for offline key storage, you will need to specify a unique id to store and retrieve the key using the builder’s offlineId(String) method. See Offline Keys for more information.

When the offline ID is specified and no key is available yet, the first startup of the playback will fetch and store the key. In addition, you can use the DrmLicenseLoader to fetch licenses without explicitly starting playback. This enables fetching the licenses _headless_ without a player instance. Note however that access to the content might be required if not all DRM related initialization data are specified in the Manifest.

The license loader works asynchronously and you need to specify a callback implementation to get informed about errors or successful license deliveries. The following example demonstrates the usage of the license loader:

// Create and show a progress dialog to indicate license loading
final ProgressDialog progress = ProgressDialog.show(
        context, "Offline License",
        "Fetching offline License", true, false);

// Implement a basic callback that hides the progress and reports
// messages.
DrmLicenseLoader.Callback callback = new DrmLicenseLoader.Callback() {
    @Override
    public void onError(CastlabsPlayerException e) {
        Log.e(TAG, "Error while fetching license: " + e.getMessage(), e);
        progress.dismiss();
        showMessage(e.getMessage());
    }

    @Override
    public void onLicenseLoaded() {
        progress.dismiss();
        showMessage("License fetched");
    }
};

// Setup the basic configuration parameters. Note that you need
// to specify the offlineId in the DrmConfiguration
String contentUrl = ...
DrmTodayConfiguration drmConfiguration = ...

// Create the license loader passing the basic configuration options
DrmLicenseLoader.Builder licenseLoaderBuilder = new DrmLicenseLoader.Builder(
        context, contentUrl, drmConfiguration, callback);
DrmLicenseLoader licenseLoader = licenseLoaderBuilder.get();

// Trigger license loading
licenseLoader.load();

The example above passes the mandatory parameters to the license loader. The DrmLicenseLoader.Builder class provides additional methods to specify query and header parameters or configure the key store.

Offline key removal

You can remove already stored keys. The required steps to perform the deletion are similar to the ones required for the license loading.

// Create and show a progress dialog to indicate license loading
final ProgressDialog progress = ProgressDialog.show(
        context, "Offline License",
        "Removing offline License", true, false);

// Implement a basic callback that hides the progress and reports
// messages.
DrmLicenseLoader.Callback callback = new DrmLicenseLoader.Callback() {
    @Override
    public void onError(CastlabsPlayerException e) {
        Log.e(TAG, "Error while removing license: " + e.getMessage(), e);
        progress.dismiss();
        showMessage(e.getMessage());
    }

    @Override
    public void onLicenseLoaded() {
    }

    @Override
    public void onLicenseLoaded() {
        Log.i(TAG, "License removed"
    }
};

// Setup the basic configuration parameters. Note that you need
// to specify the offlineId in the DrmConfiguration
String contentUrl = ...
DrmTodayConfiguration drmConfiguration = ...

// Create the license loader passing the basic configuration options
DrmLicenseLoader.Builder licenseLoaderBuilder = new DrmLicenseLoader.Builder(
        context, contentUrl, drmConfiguration, callback);
DrmLicenseLoader licenseLoader = licenseLoaderBuilder.get();

// Trigger license removal
licenseLoader.remove();

This operation will remove the local reference to the offline key, usually stored in SharedPreferences, indexed by offlineId, and also perform any DRM vendor-required action to remove the internal key.

Depending on which DRM vendor are you using, this operation may perform a request to the licensing server in order to properly remove the local license. Meaning that license removal will fail if the licensing server is not reachable.

Expired Keys and System Time Changes

When using offline keys, it can happen that a rental key is expired. In addition it is possible that the DRM engine on the device refuses to use a key if the user changes the system time. Note that Time-Zone changes are not affected by this.

In order to deal with the above scenario, it is advised to listen for expiration and decryption errors and potentially re-fetch licenses. This can be done by removing the license from the key store before re-triggering playback with an up-to-date configuration.

PlayerController pc = ...
pc.addPlayerListener(new AbstractPlayerListener() {
@Override
public void onError(CastlabsPlayerException error) {
    // Check if the error is either TYPE_KEY_EXPIRED or TYPE_VIDEO_DECRYPTION_ERROR with
    // the cause being MediaCodec.CryptoException.ERROR_NO_KEY. In the first
    // case the key is expired and we can safely decided to re-load and see if
    // we get an new key from the DRM system. The second case unfortunately has
    // more than one meaning. It could imply that no key was loaded for the
    // content, i.e. the _wrong_ key was used for instance due to a misalignment
    // with the offlineId in the keystore.
    // It could however _also_ mean that the user fiddled around with his device
    // time and the CDM loaded the license but refuses to use it. We can still
    // remove the key and load again, since the DRM response should
    // give us a license (or not) and we would have a definitive answer.
    if (error.getType() == CastlabsPlayerException.TYPE_KEY_EXPIRED ||
     ((error.getType() == CastlabsPlayerException.TYPE_VIDEO_DECRYPTION_ERROR ||
        error.getType() == CastlabsPlayerException.TYPE_AUDIO_DECRYPTION_ERROR) &&
      (error.getCause() instanceof MediaCodec.CryptoException) &&
      ((MediaCodec.CryptoException) error.getCause()).getErrorCode() == MediaCodec.CryptoException.ERROR_NO_KEY)) {

    KeyStore keyStore = PlayerController.getKeyStore();
    if(keyStore != null && drmConfiguration != null && drmConfiguration.offlineId != null) {
      if (keyStore.delete(drmConfiguration.offlineId)){
        Log.e(TAG, "DRM license key expired or not usable. Cleared key and reload");
        // This is just an example, do whatever needed to reload the content after we
        // deleted the key from the store.
        playerView.getPlayerController().open(finalPlaybackBundle);
      }
    }
  }
}
});

Automatic keys renewal

Waiting for a key to expire and re-loading the stream may not be the best option as it does not provide seamless playback and UX. The PRESTOplay SDK for Android provides the option to configure automatic keys renewal namely DrmTodayConfiguration.renewalThresholdMs. When set the PRESTOplay SDK for Android will fetch and install new license before the current license expires and without stopping the playback. The seamless playback however is only possible on devices with API >= 23 (Android-M) and for the rest of devices the PRESTOplay SDK for Android falls back to codec re-initialization which still brings benefits as there is no keys waiting delay and the codec is re-initialized in a few hundreds of milliseconds.

In case the renewal is too late and the current keys have already expired then the license request is still sent and the playback is put into the waiting for the license state.

Note that automatic license renewal works for persistent licenses only and therefore the license should first be fetched and stored (Offline Key Storage) and only then the playback can be started:

Bundle playbackBundle = ...
if (DrmLicenseLoader.fetchLicense(playbackBundle, new MemoryKeyStore())) {
    // Setup callbacks and start playback
}

DRM configuration may expire too when issuing a key renewal request and therefore ConfigurationProvider callback has to be installed:

PlayerController pc = ...
pc.setConfigurationProvider(new ConfigurationProvider() {
    @Nullable
    @Override
    public DrmConfiguration getDrmConfiguration(@NonNull DrmConfiguration drmConfiguration) {
        // We just supply here the same DRM configuration to show how the DRM configuration
        // can be provided for the license renewal. If needed any other configuration can be created and returned here.
        // In case the DRM configuration stays the same over license renewals then this callback is not needed to be
        // installed and the same DRM configuration is used by default
        return drmConfiguration;
    }
});

Multi-DRM Environments

You might run into situations where you need to use different DRM systems for video and audio tracks. You can do this by using the builder’s audioDrm(Drm) method and set a dedicated DRM used for decryption of audio tracks. This is useful on devices such as the Amazon FireTV, where the hardware protected PlayReady DRM can only be used for video decryption. In that case, you will need to specify a different DRM system for audio playback. Note that this is not strictly necessary for the Amazon FireTV. For that particular device, the SDK already implements the selection and will choose Widevine as the DRM for audio playback automatically.

Multi-key Playbacks

Multi-key playback involves content having tracks or track groups encrypted with their own DRM-keys. Multi-key playback is supported by both Widevine and PlayReady DRM environments. Depending on the DRM-backend capabilities it may be needed to execute one or more requests towards the backend in order to get all the keys needed for the playback. One of the multi-key setup is with audio and video tracks being encrypted with different keys. In this case the player creates two DRM sessions by default, one for audio and one for video, unless DRMToday backend is used and not-empty assetId(String) is provided. The application can also enforce usage of a single session if DRM-backend is certain to return all the keys in one request:

PlayerSDK.FORCE_SINGLE_DRM_SESSION = true;

More complex scenario involves video tracks or track groups having their own DRM-keys as well, consider SD-group-key, HD-group-key etc. It may happen that only some keys are delivered by the DRM-backend based on a device or service capabilities. In this case the player marks the video tracks with no keys as not-playable and disallows corresponding ABR and manual video quality switches. The application may decide to update the UI accordingly by getting the video track status with getDrmKeyStatus() or simply checking isDisabled(). DRM-key status shall also be checked in the following callback:

PlayerController playerController;
..
playerController.addPlayerListener(new AbstractPlayerListener() {
  @Override
  public void onTrackKeyStatusChanged() {
      super.onTrackKeyStatusChanged();
      for (VideoTrackQuality videoTrackQuality : playerController.getVideoTrack().getQualities()) {
          boolean isDisabled = videoTrackQuality.isDisabled();
          ..
      }
  }
});

Key rotation

PRESTOplay SDK for Android supports smooth playback of streams where DRM keys are changed every crypto period and each DRM license contains keys for crypto periods N, N-1 and N+1. ContentProtection elements in manifest are not expected to contain any PSSH data, otherwise the player issues unnecessary key requests:

<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011"/>
<ContentProtection schemeIdUri="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"/>

The same holds for MP4 init segments, which are not expected to contain any DRM data. If needed, the player can be configured to ignore PSSH data in the manifest and init segments:

PlayerSDK.FORCE_IN_STREAM_DRM_INIT_DATA = true;
FragmentedMp4Extractor.IGNORE_DRM_DATA_IN_INIT_SEGMENT = true;

Normally, the player creates a new DRM session and issues license request for each crypto period. To do so, it needs a crypto period index to be present and setup in WV header. In case the crypto period index is not set, then when changing crypto periods and video quality at the same time, the player will not be able to decide that a new session is needed. This results in late session creation when changing crypto period one more time and the playback would need to wait for the license response.

It is recommended to setup crypto period index in WV header so that the player could properly do DRM key requests.

Key rotation is supported out of the box on devices with API >= 23 and for older devices the player shall be configured explicitly with DrmConfiguration.keyRotation or DrmTodayConfiguration.keyRotation. This is needed to enable session sharing on these devices so that the player is able to rotate the keys.

Clear Lead

If you have encoded your encrypted content with a clear lead of a few seconds, the player will leverage that by default and start playback immediately while fetching the required license keys in the background. If you do not want this behavior and instead want the player to wait until license keys are fetched, use the builder’s playClearSamplesWithoutKeys(boolean) method to disable the feature.

DRMtoday OnBoard

When the player is used together with DRMtoday OnBoard, you need to configure additional parameters in the DrmTodayConfiguration. Specifically, you need to enable DRMtoday OnBoard and you need to set the base URL to the DRMtoday OnBoard server. The following example shows how you can create a configuration that can be use with the OnBoard server:

DrmTodayConfiguration.Builder builder  = new DrmTodayConfiguration.Builder(
        DrmTodayConfiguration.DRMTODAY_STAGING,
        "purchase", // The user ID
        "sessionId", // The session ID
        "six", // The merchant
        "demo", // The asset ID
        Drm.BestAvailable); // Select a DRM
builder.drmTodayMobile(true); // Set the DRM Mobile flag
builder.drmTodayMobileUrl("..."); // Set the DRMToday Mobile url

DRM Integration with Other Service Providers

You can also use DRM systems other than DRMtoday with the SDK. To do this, create instances of DrmConfiguration instead of the DRMtoday specific one.

This will not work for OMA-DRM, but you can use it for Widevine and PlayReady.

You can use the DrmConfiguration object to configure the DRM backend URL. If you need to pass additional parameters, you can use the DrmConfiguration#requestParameters Bundle. All key/value pairs in that bundle will be added as request (header) parameters to the license request.

If you need to modify not only the headers but you would like to dynamically add query parameters, change the headers, or modify and transform the data that is send to or received from a custom DRM server, you can use your own implementations of RequestModifier and ResponseModifier.

Modifiers can be added on the PlayerController (see addRequestModifier() and addResponseModifier()) and you should do that before you load content. The modifier will then receive a callback with a Request or Response instance that you can modify to change the URI or add header parameters or transform the payload or response data.

Note that you can also add query parametersf or requests by changing the URI directly. request.getUri().buildUpon() will give you a builder that can be used to change the URI. Once changed you can set the new URI in the request. The same is true for payload data.

Here is an example of a request and response modifier that adds a header parameter and a query parameter for license requests and modifies the request and reponse payload:

// We need the player controller to attach a modifier
PlayerController pc = ...

pc.addRequestModifier(new RequestModifier() {
    @NonNull
    @Override
    public Request onRequest(@NonNull Request request) {
        if (request.type == Request.DATA_TYPE_DRM_LICENSE) {
            try {
                // add a query parameter to the URI by building upon the current URI
                // and then set the new URL
                request.setUri(request
                        .getUri()
                        .buildUpon()
                        .appendQueryParameter("Query-Param", "Query Value")
                        .build();
                // Assume you need to send some custom JSON to the license backend
                // instead of the original payload.
                request.headers.put("Content-Type", "application/json");
                JSONObject jsonData = new JSONObject();
                jsonData.put("licenseData", Base64.encode(request.getData(), Base64.DEFAULT));
                // Pass the transformed data back to the request
                request.setData(jsonData.toString().getBytes());
            } catch (JSONException e) {
                // something went wrong while encoding the json object
            }
        }
        // Note that we always need to return the request!
        return request;
    }
});

pc.addResponseModifier(new ResponseModifier() {
    @NonNull
    @Override
    public Response onResponse(@NonNull Response response) {
        if (response.type == Request.DATA_TYPE_DRM_LICENSE) {
            try {
                // Lets assume that the license server responds with some
                // JSON object. In that case we need to extract the license data
                // here and transform it so that the underlying CDM accepts the
                // data
                JSONObject jsonResponse = new JSONObject(new String(response.getData()));
                // Pass the transformed data back to the response
                response.setData(
                        Base64.decode(jsonResponse.getString("licenseData"), Base64.DEFAULT)
                );
            } catch (JSONException e) {
                // something went wrong while decoding the response
            }
        }
        // note that we always need to return the response
        return response;
    }
});

...

Root Device Detection

You can setup a callback in the PlayerSDK to be informed about the device status and receive information if the current device is considered rooted or not. For that, you need to setup a callback:

PlayerSDK.SYSTEM_STATUS_CALLBACK = new PlayerSDK.SystemStatusCallback() {
    @Override
    public void onSystemStatusCheck(boolean rooted) {
        Log.i(TAG, "System status check. Rooted: " + rooted);
    }
};

The callback will be triggered shortly after the PlayerSDK.init(…) call. Please note that the callback might be triggered multiple times and will be triggered on the main thread.

Previous topic: The Player View
Next topic: Global Configuration