Using Widevine DRM with Unified Origin

This tutorial explains how to set up a full DRM implementation using Google’s Widevine for MPEG-DASH. It goes through the whole process step by step:

  • Acquiring the DRM configuration information from the Widevine test server
  • Parsing the DRM configuration response to get the necessary content protection information
  • Converting the key information from Widevine to the right format for Unified Origin
  • Using the acquired configuration information to add Widevine protection to a stream
  • Checking to make sure you did everything right and your content is indeed protected

On the side of Origin, this process is very short and simple (it requires you to add the necessary DRM configuration information when creating the server manifest that you set up a new stream with, that's it). However, this tutorial also goes into the details about the first steps of the process, that are about getting the right information from the Widevine test server.

If you're not interested in that part, it's recommended to check our regular DRM documentation on setting up Widevine instead, as that uses a quick and easy script that takes care of the whole process for you: Widevine Modular DRM.

Before you start

Before starting please make sure that:

  • Your USP software license supports DRM (if it does not and you would like to try this functionality, please contact us via sales@unified-streaming.com or directly through your account manager)
  • Unified Origin is properly installed, which includes 'mp4split' (see: How to Install)
  • You have downloaded the Tears of Steel test content (see: Verify Your Setup)
  • Your server has an outgoing connection to the internet (this tutorial will make a request to the Widevine License Aquisition Server)
  • Python 3 installed on your server

Note

Some of the commands in this tutorial may require you to use sudo, depending on the exact rights of your user and the configuration of your system.

Setting up your test environment

To set up your test environment make sure that Apache is running and that you have stored the tears-of-steel directory with the test content in the DocumentRoot of the Apache virtual host that you configured for Origin. This tutorial presumes that this DocumentRoot is located in the /var/www/ directory (i.e., the test content will be located in /var/www/tears-of-steel).

Getting the DRM configuration information (from the Widevine LA test server)

In the ISO Base Media File Format (ISO BMFF, better known as MP4) the Protection System Specific Header (PSSH) box contains information about the DRM system that the stream is protected with. Based on this information, a client can acquire the license (including the key) that is needed to decrypt and play the media content.

In certain circumstances Origin can generate the PSSH data itself, based on other DRM related information that is provided, but in this tutorial we'll explicitly add the PSSH data via a command-line option. As this tutorial is about Widevine, the option is --widevine.drm_specific_data.

Of course, we need to get this PSSH data from somewhere. In this case, we will use the Widevine's license acquisition (LA) test server to request it. In order to do that, we will send a Base64 encoded JSON object to this LA test server.

First and foremost, this JSON object specifies a Content ID, which is not the same as a KID (although it serves a similar purpose within the realm of Widevine DRM, i.e., identifying the content). In response to our request that sends the JSON object, Widevine's LA test server will send back a Base64 encoded JSON object that contains multiple Key ID (KID) and Content Encryption Key (CEK) pairs, as well as the PSSH data for each these pairs.

This is all the information we need to add Widevine DRM to our stream that is delivered through Origin.

A closer look at the JSON object that we will send to the LA test server

For this tutorial, the JSON object that we'll send to the Widevine LA test server looks like below, with ZmtqM2xqYVNkZmFsa3Izag== as the Base64 encoded Content ID:

{
  "content_id": "ZmtqM2xqYVNkZmFsa3Izag==",
  "tracks": [
    { "type": "SD" },
    { "type": "HD" },
    { "type": "AUDIO" }
  ],
  "drm_types": [ "WIDEVINE" ],
  "policy": ""
}

Besides the Content ID, there are three types of tracks specified as well. The LA test server will send back a different KID and CEK pair for each of them. These different key pairs may be used to increase the level of protection by using multiple key DRM, but we will limit ourselves to using only one of the key pairs that is returned in this tutorial (since using mutliple key DRM with Unified Origin requires Introduction to CPIX and that's not what this tutorial is about).

Now, when we Base64 encode our JSON object:

echo '{
  "content_id": "ZmtqM2xqYVNkZmFsa3Izag==",
  "tracks": [
    { "type": "SD" },
    { "type": "HD" },
    { "type": "AUDIO" }
  ],
  "drm_types": [ "WIDEVINE" ],
  "policy": ""
}' | base64

It looks like this:

ewogICJjb250ZW50X2lkIjogIlptdHFNMnhxWVZOa1ptRnNhM0l6YWc9PSIsCiAgInRyYWNrcyI6IFsKICAgIHsgInR5cGUiOiAiU0QiIH0sCiAgICB7ICJ0eXBlIjogIkhEIiB9LAogICAgeyAidHlwZSI6ICJBVURJTyIgfQogIF0sCiAgImRybV90eXBlcyI6IFsgIldJREVWSU5FIiBdLAogICJwb2xpY3kiOiAiIgp9Cg==

Sending the request for the DRM configuration information

Now that we have the data that we need to send to the Widevine LA test server, the next step is to actually send and get the response with the information we need to add Widevine protection to our stream.

To do this we need:

  • URL of the Widevine LA test server
  • Base64 encoded version of the JSON object to send to the LA test server
  • Base64 encoded, encrypted hash of the JSON object, to serve as the signature of the request [3]

And we send it like this:

la_server=http://license.uat.widevine.com/cenc/getcontentkey/widevine_test
base64_json=ewogICJjb250ZW50X2lkIjogIlptdHFNMnhxWVZOa1ptRnNhM0l6YWc9PSIsCiAgInRyYWNrcyI6IFsKICAgIHsgInR5cGUiOiAiU0QiIH0sCiAgICB7ICJ0eXBlIjogIkhEIiB9LAogICAgeyAidHlwZSI6ICJBVURJTyIgfQogIF0sCiAgImRybV90eXBlcyI6IFsgIldJREVWSU5FIiBdLAogICJwb2xpY3kiOiAiIgp9Cg==
signature=kwVLL4xVh9mnlZlPqiEWN0E+FsvG0y+/oy451XXeIMo=

export wv_response=$(curl -v POST \
  -H 'Content-Type: application/json' \
  -d '{"request": "'$base64_json'", "signature": "'$signature'", "signer": "widevine_test" }' \
  ${la_server})

After which we check the content of the response:

echo ${wv_response}

Which should result in JSON object with a long Base64 encoded string like this (n.b., the Base64 encoded string itself also represents a JSON object):

{"response":"eyJzdGF0dXMiOiJPSyIsImRybSI6W3sidHlwZSI6IldJREVWSU5FIiwic3lzdGVtX2lkIjoiZWRlZjhiYTk3OWQ2NGFjZWEzYzgyN2RjZDUxZDIxZWQifV0sInRyYWNrcyI6W3sidHlwZSI6IlNEIiwia2V5X2lkIjoiQXBTNVdaMTFYZUs3OFAzS1A2WHF0dz09Iiwia2V5IjoiTzlvdlFEUk1mZTloUWllNXdQQStKZz09IiwicHNzaCI6W3siZHJtX3R5cGUiOiJXSURFVklORSIsImRhdGEiOiJJaEJtYTJvemJHcGhVMlJtWVd4cmNqTnFTT1BjbFpzRyIsImJveGVzIjoiQUFBQU9IQnpjMmdBQUFBQTdlK0xxWG5XU3M2anlDZmMxUjBoN1FBQUFCZ2lFR1pyYWpOc2FtRlRaR1poYkd0eU0ycEk0OXlWbXdZPSJ9XSwiZW50aXRsZWRfa2V5IjpbXSwiY29udGVudF9ncm91cHMiOltdfSx7InR5cGUiOiJIRCIsImtleV9pZCI6IjYyZHF1OHMwWHBhN3oyRm1NUEdqMmc9PSIsImtleSI6IkVBdHNJSlFQZDVwRmlSVXJWOUxheXc9PSIsInBzc2giOlt7ImRybV90eXBlIjoiV0lERVZJTkUiLCJkYXRhIjoiSWhCbWEyb3piR3BoVTJSbVlXeHJjak5xU09QY2xac0ciLCJib3hlcyI6IkFBQUFPSEJ6YzJnQUFBQUE3ZStMcVhuV1NzNmp5Q2ZjMVIwaDdRQUFBQmdpRUdacmFqTnNhbUZUWkdaaGJHdHlNMnBJNDl5Vm13WT0ifV0sImVudGl0bGVkX2tleSI6W10sImNvbnRlbnRfZ3JvdXBzIjpbXX0seyJ0eXBlIjoiQVVESU8iLCJrZXlfaWQiOiJZNTJvRFBJN1ZmTzR5clAyVFBwZDlnPT0iLCJrZXkiOiJJcDlmS2JaRDRnTUFTekRFNnZOSTlBPT0iLCJwc3NoIjpbeyJkcm1fdHlwZSI6IldJREVWSU5FIiwiZGF0YSI6IkloQm1hMm96YkdwaFUyUm1ZV3hyY2pOcVNPUGNsWnNHIiwiYm94ZXMiOiJBQUFBT0hCemMyZ0FBQUFBN2UrTHFYbldTczZqeUNmYzFSMGg3UUFBQUJnaUVHWnJhak5zYW1GVFpHWmhiR3R5TTJwSTQ5eVZtd1k9In1dLCJlbnRpdGxlZF9rZXkiOltdLCJjb250ZW50X2dyb3VwcyI6W119XX0="}

Decoding the response to get the DRM configuration info

To decode the response from the Widevine LA test server and get the KID, CEK and PSSH information that we need, we will use Python to parse the JSON objects that we need to deal with.

So, start python3 and run the following:

import os,json,base64
wvResponse = os.environ["wv_response"]
jsonResponse = json.loads(wvResponse)
jsonConfig = json.loads(base64.b64decode(jsonResponse["response"]))
print(json.dumps(jsonConfig, indent=2))

This should give the following result:

{
  "status": "OK",
  "tracks": [
    {
      "pssh": [
        {
          "drm_type": "WIDEVINE",
          "data": "IhBma2ozbGphU2RmYWxrcjNqSOPclZsG",
          "boxes": "AAAAOHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABgiEGZrajNsamFTZGZhbGtyM2pI49yVmwY="
        }
      ],
      "content_groups": [],
      "entitled_key": [],
      "key": "O9ovQDRMfe9hQie5wPA+Jg==",
      "key_id": "ApS5WZ11XeK78P3KP6Xqtw==",
      "type": "SD"
    },
    {
      "pssh": [
        {
          "drm_type": "WIDEVINE",
          "data": "IhBma2ozbGphU2RmYWxrcjNqSOPclZsG",
          "boxes": "AAAAOHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABgiEGZrajNsamFTZGZhbGtyM2pI49yVmwY="
        }
      ],
      "content_groups": [],
      "entitled_key": [],
      "key": "EAtsIJQPd5pFiRUrV9Layw==",
      "key_id": "62dqu8s0Xpa7z2FmMPGj2g==",
      "type": "HD"
    },
    {
      "pssh": [
        {
          "drm_type": "WIDEVINE",
          "data": "IhBma2ozbGphU2RmYWxrcjNqSOPclZsG",
          "boxes": "AAAAOHBzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAABgiEGZrajNsamFTZGZhbGtyM2pI49yVmwY="
        }
      ],
      "content_groups": [],
      "entitled_key": [],
      "key": "Ip9fKbZD4gMASzDE6vNI9A==",
      "key_id": "Y52oDPI7VfO4yrP2TPpd9g==",
      "type": "AUDIO"
    }
  ],
  "drm": [
    {
      "system_id": "edef8ba979d64acea3c827dcd51d21ed",
      "type": "WIDEVINE"
    }
  ]
}

Getting the KID, CEK and PSSH data from the DRM configuration

Now, you can exit your Python environment as we will run a couple of Python one-liners directly from the shell to store our KID and CEK pair and the PSSH data in shell variables.

To do this, we first store the JSON object with the Base64 decoded Widevine DRM configuration in a shell variable:

export wv_config=$(python3 -c 'import os,json,base64;print(base64.b64decode(json.loads(os.environ["wv_response"])["response"]).decode("utf-8"))')

Then, we put the variable back into Python and get the first KID and CEK pair from it (which we will also convert from Base64 to Base16 (hex) encoding, as that's the format that our software requires):

kid=$(python3 -c 'import os,json,base64;print(base64.b64decode(json.loads(os.environ["wv_config"])["tracks"][0]["key_id"].encode("utf-8")).hex())')
cek=$(python3 -c 'import os,json,base64;print(base64.b64decode(json.loads(os.environ["wv_config"])["tracks"][0]["key"].encode("utf-8")).hex())')

And lastly we get the PSSH data, which is easier because we do not need to convert it and the data is the same for all key pairs (so we will store it only once):

pssh=$(python3 -c 'import os,json;print(json.loads(os.environ["wv_config"])["tracks"][0]["pssh"][0]["data"])')

If you want to make sure that you did everything correctly, below are the values that should be stored in the kid, cek and pssh variables:

# KID:
0294b9599d755de2bbf0fdca3fa5eab7
# CEK:
3bda2f40344c7def614227b9c0f03e26
# PSSH:
IhBma2ozbGphU2RmYWxrcjNqSOPclZsG

Configuring Origin using the KID, CEK and PSSH data

Now, we are ready to configure our stream using the DRM configuration info that we acquired from the Widevine LA test server. We do this using the --widevine.key, --widevine.drm_specific_data and --widevine.license_server_url when creating the server manifest that we configure our stream for Origin with.

# Change our working directory to where our test content is located
path_to_test_content="/var/www/tears-of-steel"
cd ${path_to_test_content}

# Create our server manifest with Widevine DRM options (may require 'sudo')
mp4split -o tos-widevine.ism \
  --widevine.key=${kid}:${cek} \
  --widevine.drm_specific_data=${pssh} \
  --widevine.license_server_url=${la_url} \
  tears-of-steel-aac-64k.mp4 \
  tears-of-steel-aac-128k.mp4 \
  tears-of-steel-avc1-400k.mp4 \
  tears-of-steel-avc1-750k.mp4 \
  tears-of-steel-avc1-1000k.mp4 \
  tears-of-steel-avc1-1500k.mp4

Attention

Please note that this step requires DRM support to be part of your USP software license. In case your license does not include this and you would like to try this functionality, please contact us via sales@unified-streaming.com (or directly through your account manager).

Testing your stream

To test your stream, make a request for the MPD first, so that you can inspect it:

# Configure variable with the URL of your Origin's domain
domain="https://your-domain.com"

# Request dynamically generated MPD for stream you have just set up with server manifest
curl ${domain}/tears-of-steel/tos-widevine.ism/.mpd

The MPD should contain a ContentProtection element that includes a reference to Widevine:

</ContentProtection>
 <!-- Widevine -->
 <ContentProtection
   xmlns="urn:mpeg:dash:schema:mpd:2011"
   schemeIdUri="urn:uuid:EDEF8BA9-79D6-4ACE-A3C8-27DCD51D21ED">
 </ContentProtection>

Next, you can try to play back your stream. However, do note that there are a few requirements to successfully do this:

  • Your server must be configured to serve your stream through HTTPS [4]
  • Your playback environment must support Widevine (e.g., Chrome or Firefox)

Finally, when you have loaded the URL to your MPD in your player, you can inspect the requests that the player makes (e.g., using the network tab in your browser) to see that it will make a request for the MPD first, and then connect with the Widevine LA server to get license with the key it needs to decrypt and play the content.

Footnotes

[3]More specifically, the signature is an encrypted SHA1 encoded hash of the JSON object, where the encryption used is AES in CBC mode using the Widevine test encryption key (1AE8CCD0E7985CC0B6203A55855A1034AFC252980E970CA90E5202689F947AB9), and test key initialization vector (D58CE954203B7C9A9A9D467F59839249).
[4]See https://httpd.apache.org/docs/2.4/ssl/ssl_howto.html