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:
Rene Kievits
2026-08-26 10:21:00 +02:00
co-authored by Claude Opus 5
commit 7529a60650
73 changed files with 12826 additions and 0 deletions
+136
View File
@@ -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.