Files
unshackle/docs/reference/configuration/drm.md
T
imSp4rky 89f09f8e84 perf(drm): decrypt fMP4 segments during download with mp4decrypt
Opt-in decrypt_segments runs mp4decrypt --fragments-info per segment on a thread pool as segments land (DASH, single-init HLS), then decrypts the init alone; hides the post-download decrypt step with byte-identical output.
2026-08-26 16:27:55 -06:00

7.2 KiB

DRM & CDM

These are the config keys. For concepts, device provisioning, and the wvd/prd commands see the DRM & CDM guide. Device files themselves live in directories.wvds (.wvd) and directories.prds (.prd).

cdm

  • Type: dict  ·  Default: {}

Maps a service tag to the CDM (Widevine/PlayReady device) to use, with a default fallback. Resolution is case-insensitive: an override (dl --cdm <name>, or the cdm field of an API job) wins, then the per-service entry, then default. An override pins one device for the whole run and skips any quality or Widevine/PlayReady sub-entries.

cdm:
  default: chromecdm_l3
  EXAMPLE2: android_l1

A value may also be a nested dict for advanced selection by quality, DRM system, or profile:

cdm:
  EXAMPLE1:
    ">=1080": android_l1        # by track height
    "<1080": chromecdm_l3
    widevine: android_l1        # by DRM system
    playready: sl3000
    default: chromecdm_l3

Quality keys can hold 1080, >=1080, >720, <=576, <480 style comparisons. DRM keys are widevine / playready. unshackle matches the quality keys first, but only when a video height is known. If the value is still a dict after that, unshackle uses widevine/playready when present, or else the credential profile name, then default.

unshackle finds the resolved name in remote_cdm by name first. If no entry matches, unshackle loads it as a local device file, in this order: <name>.prd in directories.prds, <name>.prd in directories.wvds, then <name>.wvd in directories.wvds.

remote_cdm

  • Type: list[dict]  ·  Default: []

A list of remote CDM definitions. unshackle matches each entry by its name (referenced from cdm), and its type selects the backend:

type Backend Key fields
decrypt_labs Decrypt Labs KeyXtractor host, device_name, secret (fields)
custom_api Fully YAML-configurable remote API host (required), device, auth, endpoints, request_mapping, response_mapping, caching, timeout (fields)
(none), Device Type: PLAYREADY pyplayready RemoteCdm host, secret, device_name, security_level (default 3000)
(none), otherwise pywidevine RemoteCdm host, secret, device_name, device_type, system_id, security_level (default 3000)
remote_cdm:
  - name: keyxtractor
    type: decrypt_labs
    device_name: L1
  - name: my_wv_server
    host: https://cdm.example.com
    secret: s3cr3t
    device_name: android_l1
    device_type: ANDROID
    system_id: 26830
    security_level: 1

For the two pywidevine/pyplayready backends, field names are read case-insensitively in both styles: Device Type/device_type, System ID/system_id, Security Level/security_level, Host/host, Secret/secret, Device Name/device_name.

!!! warning "PlayReady host needs a /playready suffix" pyplayready's RemoteCdm treats host as a base URL and appends its own endpoint paths, so a PlayReady entry's host must include the trailing /playready segment (for example https://cdm.example.com/playready). Without it the server returns 404 rather than a configuration error.

decrypt_labs fields

Field Default Notes
host https://keyxtractor.decryptlabs.com
device_name ChromeCDM ChromeCDM, L1, L2 (Widevine) or SL2, SL3 (PlayReady)
secret from decrypt_labs_api_key Sent as the decrypt-labs-api-key header. An error is raised if neither is set.
system_id 26830 (Widevine), 0 (PlayReady)
security_level Widevine 3; PlayReady 2000 for SL2, else 3000

unshackle uses PlayReady mode when device_type is PLAYREADY or device_name starts with SL.

custom_api fields

Field Type Notes
host str Required. Base URL of the API.
device dict name, type (CHROME, ANDROID, PLAYREADY), system_id, security_level
auth dict type (default header): header uses header_name (default Authorization) and key; bearer uses bearer_token or key; basic uses username and password. custom_headers is merged in for any type.
endpoints dict get_request and decrypt_response, each {path, method, timeout}
request_mapping dict Per endpoint: param_names, static_params, conditional_params, transforms, nested_params, exclude_params
response_mapping dict Per endpoint: fields, transforms, response_types, success_conditions, error_fields, key_fields
caching dict enabled, use_vaults, check_cached_first
legacy dict Legacy mode options.
timeout int Default 30. Request timeout in seconds.

Transforms cover base64/hex/JSON encoding and kid:key parsing. unshackle uses PlayReady mode when device.type is PLAYREADY or the device name is SL2/SL3.

remote_cdm:
  - name: my_custom
    type: custom_api
    host: https://cdm.example.com
    device:
      name: ChromeCDM
      type: CHROME
      system_id: 26830
      security_level: 3
    auth:
      type: bearer
      key: your-token
    endpoints:
      get_request:
        path: /get-challenge
        method: POST
      decrypt_response:
        path: /get-keys
        method: POST
    timeout: 30

decryption

  • Type: str or dict  ·  Default: "shaka"

Selects the tool that physically decrypts CENC-encrypted tracks. unshackle compares the value case-insensitively. "mp4decrypt" selects Bento4's mp4decrypt, and anything else (including "shaka") selects shaka-packager. When the value is a dict, unshackle compares the service tag case-insensitively and uses the default entry as the fallback.

unshackle decrypts HLS AES-128 ClearKey in-process and ignores this setting.

=== "Global"

```yaml
decryption: shaka
```

=== "Per-service"

```yaml
decryption:
  default: shaka
  EXAMPLE1: mp4decrypt
```

decrypt_segments

  • Type: bool  ·  Default: false

Decrypts each fragmented MP4 segment as it arrives, instead of decrypting the whole track after the download ends. The decryption runs during the download, so the wait after the last segment goes away. The output file is the same either way.

This works with mp4decrypt only. Set decryption to mp4decrypt for the service (or as the default); with shaka the option has no effect, because shaka-packager cannot decrypt a segment without its init segment. It applies to DASH tracks and to HLS tracks that use one init segment, one EXT-X-KEY and no EXT-X-DISCONTINUITY. Every other track keeps the whole-file decryption.

!!! note "No resume"

A track that decrypts segment by segment always downloads from the start.
[`continue_downloads`](download.md#continue_downloads) cannot reuse its segments, because a reused segment
is already decrypted.
decrypt_segments: true