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
+123
View File
@@ -0,0 +1,123 @@
// Entry point for the native Luna service.
//
// Nothing interesting happens here: register on the bus, hand control to the
// glib main loop, and make sure a SIGTERM from the service launcher shuts the
// capture down cleanly so sinks get to say goodbye (HyperHDR in particular
// needs its Clear, or the LEDs freeze on the last frame we sent).
#include "common/log.h"
#include "service.h"
#include <glib-unix.h>
#include <glib.h>
#include <luna-service2/lunaservice.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define SERVICE_NAME "org.webosbrew.audiocap.service"
// webOS 3.5 and earlier need the service registered on the public bus too.
// Declared weak so the same binary keeps loading on newer firmware where the
// symbol was removed.
extern bool LSRegisterPubPriv(const char* name, LSHandle** handle, bool public_bus,
LSError* error) __attribute__((weak));
static gboolean on_signal(gpointer user)
{
GMainLoop* loop = user;
INFO("Signal received, shutting down");
g_main_loop_quit(loop);
return G_SOURCE_REMOVE;
}
static void log_environment(void)
{
uid_t uid = getuid();
INFO("lgtv-audio-cap service starting (uid=%d%s)", (int)uid,
uid == 0 ? ", root" : ", unprivileged");
if (uid != 0) {
// Without root the PulseAudio socket and the ALSA devices are usually
// out of reach, so say this once rather than leaving the user to
// decode a permission error later.
WARN("Not running as root: audio devices are likely inaccessible.");
WARN("Install the Homebrew Channel 'elevate-service' patch and restart the service.");
}
}
int main(int argc, char** argv)
{
bool debug = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "-d") == 0 || strcmp(argv[i], "--debug") == 0)
debug = true;
}
log_init(debug ? LOG_DEBUG : LOG_INFO);
log_environment();
// A client that hangs up mid-stream must not kill the service. Every send
// path already asks for MSG_NOSIGNAL; this covers the rest.
signal(SIGPIPE, SIG_IGN);
GMainLoop* loop = g_main_loop_new(NULL, false);
LSError lserror;
LSErrorInit(&lserror);
LSHandle* handle = NULL;
bool registered = LSRegisterPubPriv
? LSRegisterPubPriv(SERVICE_NAME, &handle, true, &lserror)
: LSRegister(SERVICE_NAME, &handle, &lserror);
if (!registered) {
ERR("Cannot register %s on the Luna bus: %s", SERVICE_NAME, lserror.message);
LSErrorFree(&lserror);
g_main_loop_unref(loop);
return 1;
}
if (!LSGmainAttach(handle, loop, &lserror)) {
ERR("Cannot attach to the main loop: %s", lserror.message);
LSErrorFree(&lserror);
LSUnregister(handle, &lserror);
g_main_loop_unref(loop);
return 1;
}
service_t* service = service_create(handle, loop);
if (!service) {
ERR("Cannot create the service");
LSUnregister(handle, &lserror);
g_main_loop_unref(loop);
return 1;
}
char err[256] = { 0 };
if (!service_register(service, err, sizeof(err))) {
ERR("%s", err);
service_destroy(service);
LSUnregister(handle, &lserror);
g_main_loop_unref(loop);
return 1;
}
g_unix_signal_add(SIGTERM, on_signal, loop);
g_unix_signal_add(SIGINT, on_signal, loop);
INFO("Registered as %s", SERVICE_NAME);
service_autostart(service);
g_main_loop_run(loop);
INFO("Shutting down");
service_destroy(service);
if (!LSUnregister(handle, &lserror)) {
ERR("Unregister failed: %s", lserror.message);
LSErrorFree(&lserror);
}
g_main_loop_unref(loop);
return 0;
}