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.
This commit is contained in:
commit
2fa8f2435f
121 changed files with 17802 additions and 0 deletions
183
docs/streamd.md
Normal file
183
docs/streamd.md
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
# 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue