Null-DRM-Official/docs/quickstart.md
404errordeveloper 2fa8f2435f Initial commit: Null DRM Official
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.
2026-10-06 00:25:35 +02:00

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