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

183 lines
5.5 KiB
Markdown

# 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
```bash
./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
```bash
./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:
```text
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
```bash
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:
```text
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
```bash
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
```bash
./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`:
```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
```bash
# 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:
```bash
./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
```text
.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.