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:
co-authored by
Claude Opus 5
parent
e9f6c87d27
commit
d3e4cb6410
+17
-2
@@ -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) |
|
| `level` | `"rms"` | `rms` (steadier) or `peak` (punchier) |
|
||||||
| `minBrightness` | `20` | brightness (0-100) during silence |
|
| `minBrightness` | `20` | brightness (0-100) during silence |
|
||||||
| `maxBrightness` | `100` | brightness (0-100) at full level; HyperHDR does not go above 100 |
|
| `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`
|
### `udp`, `tcp`, `http`
|
||||||
|
|
||||||
@@ -149,7 +162,8 @@ luna-send -n 1 -f luna://org.webosbrew.audiocap.service/getStatus '{}'
|
|||||||
| `resetConfig` | `{}` | `{saved, settings}` |
|
| `resetConfig` | `{}` | `{saved, settings}` |
|
||||||
| `listBackends` | `{}` | `{backends:[{id,name,description,available}]}` |
|
| `listBackends` | `{}` | `{backends:[{id,name,description,available}]}` |
|
||||||
| `listSinks` | `{}` | `{sinks:[{id,name,description}]}` |
|
| `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}` |
|
| `getLogs` | `{"clear":true}` optional | `{logs}` |
|
||||||
| `quit` | `{}` | ends the process; the next call starts a new one |
|
| `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,
|
datagram sinks, `clients`/`droppedBytes` for the stream servers,
|
||||||
`connected`/`registered`/`framesSent`/`connectFailures`/`lastError` for the
|
`connected`/`registered`/`framesSent`/`connectFailures`/`lastError` for the
|
||||||
visualiser, `connected`/`updatesSent`/`minBrightness`/`maxBrightness`/`lastError`
|
visualiser, `connected`/`updatesSent`/`minBrightness`/`maxBrightness`/`lastError`
|
||||||
for the brightness sink.
|
for the brightness sink, plus `restrictToApp`/`restrictedAppActive` when a
|
||||||
|
restriction is set.
|
||||||
|
|
||||||
### Diagnostics
|
### Diagnostics
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
engine.c the capture thread: read a block, analyse it, hand it to every sink
|
||||||
config.c load/merge/atomic-save of config.json
|
config.c load/merge/atomic-save of config.json
|
||||||
dsp.c peak/RMS envelopes and the 16-band analysis
|
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
|
capture/ one file per backend, all dlopen-based
|
||||||
sinks/ one file per output
|
sinks/ one file per output
|
||||||
net/ RTP, FlatBuffers, the shared stream server
|
net/ RTP, FlatBuffers, the shared stream server
|
||||||
|
|||||||
@@ -195,6 +195,15 @@ TV ──RTP or local──► audiocap-service ──JSON-RPC "adjustment"─
|
|||||||
| Follows | Average level (steadier) or Peak level (punchier) |
|
| Follows | Average level (steadier) or Peak level (punchier) |
|
||||||
| Minimum brightness | `0`-`100`; applied during quiet parts |
|
| Minimum brightness | `0`-`100`; applied during quiet parts |
|
||||||
| Maximum brightness | `0`-`100`; applied at full level. HyperHDR does not go above 100 |
|
| 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
|
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
|
of this document for picking one. On close, the sink resets brightness to
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "org.webosbrew.audiocap",
|
"id": "org.webosbrew.audiocap",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"vendor": "Homebrew",
|
"vendor": "Homebrew",
|
||||||
"type": "web",
|
"type": "web",
|
||||||
"main": "index.html",
|
"main": "index.html",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
status: null,
|
status: null,
|
||||||
backends: [],
|
backends: [],
|
||||||
sinkDefs: [],
|
sinkDefs: [],
|
||||||
|
installedApps: [],
|
||||||
configPath: '',
|
configPath: '',
|
||||||
persistent: true,
|
persistent: true,
|
||||||
bootLinked: false,
|
bootLinked: false,
|
||||||
@@ -244,6 +245,17 @@
|
|||||||
return out;
|
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 = [
|
var CAPTURE_FIELDS = [
|
||||||
{
|
{
|
||||||
path: 'capture.backend', label: 'Backend', type: 'choice',
|
path: 'capture.backend', label: 'Backend', type: 'choice',
|
||||||
@@ -408,6 +420,13 @@
|
|||||||
path: 'hyperhdrAdjust.maxBrightness', label: 'Maximum brightness', type: 'number',
|
path: 'hyperhdrAdjust.maxBrightness', label: 'Maximum brightness', type: 'number',
|
||||||
hint: '0-100. Applied at full level. HyperHDR does not go above 100.',
|
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: [
|
udp: [
|
||||||
@@ -699,6 +718,9 @@
|
|||||||
if (s.connected !== undefined) {
|
if (s.connected !== undefined) {
|
||||||
bits.push(s.connected ? 'connected' : 'not connected');
|
bits.push(s.connected ? 'connected' : 'not connected');
|
||||||
}
|
}
|
||||||
|
if (s.restrictToApp) {
|
||||||
|
bits.push(s.restrictedAppActive ? 'active now' : 'waiting for that app');
|
||||||
|
}
|
||||||
if (s.sendErrors) {
|
if (s.sendErrors) {
|
||||||
bits.push(s.sendErrors + ' send errors');
|
bits.push(s.sendErrors + ' send errors');
|
||||||
}
|
}
|
||||||
@@ -902,6 +924,16 @@
|
|||||||
renderSinks();
|
renderSinks();
|
||||||
}, fail);
|
}, 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();
|
renderCapture();
|
||||||
renderSystem();
|
renderSystem();
|
||||||
checkBootLink();
|
checkBootLink();
|
||||||
|
|||||||
@@ -101,6 +101,9 @@
|
|||||||
listSinks: function (ok, fail) {
|
listSinks: function (ok, fail) {
|
||||||
return call(SERVICE + 'listSinks', {}, ok, fail);
|
return call(SERVICE + 'listSinks', {}, ok, fail);
|
||||||
},
|
},
|
||||||
|
listApps: function (ok, fail) {
|
||||||
|
return call(SERVICE + 'listApps', {}, ok, fail);
|
||||||
|
},
|
||||||
getDiagnostics: function (ok, fail) {
|
getDiagnostics: function (ok, fail) {
|
||||||
return call(SERVICE + 'getDiagnostics', {}, ok, fail);
|
return call(SERVICE + 'getDiagnostics', {}, ok, fail);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
},
|
},
|
||||||
hyperhdrAdjust: {
|
hyperhdrAdjust: {
|
||||||
host: '', port: 19444, minBrightness: 20, maxBrightness: 100, level: 'rms',
|
host: '', port: 19444, minBrightness: 20, maxBrightness: 100, level: 'rms',
|
||||||
|
restrictToApp: '',
|
||||||
},
|
},
|
||||||
udp: { host: '', port: 4010, multicastTtl: 4 },
|
udp: { host: '', port: 4010, multicastTtl: 4 },
|
||||||
tcp: { port: 4011, maxClients: 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,
|
// Same shape as capture_write_diagnostics(): backends at the top level,
|
||||||
// everything about the machine under "system".
|
// everything about the machine under "system".
|
||||||
case 'getDiagnostics':
|
case 'getDiagnostics':
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ add_executable(audiocap-service
|
|||||||
src/main.c
|
src/main.c
|
||||||
src/service.c
|
src/service.c
|
||||||
src/engine.c
|
src/engine.c
|
||||||
|
src/foreground_app.c
|
||||||
src/config.c
|
src/config.c
|
||||||
src/dsp.c
|
src/dsp.c
|
||||||
src/common/log.c
|
src/common/log.c
|
||||||
|
|||||||
+2
-1
@@ -53,7 +53,8 @@ static const char* DEFAULTS_JSON =
|
|||||||
" \"port\": 19444,"
|
" \"port\": 19444,"
|
||||||
" \"minBrightness\": 20,"
|
" \"minBrightness\": 20,"
|
||||||
" \"maxBrightness\": 100,"
|
" \"maxBrightness\": 100,"
|
||||||
" \"level\": \"rms\""
|
" \"level\": \"rms\","
|
||||||
|
" \"restrictToApp\": \"\""
|
||||||
" },"
|
" },"
|
||||||
" \"udp\": { \"host\": \"\", \"port\": 4010, \"multicastTtl\": 4 },"
|
" \"udp\": { \"host\": \"\", \"port\": 4010, \"multicastTtl\": 4 },"
|
||||||
" \"tcp\": { \"port\": 4011, \"maxClients\": 4 },"
|
" \"tcp\": { \"port\": 4011, \"maxClients\": 4 },"
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#include "foreground_app.h"
|
||||||
|
#include "common/json.h"
|
||||||
|
#include "common/log.h"
|
||||||
|
|
||||||
|
#include <luna-service2/lunaservice.h>
|
||||||
|
#include <pthread.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static pthread_mutex_t s_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||||
|
static char s_current[192] = { 0 };
|
||||||
|
static bool s_known = false;
|
||||||
|
static LSMessageToken s_token = 0;
|
||||||
|
static bool s_subscribed = false;
|
||||||
|
|
||||||
|
static bool on_reply(LSHandle* sh, LSMessage* msg, void* ctx)
|
||||||
|
{
|
||||||
|
(void)sh;
|
||||||
|
(void)ctx;
|
||||||
|
const char* payload = LSMessageGetPayload(msg);
|
||||||
|
json_value_t* root = payload ? json_parse(payload) : NULL;
|
||||||
|
if (!root)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
const char* app_id = json_str(root, "appId", "");
|
||||||
|
pthread_mutex_lock(&s_lock);
|
||||||
|
snprintf(s_current, sizeof(s_current), "%s", app_id);
|
||||||
|
s_known = true;
|
||||||
|
pthread_mutex_unlock(&s_lock);
|
||||||
|
|
||||||
|
json_free(root);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool foreground_app_start(LSHandle* handle, char* err, size_t errlen)
|
||||||
|
{
|
||||||
|
LSError lserror;
|
||||||
|
LSErrorInit(&lserror);
|
||||||
|
bool ok = LSCall(handle, "luna://com.webos.applicationManager/getForegroundAppInfo",
|
||||||
|
"{\"subscribe\":true}", on_reply, NULL, &s_token, &lserror);
|
||||||
|
if (!ok) {
|
||||||
|
if (err)
|
||||||
|
snprintf(err, errlen, "getForegroundAppInfo: %s", lserror.message);
|
||||||
|
LSErrorFree(&lserror);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
s_subscribed = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void foreground_app_stop(LSHandle* handle)
|
||||||
|
{
|
||||||
|
if (!s_subscribed)
|
||||||
|
return;
|
||||||
|
LSError lserror;
|
||||||
|
LSErrorInit(&lserror);
|
||||||
|
if (!LSCallCancel(handle, s_token, &lserror)) {
|
||||||
|
DBG("LSCallCancel(foreground app subscription): %s", lserror.message);
|
||||||
|
LSErrorFree(&lserror);
|
||||||
|
}
|
||||||
|
s_subscribed = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool foreground_app_known(void)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&s_lock);
|
||||||
|
bool known = s_known;
|
||||||
|
pthread_mutex_unlock(&s_lock);
|
||||||
|
return known;
|
||||||
|
}
|
||||||
|
|
||||||
|
void foreground_app_current(char* out, size_t outlen)
|
||||||
|
{
|
||||||
|
pthread_mutex_lock(&s_lock);
|
||||||
|
snprintf(out, outlen, "%s", s_current);
|
||||||
|
pthread_mutex_unlock(&s_lock);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Tracks which app is currently in the foreground on the TV, so a sink can
|
||||||
|
// ask "is it Spotify right now" without making a blocking Luna call from the
|
||||||
|
// audio capture thread.
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// Opaque: only foreground_app.c itself needs the real luna-service2 API
|
||||||
|
// surface. Everything else here (in particular sink_hyperhdr_adjust.c, which
|
||||||
|
// only ever calls the two read-only accessors below) stays free of that
|
||||||
|
// dependency.
|
||||||
|
typedef struct LSHandle LSHandle;
|
||||||
|
|
||||||
|
// Subscribes once to com.webos.applicationManager/getForegroundAppInfo.
|
||||||
|
// Not fatal if it fails (logs and returns false): callers should treat an
|
||||||
|
// unknown foreground app as "no restriction applies" rather than silently
|
||||||
|
// freezing every app-restricted sink forever.
|
||||||
|
bool foreground_app_start(LSHandle* handle, char* err, size_t errlen);
|
||||||
|
void foreground_app_stop(LSHandle* handle);
|
||||||
|
|
||||||
|
// True once at least one reply has come back, i.e. `current` is meaningful
|
||||||
|
// rather than just "nothing heard yet".
|
||||||
|
bool foreground_app_known(void);
|
||||||
|
|
||||||
|
// Copies the current foreground app id into `out` (best effort; may lag the
|
||||||
|
// real state by a fraction of a second). Empty string if not known yet.
|
||||||
|
void foreground_app_current(char* out, size_t outlen);
|
||||||
@@ -4,6 +4,7 @@
|
|||||||
#include "common/log.h"
|
#include "common/log.h"
|
||||||
#include "config.h"
|
#include "config.h"
|
||||||
#include "engine.h"
|
#include "engine.h"
|
||||||
|
#include "foreground_app.h"
|
||||||
#include "sinks/sink.h"
|
#include "sinks/sink.h"
|
||||||
|
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
@@ -347,6 +348,67 @@ static bool method_list_sinks(LSHandle* sh, LSMessage* msg, void* ctx)
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bridges to com.webos.applicationManager/listApps so the UI can offer a
|
||||||
|
// picker of installed apps by name, without the frontend needing its own
|
||||||
|
// permission to call another service directly -- everything it does goes
|
||||||
|
// through us, the same as every other method here. `msg` outlives this
|
||||||
|
// handler's return (the reply comes later, from `on_list_apps_reply`), so it
|
||||||
|
// is ref-counted for that stretch and always unref'd exactly once.
|
||||||
|
static bool on_list_apps_reply(LSHandle* sh, LSMessage* reply, void* ctx)
|
||||||
|
{
|
||||||
|
LSMessage* original = ctx;
|
||||||
|
const char* payload = LSMessageGetPayload(reply);
|
||||||
|
json_value_t* root = payload ? json_parse(payload) : NULL;
|
||||||
|
const json_value_t* apps = root ? json_get(root, "apps") : NULL;
|
||||||
|
|
||||||
|
json_writer_t w;
|
||||||
|
jw_init(&w);
|
||||||
|
jw_obj_open(&w, NULL);
|
||||||
|
jw_bool(&w, "returnValue", true);
|
||||||
|
jw_arr_open(&w, "apps");
|
||||||
|
size_t count = json_len(apps);
|
||||||
|
for (size_t i = 0; i < count; i++) {
|
||||||
|
const json_value_t* app = json_at(apps, i);
|
||||||
|
const char* id = json_str(app, "id", NULL);
|
||||||
|
const char* title = json_str(app, "title", NULL);
|
||||||
|
// Skip anything without a real title (bare service ids, mostly) and
|
||||||
|
// this app itself -- restricting the sink "to itself" is meaningless.
|
||||||
|
if (!id || !title || !*title)
|
||||||
|
continue;
|
||||||
|
if (strcmp(id, "org.webosbrew.audiocap") == 0)
|
||||||
|
continue;
|
||||||
|
jw_obj_open(&w, NULL);
|
||||||
|
jw_str(&w, "id", id);
|
||||||
|
jw_str(&w, "title", title);
|
||||||
|
jw_obj_close(&w);
|
||||||
|
}
|
||||||
|
jw_arr_close(&w);
|
||||||
|
jw_obj_close(&w);
|
||||||
|
|
||||||
|
reply_json(sh, original, jw_take(&w));
|
||||||
|
if (root)
|
||||||
|
json_free(root);
|
||||||
|
LSMessageUnref(original);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool method_list_apps(LSHandle* sh, LSMessage* msg, void* ctx)
|
||||||
|
{
|
||||||
|
(void)ctx;
|
||||||
|
LSMessageRef(msg);
|
||||||
|
|
||||||
|
LSError lserror;
|
||||||
|
LSErrorInit(&lserror);
|
||||||
|
bool ok = LSCallOneReply(sh, "luna://com.webos.applicationManager/listApps",
|
||||||
|
"{\"properties\":[\"id\",\"title\",\"type\"]}", on_list_apps_reply, msg, NULL, &lserror);
|
||||||
|
if (!ok) {
|
||||||
|
reply_error(sh, msg, "listApps: %s", lserror.message);
|
||||||
|
LSErrorFree(&lserror);
|
||||||
|
LSMessageUnref(msg);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static bool method_get_diagnostics(LSHandle* sh, LSMessage* msg, void* ctx)
|
static bool method_get_diagnostics(LSHandle* sh, LSMessage* msg, void* ctx)
|
||||||
{
|
{
|
||||||
(void)ctx;
|
(void)ctx;
|
||||||
@@ -356,6 +418,13 @@ static bool method_get_diagnostics(LSHandle* sh, LSMessage* msg, void* ctx)
|
|||||||
jw_obj_open(&w, NULL);
|
jw_obj_open(&w, NULL);
|
||||||
jw_bool(&w, "returnValue", true);
|
jw_bool(&w, "returnValue", true);
|
||||||
capture_write_diagnostics(&w);
|
capture_write_diagnostics(&w);
|
||||||
|
if (foreground_app_known()) {
|
||||||
|
char app[192];
|
||||||
|
foreground_app_current(app, sizeof(app));
|
||||||
|
jw_str(&w, "foregroundApp", app);
|
||||||
|
} else {
|
||||||
|
jw_null(&w, "foregroundApp");
|
||||||
|
}
|
||||||
jw_obj_close(&w);
|
jw_obj_close(&w);
|
||||||
reply_json(sh, msg, jw_take(&w));
|
reply_json(sh, msg, jw_take(&w));
|
||||||
return true;
|
return true;
|
||||||
@@ -407,6 +476,7 @@ static LSMethod s_methods[] = {
|
|||||||
{ "resetConfig", method_reset_config, LUNA_METHOD_FLAGS_NONE },
|
{ "resetConfig", method_reset_config, LUNA_METHOD_FLAGS_NONE },
|
||||||
{ "listBackends", method_list_backends, LUNA_METHOD_FLAGS_NONE },
|
{ "listBackends", method_list_backends, LUNA_METHOD_FLAGS_NONE },
|
||||||
{ "listSinks", method_list_sinks, LUNA_METHOD_FLAGS_NONE },
|
{ "listSinks", method_list_sinks, LUNA_METHOD_FLAGS_NONE },
|
||||||
|
{ "listApps", method_list_apps, LUNA_METHOD_FLAGS_NONE },
|
||||||
{ "getDiagnostics", method_get_diagnostics, LUNA_METHOD_FLAGS_NONE },
|
{ "getDiagnostics", method_get_diagnostics, LUNA_METHOD_FLAGS_NONE },
|
||||||
{ "getLogs", method_get_logs, LUNA_METHOD_FLAGS_NONE },
|
{ "getLogs", method_get_logs, LUNA_METHOD_FLAGS_NONE },
|
||||||
{ "quit", method_quit, LUNA_METHOD_FLAGS_NONE },
|
{ "quit", method_quit, LUNA_METHOD_FLAGS_NONE },
|
||||||
@@ -447,6 +517,13 @@ service_t* service_create(LSHandle* handle, GMainLoop* loop)
|
|||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Powers the brightness sink's "only while this app is running" option.
|
||||||
|
// Not fatal on failure: that option just has nothing to compare against,
|
||||||
|
// same as if it were left unset.
|
||||||
|
char err[192];
|
||||||
|
if (!foreground_app_start(handle, err, sizeof(err)))
|
||||||
|
WARN("Foreground app tracking unavailable: %s", err);
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,6 +531,7 @@ void service_destroy(service_t* s)
|
|||||||
{
|
{
|
||||||
if (!s)
|
if (!s)
|
||||||
return;
|
return;
|
||||||
|
foreground_app_stop(s->handle);
|
||||||
engine_destroy(s->engine);
|
engine_destroy(s->engine);
|
||||||
config_free(s->config);
|
config_free(s->config);
|
||||||
free(s);
|
free(s);
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
|
|
||||||
#include "sink.h"
|
#include "sink.h"
|
||||||
#include "../common/log.h"
|
#include "../common/log.h"
|
||||||
|
#include "../foreground_app.h"
|
||||||
|
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
@@ -52,6 +53,7 @@ typedef struct {
|
|||||||
int min_brightness; // 0-100
|
int min_brightness; // 0-100
|
||||||
int max_brightness; // 0-100
|
int max_brightness; // 0-100
|
||||||
bool use_rms; // rms is steadier than peak, which reacts to single transients
|
bool use_rms; // rms is steadier than peak, which reacts to single transients
|
||||||
|
char restrict_to_app[192]; // empty = always active
|
||||||
|
|
||||||
int fd; // -1 when not connected or still connecting
|
int fd; // -1 when not connected or still connecting
|
||||||
bool connected; // fd is open and the non-blocking connect finished
|
bool connected; // fd is open and the non-blocking connect finished
|
||||||
@@ -59,6 +61,7 @@ typedef struct {
|
|||||||
time_t last_connect_attempt;
|
time_t last_connect_attempt;
|
||||||
struct timespec last_send;
|
struct timespec last_send;
|
||||||
char last_error[192];
|
char last_error[192];
|
||||||
|
bool app_was_active; // for sending exactly one reset on the active->inactive edge
|
||||||
|
|
||||||
unsigned long long updates_sent;
|
unsigned long long updates_sent;
|
||||||
unsigned long long connect_failures;
|
unsigned long long connect_failures;
|
||||||
@@ -156,6 +159,38 @@ static void adjust_write(sink_t* s, const int16_t* pcm, int frames, const dsp_le
|
|||||||
if (!adjust_pump_connect(p))
|
if (!adjust_pump_connect(p))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// "Only while Spotify is running": until we can actually confirm that,
|
||||||
|
// the safe default is inactive, not active -- silently reacting to audio
|
||||||
|
// when the user asked to restrict it would be the wrong failure mode.
|
||||||
|
bool restricted = p->restrict_to_app[0] != '\0';
|
||||||
|
bool app_active = true;
|
||||||
|
if (restricted) {
|
||||||
|
if (!foreground_app_known()) {
|
||||||
|
app_active = false;
|
||||||
|
} else {
|
||||||
|
char current[192];
|
||||||
|
foreground_app_current(current, sizeof(current));
|
||||||
|
app_active = strcmp(current, p->restrict_to_app) == 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!app_active) {
|
||||||
|
// Send exactly one reset on the active->inactive edge, then go
|
||||||
|
// quiet, rather than leaving the LEDs stuck at whatever level the
|
||||||
|
// last audio block happened to produce.
|
||||||
|
if (p->app_was_active) {
|
||||||
|
char reset_msg[80];
|
||||||
|
int rn = snprintf(reset_msg, sizeof(reset_msg),
|
||||||
|
"{\"command\":\"adjustment\",\"adjustment\":{\"brightness\":%d}}\n", p->max_brightness);
|
||||||
|
if (rn > 0 && (size_t)rn < sizeof(reset_msg)
|
||||||
|
&& send(p->fd, reset_msg, (size_t)rn, MSG_NOSIGNAL) >= 0)
|
||||||
|
p->updates_sent++;
|
||||||
|
p->app_was_active = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
p->app_was_active = true;
|
||||||
|
|
||||||
if (elapsed_ms(&p->last_send) < SEND_INTERVAL_MS)
|
if (elapsed_ms(&p->last_send) < SEND_INTERVAL_MS)
|
||||||
return;
|
return;
|
||||||
clock_gettime(CLOCK_MONOTONIC, &p->last_send);
|
clock_gettime(CLOCK_MONOTONIC, &p->last_send);
|
||||||
@@ -197,6 +232,12 @@ static void adjust_status(sink_t* s, json_writer_t* w)
|
|||||||
jw_bool(w, "connected", p->connected);
|
jw_bool(w, "connected", p->connected);
|
||||||
jw_int(w, "minBrightness", p->min_brightness);
|
jw_int(w, "minBrightness", p->min_brightness);
|
||||||
jw_int(w, "maxBrightness", p->max_brightness);
|
jw_int(w, "maxBrightness", p->max_brightness);
|
||||||
|
if (p->restrict_to_app[0]) {
|
||||||
|
jw_str(w, "restrictToApp", p->restrict_to_app);
|
||||||
|
jw_bool(w, "restrictedAppActive", p->app_was_active);
|
||||||
|
} else {
|
||||||
|
jw_null(w, "restrictToApp");
|
||||||
|
}
|
||||||
jw_int(w, "updatesSent", (long long)p->updates_sent);
|
jw_int(w, "updatesSent", (long long)p->updates_sent);
|
||||||
jw_int(w, "connectFailures", (long long)p->connect_failures);
|
jw_int(w, "connectFailures", (long long)p->connect_failures);
|
||||||
if (p->last_error[0])
|
if (p->last_error[0])
|
||||||
@@ -229,6 +270,7 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
|
|||||||
int min_brightness = json_int(sc, "minBrightness", 20);
|
int min_brightness = json_int(sc, "minBrightness", 20);
|
||||||
int max_brightness = json_int(sc, "maxBrightness", 100);
|
int max_brightness = json_int(sc, "maxBrightness", 100);
|
||||||
const char* level_source = json_str(sc, "level", "rms");
|
const char* level_source = json_str(sc, "level", "rms");
|
||||||
|
const char* restrict_to_app = json_str(sc, "restrictToApp", "");
|
||||||
|
|
||||||
if (!host || !*host) {
|
if (!host || !*host) {
|
||||||
snprintf(err, errlen, "set the HyperHDR host address first");
|
snprintf(err, errlen, "set the HyperHDR host address first");
|
||||||
@@ -276,6 +318,7 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
|
|||||||
p->min_brightness = min_brightness;
|
p->min_brightness = min_brightness;
|
||||||
p->max_brightness = max_brightness;
|
p->max_brightness = max_brightness;
|
||||||
p->use_rms = strcmp(level_source, "peak") != 0;
|
p->use_rms = strcmp(level_source, "peak") != 0;
|
||||||
|
snprintf(p->restrict_to_app, sizeof(p->restrict_to_app), "%s", restrict_to_app);
|
||||||
|
|
||||||
s->driver = &sink_driver_hyperhdr_adjust;
|
s->driver = &sink_driver_hyperhdr_adjust;
|
||||||
s->priv = p;
|
s->priv = p;
|
||||||
@@ -284,8 +327,9 @@ static sink_t* adjust_open(const json_value_t* cfg, const audio_format_t* fmt, c
|
|||||||
s->status = adjust_status;
|
s->status = adjust_status;
|
||||||
s->close = adjust_close;
|
s->close = adjust_close;
|
||||||
|
|
||||||
INFO("HyperHDR adjustment sink: %s:%d, brightness %d..%d from %s", host, port,
|
INFO("HyperHDR adjustment sink: %s:%d, brightness %d..%d from %s%s%s", host, port,
|
||||||
min_brightness, max_brightness, p->use_rms ? "rms" : "peak");
|
min_brightness, max_brightness, p->use_rms ? "rms" : "peak",
|
||||||
|
p->restrict_to_app[0] ? ", restricted to " : "", p->restrict_to_app);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "lgtv-audio-cap",
|
"name": "lgtv-audio-cap",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Captures audio on an LG webOS 5/6 TV and streams it out \u2014 HyperHDR first, plus raw UDP, TCP and HTTP.",
|
"description": "Captures audio on an LG webOS 5/6 TV and streams it out \u2014 HyperHDR first, plus raw UDP, TCP and HTTP.",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"id": "org.webosbrew.audiocap.service",
|
"id": "org.webosbrew.audiocap.service",
|
||||||
"version": "1.0.4",
|
"version": "1.0.5",
|
||||||
"description": "Captures TV audio and streams it to HyperHDR and other receivers",
|
"description": "Captures TV audio and streams it to HyperHDR and other receivers",
|
||||||
"main": "audiocap-service"
|
"main": "audiocap-service"
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-3
@@ -204,12 +204,22 @@ static void test_status(engine_t* e)
|
|||||||
check(json_len(json_get(levels, "bands")) == DSP_BANDS, "all bands reported");
|
check(json_len(json_get(levels, "bands")) == DSP_BANDS, "all bands reported");
|
||||||
|
|
||||||
const json_value_t* sinks = json_get(v, "sinks");
|
const json_value_t* sinks = json_get(v, "sinks");
|
||||||
check(json_len(sinks) == 2, "two sinks reported");
|
check(json_len(sinks) == 3, "three sinks reported");
|
||||||
for (size_t i = 0; i < json_len(sinks); i++) {
|
for (size_t i = 0; i < json_len(sinks); i++) {
|
||||||
const json_value_t* s = json_at(sinks, i);
|
const json_value_t* s = json_at(sinks, i);
|
||||||
char label[64];
|
char label[64];
|
||||||
snprintf(label, sizeof(label), "sink '%s' started cleanly", json_str(s, "id", "?"));
|
snprintf(label, sizeof(label), "sink '%s' started cleanly", json_str(s, "id", "?"));
|
||||||
check(json_bool(s, "ok", false), label);
|
check(json_bool(s, "ok", false), label);
|
||||||
|
|
||||||
|
if (strcmp(json_str(s, "id", ""), "hyperhdrAdjust") == 0) {
|
||||||
|
// A host has no Luna bus (see lunaservice_stub.c), so the
|
||||||
|
// sink can never confirm the restricted app is foreground.
|
||||||
|
// The correct failure mode is inactive, not "assume yes".
|
||||||
|
check(strcmp(json_str(s, "restrictToApp", ""), "some.other.app") == 0,
|
||||||
|
"restriction target reported back");
|
||||||
|
check(json_bool(s, "restrictedAppActive", true) == false,
|
||||||
|
"restricted app correctly reported as not active (fail-closed)");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
json_free(v);
|
json_free(v);
|
||||||
}
|
}
|
||||||
@@ -224,9 +234,11 @@ int main(void)
|
|||||||
char cfg_text[512];
|
char cfg_text[512];
|
||||||
snprintf(cfg_text, sizeof(cfg_text),
|
snprintf(cfg_text, sizeof(cfg_text),
|
||||||
"{\"capture\":{\"backend\":\"tone\",\"rate\":48000,\"channels\":2},"
|
"{\"capture\":{\"backend\":\"tone\",\"rate\":48000,\"channels\":2},"
|
||||||
"\"sinks\":[\"tcp\",\"http\"],"
|
"\"sinks\":[\"tcp\",\"http\",\"hyperhdrAdjust\"],"
|
||||||
"\"tcp\":{\"port\":%d},"
|
"\"tcp\":{\"port\":%d},"
|
||||||
"\"http\":{\"port\":%d}}",
|
"\"http\":{\"port\":%d},"
|
||||||
|
"\"hyperhdrAdjust\":{\"host\":\"127.0.0.1\",\"port\":19444,"
|
||||||
|
"\"restrictToApp\":\"some.other.app\"}}",
|
||||||
TCP_PORT, HTTP_PORT);
|
TCP_PORT, HTTP_PORT);
|
||||||
|
|
||||||
json_value_t* cfg = json_parse(cfg_text);
|
json_value_t* cfg = json_parse(cfg_text);
|
||||||
|
|||||||
+4
-2
@@ -13,12 +13,14 @@ export PYTHONDONTWRITEBYTECODE=1
|
|||||||
OUT=$(mktemp -d)
|
OUT=$(mktemp -d)
|
||||||
trap 'rm -rf "$OUT"' EXIT
|
trap 'rm -rf "$OUT"' EXIT
|
||||||
|
|
||||||
CFLAGS=(-std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -Inative/src -O1 -g)
|
CFLAGS=(-std=c11 -Wall -Wextra -Wno-unused-parameter -D_GNU_SOURCE -Inative/src -Itest/stubs -O1 -g)
|
||||||
|
|
||||||
SOURCES=(
|
SOURCES=(
|
||||||
native/src/engine.c
|
native/src/engine.c
|
||||||
native/src/config.c
|
native/src/config.c
|
||||||
native/src/dsp.c
|
native/src/dsp.c
|
||||||
|
native/src/foreground_app.c
|
||||||
|
test/stubs/luna-service2/lunaservice_stub.c
|
||||||
native/src/common/log.c
|
native/src/common/log.c
|
||||||
native/src/common/json.c
|
native/src/common/json.c
|
||||||
native/src/common/ringbuf.c
|
native/src/common/ringbuf.c
|
||||||
@@ -41,7 +43,7 @@ SOURCES=(
|
|||||||
|
|
||||||
echo "== Syntax-checking the webOS-only sources against stub headers"
|
echo "== Syntax-checking the webOS-only sources against stub headers"
|
||||||
for f in native/src/service.c native/src/main.c; do
|
for f in native/src/service.c native/src/main.c; do
|
||||||
"$CC" "${CFLAGS[@]}" -Itest/stubs -fsyntax-only "$f"
|
"$CC" "${CFLAGS[@]}" -fsyntax-only "$f"
|
||||||
echo " ok $f"
|
echo " ok $f"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ typedef struct {
|
|||||||
} LSError;
|
} LSError;
|
||||||
|
|
||||||
typedef bool (*LSMethodFunction)(LSHandle* sh, LSMessage* msg, void* category_context);
|
typedef bool (*LSMethodFunction)(LSHandle* sh, LSMessage* msg, void* category_context);
|
||||||
|
typedef bool (*LSFilterFunc)(LSHandle* sh, LSMessage* reply, void* ctx);
|
||||||
|
typedef unsigned long LSMessageToken;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
LUNA_METHOD_FLAGS_NONE = 0,
|
LUNA_METHOD_FLAGS_NONE = 0,
|
||||||
@@ -58,6 +60,17 @@ bool LSGmainAttach(LSHandle* handle, GMainLoop* loop, LSError* error);
|
|||||||
const char* LSMessageGetPayload(LSMessage* message);
|
const char* LSMessageGetPayload(LSMessage* message);
|
||||||
bool LSMessageIsSubscription(LSMessage* message);
|
bool LSMessageIsSubscription(LSMessage* message);
|
||||||
bool LSMessageReply(LSHandle* sh, LSMessage* message, const char* reply, LSError* error);
|
bool LSMessageReply(LSHandle* sh, LSMessage* message, const char* reply, LSError* error);
|
||||||
|
void LSMessageRef(LSMessage* message);
|
||||||
|
void LSMessageUnref(LSMessage* message);
|
||||||
|
|
||||||
bool LSSubscriptionAdd(LSHandle* sh, const char* key, LSMessage* message, LSError* error);
|
bool LSSubscriptionAdd(LSHandle* sh, const char* key, LSMessage* message, LSError* error);
|
||||||
bool LSSubscriptionReply(LSHandle* sh, const char* key, const char* payload, LSError* error);
|
bool LSSubscriptionReply(LSHandle* sh, const char* key, const char* payload, LSError* error);
|
||||||
|
|
||||||
|
// Client-call API: this service acting as a caller of another service, not
|
||||||
|
// just a callee. LSCall keeps calling `callback` for every reply (used for
|
||||||
|
// subscribe:true); LSCallOneReply auto-cancels after the first one.
|
||||||
|
bool LSCall(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||||
|
void* ctx, LSMessageToken* ret_token, LSError* error);
|
||||||
|
bool LSCallOneReply(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||||
|
void* ctx, LSMessageToken* ret_token, LSError* error);
|
||||||
|
bool LSCallCancel(LSHandle* sh, LSMessageToken token, LSError* error);
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Linkable bodies for the handful of luna-service2 client-call functions
|
||||||
|
// foreground_app.c calls. service.c/main.c only ever get -fsyntax-only'd, so
|
||||||
|
// declarations alone are enough for them; foreground_app.c is linked into
|
||||||
|
// real host test binaries (engine_smoke, rtp_send) via SOURCES[] in
|
||||||
|
// run-tests.sh, so those symbols need bodies too, or the link fails.
|
||||||
|
//
|
||||||
|
// A host has no Luna bus, so "the call failed" is exactly the right
|
||||||
|
// simulated behaviour -- every caller here already treats that as
|
||||||
|
// "foreground app tracking unavailable" and degrades accordingly, which is
|
||||||
|
// also genuinely exercised by the test suite (see the fail-closed check in
|
||||||
|
// engine_smoke.c).
|
||||||
|
#include "luna-service2/lunaservice.h"
|
||||||
|
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
void LSErrorInit(LSError* error)
|
||||||
|
{
|
||||||
|
memset(error, 0, sizeof(*error));
|
||||||
|
}
|
||||||
|
|
||||||
|
void LSErrorFree(LSError* error)
|
||||||
|
{
|
||||||
|
(void)error;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LSCall(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||||
|
void* ctx, LSMessageToken* ret_token, LSError* error)
|
||||||
|
{
|
||||||
|
(void)sh;
|
||||||
|
(void)uri;
|
||||||
|
(void)payload;
|
||||||
|
(void)callback;
|
||||||
|
(void)ctx;
|
||||||
|
(void)ret_token;
|
||||||
|
if (error)
|
||||||
|
error->message = (char*)"no Luna bus on this host";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LSCallOneReply(LSHandle* sh, const char* uri, const char* payload, LSFilterFunc callback,
|
||||||
|
void* ctx, LSMessageToken* ret_token, LSError* error)
|
||||||
|
{
|
||||||
|
return LSCall(sh, uri, payload, callback, ctx, ret_token, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool LSCallCancel(LSHandle* sh, LSMessageToken token, LSError* error)
|
||||||
|
{
|
||||||
|
(void)sh;
|
||||||
|
(void)token;
|
||||||
|
(void)error;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* LSMessageGetPayload(LSMessage* message)
|
||||||
|
{
|
||||||
|
(void)message;
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
@@ -133,6 +133,16 @@ async function main() {
|
|||||||
check('hyperhdr card is first and marked',
|
check('hyperhdr card is first and marked',
|
||||||
doc.querySelector('.sink-card .badge').textContent === 'Recommended');
|
doc.querySelector('.sink-card .badge').textContent === 'Recommended');
|
||||||
check('hyperhdr host field exists', !!doc.querySelector('[data-path="hyperhdr.host"]'));
|
check('hyperhdr host field exists', !!doc.querySelector('[data-path="hyperhdr.host"]'));
|
||||||
|
const restrictPicker = doc.querySelector('[data-path="hyperhdrAdjust.restrictToApp"]');
|
||||||
|
check('restrict-to-app picker exists', !!restrictPicker);
|
||||||
|
eq('restrict-to-app picker starts on Always active', restrictPicker.textContent, 'Always active');
|
||||||
|
// Always active -> the first installed app alphabetically by title
|
||||||
|
// ("Live TV", ahead of Netflix/Spotify/YouTube in the mock's list).
|
||||||
|
click(restrictPicker);
|
||||||
|
await wait(600);
|
||||||
|
eq('picking an app reaches settings by id, not a typed value',
|
||||||
|
window.App.state.settings.hyperhdrAdjust.restrictToApp, 'com.webos.app.livetv');
|
||||||
|
eq('picker now shows the app name, not the id', restrictPicker.textContent, 'Live TV');
|
||||||
check('backend choice exists', !!doc.querySelector('[data-path="capture.backend"]'));
|
check('backend choice exists', !!doc.querySelector('[data-path="capture.backend"]'));
|
||||||
check('log level choice exists', !!doc.querySelector('[data-path="logLevel"]'));
|
check('log level choice exists', !!doc.querySelector('[data-path="logLevel"]'));
|
||||||
check('boot toggle exists', !!doc.querySelector('[data-path="autoStart"]'));
|
check('boot toggle exists', !!doc.querySelector('[data-path="autoStart"]'));
|
||||||
|
|||||||
Reference in New Issue
Block a user