Capture, decrypt, and restream toolkit with compiled-in app modules (RTE, TG4, BBC), on-device MITM proxy, streamd control plane, and www. BBC module.yaml is published (clear streams); other module values stay local.
6.6 KiB
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:
go version
If Go is installed but not on PATH, set GOROOT and prepend its bin:
export GOROOT=/path/to/go
export PATH="$GOROOT/bin:$PATH"
2. Clone and build
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:
./bin/drm help
./bin/drm modules
Expected on a fresh clone — modules exist, values do not:
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.
python -m venv .venv
.venv/bin/pip install -r apps/wvkey/requirements.txt
On Windows:
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):
data/device.wvd
4. Add values for a module
Create the module's values file. It is gitignored, so it never leaves your machine.
$EDITOR apps/modules/tg4/module.yaml
Minimum for TG4 — see providers/tg4.md for where each comes from:
package: <android package id>
account_id: "<brightcove account id>"
policy_key: "BCpkAD..."
playback_config_url: "https://.../playback_config.json"
channels:
ioi:
label: "Main channel"
video_id: "<brightcove video id>"
token_field: <field name in the playback config>
live_card_index: 0
Confirm it loaded:
./bin/drm modules
./bin/drm catalog --app tg4 --list
Nothing stored? Pass values per run instead:
./bin/drm catalog --app tg4 --channel ioi \
--tg4.account-id 1234567890 --tg4.policy-key 'BCpkAD...'
Or through the environment (<APP>_<FIELD>, upper snake case):
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:
./bin/drm catalog --app tg4 --channel ioi --keys --wvd data/device.wvd
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:
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
if the reinject does not report success.
Then capture:
./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.
[*] 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:
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
./bin/drm serve --bind 127.0.0.1:8083 --data .cache/streamd --token SECRET
Open http://127.0.0.1:8083. 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:
./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.
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.
9. Tests
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:
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 |
INVALID_POLICY_KEY |
wrong or stale policy_key |