DRM

Digital Rights Management is a technology used to protect and manage the distribution of digital content, including streaming media. DRM systems aim to prevent unauthorized access, copying, redistribution, and piracy of digital content by implementing various security measures.

Configuration

All DRM-related settings can be found in DrmConfiguration, which is an optional component of PlayerConfiguration.

These settings can be specified before creating a player like so:

let playerConfig = PlayerConfiguration(with: streamUrl, contentType: .hls)
playerConfig.drmConfiguration = DrmConfiguration(with: .drmToday)
playerConfig.drmConfiguration?.environment = .staging
playerConfig.drmConfiguration?.userId = "..."
playerConfig.drmConfiguration?.sessionId = "..."
playerConfig.drmConfiguration?.merchant = "..."
playerConfig.drmConfiguration?.assetId = "..."
playerConfig.drmSystem = .fairplay
let player = PRESTOplaySDK.shared.player(for: playerConfig)

If the drmConfiguration is left empty (nil), then it is assumed that there is no DRM system used and the stream is clear (unencrypted).

PlayerConfiguration has the property drmSystem which by default will set to .auto to attempt to automatically choose the DRM system used. However this can be explicitly set for example to .widevine.

Summary of DrmConfiguration properties

Property Description
environment DRM backend environment selector.
base Optional backend base URL hint.
preventSecondScreenPlayback Enables secondary-screen protection checks where supported.
userId Provider-specific user identifier.
sessionId Provider-specific session identifier.
merchant Merchant/account identifier used by the DRM backend.
assetId Asset identifier used by the DRM backend.
variantId Variant identifier used by the DRM backend.
authToken Authorization token used in DRM requests.
trackingToken Tracking token used by backend integrations.
keyUri Content key URI used by key retrieval flows.
license Inline license payload (Data).
licensingUrl License server URL.
licensingParameters Additional license-request parameters.
certificate Inline certificate payload (Data).
certificateUrl Certificate server URL.
certificateParameters Additional certificate-request parameters.
persistLicense Enables persistent-license behavior for offline-capable flows.

FairPlay Streaming (FPS)

FairPlay is a digital rights management (DRM) system developed by Apple Inc. It is specifically designed to protect and control the distribution of digital content, such as video and audio, in Apple’s ecosystem, including iTunes, Apple TV, and other Apple devices.

Set up FairPlay Streaming with the following steps:

  1. Prepare your HLS playlist by including the following #EXT-X-KEY entry
   #EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://"
  1. Set the DrmConfiguration object parameters
   let playerConfig = PlayerConfiguration(with: streamUrl)
   playerConfig.drmConfiguration = DrmConfiguration(with: .drmToday)
   playerConfig.drmConfiguration?.environment = .staging
   playerConfig.drmConfiguration?.userId = "..."
   playerConfig.drmConfiguration?.sessionId = "..."
   playerConfig.drmConfiguration?.merchant = "..."
   playerConfig.drmConfiguration?.assetId = "..."
   playerConfig.drmSystem = .fairplay
  1. (Optional) If you use a backend different than DRMToday, you need to set additional values for license and provisioning certificate requests
   playerConfig.drmConfiguration?.licensingUrl = "..."
   playerConfig.drmConfiguration?.certificateUrl = "..."
  1. (Optional) You can also set the assetId and variantId in the manifest. Note: if these values are set in the DrmConfiguration, then the values parsed from the manifest will be overwritten.
   #EXT-X-KEY:METHOD=SAMPLE-AES,URI="skd://drmtoday?assetId=yourAssetId&variantId=yourVariantId"

Widevine (WV)

Google Widevine is a digital rights management (DRM) solution developed by Google to protect and secure digital content distribution, particularly for video streaming and media playback. It is designed to prevent unauthorized copying and distribution of copyrighted content, ensuring that content providers can control how their content is consumed and accessed by users.

Security Levels: Widevine offers different security levels (L1, L2, L3) that correspond to different hardware and software capabilities on user devices. L1, the highest level, typically involves hardware-based DRM protection, making it more secure than L2 and L3.

CastlabsVTWithWidevine is an L3 implementation.

Set up Widevine with the following steps:

  1. Prepare your DASH manifest by including the required encryption configuration. Example can be found here:
   https://demo.cf.castlabs.com/media/bbb_abr/Manifest.mpd
  1. Set the DrmConfiguration object parameters
   let playerConfig = PlayerConfiguration(with: streamUrl)
   playerConfig.drmConfiguration = DrmConfiguration(with: .drmToday)
   playerConfig.drmConfiguration?.environment = .production
   playerConfig.drmConfiguration?.userId = "..."
   playerConfig.drmConfiguration?.sessionId = "..."
   playerConfig.drmConfiguration?.merchant = "..."
   playerConfig.drmConfiguration?.assetId = "..."
   playerConfig.drmSystem = .widevine
  1. (Optional) If you use a backend different than DRMToday, you need to set additional values for license and provisioning certificate requests
   playerConfig.drmConfiguration?.licensingUrl = "..."

The CastlabsVTWithWidevine plugin tightly cooperates with CastlabsVT (the .castlabs playback engine). Enable and register both plugins in the SDK to play protected content.

Clear Key

Clear Key DRM is a simple encryption method for content protection. To play Clear Key protected content, set the playerConfig.drmSystem to .clear_key.

```swift
playerConfig.contentUrl = URL(string: "https://demo.cf.castlabs.com/media/QA/oceans_aes/oceans_aes_drmtodayS_only_kid.m3u8")!
playerConfig.drmConfiguration?.userId = "..."
playerConfig.drmConfiguration?.sessionId = "..."
playerConfig.drmConfiguration?.merchant = "..."
playerConfig.contentType = .hls
playerConfig.drmSystem = .clear_key
```

When PRESTOplaySDK.shared.enableClearKeyCache is set to true, Clear Key licenses are stored locally in memory. These caches are automatically destroyed when the application is closed.

```swift
PRESTOplaySDK.shared.enableClearKeyCache = true
```

Request interceptors

You can intercept HTTP/S requests and responses issued for DRM-related steps through RequestModifier and ResponseModifier classes.

Example request and response modifier:

struct RequestModifier: RequestModifierProtocol {
    func modify(_ request: Request,
                completionHandler: @escaping (Request?) -> Void) {
        completionHandler(request)
    }
}

struct ResponseModifier: ResponseModifierProtocol {
    func modify(_ response: Response,
                completionHandler: @escaping (Response?) -> Void) {
        completionHandler(response)
    }
}

To install the interception at the SDK level:

PRESTOplaySDK.shared.requestModifiers.append(requestModifier)
PRESTOplaySDK.shared.responseModifiers.append(responseModifier)

To install the interception at player level:

let player = PRESTOplaySDK.shared.player()
// ...
player.requestModifiers = [requestModifier]
player.responseModifiers = [responseModifier]

A sample demo DRMTodayDemo, is provided to demonstrate integration with DRMToday. This approach can also be adapted for other DRM backends.

To intentionally fail a given Response, set its data field to nil.

Accessing DRMToday error codes

There are two integration paths:

  1. Use request/response modifiers to check directly the response from DRMtoday.
  2. Inspect PRESTOerror in your playback callbacks.

1. Request/response modifiers

Use this when you want explicit DRMtoday error from license/provisioning responses.

struct ResponseModifier: ResponseModifierProtocol {
    func modify(
        _ response: Response,
        completionHandler: @escaping (Response?, PRESTOerror?) -> Void
    ) {
        if response.type == .license,
           let error = Utils.mapToDRMTodayErrors(response) {
            completionHandler(response, error)
            return
        }
        // Optional: manually parse DRMtoday headers/body here and map to your own app error model.
        completionHandler(response, nil)
    }

    func modify(_ response: Response, completionHandler: @escaping (Response?) -> Void) {
        completionHandler(response)
    }
}

Attach this modifier at SDK level (PRESTOplaySDK.shared.responseModifiers) or player level (player.responseModifiers).

2. Check PRESTOerror properties

Use this in playback callbacks (player.error, onState, onError) to read the propagated details:

if let error = player.error, error.type == .drm_today {
    let responseCode = error.data["DRMTodayResponseCode"] // DRMtoday x-dt-resp-code, if present
    let parent = error.parentError as NSError?
    let mappedCode = parent?.code // mapped DRMtoday exception id
    let mappedMessage = parent?.localizedDescription // mapped DRMtoday message
}

Offline licensing and prefetching

Use PRESTOplaySDK.shared.prefetcher(for:) to retrieve DRM keys before playback/download. This is the public API for offline-license prefetching.

var config = PlayerConfiguration(with: streamUrl)
config.drmConfiguration = DrmConfiguration(with: .drmToday)
config.drmConfiguration?.persistLicense = true

guard let prefetcher = PRESTOplaySDK.shared.prefetcher(for: config) else {
    return
}

guard prefetcher.canPrefetch(config) else {
    return
}

prefetcher.prefetchKeys { error in
    if let error {
        // Handle prefetch failure
        return
    }
    // Prefetch completed
}

Prefetcher API surface

  • canPrefetch(_:): check if the current configuration is supported by the active DRM implementation.
  • prefetchKeys(_:): trigger key acquisition.
  • deleteKeys(): remove keys for the current content scope (engine-specific behavior).
  • deleteAllPersistentData(): remove all stored DRM key material/metadata.
  • playDurationLeft(...): remaining play window, when available.
  • storageDurationLeft(...): remaining storage window, when available.

FairPlay-specific notes

  • Offline FairPlay HLS should expose keys using EXT-X-SESSION-KEY (instead of EXT-X-KEY) so eligible keys can be prefetched (source):
#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI="skd://"
  • For offline playback from local files, use the local URL as contentUrl.
  • PlayerConfiguration(with: localUrl) restores DRM-related fields from downloader metadata when available (including originUrl for protected offline playback).

Widevine-specific notes

  • Widevine prefetching is available through CastlabsVTWithWidevine with the .castlabs playback engine.
  • Use the same prefetcher flow (canPrefetch -> prefetchKeys) and handle completion errors from the callback.

Expiry inspection

After successful prefetch, use duration APIs to inspect license windows:

let playLeft = prefetcher.playDurationLeft()
let storageLeft = prefetcher.storageDurationLeft()

Meaning of each value:

  • playDurationLeft: remaining time the user can continue playback once the play window is active.
  • storageDurationLeft: remaining time the stored/offline license remains valid on device.

Availability of both values depends on DRM backend and license policy.

Dual license expiry for offline rentals

To limit the license usage time. Dual expiry window is used:

  • playDuration can be defined to limit total play duration of a stream
  • expirationDate can be defined to set the absolute date when stream will no longer be possible to play within the license.

Upfront authentication token

A token used for upfront authentication is considered invalid after 10 minutes of its creation or because it was already used by another device. If the client attempts to get a license with an invalid token DRMToday will reply with a 403.

To renew an expired authentication token, implement the PRESTOplaySDK.shared.onDeviceAuthTokenRequired() method to provide a new authentication key. For example,

PRESTOplaySDK.shared.onDeviceAuthTokenRequired = { [weak self] playerConfiguration in
    guard let self,
          let configurationUrl = playerConfiguration.configurationUrl else {
        return nil
    }

    var authToken: String?
    let group = DispatchGroup()
    group.enter()
    Utils.loadAuthToken(from: configurationUrl, userToken: self.userToken) { token in
        authToken = token
        group.leave()
    }
    let _ = group.wait(timeout: .now() + 10)
    return authToken
}

In the example code, the userToken is a key obtained from your DRM provider.

HDCP protection level support

PRESTOplay does not expose a public API to read or force a specific HDCP level (for example HDCP 1.x vs 2.x). The effective HDCP requirement is resolved by platform DRM and license policy at runtime.

For PlayerEngine.apple:

  • HDCP/output protection requirements come from FairPlay content policy (license server + stream signaling).
  • In HLS/FairPlay workflows, HDCP policy is typically signaled and enforced by the FairPlay pipeline (for example via playlist and license policy).
  • The final behavior depends on sink and route capabilities (for example HDMI path, adapter, display, mirroring route).
  • When output protection is not sufficient, use SDK integration signals:
    • throwFatalErrorOnContentObscured callback
    • DRM/system playback errors (for example second-screen protection errors when enabled)

For castLabs playback (PlayerEngine.castlabs, including GStreamer-based pipelines configured through player setup):

In short: HDCP level selection is platform/license driven.

Second-screen and screen-capture protection

Second-screen output (for example AirPlay, mirroring, and external displays) and on-device screen recording are handled through SDK protection controls and runtime errors.

Public controls

protectedWithoutDRM is intended for streams protected by transport/security policy outside SDK DRM workflows. When enabled, the player applies capture-protection behavior even if no DRM configuration is attached.

Runtime behavior

If drmConfiguration.preventSecondScreenPlayback == true:

  • Second-screen/external-display output triggers .drm_second_screen_detected.
  • Screen capture/recording triggers .drm_screen_recording_detected.
  • Playback is stopped by the SDK.

If drmConfiguration.preventSecondScreenPlayback == false:

  • SDK second-screen/capture checks are not enforced.
  • DRM/provider/platform policy can still restrict playback.

If player.protectedWithoutDRM == true:

  • Capture protection is enforced even without a DRM session.
  • This is useful for protected non-DRM playback pipelines (for example externally managed key delivery).
  1. Keep drmConfiguration.preventSecondScreenPlayback = true for protected content.
  2. Set player.protectedWithoutDRM = true when content requires capture safeguards without a DRM session.
  3. Handle .drm_second_screen_detected and .drm_screen_recording_detected in SDK error callbacks.
  4. For PlayerEngine.apple, optionally use throwFatalErrorOnContentObscured to control app behavior when output becomes obscured.

throwFatalErrorOnContentObscured behavior (PlayerEngine.apple)

Use this callback when you want explicit control over app behavior when player reports obscured output caused by insufficient external protection.

  • Called with obscured == true when output becomes obscured.
  • Called with obscured == false when output is no longer obscured.
  • Return true to let the SDK raise a playback error for the obscured state.
  • Return false to avoid raising that error and wait for output conditions to improve.
  • If callback is not set, SDK uses default behavior and raises the playback error on obscured output.

Example:

player.throwFatalErrorOnContentObscured = { obscured in
    if obscured {
        // Update UI and pause user interaction if needed.
        // Return true to fail playback, false to wait.
        return true
    }
    // Output protection is restored.
    return false
}

Protect content without DRM

You can enable content protection without using DRM by manually setting the protectedWithoutDRM property of the player. When enabled, the player will enforce screen capture protection. This is useful when the decryption key is obtained by other means, such as when HLS content is protected using standard transport method.

This property does not create a DRM license session. It enables player-side protection checks/capture safeguards for non-DRM playback pipelines.

player.protectedWithoutDRM = true