Null-DRM-Official/docs/streamd.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

5.5 KiB

Control plane and agent

Two long-running pieces: drm serve holds the state and the dashboard, drm agent run keeps credentials fresh using real phones.

drm serve

./bin/drm serve --bind 127.0.0.1:8083 --data .cache/streamd --token SECRET
Flag Default Purpose
--bind 0.0.0.0:8083 listen address
--data .cache/streamd SQLite, work dirs, HLS output, logs
--token $STREAMD_TOKEN bearer token for mutating routes
--nre $NRE_PATH, bin/, PATH downloader binary
--ffmpeg $FFMPEG_PATH, bin/, PATH ffmpeg
--mp4decrypt $MP4DECRYPT_PATH, bin/, PATH decrypter

Open http://127.0.0.1:8083. GET routes are unauthenticated; anything mutating needs Authorization: Bearer <token>.

API

Route Purpose
GET /api/health liveness + uptime
GET /api/apps compiled-in modules, their channels, registered rewriters
GET /api/streams all streams with runtime state
POST /api/streams create
PATCH /api/streams/:id update
DELETE /api/streams/:id remove
POST /api/streams/:id/start|stop|restart desired state
POST /api/streams/:id/credentials push a fresh manifest + keys
POST /api/streams/:id/claim agent claims the stream

The dashboard's app and channel pickers are filled from /api/apps, so they reflect whatever modules the binary was built with. Nothing about a provider is hardcoded in the UI or the schema.

Creating a stream

./bin/drm catalog --app tg4 --channel ioi --keys --wvd data/device.wvd --json > s.json

curl -X POST http://127.0.0.1:8083/api/streams \
  -H 'Authorization: Bearer SECRET' -H 'Content-Type: application/json' \
  --data @s.json

A stream created without app, video_select, audio_select or rewriter keeps those empty on purpose. They are resolved when the stream starts:

stored row  ->  the stream's app module (app.StreamDefaults)  ->  neutral defaults

So a module decides its own downloader settings, an operator override in the row still wins, and an unknown app gets something harmless rather than another provider's preferences.

Refreshing credentials

curl -X POST http://127.0.0.1:8083/api/streams/1/credentials \
  -H 'Authorization: Bearer SECRET' -H 'Content-Type: application/json' \
  -d '{"mpd":"https://...","key":"KID:KEY\nKID:KEY","headers_json":"{}"}'

This is exactly what the agent does after a successful capture.

Manifest rewriting

If a stream's rewriter names a registered rewriter and the manifest is not HLS, the supervisor starts a local rewrite server and points the downloader at it:

stream rewriter="rte"  ->  mpd.Lookup("rte")  ->  http://127.0.0.1:PORT/manifest.mpd

The log line is stream X: rewritten MPD http://127.0.0.1:…. Streamd never imports a provider package to do this — the module registered the rewriter under that name at init time.

drm agent run

export STREAMD_TOKEN=SECRET
./bin/drm agent run --config apps/agent/agent.yaml

What it does, in a loop:

  1. poll streamd for enabled streams that are down or missing credentials
  2. enqueue a job in a durable SQLite queue
  3. claim a free ADB device that already has the app installed
  4. run a phone capture through the stream's app module
  5. POST /api/streams/:id/credentials
  6. force-stop the app and release the device

It never installs apps and never runs a downloader. A websocket heartbeat keeps its claims alive, so claims expire automatically if the agent dies.

Subcommands

./bin/drm agent devices                              # phones it can use
./bin/drm agent status                               # recent jobs
./bin/drm agent enqueue --stream tg4-ioi --reason manual
./bin/drm agent cancel --job 3

Config

apps/agent/agent.yaml:

streamd_url: http://127.0.0.1:8083
# token: taken from $STREAMD_TOKEN when unset
poll_interval_sec: 30
data_dir: .cache/agent
workers: 1
max_attempts: 5
capture_wait_sec: 180
# devices: [SERIAL1, SERIAL2]    # empty = any authorised device
# python: path to python for wvkey.py
# wvd: data/device.wvd
# user_agent: ""
# agent_id: defaults to hostname

There is no modules directory to configure — app modules are compiled into the binary.

wvd must be set (here or on the phone capture) or key fetches fail.

Running both locally

# terminal 1
./bin/drm serve --bind 127.0.0.1:8083 --data .cache/streamd --token SECRET

# terminal 2
export STREAMD_TOKEN=SECRET
./bin/drm agent run --config apps/agent/agent.yaml

Then create a stream, enable it, and watch the agent pick it up:

./bin/drm agent status

External tools

The media supervisor shells out to three binaries. Put them on PATH, in bin/, or name them with flags/env:

Tool Flag Env
N_m3u8DL-RE --nre NRE_PATH, N_M3U8DL_RE
ffmpeg --ffmpeg FFMPEG_PATH, FFMPEG
mp4decrypt --mp4decrypt MP4DECRYPT_PATH, MP4DECRYPT

Streams can be created, claimed and credentialed without any of them; only actually pulling media needs them. The supervisor is the least exercised part of the system — treat its health reporting as control-plane bookkeeping.

Data

.cache/streamd/
  streamd.db        SQLite: streams, runtime, claims
  work/<name>/      downloader scratch + keys.txt
  www/<name>/       HLS output
  logs/             per-stream downloader + ffmpeg logs

.cache/agent/
  queue.db          durable job queue

Back up streamd.db before upgrading — schema defaults change between versions.