# Quickstart From an empty directory to a captured Widevine key. ## 1. Prerequisites | Need | Version | Needed for | |---|---|---| | Go | 1.25+ | everything | | Python | 3.10+ | the CDM helper (`apps/wvkey/wvkey.py`) | | adb | any recent platform-tools, on `PATH` | phone capture only | | `.wvd` device file | — | fetching content keys | | Rooted Android phone (Magisk) | — | phone capture only | Check Go: ```bash go version ``` If Go is installed but not on `PATH`, set `GOROOT` and prepend its `bin`: ```bash export GOROOT=/path/to/go export PATH="$GOROOT/bin:$PATH" ``` ## 2. Clone and build ```bash git clone https://git.nulldrm.com/nulldrm/Null-DRM-Official.git cd Null-DRM-Official go -C apps/cli build -o ../../bin/drm . # Windows: -o ../../bin/drm.exe ``` That single command builds the whole toolchain: capture, catalog, agent, the streamd control plane, the MITM host CLI, and every app module. Verify: ```bash ./bin/drm help ./bin/drm modules ``` Expected on a fresh clone — modules exist, values do not: ```text APP PACKAGE KEY MODE REWRITER CHANNELS VALUES rte (not configured) modulardrm rte 0 missing: .../apps/modules/rte/module.yaml tg4 (not configured) raw none 0 missing: .../apps/modules/tg4/module.yaml ``` ## 3. The Python CDM helper Only `wvkey.py` is Python; it turns a PSSH + license URL into `KID:KEY` lines. ```bash python -m venv .venv .venv/bin/pip install -r apps/wvkey/requirements.txt ``` On Windows: ```powershell python -m venv .venv .venv\Scripts\pip install -r apps\wvkey\requirements.txt ``` The binary finds `.venv` automatically; override with `--python /path/to/python`. Place your Widevine device file under `data/` (gitignored): ```text data/device.wvd ``` ## 4. Add values for a module Create the module's values file. It is gitignored, so it never leaves your machine. ```bash $EDITOR apps/modules/tg4/module.yaml ``` Minimum for TG4 — see [providers/tg4.md](providers/tg4.md) for where each comes from: ```yaml package: account_id: "" policy_key: "BCpkAD..." playback_config_url: "https://.../playback_config.json" channels: ioi: label: "Main channel" video_id: "" token_field: live_card_index: 0 ``` Confirm it loaded: ```bash ./bin/drm modules ./bin/drm catalog --app tg4 --list ``` Nothing stored? Pass values per run instead: ```bash ./bin/drm catalog --app tg4 --channel ioi \ --tg4.account-id 1234567890 --tg4.policy-key 'BCpkAD...' ``` Or through the environment (`_`, upper snake case): ```bash export TG4_POLICY_KEY='BCpkAD...' ``` Precedence is **flag → environment → module.yaml → built-in default**. Only mechanical defaults (timeouts, DASH timescales, card geometry) have built-ins. ## 5. Get keys without a phone If the app has a public playback catalog: ```bash ./bin/drm catalog --app tg4 --channel ioi --keys --wvd data/device.wvd ``` ```text manifest: https://.../playlist-hls-dvr.m3u8 license_url: https://.../lic/wv?token=... pssh: AAAAYHBzc2gAAAAA7e+LqXnWSs6j... key: 27c38b955b983ac2827204f8d260d628:f31efbc58fc89526cce5e5dee50a7ebd key[0]: 5b9d58edf81f3b5b9c3cc61f3e81845e:688236976dcf8cdde5cdfac4a698f256 ... ``` Add `--json` for a machine-readable object, `--out` to write a session directory without fetching keys. ## 6. Get keys from the phone One-time device setup: ```bash adb devices # your phone must be listed and authorised ./bin/drm proxy build # cross-compiles the on-device MITM (linux/arm64) ./bin/drm proxy install-ca --reinject ``` `install-ca` pushes the MITM CA and Magisk-mounts it into the system trust store. Without it apps fail TLS with `unknown certificate`. See [capture.md](capture.md) if the reinject does not report success. Then capture: ```bash ./bin/drm capture --app rte --channel rteone --auto-play --wvd data/device.wvd ``` What it does: start the on-device MITM → point the phone's HTTP proxy at it → reinject the CA → launch the app → drive its UI to the channel → wait for the license/manifest traffic → run the CDM → write a session → close the app. ```text [*] Launching rte (...) [*] Auto-play RTE One… [*] tap_ui "Live"/"Live tab, 2 out of 5" @ 324,2253 [*] tap Play @ 540,720 [+] ... is PLAYING [+] Capture: pid: 8T8Ohq7XT5KN mpd: https://dai.google.com/linear/dash/pa/event/... [+] key: df1633821dddfdd5bec9822c1ec0f052:b6324f76d2bb4dfe63e4272f803274f4 [+] session: outputs/rte/20261005-215448/session.json ``` Keep the screen on for the whole run: ```bash adb shell svc power stayon true ``` Drop `--auto-play` to launch the app and navigate by hand, or drop `--app` entirely for passive mode — the MITM records whatever you play. ## 7. Control plane ```bash ./bin/drm serve --bind 127.0.0.1:8083 --data .cache/streamd --token SECRET ``` Open . The app and channel pickers are populated from `GET /api/apps`, i.e. from the modules compiled into the binary. Create a stream from a capture: ```bash ./bin/drm catalog --app tg4 --channel ioi --keys --wvd data/device.wvd --json \ > /tmp/s.json curl -X POST http://127.0.0.1:8083/api/streams \ -H 'Authorization: Bearer SECRET' -H 'Content-Type: application/json' \ --data @/tmp/s.json ``` ## 8. Agent The agent polls streamd, queues any enabled stream that is down or missing credentials, claims a free phone, re-captures it and POSTs the result back. ```bash export STREAMD_TOKEN=SECRET ./bin/drm agent run --config apps/agent/agent.yaml ./bin/drm agent devices # which phones it can use ./bin/drm agent status # recent jobs ./bin/drm agent enqueue --stream tg4-ioi --reason manual ``` Details in [streamd.md](streamd.md). ## 9. Tests ```bash go -C apps/pkg test ./... go -C apps/modules test ./... go -C apps/streamd test ./... ``` Live-origin tests are tag-gated and need your local values: ```bash go -C apps/modules test -tags live ./rte/ -v ``` ## Troubleshooting | Symptom | Cause | |---|---| | `unknown app "x"` | not compiled in — check `apps/modules/all/all.go`, then rebuild | | `channels 0` in `drm modules` | no `module.yaml`, or `channels:` is empty | | `unknown channel "y"` | id not in `channels:` and not in `aliases:` | | `tls: unknown certificate` on the phone | CA not mounted — `drm proxy install-ca --reinject` | | `wvkey requires --wvd` | pass `--wvd path/to/device.wvd` | | `timed out waiting for capture` | playback never started, or the app used a path the MITM missed — see [capture.md](capture.md) | | `INVALID_POLICY_KEY` | wrong or stale `policy_key` |