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.
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:
- poll streamd for enabled streams that are down or missing credentials
- enqueue a job in a durable SQLite queue
- claim a free ADB device that already has the app installed
- run a phone capture through the stream's app module
POST /api/streams/:id/credentials- 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.