Captures the TV's audio and sends it out over several transports. The
primary one is HyperHDR: RTP/L16 to a host-side loopback device, since
HyperHDR has no network audio input of its own. A second route renders
the spectrum on the TV and sends FlatBuffers images to port 19400
instead, for setups where touching the host's sound config is not an
option.
native/ the service: capture backends (PulseAudio, ALSA, exec,
test tone, all dlopen-based), DSP, and one file per sink
frontend/ D-pad driven UI at a fixed 1920x1080
servicefiles/ native service manifest plus the boot script
host/ RTP receiver and the loopback installer for the HyperHDR
machine
tools/ build/package, asset generation, Homebrew Channel
manifest, on-TV probe
test/ host-side suites: FlatBuffers and RTP verified against
real decoders, the engine end to end, the page in jsdom
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
223 lines
7.6 KiB
Markdown
223 lines
7.6 KiB
Markdown
# Configuration and the Luna API
|
|
|
|
Everything the UI does goes through the service's Luna API, so anything the UI
|
|
can do you can also do from an ssh session with `luna-send`.
|
|
|
|
## Where the settings live
|
|
|
|
```text
|
|
/var/lib/webosbrew/audiocap/config.json when the service can write there
|
|
/tmp/audiocap-config.json fallback, lost on reboot
|
|
$AUDIOCAP_CONFIG overrides both
|
|
```
|
|
|
|
The service falls back to `/tmp` when it is not running as root, and the UI says
|
|
so under *System → Settings file*. Settings written there survive the session
|
|
but not the TV.
|
|
|
|
Edits are merged, not replaced: sending `{"hyperhdr":{"port":5005}}` changes the
|
|
port and leaves everything else alone. Writes are atomic — a temporary file,
|
|
`fsync`, `rename` — so a power cut during a save cannot leave a truncated
|
|
config behind.
|
|
|
|
## Settings
|
|
|
|
### Top level
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `autoStart` | `false` | start capturing as soon as the service starts |
|
|
| `logLevel` | `"info"` | `error`, `warn`, `info` or `debug` |
|
|
| `sinks` | `["hyperhdr"]` | which outputs to open |
|
|
|
|
`autoStart` only matters if something starts the service at boot — that is what
|
|
the *Start on boot* toggle installs, a script in `/var/lib/webosbrew/init.d/`
|
|
that pokes the service so the Homebrew Channel launches it.
|
|
|
|
### `capture`
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `backend` | `"auto"` | `auto`, `pulse`, `alsa`, `exec` or `tone` |
|
|
| `device` | `""` | PulseAudio source, or an ALSA PCM like `hw:0,0` |
|
|
| `server` | `""` | PulseAudio server address; blank autodetects |
|
|
| `command` | `""` | for the `exec` backend |
|
|
| `rate` | `48000` | 44100 or 48000 |
|
|
| `channels` | `2` | 1 or 2 |
|
|
|
|
`auto` tries PulseAudio, then ALSA. It never tries `exec` — that one needs a
|
|
command only you can supply.
|
|
|
|
The backends:
|
|
|
|
| id | Name | Notes |
|
|
| --- | --- | --- |
|
|
| `pulse` | PulseAudio monitor | records the monitor source of the active sink; `libpulse.so.0` is `dlopen`ed at run time, so a TV without it simply reports the backend unavailable |
|
|
| `alsa` | ALSA PCM | same arrangement with `libasound.so.2` |
|
|
| `exec` | External command | runs a command and reads raw interleaved S16LE from its stdout, e.g. `arecord -D hw:0,0 -f S16_LE -r 48000 -c 2 -t raw` |
|
|
| `tone` | Test tone | a sweep; proves the network path without touching the TV's audio at all |
|
|
|
|
### `dsp`
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `attack` | `0.6` | seconds for the reported level to catch a rise |
|
|
| `release` | `0.12` | seconds for it to fall away |
|
|
|
|
These shape the numbers in the status document and the on-TV visualiser. They do
|
|
not touch the audio sent to any sink.
|
|
|
|
### `hyperhdr` — RTP/L16 audio
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `host` | `""` | the receiving machine |
|
|
| `port` | `5004` | UDP port |
|
|
| `multicast` | `false` | send to a group instead of a host |
|
|
| `multicastTtl` | `4` | hop limit when multicasting |
|
|
| `sapAnnounce` | `true` | announce over SAP so PulseAudio can find the stream |
|
|
|
|
Payload type 96, 16-bit big-endian PCM, packets kept under 1400 bytes of
|
|
payload so nothing fragments on a normal Ethernet MTU.
|
|
|
|
### `hyperhdrViz` — Flatbuffers images
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `host` | `""` | HyperHDR's address |
|
|
| `port` | `19400` | Flatbuffers port |
|
|
| `priority` | `150` | HyperHDR priority; lower wins |
|
|
| `width` / `height` | `64` / `36` | image size |
|
|
| `fps` | `30` | frames per second |
|
|
| `mode` | `"spectrum"` | `spectrum`, `level` or `pulse` |
|
|
| `saturation` | `1.0` | colour intensity |
|
|
| `minBrightness` | `0.02` | floor so the lights never go fully black |
|
|
|
|
### `udp`, `tcp`, `http`
|
|
|
|
| Key | Default | Meaning |
|
|
| --- | --- | --- |
|
|
| `udp.host` | `""` | destination; a host, a multicast group, or `255.255.255.255` |
|
|
| `udp.port` | `4010` | |
|
|
| `udp.multicastTtl` | `4` | |
|
|
| `tcp.port` | `4011` | the TV listens on this |
|
|
| `tcp.maxClients` | `4` | |
|
|
| `http.port` | `4012` | `/audio.wav` and `/audio.raw` |
|
|
| `http.maxClients` | `4` | |
|
|
|
|
All three carry interleaved S16**LE** — little-endian, unlike the RTP sink,
|
|
because that is what everything reading a raw pipe expects.
|
|
|
|
---
|
|
|
|
## The Luna API
|
|
|
|
Service name `org.webosbrew.audiocap.service`, all methods on `/`.
|
|
|
|
```sh
|
|
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/getStatus '{}'
|
|
```
|
|
|
|
| Method | Payload | Reply |
|
|
| --- | --- | --- |
|
|
| `start` | optional settings patch, applied and saved first | the status document |
|
|
| `stop` | `{}` | the status document |
|
|
| `getStatus` | `{"subscribe":true}` for a feed every 100 ms | the status document |
|
|
| `isRunning` | `{}` | `{isRunning, state}` |
|
|
| `getConfig` | `{}` | `{path, persistent, settings}` |
|
|
| `setConfig` | a patch, bare or under `settings` | `{saved, restartRequired, settings}` |
|
|
| `resetConfig` | `{}` | `{saved, settings}` |
|
|
| `listBackends` | `{}` | `{backends:[{id,name,description,available}]}` |
|
|
| `listSinks` | `{}` | `{sinks:[{id,name,description}]}` |
|
|
| `getDiagnostics` | `{}` | `{backends, system}` — see below |
|
|
| `getLogs` | `{"clear":true}` optional | `{logs}` |
|
|
| `quit` | `{}` | ends the process; the next call starts a new one |
|
|
|
|
`setConfig` reports `restartRequired: true` when the capture is running, because
|
|
most settings are read when a run starts.
|
|
|
|
### The status document
|
|
|
|
```json
|
|
{
|
|
"returnValue": true,
|
|
"state": "running",
|
|
"running": true,
|
|
"error": null,
|
|
"capture": {
|
|
"backend": "pulse",
|
|
"backendName": "PulseAudio monitor",
|
|
"device": "…monitor",
|
|
"rate": 48000,
|
|
"channels": 2,
|
|
"frames": 4915200,
|
|
"blocks": 9600,
|
|
"timeouts": 0,
|
|
"uptimeMs": 102400
|
|
},
|
|
"levels": {
|
|
"peak": 0.42, "rms": 0.19,
|
|
"peakDb": -7.5, "rmsDb": -14.4,
|
|
"clipping": false,
|
|
"bands": [0.0, "…16 values…"]
|
|
},
|
|
"sinks": [
|
|
{ "id": "hyperhdr", "ok": true, "name": "HyperHDR audio (RTP)",
|
|
"error": null, "target": "192.168.1.50", "port": 5004,
|
|
"packetsSent": 12345, "bytesSent": 4321000, "sendErrors": 0 }
|
|
],
|
|
"configPath": "/var/lib/webosbrew/audiocap/config.json",
|
|
"configPersistent": true
|
|
}
|
|
```
|
|
|
|
`state` is `stopped`, `starting`, `running` or `error`. A sink that failed to
|
|
open reports `ok: false` and an `error`, and the run continues without it — one
|
|
broken output does not take the others down.
|
|
|
|
Each sink adds its own fields. `packetsSent`/`bytesSent`/`sendErrors` for the
|
|
datagram sinks, `clients`/`droppedBytes` for the stream servers,
|
|
`connected`/`registered`/`framesSent`/`connectFailures`/`lastError` for the
|
|
visualiser.
|
|
|
|
### Diagnostics
|
|
|
|
```json
|
|
{
|
|
"backends": [{ "id": "pulse", "name": "…", "available": false }],
|
|
"system": {
|
|
"root": true,
|
|
"uid": 0,
|
|
"libraries": { "libpulse.so.0": "/usr/lib/libpulse.so.0",
|
|
"libasound.so.2": null },
|
|
"binaries": { "parec": false, "pactl": true },
|
|
"pulseSockets": ["/var/run/pulse/native"],
|
|
"pactlSources": "…",
|
|
"alsaCards": ["0 [Loopback]: …"],
|
|
"alsaCapturePcms": ["00-01: …"]
|
|
}
|
|
}
|
|
```
|
|
|
|
This is the fastest way to find out why a backend reports itself unavailable.
|
|
`tools/tv-probe.sh` collects the same picture from a shell, plus a few things
|
|
the service does not look at.
|
|
|
|
## Doing it from a shell
|
|
|
|
```sh
|
|
# change settings and start in one call (the patch is saved, like setConfig)
|
|
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/start \
|
|
'{"capture":{"backend":"tone"},"sinks":["http"]}'
|
|
|
|
# point the RTP sink somewhere else and keep it
|
|
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/setConfig \
|
|
'{"settings":{"hyperhdr":{"host":"192.168.1.50"}}}'
|
|
|
|
# watch the level
|
|
luna-send -i -f luna://org.webosbrew.audiocap.service/getStatus '{"subscribe":true}'
|
|
|
|
# what went wrong
|
|
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/getLogs '{}'
|
|
```
|