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:
@@ -0,0 +1,132 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user