### Get License for Bitmovin Demo Source: https://github.com/devine-dl/pywidevine/blob/master/README.md Minimal example of using pywidevine to obtain a license for Bitmovin's Art of Motion Demo. Requires a WVD file for device provisioning and uses the requests library to communicate with the license server. ```python from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH import requests # prepare pssh (usually inside the MPD/M3U8, an API response, the player page, or inside the pssh mp4 box) pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa" "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==") # load device from a WVD file (your provision) device = Device.load("C:/Path/To/A/Provision.wvd") # load cdm (creating a CDM instance using that device) cdm = Cdm.from_device(device) # open cdm session (note that any one device should have a practical limit to amount of sessions open at any one time) session_id = cdm.open() # get license challenge (generate a license request message, signed using the device with the pssh) challenge = cdm.get_license_challenge(session_id, pssh) # send license challenge to bitmovin's license server (which has no auth and asks simply for the license challenge as-is) # another license server may require authentication and ask for it as JSON or form data instead # you may also be required to use privacy mode, where you use their service certificate when creating the challenge licence = requests.post("https://cwip-shaka-proxy.appspot.com/no_auth", data=challenge) licence.raise_for_status() # parse the license response message received from the license server API cdm.parse_license(session_id, licence.content) # print keys for key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") # finished, close the session, disposing of all keys and other related data cdm.close(session_id) ``` -------------------------------- ### Install Pywidevine Source: https://context7.com/devine-dl/pywidevine/llms.txt Install the pywidevine package. Use the `[serve]` extra for CDM HTTP API server support. ```bash pip install pywidevine # or with serve support: pip install "pywidevine[serve]" ``` -------------------------------- ### Install Pre-commit Tooling Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md Installs pre-commit hooks to ensure code quality and consistency during commits. ```shell pre-commit install ``` -------------------------------- ### Start CDM HTTP API Server Source: https://context7.com/devine-dl/pywidevine/llms.txt Start the CDM HTTP API server using a configuration file. Specify host and port. ```bash pywidevine serve serve.example.yml --host 0.0.0.0 --port 8786 ``` -------------------------------- ### Install Project Dependencies with Poetry Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md Installs all project dependencies and executables into a virtual environment managed by Poetry. This does not affect the system's Python environment. ```shell poetry install ``` -------------------------------- ### serve - HTTP CDM API Server Source: https://context7.com/devine-dl/pywidevine/llms.txt Details on how to start and use the `pywidevine serve` HTTP API server. ```APIDOC ## serve — HTTP CDM API Server `serve.run()` starts an aiohttp web application that exposes the CDM operations as authenticated REST endpoints. All non-root requests must include an `X-Secret-Key` header. The server sets a `Server` response header advertising the pywidevine version for client compatibility checks. ### Configuration Example (`serve.example.yml`) ```yaml # devices: # - '/path/to/device.wvd' # users: # fvYBh0C3fRAxlvyJcynD1see3GmNbIiC: # username: jane # devices: # - device # force_privacy_mode: true ``` ### Starting the Server Programmatically ```python import yaml from pywidevine import serve config = yaml.safe_load(open("serve.example.yml").read()) serve.run(config, host="0.0.0.0", port=8786) ``` ### REST API Endpoints All endpoints are authenticated via the `X-Secret-Key` header. - **GET /** - Description: Health check endpoint. - Response: `{"status": 200, "message": "Pong!"}` - **GET /{device}/open** - Description: Opens a new CDM session. - Response: `{"data": {"session_id": "...", "device": {...}}}` - **GET /{device}/close/{session_id}** - Description: Closes an existing CDM session. - Response: `{"status": 200, ...}` - **POST /{device}/set_service_certificate** - Description: Sets the service certificate for a session. - Request Body: `{"session_id": "...", "certificate": ""}` - **POST /{device}/get_service_certificate** - Description: Retrieves the service certificate for a session. - Request Body: `{"session_id": "..."}` - **POST /{device}/get_license_challenge/{license_type}** - Description: Generates a license challenge. - Request Body: `{"session_id": "...", "init_data": "", "privacy_mode": true}` - Response: `{"data": {"challenge_b64": "..."}}` - **POST /{device}/parse_license** - Description: Parses a license response. - Request Body: `{"session_id": "...", "license_message": ""}` - **POST /{device}/get_keys/{key_type}** - Description: Retrieves keys for a session. - Request Body: `{"session_id": "..."}` - Response: `{"data": {"keys": [{"key_id": "...", "key": "...", "type": "CONTENT"}]}}` ### cURL Examples - **Open Session**: ```bash curl -H "X-Secret-Key: fvYBh0C3fRAxlvyJcynD1see3GmNbIiC" \ http://localhost:8786/test_device_001/open ``` - **Get License Challenge**: ```bash curl -X POST -H "X-Secret-Key: fvYBh0C3fRAxlvyJcynD1see3GmNbIiC" \ -H "Content-Type: application/json" \ -d '{"session_id":"","init_data":"","privacy_mode":true}' \ http://localhost:8786/test_device_001/get_license_challenge/STREAMING ``` ``` -------------------------------- ### PyWidevine CLI Help Source: https://github.com/devine-dl/pywidevine/blob/master/README.md Displays the main help message for the PyWidevine CLI, listing available commands and options. Use this to get an overview of the tool's capabilities. ```plain Usage: pywidevine [OPTIONS] COMMAND [ARGS]... pywidevine—Python Widevine CDM implementation. Options: -v, --version Print version information. -d, --debug Enable DEBUG level logs. --help Show this message and exit. Commands: create-device Create a Widevine Device (.wvd) file from an RSA Private... export-device Export a Widevine Device (.wvd) file to an RSA Private... license Make a License Request for PSSH to SERVER using DEVICE. migrate Upgrade from earlier versions of the Widevine Device... serve Serve your local CDM and Widevine Devices Remotely. test Test the CDM code by getting Content Keys for Bitmovin's... ``` -------------------------------- ### PyWidevine Test Command Help Source: https://github.com/devine-dl/pywidevine/blob/master/README.md Shows the help message for the 'test' command, detailing its purpose, arguments, and options. This command is used to test the CDM by obtaining content keys for a specific example. ```plain Usage: pywidevine test [OPTIONS] DEVICE Test the CDM code by getting Content Keys for Bitmovin's Art of Motion example. https://bitmovin.com/demos/drm https://bitmovin-a.akamaihd.net/content/art-of-motion_drm/mpds/11331.mpd The device argument is a Path to a Widevine Device (.wvd) file which contains the device private key among other required information. Options: -p, --privacy Use Privacy Mode, off by default. --help Show this message and exit. ``` -------------------------------- ### Pywidevine HTTP API Server (serve) Source: https://context7.com/devine-dl/pywidevine/llms.txt Starts an aiohttp web application exposing CDM operations as REST endpoints. All non-root requests require an `X-Secret-Key` header for authentication. The server advertises its pywidevine version for client compatibility checks. ```yaml # serve.example.yml # devices: # - '/path/to/device.wvd' # users: # fvYBh0C3fRAxlvyJcynD1see3GmNbIiC: # username: jane # devices: # - device # force_privacy_mode: true ``` ```python import yaml from pywidevine import serve config = yaml.safe_load(open("serve.example.yml").read()) serve.run(config, host="0.0.0.0", port=8786) ``` ```http # GET / -> {"status": 200, "message": "Pong!"} # GET /{device}/open -> {"data": {"session_id": "...", "device": {...}}} # GET /{device}/close/{session_id} -> {"status": 200, ...} # POST /{device}/set_service_certificate -> body: {"session_id": "...", "certificate": ""} # POST /{device}/get_service_certificate -> body: {"session_id": "..."} # POST /{device}/get_license_challenge/{license_type} # -> body: {"session_id": "...", "init_data": "", "privacy_mode": true} # <- {"data": {"challenge_b64": "..."}} # POST /{device}/parse_license -> body: {"session_id": "...", "license_message": ""} # POST /{device}/get_keys/{key_type} -> body: {"session_id": "..."} # <- {"data": {"keys": [{"key_id": "...", "key": "...", "type": "CONTENT"}]}} ``` ```bash # curl examples: # curl -H "X-Secret-Key: fvYBh0C3fRAxlvyJcynD1see3GmNbIiC" \ # http://localhost:8786/test_device_001/open # curl -X POST -H "X-Secret-Key: fvYBh0C3fRAxlvyJcynD1see3GmNbIiC" \ # -H "Content-Type: application/json" \ # -d '{"session_id":"","init_data":"","privacy_mode":true}' \ ``` -------------------------------- ### Clone Repository and Navigate Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md Standard commands to clone the project repository and change the current directory into the project's root. ```shell git clone https://github.com/devine-dl/pywidevine cd pywidevine ``` -------------------------------- ### Create Device WVD File Source: https://context7.com/devine-dl/pywidevine/llms.txt Create a WVD file from raw key material. Specify device type, security level, key, client ID, and output directory. ```bash pywidevine create-device \ --type ANDROID \ --level 3 \ --key private_key.pem \ --client_id client_id.bin \ --output ./devices/ ``` -------------------------------- ### Acquire Generic License Source: https://context7.com/devine-dl/pywidevine/llms.txt Acquire a license by sending the challenge as raw bytes to a specified URL. Supports STREAMING type and privacy mode. ```bash pywidevine license provision.wvd \ "AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ..." \ "https://cwip-shaka-proxy.appspot.com/no_auth" \ --type STREAMING \ --privacy ``` -------------------------------- ### Build Source and Wheel Distributions Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md Builds both source (sdist) and wheel distributions for the project. Built files are located in the `/dist` directory. Use `-f sdist` or `-f wheel` to build only one type. ```shell poetry build ``` -------------------------------- ### Local CDM Operations with Pywidevine Source: https://context7.com/devine-dl/pywidevine/llms.txt Demonstrates a typical workflow for interacting with a local Widevine CDM. This includes obtaining a service certificate, generating a license challenge, parsing the license response, and retrieving keys. Ensure the CDM is correctly set up and accessible. ```python cert_response = requests.post( "https://license.example.com/widevine/", data=cdm.service_certificate_challenge # b"\x08\x04" ) cert_response.raise_for_status() provider_id = cdm.set_service_certificate(session_id, cert_response.content) print("Privacy cert provider:", provider_id) # e.g. "license.widevine.com" # --- Alternatively use Google's well-known common privacy cert --- cdm.set_service_certificate(session_id, Cdm.common_privacy_cert) # --- Generate a license challenge --- pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6j...") challenge = cdm.get_license_challenge( session_id, pssh, license_type="STREAMING", # or "OFFLINE" / "AUTOMATIC" privacy_mode=True ) # --- Send challenge to license server --- response = requests.post( "https://cwip-shaka-proxy.appspot.com/no_auth", data=challenge ) response.raise_for_status() # --- Parse the license response; loads keys into session --- cdm.parse_license(session_id, response.content) # --- Retrieve all keys --- for key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") # Output example: # [CONTENT] eb676abcb74c1384eb67d115713f3f27:100b6c20940f779a4589152b57d2dacb # [SIGNING] ... # --- Filter by key type --- content_keys = cdm.get_keys(session_id, type_="CONTENT") # --- Close session when finished --- cdm.close(session_id) # --- Decrypt a file using shaka-packager (requires shaka-packager in PATH) --- session_id2 = cdm.open() cdm.parse_license(session_id2, license_bytes) exit_code = cdm.decrypt( session_id2, input_file="encrypted.mp4", output_file="decrypted.mp4", exists_ok=False ) cdm.close(session_id2) ``` -------------------------------- ### Load and Manage WVD Provision Files with Device Source: https://context7.com/devine-dl/pywidevine/llms.txt Load, build, and serialize Widevine device provision files (WVD). Supports existing files, raw bytes, and migration of v1 files. ```python from pywidevine.device import Device, DeviceTypes from pathlib import Path # --- Load an existing WVD file --- device = Device.load("provision.wvd") print(device.type) # DeviceTypes.ANDROID print(device.system_id) # e.g. 15071 print(device.security_level) # 3 # --- Load from raw bytes / base64 --- raw_bytes = Path("provision.wvd").read_bytes() device = Device.loads(raw_bytes) # --- Build a new Device from raw key material --- device = Device( type_=DeviceTypes.CHROME, security_level=3, flags=None, private_key=Path("private_key.pem").read_bytes(), client_id=Path("client_id.bin").read_bytes() ) # --- Serialize back to WVD bytes and save --- device.dump("output.wvd") wvd_bytes = device.dumps() # returns bytes # --- Migrate a v1 WVD to v2 in-place --- migrated_device = Device.migrate(Path("old_v1.wvd").read_bytes()) migrated_device.dump("old_v1.wvd") # overwrite original ``` -------------------------------- ### Test CDM Provisioning Source: https://context7.com/devine-dl/pywidevine/llms.txt Test the CDM with Bitmovin's public DRM demo. The `--privacy` flag enables privacy-preserving mode. ```bash pywidevine test provision.wvd pywidevine test provision.wvd --privacy ``` -------------------------------- ### Parse, Create, and Convert PSSH Boxes with PSSH Source: https://context7.com/devine-dl/pywidevine/llms.txt Handle MP4 PSSH boxes, WidevineCencHeader protobufs, or PlayReady objects. Extract key IDs and convert between DRM systems. ```python from pywidevine.pssh import PSSH from uuid import UUID # --- Parse from base64 (common in MPD manifests) --- pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6jyCfc1R0h7QAAADsIARIQ62dqu8s0Xpa "7z2FmMPGj2hoNd2lkZXZpbmVfdGVzdCIQZmtqM2xqYVNkZmFsa3IzaioCSEQyAA==") print(pssh.version) # 0 print(pssh.system_id) # UUID('edef8ba9-79d6-4ace-a3c8-27dcd51d21ed') print(pssh.key_ids) # [UUID('eb67...'), UUID('...')] # --- Parse from bytes in strict mode (rejects custom init data) --- pssh_strict = PSSH(some_bytes, strict=True) # --- Craft a new v1 Widevine PSSH box with explicit Key IDs --- pssh_new = PSSH.new( system_id=PSSH.SystemId.Widevine, key_ids=["eb676abcb74c1384eb67d115713f3f27"], # hex string accepted version=1 ) print(pssh_new.dumps()) # base64-encoded PSSH box # --- Convert a PlayReady PSSH to Widevine --- pr_pssh = PSSH(playready_pssh_bytes) pr_pssh.to_widevine() print(pr_pssh.system_id) # now Widevine # --- Convert a Widevine PSSH to PlayReady v4.3 --- wv_pssh = PSSH(widevine_pssh_bytes) wv_pssh.to_playready(la_url="https://playready.license.server/acquire") # --- Re-export as bytes or base64 --- box_bytes = pssh.dump() box_b64 = pssh.dumps() ``` -------------------------------- ### Configure Poetry to use in-project virtual environments Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md This command configures Poetry to create virtual environments within the project directory, which can help with IDE integration and prevent duplicate environments. ```shell poetry config virtualenvs.in-project true ``` -------------------------------- ### Activate Poetry Virtual Environment Source: https://github.com/devine-dl/pywidevine/blob/master/CONTRIBUTING.md Activates the virtual environment created by Poetry. Alternatively, commands can be prefixed with `poetry run`. ```shell poetry shell ``` -------------------------------- ### Migrate WVD File Source: https://context7.com/devine-dl/pywidevine/llms.txt Migrate a v1 WVD file to v2 format in-place, or migrate all WVD files in a specified folder. ```bash pywidevine migrate provision_v1.wvd # Migrate all WVD files in a folder pywidevine migrate ./devices/ ``` -------------------------------- ### Local CDM Operations Source: https://context7.com/devine-dl/pywidevine/llms.txt Demonstrates the typical workflow for interacting with a local Widevine CDM instance to request certificates, generate license challenges, and retrieve keys. ```APIDOC ## Local CDM Operations ### Description This section outlines the steps for interacting with a local CDM instance to manage Widevine licenses and keys. ### Usage 1. **Request Service Certificate**: Obtain a service certificate from the license server. ```python cert_response = requests.post( "https://license.example.com/widevine/", data=cdm.service_certificate_challenge # b"\x08\x04" ) cert_response.raise_for_status() provider_id = cdm.set_service_certificate(session_id, cert_response.content) print("Privacy cert provider:", provider_id) ``` Alternatively, use the common privacy certificate: ```python cdm.set_service_certificate(session_id, Cdm.common_privacy_cert) ``` 2. **Generate License Challenge**: Create a license challenge using PSSH data. ```python pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6j...") challenge = cdm.get_license_challenge( session_id, pssh, license_type="STREAMING", # or "OFFLINE" / "AUTOMATIC" privacy_mode=True ) ``` 3. **Send Challenge to License Server**: Submit the generated challenge to the license server. ```python response = requests.post( "https://cwip-shaka-proxy.appspot.com/no_auth", data=challenge ) response.raise_for_status() ``` 4. **Parse License Response**: Process the license response to load keys into the session. ```python cdm.parse_license(session_id, response.content) ``` 5. **Retrieve Keys**: Fetch all available keys for the session. ```python for key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") ``` Filter by key type: ```python content_keys = cdm.get_keys(session_id, type_="CONTENT") ``` 6. **Close Session**: Terminate the session when finished. ```python cdm.close(session_id) ``` 7. **Decrypt File**: Decrypt a file using `shaka-packager`. ```python session_id2 = cdm.open() cdm.parse_license(session_id2, license_bytes) exit_code = cdm.decrypt( session_id2, input_file="encrypted.mp4", output_file="decrypted.mp4", exists_ok=False ) cdm.close(session_id2) ``` ``` -------------------------------- ### Export Device WVD Source: https://context7.com/devine-dl/pywidevine/llms.txt Export a WVD file back to its components (private key, client ID, metadata). Specify the WVD file and output directory. ```bash pywidevine export-device provision.wvd --out_dir ./exported/ ``` -------------------------------- ### Pywidevine Key Object Usage Source: https://context7.com/devine-dl/pywidevine/llms.txt Demonstrates accessing properties of a Key object obtained from `cdm.get_keys()`. Direct construction is rarely needed. ```python from pywidevine.key import Key from uuid import UUID # Keys are produced automatically by Cdm.get_keys(); direct construction is rarely needed. # Typical usage after license parsing: for key in cdm.get_keys(session_id): print(key.type) # "CONTENT", "SIGNING", "OPERATOR_SESSION", etc. print(key.kid) # UUID('eb676abc-b74c-1384-eb67-d115713f3f27') print(key.kid.hex) # 'eb676abcb74c1384eb67d115713f3f27' (no dashes) print(key.key.hex()) # '100b6c20940f779a4589152b57d2dacb' print(key.permissions) # [] for CONTENT keys; list of strings for OPERATOR_SESSION # Convert a raw KID (bytes or base64 string) to UUID: uuid = Key.kid_to_uuid(b"\xebgj\xbc\xb7L\x13\x84\xebg\xd1\x15q??(") print(uuid) # UUID('eb676abc-b74c-1384-eb67-d115713f3f27') ``` -------------------------------- ### Local CDM Session and License Negotiation with Cdm Source: https://context7.com/devine-dl/pywidevine/llms.txt Manage Widevine CDM sessions, generate license requests, verify responses, and decrypt content keys using a WVD device file. ```python import requests from pywidevine.cdm import Cdm from pywidevine.device import Device from pywidevine.pssh import PSSH # --- Build CDM from a WVD device file --- device = Device.load("provision.wvd") cdm = Cdm.from_device(device) # --- Open a session (max 16 concurrent) --- session_id = cdm.open() # --- Optional: Privacy Mode with service certificate --- ``` -------------------------------- ### RemoteCdm Client for Network-Transparent CDM Source: https://context7.com/devine-dl/pywidevine/llms.txt Connects to a remote `pywidevine serve` HTTP API. The API is identical to the local `Cdm` class, simplifying integration. Ensure the host and secret are correctly configured for authentication. ```python cdm = RemoteCdm( device_type="ANDROID", system_id=15071, security_level=3, host="http://192.168.1.10:8786", secret="fvYBh0C3fRAxlvyJcynD1see3GmNbIiC", # X-Secret-Key header device_name="test_device_001" ) # --- API is identical to local Cdm --- session_id = cdm.open() pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6j...") challenge = cdm.get_license_challenge(session_id, pssh, privacy_mode=True) license_resp = requests.post("https://license.example.com/", data=challenge) cdm.parse_license(session_id, license_resp.content) for key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) ``` -------------------------------- ### RemoteCdm Client Source: https://context7.com/devine-dl/pywidevine/llms.txt Connect to a remote Widevine CDM service via an HTTP API using the `RemoteCdm` class. ```APIDOC ## RemoteCdm — Network-Transparent CDM Client `RemoteCdm` is a drop-in subclass of `Cdm` that forwards every operation (open, close, set_service_certificate, get_license_challenge, parse_license, get_keys) to a remote `pywidevine serve` HTTP API. It validates the server version on construction and authenticates with a shared secret header. ### Usage 1. **Connect to a remote CDM serve instance**: Initialize `RemoteCdm` with connection details and authentication. ```python from pywidevine.remotecdm import RemoteCdm from pywidevine.pssh import PSSH import requests cdm = RemoteCdm( device_type="ANDROID", system_id=15071, security_level=3, host="http://192.168.1.10:8786", secret="fvYBh0C3fRAxlvyJcynD1see3GmNbIiC", # X-Secret-Key header device_name="test_device_001" ) ``` 2. **API is identical to local Cdm**: Use the `RemoteCdm` instance just like a local `Cdm` object. ```python session_id = cdm.open() pssh = PSSH("AAAAW3Bzc2gAAAAA7e+LqXnWSs6j...") challenge = cdm.get_license_challenge(session_id, pssh, privacy_mode=True) license_resp = requests.post("https://license.example.com/", data=challenge) cdm.parse_license(session_id, license_resp.content) for key in cdm.get_keys(session_id): print(f"[{key.type}] {key.kid.hex}:{key.key.hex()}") cdm.close(session_id) ``` ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.