Confirmed the brightness command applies globally, not per-LED
(serverinfo showed exactly one adjustment object, "id": "default",
covering the whole string), so no LED-count configuration is needed
for this at all -- that question resolved itself once the mechanism
was actually inspected instead of assumed.
For "only react while Spotify is running": only one app can be in the
foreground on webOS at a time, so a "capture the current app" button
in this app's own UI can never work -- pressing it means this app is
foreground, not Spotify. The only workable UI is picking a target from
every *installed* app by name, regardless of what's currently running.
That needed a new native capability this service never had: calling
OUT to another Luna service, not just being called. Two additions:
foreground_app.c subscribes once, at startup, to
com.webos.applicationManager/getForegroundAppInfo
and keeps a thread-safe cache the audio thread can
read without a blocking Luna call
service.c new listApps method, bridging to
com.webos.applicationManager/listApps so the
frontend never has to call another service
directly -- same rule as everywhere else here
Until the subscription has delivered at least one reply, a restricted
sink treats the target app as inactive, not active -- reacting to
audio when the user explicitly restricted it to one app would be the
wrong failure mode. Verified end to end on the host: engine_smoke.c
opens the sink with a restriction set, confirms it reports itself
correctly inactive against the stub Luna bus (which always "fails" to
call out, exactly like a real host with no bus).
Needed real, linkable stub bodies for LSCall/LSCallOneReply/
LSCallCancel/LSMessageGetPayload/LSErrorInit/LSErrorFree
(test/stubs/luna-service2/lunaservice_stub.c) since foreground_app.c
is the first source file here that's actually linked into a host test
binary rather than only syntax-checked -- service.c/main.c's existing
stub declarations were never called, only compiled against. Confirmed
those really are the correct symbol names by cross-compiling clean
against the real webOS SDK's actual libluna-service2, not just the
stub.
Bumped to 1.0.5.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LG TV Audio Cap
Captures the audio playing on an LG webOS 5 or 6 TV and streams it off the set. The reason it exists is HyperHDR: ambient lighting that reacts to what the TV is actually playing, without a microphone in the room or an audio splitter behind the telly.
It installs through the Homebrew Channel and runs as a native background service with a remote-friendly UI in front of it.
┌────────────────────── LG webOS TV ──────────────────────┐
│ PulseAudio / ALSA / a command │
│ │ │
│ capture ─► level + 16-band analysis │
│ │ │
│ ├─► RTP/L16 ──────────────► HyperHDR host │ ← the main path
│ ├─► Flatbuffers images ───► HyperHDR │
│ ├─► raw PCM over UDP │
│ ├─► raw PCM over TCP │
│ └─► WAV over HTTP │
└─────────────────────────────────────────────────────────┘
Why it works this way
HyperHDR has no network audio input. Its sound-reactive effects read a
local capture device. So the main path here does not try to talk to HyperHDR
at all: it sends the TV's audio to the HyperHDR machine as RTP/L16, and a small
receiver there turns it into a normal sound device that HyperHDR can select.
That is host/lgtv-audiocap-receiver.py, and
host/install-loopback.sh sets up the loopback for
it.
If you would rather not run anything on the HyperHDR machine, there is a second path: the TV does the frequency analysis itself and sends finished images to HyperHDR's Flatbuffers port. Fewer moving parts, but the lights react to the TV's idea of the spectrum rather than to real audio.
See docs/hyperhdr.md for both, step by step.
Requirements
- An LG TV on webOS 5 or 6, rooted, with the Homebrew Channel installed.
- Root for the service. The TV's audio devices are not readable otherwise; the
app has a Grant root access button that calls the Homebrew Channel's
elevate-servicefor you. - For the main HyperHDR path: a Linux machine running HyperHDR with either
snd-aloopor PulseAudio/PipeWire available.
Installing
This app is not in the official Homebrew Channel repository (that needs a review submission — see docs/development.md). Two ways to get it onto the TV instead, neither needing SSH or Developer Mode:
Add it as a custom repository (recommended). The Homebrew Channel can browse-and-install from any repository URL you give it, not just the official one. Build the ipk, publish it plus a generated repo index, then add that URL on the TV:
npm install # ares-cli, only needed for step 2
./tools/docker-build.sh # cross-compile; Linux: ./tools/build.sh native
./tools/build.sh package # -> out/*.ipk
python3 tools/make-manifest.py --base-url <where you'll host the release assets>
# -> out/repo.json, out/manifest.json
Attach the ipk, frontend/assets/icon.png and out/repo.json to a release at
that URL, then on the TV: Homebrew Channel → ⚙ → Add repository → paste the
repo.json URL → back to Browse → Audio Cap → Install. Full walkthrough,
including why the URLs have to match exactly, in
docs/development.md.
Install the ipk directly, if you do have ares-cli talking to the TV (e.g. LG Developer Mode's ssh on port 9922, or a root SSH server you've enabled):
ares-install --device tv out/org.webosbrew.audiocap_1.0.0_all.ipk
First run
- Launch Audio Cap on the TV.
- System → Root access: press Grant root access if it says the service is not root. It restarts itself.
- Capture → Backend: leave it on Automatic to begin with. If nothing is
captured, run
tools/tv-probe.shon the TV to see what your firmware actually offers, then pick a backend by hand. - Outputs → HyperHDR audio (RTP/L16): turn it on and enter the address of the machine running HyperHDR.
- On that machine:
sudo ./host/install-loopback.sh --install-service, then point HyperHDR's sound capture at the device it prints. - Press Start on the TV. The level meter should move.
Nothing captured, no idea why? docs/troubleshooting.md.
The other outputs
Each can run at the same time as the others.
| Output | What it is | Use it for |
|---|---|---|
| HyperHDR audio | RTP/L16, port 5004 | the main path; also readable by PulseAudio's module-rtp-recv with no custom software |
| HyperHDR visualiser | Flatbuffers images, port 19400 | HyperHDR with nothing installed on the host |
| HyperHDR brightness | JSON-RPC, port 19444 | an existing grabber/ambilight setup — keeps its colour, only pulses brightness |
| Raw PCM over UDP | S16LE datagrams, port 4010 | your own scripts; lowest latency |
| Raw PCM over TCP | S16LE stream, port 4011 | anything that would rather connect than listen |
| HTTP WAV | http://tv:4012/audio.wav |
opening the TV's audio in VLC |
Layout
native/ the webOS service: capture, DSP, sinks, Luna API (C)
frontend/ the on-TV app (plain HTML/CSS/JS, no framework)
servicefiles/ services.json, package.json and the boot script
host/ the receiver and loopback setup for the HyperHDR machine
docker/ the receiver, packaged as a container (e.g. for Unraid)
unraid/ the plugin for the one part a container can't do: the
ALSA loopback kernel module, persisted across reboots
tools/ build, packaging, asset generation, on-TV probe
test/ host-side tests: wire formats, the capture pipeline, the UI
docs/ the longer explanations
Documentation
- docs/hyperhdr.md — connecting it to HyperHDR, both ways
- docs/configuration.md — every setting, and the Luna API
- docs/development.md — building, testing, packaging, publishing
- docs/troubleshooting.md — when it does not work
License
MIT.