# 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 . GET routes are unauthenticated; anything mutating needs `Authorization: Bearer `. ### 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// downloader scratch + keys.txt www// 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.