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
+1 -1
View File
@@ -1,6 +1,6 @@
{
"id": "org.webosbrew.audiocap",
"version": "1.0.4",
"version": "1.0.5",
"vendor": "Homebrew",
"type": "web",
"main": "index.html",
+32
View File
@@ -28,6 +28,7 @@
status: null,
backends: [],
sinkDefs: [],
installedApps: [],
configPath: '',
persistent: true,
bootLinked: false,
@@ -244,6 +245,17 @@
return out;
}
// Fetched once at boot (see the listApps call near the bottom of this
// file). Sorted by title there, so this just adds the "no restriction"
// default at the front.
function installedAppOptions() {
var out = [{ value: '', label: 'Always active' }];
state.installedApps.forEach(function (a) {
out.push({ value: a.id, label: a.title });
});
return out;
}
var CAPTURE_FIELDS = [
{
path: 'capture.backend', label: 'Backend', type: 'choice',
@@ -408,6 +420,13 @@
path: 'hyperhdrAdjust.maxBrightness', label: 'Maximum brightness', type: 'number',
hint: '0-100. Applied at full level. HyperHDR does not go above 100.',
},
{
path: 'hyperhdrAdjust.restrictToApp', label: 'Restrict to app', type: 'choice',
options: installedAppOptions,
hint: 'Only react to audio while this app is in the foreground. '
+ 'Picked by name, not typed — only one app can be in front at a '
+ 'time, so there is no way to "capture the current app" from here.',
},
],
udp: [
@@ -699,6 +718,9 @@
if (s.connected !== undefined) {
bits.push(s.connected ? 'connected' : 'not connected');
}
if (s.restrictToApp) {
bits.push(s.restrictedAppActive ? 'active now' : 'waiting for that app');
}
if (s.sendErrors) {
bits.push(s.sendErrors + ' send errors');
}
@@ -902,6 +924,16 @@
renderSinks();
}, fail);
// Powers the "restrict to app" picker on the brightness sink. Fetched
// once at boot, same as backends/sinks above — the installed-app list
// does not change during a session.
Luna.listApps(function (reply) {
state.installedApps = (reply.apps || []).slice().sort(function (a, b) {
return a.title.localeCompare(b.title);
});
renderSinks();
}, fail);
renderCapture();
renderSystem();
checkBootLink();
+3
View File
@@ -101,6 +101,9 @@
listSinks: function (ok, fail) {
return call(SERVICE + 'listSinks', {}, ok, fail);
},
listApps: function (ok, fail) {
return call(SERVICE + 'listApps', {}, ok, fail);
},
getDiagnostics: function (ok, fail) {
return call(SERVICE + 'getDiagnostics', {}, ok, fail);
},
+12
View File
@@ -17,6 +17,7 @@
},
hyperhdrAdjust: {
host: '', port: 19444, minBrightness: 20, maxBrightness: 100, level: 'rms',
restrictToApp: '',
},
udp: { host: '', port: 4010, multicastTtl: 4 },
tcp: { port: 4011, maxClients: 4 },
@@ -156,6 +157,17 @@
],
});
case 'listApps':
return respond(onReply, {
returnValue: true,
apps: [
{ id: 'spotify-beehive', title: 'Spotify' },
{ id: 'netflix', title: 'Netflix' },
{ id: 'youtube.leanback.v4', title: 'YouTube' },
{ id: 'com.webos.app.livetv', title: 'Live TV' },
],
});
// Same shape as capture_write_diagnostics(): backends at the top level,
// everything about the machine under "system".
case 'getDiagnostics':