On this page

DoveRunner DRM

Introduction

Qencode offers built-in integration with DoveRunner DRM. Through this integration, Qencode can encrypt video streams and deliver them with DRM protections that DoveRunner will license at playback time.

Prerequisites

  • Qencode
    • Project API key - used to obtain a session token (/v1/access_token) and run jobs.
  • DoveRunner
    • Site ID, Site Key, Access Key
    • KMS Token - used to authenticate CPIX API requests. You can find it in the DoveRunner Console under Multi-DRM > DRM Setting.
    • Content ID (CID / content_id) (the identifier you'll use when generating license tokens)

1
Get encryption material from DoveRunner (CPIX)

DoveRunner provides a CPIX API for exchanging encryption keys with third-party encoders and transcoders. You send a CPIX XML request with your content ID and the DRM system(s) you need, and DoveRunner returns the encryption keys.

The CPIX API endpoint is:

POST https://drm-kms.doverunner.com/v2/cpix/doverunner/getKey/{kms-token}

CPIX request

The request body is an XML document specifying the content ID and the DRM system. For Widevine, use systemId edef8ba9-79d6-4ace-a3c8-27dcd51d21ed:

<?xml version="1.0" encoding="UTF-8"?>
<cpix:CPIX id="test-content-01"
    xmlns:cpix="urn:dashif:org:cpix"
    xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc"
    xmlns:speke="urn:aws:amazon:com:speke">
  <cpix:ContentKeyList>
    <cpix:ContentKey kid="786d2d0a-9857-4ab4-8377-21a6cba1ca24"/>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="786d2d0a-9857-4ab4-8377-21a6cba1ca24"
        systemId="edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"/>
  </cpix:DRMSystemList>
</cpix:CPIX>

The kid attribute can be any UUID you generate. The id attribute on the root element is your content ID.

Request Example

Sending a CPIX request to DoveRunner KMS:

curl -X POST \
  "https://drm-kms.doverunner.com/v2/cpix/doverunner/getKey/YOUR_KMS_TOKEN" \
  -H "Content-Type: application/xml" \
  -d '<?xml version="1.0" encoding="UTF-8"?>
<cpix:CPIX id="test-content-01"
    xmlns:cpix="urn:dashif:org:cpix"
    xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc"
    xmlns:speke="urn:aws:amazon:com:speke">
  <cpix:ContentKeyList>
    <cpix:ContentKey kid="786d2d0a-9857-4ab4-8377-21a6cba1ca24"/>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="786d2d0a-9857-4ab4-8377-21a6cba1ca24"
        systemId="edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"/>
  </cpix:DRMSystemList>
</cpix:CPIX>'
import uuid
import urllib.request

KMS_TOKEN = "YOUR_KMS_TOKEN"
CONTENT_ID = "test-content-01"
KID = str(uuid.uuid4())

body = f"""<?xml version="1.0" encoding="UTF-8"?>
<cpix:CPIX id="{CONTENT_ID}"
    xmlns:cpix="urn:dashif:org:cpix"
    xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc"
    xmlns:speke="urn:aws:amazon:com:speke">
  <cpix:ContentKeyList>
    <cpix:ContentKey kid="{KID}"/>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="{KID}"
        systemId="edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"/>
  </cpix:DRMSystemList>
</cpix:CPIX>"""

req = urllib.request.Request(
    f"https://drm-kms.doverunner.com/v2/cpix/doverunner/getKey/{KMS_TOKEN}",
    data=body.encode("utf-8"),
    headers={"Content-Type": "application/xml"},
    method="POST",
)
with urllib.request.urlopen(req) as resp:
    print(resp.read().decode("utf-8"))

CPIX response

DoveRunner returns a CPIX XML response containing the encryption material:

<?xml version="1.0" ?>
<cpix:CPIX id="test-content-01"
    xmlns:cpix="urn:dashif:org:cpix"
    xmlns:pskc="urn:ietf:params:xml:ns:keyprov:pskc"
    xmlns:speke="urn:aws:amazon:com:speke">
  <cpix:ContentKeyList>
    <cpix:ContentKey commonEncryptionScheme="cenc"
        explicitIV="NHc5dmE5aHF0Z2NsZXVlMQ=="
        kid="e2b281ba-fba5-8760-cfa6-5ae6f3ddd630">
      <cpix:Data>
        <pskc:Secret>
          <pskc:PlainValue>xgjCQm5OdSC4tQ8Lgl9uUg==</pskc:PlainValue>
        </pskc:Secret>
      </cpix:Data>
    </cpix:ContentKey>
  </cpix:ContentKeyList>
  <cpix:DRMSystemList>
    <cpix:DRMSystem kid="e2b281ba-fba5-8760-cfa6-5ae6f3ddd630"
        systemId="edef8ba9-79d6-4ace-a3c8-27dcd51d21ed">
      <cpix:PSSH>AAAAUXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADEIARIQ4rKBuvulh2DPplrm893WMBoKZG92ZXJ1bm5lciIPdGVzdC1jb250ZW50LTAx</cpix:PSSH>
    </cpix:DRMSystem>
  </cpix:DRMSystemList>
</cpix:CPIX>

Extract values for Qencode

From the CPIX response, extract and convert the values needed for the Qencode cenc_drm object:

  • key_id: the kid attribute from ContentKey, with dashes removed (e.g. e2b281bafba58760cfa65ae6f3ddd630)
  • key: the pskc:PlainValue content, decoded from base64 to hex (e.g. c608c2426e4e7520b8b50f0b825f6e52)
  • iv: the explicitIV attribute, decoded from base64 to hex (e.g. 34773976613968717467636c65756531)
  • pssh: the cpix:PSSH content, used as-is in base64

2
Encrypt during transcoding in Qencode (cenc_drm)

In your Qencode job JSON, you'll:

  • choose a packaging output such as 'advanced_dash'
  • add a cenc_drm block inside the same format item
  • provide key_id, key, iv, pssh, and la_url

Example Qencode job (DASH + Widevine CENC)

{
  "query": {
    "source": "https://example.com/source.mp4",
    "format": [
      {
        "output": "advanced_dash",
        "destination": {
          "url": "s3://YOUR_BUCKET/path/to/output/",
          "key": "YOUR_STORAGE_KEY",
          "secret": "YOUR_STORAGE_SECRET"
        },
        "stream": [
          {
            "video_codec": "libx264",
            "audio_codec": "aac",
            "width": 1280,
            "height": 720,
            "bitrate": 3000,
            "audio_bitrate": 128
          }
        ],
        "cenc_drm": {
          "key_id": "e2b281bafba58760cfa65ae6f3ddd630",
          "key": "c608c2426e4e7520b8b50f0b825f6e52",
          "iv": "34773976613968717467636c65756531",
          "pssh": "AAAAUXBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADEIARIQ4rKBuvulh2DPplrm893WMBoKZG92ZXJ1bm5lciIPdGVzdC1jb250ZW50LTAx",
          "la_url": "https://drm-license.doverunner.com/ri/licenseManager.do"
        }
      }
    ]
  }
}

cenc_drm object params description

  • key_id: the kid from the CPIX response, hex without dashes.
  • key: the content key from the CPIX response, decoded from base64 to hex.
  • iv (optional): the explicitIV from the CPIX response, decoded from base64 to hex. Should be specified if present in the CPIX response.
  • pssh: the Widevine PSSH from the CPIX response, base64-encoded.
  • la_url (optional): license server URL. DoveRunner uses the same URL for both Widevine and PlayReady: https://drm-license.doverunner.com/ri/licenseManager.do.

The full example Python script that performs CPIX key exchange and launches a Qencode encoding job with Widevine encryption is available here.

3
Generate DoveRunner license token (server-side)

This part is not Qencode API — it's your application backend that prepares playback authorization for DoveRunner. The license token must be generated server-side and has a 10-minute validity window from the timestamp.

Never generate tokens in the browser/app — keep site_key and access_key private.

Python example

DoveRunner (formerly PallyCon) provides a Python SDK on PyPI. Install it with pip install python-pallycon.

Request Example

Generating a DoveRunner license token:

from pallycon import PallyConClient

client = PallyConClient(
    site_id="YOUR_SITE_ID",
    site_key="YOUR_SITE_KEY",
    access_key="YOUR_ACCESS_KEY",
    drm_type=PallyConClient.DrmType.WIDEVINE.value,
    user_id="your-user-id",
    content_id="your-content-id",
    license_rule={
        "policy_version": 2,
        "playback_policy": {
            "persistent": False,
            "license_duration": 3600
        }
    },
)

license_token = client.license_token
print(license_token)
Warning
Warning:
The content_id must match the content ID used in the CPIX request on step 1.

Recommended backend pattern

Create an endpoint like:

  • GET /api/drm/token?content_id=...

It should:

  1. Authenticate the viewer (session/JWT).
  2. Check entitlement (is this user allowed this content?).
  3. Generate and return the license token string.

Because the token expires after 10 minutes, generate a fresh token for each playback session.

4
Configure playback to request the license from DoveRunner

At playback time, your player needs:

  • Manifest URL — your Qencode output .mpd
  • DoveRunner license server URLhttps://drm-license.doverunner.com/ri/licenseManager.do
  • License token — from your backend (step 3)

The license token must be sent as a custom HTTP header with the license request:

  • Header name: pallycon-customdata-v2
  • Header value: the license token (base64-encoded string)

Qencode Player example

Request Example

Configuring Qencode Player for DoveRunner Widevine playback:

var params = {
    licenseKey: "YOUR_QENCODE_PLAYER_LICENSE_KEY",
    videoSources: {
        src: "https://yourserver.com/content/playlist.mpd",
        keySystems: {
            "com.widevine.alpha": "https://drm-license.doverunner.com/ri/licenseManager.do"
        },
        emeHeaders: {
            "pallycon-customdata-v2": "YOUR_LICENSE_TOKEN"
        }
    }
}

qPlayer('my_player', params, function() {
    console.log("Player is loaded");
});

Player flow

  1. Player loads the MPD manifest (encrypted segments).
  2. Player detects DRM init data (PSSH) in the manifest.
  3. Player sends a license request to the DoveRunner license server with the pallycon-customdata-v2 header.
  4. DoveRunner validates the token and returns the license.
  5. Playback starts.