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>
133 lines
4.1 KiB
JavaScript
133 lines
4.1 KiB
JavaScript
// Luna bus access.
|
|
//
|
|
// Talks to PalmServiceBridge directly rather than pulling in webOSTV.js: it is
|
|
// the same object webOSTV.js wraps, it is injected into every webOS app, and
|
|
// it means there is no third-party file to keep in sync.
|
|
//
|
|
// Opened in a desktop browser the bridge is absent, so everything falls back
|
|
// to a small mock. That is what makes the UI developable without a TV.
|
|
|
|
(function (global) {
|
|
'use strict';
|
|
|
|
var SERVICE = 'luna://org.webosbrew.audiocap.service/';
|
|
var HBCHANNEL = 'luna://org.webosbrew.hbchannel.service/';
|
|
|
|
var haveBridge = typeof global.PalmServiceBridge !== 'undefined';
|
|
|
|
// A call in flight. `cancel()` tears down a subscription.
|
|
function Request(bridge) {
|
|
this.bridge = bridge;
|
|
this.cancelled = false;
|
|
}
|
|
|
|
Request.prototype.cancel = function () {
|
|
this.cancelled = true;
|
|
if (this.bridge && this.bridge.cancel) {
|
|
this.bridge.cancel();
|
|
}
|
|
};
|
|
|
|
// Low-level call. `onReply` fires once per reply, so subscriptions keep
|
|
// calling it until cancelled.
|
|
function call(uri, params, onReply, onError) {
|
|
if (!haveBridge) {
|
|
return global.LunaMock.call(uri, params, onReply, onError);
|
|
}
|
|
|
|
var bridge = new global.PalmServiceBridge();
|
|
var request = new Request(bridge);
|
|
|
|
bridge.onservicecallback = function (raw) {
|
|
if (request.cancelled) {
|
|
return;
|
|
}
|
|
var reply;
|
|
try {
|
|
reply = JSON.parse(raw);
|
|
} catch (e) {
|
|
if (onError) {
|
|
onError('Malformed reply from ' + uri);
|
|
}
|
|
return;
|
|
}
|
|
// Luna reports both its own failures and ours through returnValue.
|
|
if (reply.returnValue === false) {
|
|
if (onError) {
|
|
onError(reply.errorText || reply.errorMessage || 'Call to ' + uri + ' failed');
|
|
}
|
|
return;
|
|
}
|
|
if (onReply) {
|
|
onReply(reply);
|
|
}
|
|
};
|
|
|
|
try {
|
|
bridge.call(uri, JSON.stringify(params || {}));
|
|
} catch (e) {
|
|
if (onError) {
|
|
onError(String(e));
|
|
}
|
|
}
|
|
return request;
|
|
}
|
|
|
|
var Luna = {
|
|
available: haveBridge,
|
|
|
|
// --- our service -------------------------------------------------------
|
|
start: function (patch, ok, fail) {
|
|
return call(SERVICE + 'start', patch || {}, ok, fail);
|
|
},
|
|
stop: function (ok, fail) {
|
|
return call(SERVICE + 'stop', {}, ok, fail);
|
|
},
|
|
subscribeStatus: function (ok, fail) {
|
|
return call(SERVICE + 'getStatus', { subscribe: true }, ok, fail);
|
|
},
|
|
getConfig: function (ok, fail) {
|
|
return call(SERVICE + 'getConfig', {}, ok, fail);
|
|
},
|
|
setConfig: function (patch, ok, fail) {
|
|
return call(SERVICE + 'setConfig', { settings: patch }, ok, fail);
|
|
},
|
|
resetConfig: function (ok, fail) {
|
|
return call(SERVICE + 'resetConfig', {}, ok, fail);
|
|
},
|
|
listBackends: function (ok, fail) {
|
|
return call(SERVICE + 'listBackends', {}, ok, fail);
|
|
},
|
|
listSinks: function (ok, fail) {
|
|
return call(SERVICE + 'listSinks', {}, ok, fail);
|
|
},
|
|
getDiagnostics: function (ok, fail) {
|
|
return call(SERVICE + 'getDiagnostics', {}, ok, fail);
|
|
},
|
|
// Ends the service process. Any later call starts a fresh one, which is
|
|
// how the service picks up new bus permissions after being elevated.
|
|
quit: function (ok, fail) {
|
|
return call(SERVICE + 'quit', {}, ok, fail);
|
|
},
|
|
// `clear` empties the ring buffer after reading it, so the reply is the
|
|
// last thing anyone sees of those lines.
|
|
getLogs: function (clear, ok, fail) {
|
|
return call(SERVICE + 'getLogs', { clear: !!clear }, ok, fail);
|
|
},
|
|
|
|
// --- Homebrew Channel --------------------------------------------------
|
|
// Used for the boot symlink. Needs the Homebrew Channel installed, which
|
|
// it will be on any TV that can run this app.
|
|
exec: function (command, ok, fail) {
|
|
return call(HBCHANNEL + 'exec', { command: command }, ok, fail);
|
|
},
|
|
|
|
// --- system ------------------------------------------------------------
|
|
getNetworkStatus: function (ok, fail) {
|
|
return call('luna://com.palm.connectionmanager/getStatus', {}, ok, fail);
|
|
},
|
|
};
|
|
|
|
global.Luna = Luna;
|
|
})(window);
|