Audio capture and streaming app for webOS 5/6
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>
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
# 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 '{}'
|
||||
```
|
||||
@@ -0,0 +1,152 @@
|
||||
# Building, testing and publishing
|
||||
|
||||
## What you need
|
||||
|
||||
| For | Install |
|
||||
| --- | --- |
|
||||
| the native service | the [openlgtv buildroot NDK](https://github.com/openlgtv/buildroot-nc4/releases), `arm-webos-linux-gnueabi_sdk-buildroot` |
|
||||
| packaging | `npm install -g @webosose/ares-cli` |
|
||||
| the tests | a host C compiler, Python 3, Node (optional: `flatbuffers`, `jsdom`) |
|
||||
|
||||
Unpack the NDK and relocate it once:
|
||||
|
||||
```sh
|
||||
tar xf arm-webos-linux-gnueabi_sdk-buildroot.tar.gz -C "$HOME"
|
||||
"$HOME/arm-webos-linux-gnueabi_sdk-buildroot/relocate-sdk.sh"
|
||||
```
|
||||
|
||||
Register the TV with ares once, using the Homebrew Channel's ssh (port 9922,
|
||||
root):
|
||||
|
||||
```sh
|
||||
ares-setup-device --add tv \
|
||||
--info "{'host':'192.168.1.20','port':9922,'username':'root'}"
|
||||
```
|
||||
|
||||
## Build and deploy
|
||||
|
||||
```sh
|
||||
./tools/build.sh # cross-compile, stage, package -> out/*.ipk
|
||||
./tools/build.sh install # ares-install on device "tv"
|
||||
./tools/build.sh launch
|
||||
./tools/build.sh logs
|
||||
```
|
||||
|
||||
`DEVICE=livingroom ./tools/build.sh install` targets a different device;
|
||||
`WEBOS_SDK=/opt/webos-sdk ./tools/build.sh` a differently placed NDK.
|
||||
|
||||
The same commands exist as npm scripts (`npm run build`, `npm run deploy`, …)
|
||||
if that is more your habit.
|
||||
|
||||
### What the packaging step does
|
||||
|
||||
`ares-package` takes two directories:
|
||||
|
||||
```text
|
||||
build/stage/app frontend/, minus js/mock.js and its <script> tag
|
||||
build/stage/service servicefiles/ plus the compiled audiocap-service
|
||||
```
|
||||
|
||||
The service is declared native in `servicefiles/services.json`
|
||||
(`"engine": "native"`), which is what makes webOS exec the binary rather than
|
||||
look for a Node entry point.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
./test/run-tests.sh
|
||||
```
|
||||
|
||||
Everything runs on the host — no TV involved:
|
||||
|
||||
| Suite | What it proves |
|
||||
| --- | --- |
|
||||
| syntax check | `service.c` and `main.c` compile against stub Luna/glib headers |
|
||||
| `verify_flatbuf.py` | the hand-rolled FlatBuffers encoder matches what the official Python runtime decodes — union tags, defaults, vector contents |
|
||||
| `verify_rtp.py` | the RTP sink and `host/lgtv-audiocap-receiver.py` agree: header layout, sequencing, timestamps, big-endian payload, MTU, and the SDP text |
|
||||
| `engine_smoke.c` | capture → DSP → fan-out → sockets, including all 44 bytes of the WAV header and the status document |
|
||||
| `ui_smoke.js` | the real `index.html` loaded in jsdom and driven like a remote |
|
||||
|
||||
The two optional dependencies:
|
||||
|
||||
```sh
|
||||
python3 -m venv /tmp/fbvenv && /tmp/fbvenv/bin/pip install flatbuffers
|
||||
npm install # jsdom
|
||||
```
|
||||
|
||||
Without them those two suites print SKIP and the rest still runs.
|
||||
|
||||
To open the UI in a desktop browser — `js/mock.js` stands in for the Luna bus:
|
||||
|
||||
```sh
|
||||
npm run serve # http://localhost:8000
|
||||
```
|
||||
|
||||
## Publishing to the Homebrew Channel
|
||||
|
||||
1. Bump `version` in `frontend/appinfo.json`, `servicefiles/package.json` and
|
||||
`package.json`.
|
||||
2. `./tools/build.sh` and test the ipk on a real set.
|
||||
3. Attach the ipk and `frontend/assets/icon.png` to a release.
|
||||
4. Generate the manifest and attach that too:
|
||||
|
||||
```sh
|
||||
python3 tools/make-manifest.py \
|
||||
--base-url https://github.com/you/lgtv-audio-cap/releases/download/v1.0.0
|
||||
```
|
||||
|
||||
5. Submit the manifest URL to [webosbrew/repo](https://github.com/webosbrew/repo).
|
||||
|
||||
The manifest sets `"rootRequired": true`, which tells the Homebrew Channel the
|
||||
service needs elevating. The app can also do it on demand — *System → Grant root
|
||||
access* runs the Channel's `elevate-service`.
|
||||
|
||||
## How the service is put together
|
||||
|
||||
```text
|
||||
main.c registers on the bus, runs the glib loop
|
||||
service.c the Luna methods and the status subscription
|
||||
engine.c the capture thread: read a block, analyse it, hand it to every sink
|
||||
config.c load/merge/atomic-save of config.json
|
||||
dsp.c peak/RMS envelopes and the 16-band analysis
|
||||
capture/ one file per backend, all dlopen-based
|
||||
sinks/ one file per output
|
||||
net/ RTP, FlatBuffers, the shared stream server
|
||||
common/ JSON, logging, ring buffer, audio format
|
||||
```
|
||||
|
||||
Three rules hold the design together:
|
||||
|
||||
**One format inside.** Everything between a backend and a sink is interleaved
|
||||
signed 16-bit little-endian PCM at the configured rate. Backends convert on the
|
||||
way in, sinks convert on the way out. Nothing in the middle branches on sample
|
||||
type.
|
||||
|
||||
**Sinks must never block.** The engine calls every sink from the capture thread,
|
||||
in order, and a sink that stalls stalls capture. Anything that can wait — a TCP
|
||||
client that stopped reading, a HyperHDR host that is switched off — buffers
|
||||
internally and drops the oldest audio instead. The Flatbuffers sink connects
|
||||
non-blockingly and finishes the handshake on later blocks; sends are bounded to
|
||||
200 ms.
|
||||
|
||||
**Audio libraries are `dlopen`ed, never linked.** A TV without `libpulse` must
|
||||
still run the ALSA backend, and one with neither must still run the test tone
|
||||
and start up cleanly. `ldd` on the binary shows glib, luna-service2, libc — no
|
||||
audio.
|
||||
|
||||
### Adding a sink
|
||||
|
||||
1. Write `native/src/sinks/sink_yours.c` with an `open` that reads its own key
|
||||
out of the settings object, plus `write`, `status` and `close`.
|
||||
2. Define `const sink_driver_t sink_driver_yours` at the bottom and declare it
|
||||
in `sinks/sink.h`.
|
||||
3. Add it to the table in `sinks/sink.c` and to `DEFAULTS_JSON` in `config.c`.
|
||||
4. Add its fields to `SINK_FIELDS` in `frontend/js/app.js`.
|
||||
|
||||
The UI needs nothing else: it builds the Outputs panel from `listSinks`.
|
||||
|
||||
### Adding a capture backend
|
||||
|
||||
The same shape in `native/src/capture/`, with an `available()` that answers
|
||||
honestly on a TV that lacks the library, and an optional `describe()` that adds
|
||||
its own fields to the diagnostics.
|
||||
@@ -0,0 +1,163 @@
|
||||
# Connecting to HyperHDR
|
||||
|
||||
HyperHDR's music effects read a **local capture device**. There is no network
|
||||
audio input to send to, no API to push samples into. Everything below is a way
|
||||
of working around that.
|
||||
|
||||
Three routes, in the order you should try them.
|
||||
|
||||
---
|
||||
|
||||
## 1. RTP audio into a loopback device (recommended)
|
||||
|
||||
The TV sends RTP/L16 to the HyperHDR machine; a receiver there plays it into a
|
||||
loopback; HyperHDR captures the other end of that loopback. HyperHDR sees a
|
||||
perfectly ordinary sound card and does its own analysis, so every effect works
|
||||
exactly as it would with a real input.
|
||||
|
||||
```
|
||||
TV ──RTP/L16 udp/5004──► lgtv-audiocap-receiver.py ──► hw:Loopback,0,0
|
||||
║ snd-aloop
|
||||
HyperHDR ◄──── hw:Loopback,1,0
|
||||
```
|
||||
|
||||
### On the HyperHDR machine
|
||||
|
||||
```sh
|
||||
git clone <this repo> && cd lgtv-audio-cap
|
||||
sudo ./host/install-loopback.sh --install-service
|
||||
```
|
||||
|
||||
That loads `snd-aloop` (persisting it across reboots), keeps PulseAudio's hands
|
||||
off the loopback card, installs the receiver into `/usr/local/bin` and starts it
|
||||
as a systemd unit. It finishes by printing the exact device name to give
|
||||
HyperHDR.
|
||||
|
||||
To do it by hand instead:
|
||||
|
||||
```sh
|
||||
sudo modprobe snd-aloop index=10 pcm_substreams=1 id=Loopback
|
||||
./host/lgtv-audiocap-receiver.py --output aplay --device hw:Loopback,0,0
|
||||
```
|
||||
|
||||
### On the TV
|
||||
|
||||
*Outputs → HyperHDR audio (RTP/L16)*
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| Receiver address | the HyperHDR machine's IP |
|
||||
| UDP port | 5004 |
|
||||
| Multicast | off |
|
||||
| Announce over SAP | on (harmless, and needed for route 2) |
|
||||
|
||||
Press **Start**.
|
||||
|
||||
### In HyperHDR
|
||||
|
||||
Settings → *Sound capture* (in newer builds; older ones put it under the music
|
||||
effect itself) → input device `hw:Loopback,1,0`, then choose a music effect.
|
||||
|
||||
### Checking it
|
||||
|
||||
```sh
|
||||
# is anything arriving at all?
|
||||
./host/lgtv-audiocap-receiver.py --port 5004 --output - | \
|
||||
aplay -f S16_LE -r 48000 -c 2 -
|
||||
```
|
||||
|
||||
The receiver prints a line every 30 seconds with packet, loss and restart
|
||||
counts. Losses in the low hundreds over hours are normal on Wi-Fi; a steady
|
||||
stream of them means the TV's Wi-Fi is the bottleneck and the set really wants
|
||||
Ethernet.
|
||||
|
||||
---
|
||||
|
||||
## 2. RTP straight into PulseAudio, nothing installed
|
||||
|
||||
If the HyperHDR machine runs PulseAudio or PipeWire and HyperHDR can reach it
|
||||
through the ALSA `pulse` device, you do not need the receiver at all. The TV
|
||||
announces the stream over SAP and PulseAudio builds a source from it.
|
||||
|
||||
```sh
|
||||
pactl load-module module-rtp-recv sap_address=224.0.0.56
|
||||
```
|
||||
|
||||
With *Announce over SAP* enabled on the TV, a source called something like
|
||||
`rtp_recv.LG TV Audio Cap` appears within five seconds. Point HyperHDR at its
|
||||
monitor.
|
||||
|
||||
This is the least code, but it is also the least predictable: PulseAudio's RTP
|
||||
receiver has no jitter buffer worth the name, and PipeWire's compatibility layer
|
||||
does not always implement the module. Treat it as a nice surprise if it works.
|
||||
|
||||
For unicast rather than SAP discovery, turn *Announce over SAP* off and load:
|
||||
|
||||
```sh
|
||||
pactl load-module module-rtp-recv sap_address=0.0.0.0 port=5004
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. The TV does the visualising
|
||||
|
||||
No host software, no sound device. The TV analyses the audio, renders a small
|
||||
image and sends it to HyperHDR's Flatbuffers port, the same way a
|
||||
`hyperion-remote` or a screen grabber would.
|
||||
|
||||
*Outputs → HyperHDR visualiser*
|
||||
|
||||
| Setting | Value |
|
||||
| --- | --- |
|
||||
| HyperHDR address | the HyperHDR machine's IP |
|
||||
| Flatbuffers port | 19400 |
|
||||
| Style | Spectrum, Level bar or Pulse |
|
||||
| Priority | 150 (lower numbers win in HyperHDR) |
|
||||
|
||||
In HyperHDR, make sure the Flatbuffers server is enabled (Settings → Network
|
||||
Services → Flatbuffers server, default port 19400).
|
||||
|
||||
What you give up: HyperHDR's own effects, colour calibration on the audio path,
|
||||
and any hope of the lights matching an effect you have configured elsewhere. The
|
||||
TV decides what the lights show. What you gain: it works in about a minute.
|
||||
|
||||
The three styles:
|
||||
|
||||
- **Spectrum** — 16 bands across the image, hue by frequency.
|
||||
- **Level bar** — one bar that tracks the overall level.
|
||||
- **Pulse** — the whole image flashes with the beat.
|
||||
|
||||
`saturation` and `minBrightness` shape the output; `minBrightness: 0` lets the
|
||||
lights go fully dark between beats, which looks dramatic and slightly broken.
|
||||
|
||||
---
|
||||
|
||||
## Which one to use
|
||||
|
||||
| | Route 1 | Route 2 | Route 3 |
|
||||
| --- | --- | --- | --- |
|
||||
| Host software | receiver + loopback | none | none |
|
||||
| HyperHDR effects | all of them | all of them | none, the TV renders |
|
||||
| Latency | ~100 ms | ~100 ms, less stable | ~40 ms |
|
||||
| Robustness | good | depends on your PulseAudio | good |
|
||||
| Setup time | 10 minutes | 2 minutes if it works | 1 minute |
|
||||
|
||||
Route 1 unless you have a reason.
|
||||
|
||||
---
|
||||
|
||||
## Latency
|
||||
|
||||
Roughly, end to end on route 1:
|
||||
|
||||
| Stage | Typical |
|
||||
| --- | --- |
|
||||
| TV capture block | 11 ms (512 frames at 48 kHz) |
|
||||
| Network | 1–5 ms wired, 5–40 ms Wi-Fi |
|
||||
| Receiver prebuffer | 60 ms, `--prebuffer-ms` |
|
||||
| Playback buffer | 80 ms, `--latency-ms` |
|
||||
| HyperHDR's own analysis | 20–50 ms |
|
||||
|
||||
Around 150–200 ms in total, which for ambient lighting is imperceptible. If you
|
||||
want it tighter, lower `--latency-ms` and `--prebuffer-ms` until the audio
|
||||
starts crackling, then go back up one step.
|
||||
@@ -0,0 +1,136 @@
|
||||
# When it does not work
|
||||
|
||||
Work down the chain: does the service run, does it capture, does the audio
|
||||
leave the TV, does it arrive, does HyperHDR see it.
|
||||
|
||||
## The app says "No service"
|
||||
|
||||
The service did not start. Reinstall the app, then check from a shell on the TV:
|
||||
|
||||
```sh
|
||||
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/isRunning '{}'
|
||||
ls -l /media/developer/apps/usr/palm/services/org.webosbrew.audiocap.service/
|
||||
```
|
||||
|
||||
The binary must be there and executable. If `luna-send` reports
|
||||
`Service does not exist`, the service failed to register on the bus — usually a
|
||||
missing `services.json` or a binary built for the wrong architecture. Check with
|
||||
`file audiocap-service`: it should say ARM, EABI5.
|
||||
|
||||
## "The service is running as uid …" instead of as root
|
||||
|
||||
It has not been elevated. Press *System → Grant root access*, or run:
|
||||
|
||||
```sh
|
||||
/media/developer/apps/usr/palm/services/org.webosbrew.hbchannel.service/elevate-service \
|
||||
org.webosbrew.audiocap.service
|
||||
```
|
||||
|
||||
Then restart the service (`quit` on the bus, or just relaunch the app).
|
||||
Without root the audio devices are unreadable and the settings file falls back
|
||||
to `/tmp`.
|
||||
|
||||
## Nothing is captured
|
||||
|
||||
*Capture → Backend* on Automatic and the meter stays flat.
|
||||
|
||||
1. **System → Run diagnostics.** Look at `libraries` and `pulseSockets`. If
|
||||
`libpulse.so.0` and `libasound.so.2` are both `null`, no backend can work as
|
||||
built and you need the `exec` backend with whatever binary the TV does have.
|
||||
2. **Run the probe** for the fuller picture:
|
||||
|
||||
```sh
|
||||
ssh -p 9922 root@TV-IP 'sh -s' < tools/tv-probe.sh
|
||||
```
|
||||
|
||||
3. **Prove the rest of the chain first.** Set the backend to *Test tone* and
|
||||
start. If the tone reaches HyperHDR, the problem is only the capture end.
|
||||
|
||||
Common outcomes by firmware:
|
||||
|
||||
| What the probe shows | What to do |
|
||||
| --- | --- |
|
||||
| a PulseAudio socket and `pactl` lists a `.monitor` source | backend `pulse`, device = that source name (or blank) |
|
||||
| `/proc/asound` with a capture PCM | backend `alsa`, device `hw:X,Y` from `arecord -l` |
|
||||
| only `arecord` or `gst-launch-1.0` | backend `exec`, e.g. `arecord -D hw:0,0 -f S16_LE -r 48000 -c 2 -t raw` |
|
||||
| nothing at all | the audio path is inside the closed audio daemon; only the test tone will run |
|
||||
|
||||
## The meter moves but the lights do not
|
||||
|
||||
Audio is being captured, so it is the transport or HyperHDR.
|
||||
|
||||
**Is it leaving the TV?** Enable the HTTP output and open
|
||||
`http://TV-IP:4012/audio.wav` in VLC. If you hear the TV, the TV's side is
|
||||
fine.
|
||||
|
||||
**Is it arriving?** On the HyperHDR machine:
|
||||
|
||||
```sh
|
||||
./host/lgtv-audiocap-receiver.py --port 5004 --output - | \
|
||||
aplay -f S16_LE -r 48000 -c 2 -
|
||||
```
|
||||
|
||||
Nothing? Check the firewall (`sudo ufw allow 5004/udp`) and that the receiver
|
||||
address on the TV is right. Watch the sink's own counters in
|
||||
*Status → Outputs*: `sendErrors` climbing means the TV cannot even send.
|
||||
|
||||
**Is HyperHDR listening to the right device?** The loopback has two ends and
|
||||
they are easy to swap. The receiver plays into `hw:Loopback,0,0`; HyperHDR must
|
||||
capture `hw:Loopback,1,0`.
|
||||
|
||||
**Is something else holding the loopback?** PulseAudio grabs cards it finds.
|
||||
`install-loopback.sh` writes a udev rule to keep it away; if you set the card up
|
||||
by hand, add it yourself:
|
||||
|
||||
```text
|
||||
/etc/udev/rules.d/89-lgtv-audiocap-loopback.rules
|
||||
ATTRS{id}=="Loopback", ENV{PULSE_IGNORE}="1", ENV{ACP_IGNORE}="1"
|
||||
```
|
||||
|
||||
## The audio crackles or drops out
|
||||
|
||||
The receiver prints loss statistics every 30 seconds. Read those first.
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| steady loss on Wi-Fi | the TV's radio | Ethernet, or the visualiser output instead |
|
||||
| loss in bursts | buffer too small for the jitter | `--latency-ms 150 --prebuffer-ms 120` |
|
||||
| clicks with no reported loss | the loopback and the stream disagree about the rate | make the TV's rate, the receiver's `--rate` and HyperHDR's device all 48000 |
|
||||
| audio slowly drifts out of sync | free-running clocks, unavoidable in a one-way stream | it is a few ms per hour; restart the receiver if it ever matters |
|
||||
|
||||
## The lights react to the wrong thing
|
||||
|
||||
HyperHDR is showing another source at a higher priority. The visualiser sink
|
||||
sends at priority 150 by default; a screen grabber usually sits at 240 and a
|
||||
static colour at 100. Lower numbers win. Set the visualiser's priority below
|
||||
whatever is currently on screen.
|
||||
|
||||
## Settings do not survive a reboot
|
||||
|
||||
*System → Settings file* says `Temporary (/tmp)`. The service is not root, so it
|
||||
cannot write `/var/lib/webosbrew/audiocap/`. Grant root access; the next save
|
||||
lands in the persistent path.
|
||||
|
||||
## It does not start with the TV
|
||||
|
||||
*System → Start on boot* installs a symlink in `/var/lib/webosbrew/init.d/`,
|
||||
which the Homebrew Channel runs at boot. Check it:
|
||||
|
||||
```sh
|
||||
ls -l /var/lib/webosbrew/init.d/audiocapautostart
|
||||
```
|
||||
|
||||
If it is missing, the Homebrew Channel's `exec` refused the call — its
|
||||
"root access" toggle has to be on. If it is there and nothing happens at boot,
|
||||
check that *autoStart* is also on: the script only wakes the service, and the
|
||||
service decides for itself whether to start capturing.
|
||||
|
||||
## Reading the log
|
||||
|
||||
```sh
|
||||
luna-send -n 1 -f luna://org.webosbrew.audiocap.service/getLogs '{}'
|
||||
```
|
||||
|
||||
or *System → Show log* in the app. Set *Log level* to Debug first if you are
|
||||
chasing something specific — it is the last 200 lines only, and at Debug they
|
||||
go by quickly.
|
||||
Reference in New Issue
Block a user