Restrict the brightness sink to one app, picked by name not typed

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>
This commit is contained in:
Rene Kievits
2026-08-26 15:46:09 +02:00
co-authored by Claude Opus 5
parent e9f6c87d27
commit d3e4cb6410
20 changed files with 411 additions and 13 deletions
+17 -2
View File
@@ -112,6 +112,19 @@ and what a given build actually acts on are not always the same thing.
| `level` | `"rms"` | `rms` (steadier) or `peak` (punchier) |
| `minBrightness` | `20` | brightness (0-100) during silence |
| `maxBrightness` | `100` | brightness (0-100) at full level; HyperHDR does not go above 100 |
| `restrictToApp` | `""` | a webOS app id; blank means always active |
`restrictToApp` is set from a picker of installed apps in the UI (by name,
never typed), backed by a new `listApps` Luna method that proxies to
`com.webos.applicationManager/listApps` — the frontend never calls another
service's Luna API directly, everything goes through this service, same as
everywhere else. The service also subscribes once, at startup, to
`com.webos.applicationManager/getForegroundAppInfo` to know which app is
currently in front. Until that subscription has delivered at least one
reply, a restricted sink treats the target app as *not* active — the safe
failure mode, since silently reacting to audio when the user explicitly
restricted it to one app would be the wrong one. `getDiagnostics` exposes
the live value as `foregroundApp` if you want to confirm tracking is working.
### `udp`, `tcp`, `http`
@@ -149,7 +162,8 @@ luna-send -n 1 -f luna://org.webosbrew.audiocap.service/getStatus '{}'
| `resetConfig` | `{}` | `{saved, settings}` |
| `listBackends` | `{}` | `{backends:[{id,name,description,available}]}` |
| `listSinks` | `{}` | `{sinks:[{id,name,description}]}` |
| `getDiagnostics` | `{}` | `{backends, system}` — see below |
| `listApps` | `{}` | `{apps:[{id,title}]}` — every installed app, for the "restrict to app" picker |
| `getDiagnostics` | `{}` | `{backends, system, foregroundApp}` — see below |
| `getLogs` | `{"clear":true}` optional | `{logs}` |
| `quit` | `{}` | ends the process; the next call starts a new one |
@@ -199,7 +213,8 @@ 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, `connected`/`updatesSent`/`minBrightness`/`maxBrightness`/`lastError`
for the brightness sink.
for the brightness sink, plus `restrictToApp`/`restrictedAppActive` when a
restriction is set.
### Diagnostics
+3
View File
@@ -186,6 +186,9 @@ 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
foreground_app.c tracks which app is in front, for the "restrict to app"
brightness option -- the one place this service calls out
to another Luna service instead of being called
capture/ one file per backend, all dlopen-based
sinks/ one file per output
net/ RTP, FlatBuffers, the shared stream server
+9
View File
@@ -195,6 +195,15 @@ TV ──RTP or local──► audiocap-service ──JSON-RPC "adjustment"─
| Follows | Average level (steadier) or Peak level (punchier) |
| Minimum brightness | `0`-`100`; applied during quiet parts |
| Maximum brightness | `0`-`100`; applied at full level. HyperHDR does not go above 100 |
| Restrict to app | optional — only react to audio while one specific app is in the foreground |
*Restrict to app* is picked from a list of installed apps by name (e.g.
"Spotify"), never typed — only one app can be foreground at a time, so
there is no way to "capture the current app" from a button in this app's
own UI; you'd just be capturing yourself. Until the TV confirms which app
is actually in front, the sink treats the target as inactive rather than
guessing yes, so it can't accidentally react to the wrong thing while
starting up.
Needs a working capture source the same as every other route — see the top
of this document for picking one. On close, the sink resets brightness to