// Stand-in for the Luna bus so the UI can be opened in a desktop browser. // Only loaded when PalmServiceBridge is missing, which never happens on a TV. (function (global) { 'use strict'; var settings = { autoStart: false, logLevel: 'info', capture: { backend: 'auto', device: '', server: '', command: '', rate: 48000, channels: 2 }, dsp: { attack: 0.6, release: 0.12 }, sinks: ['hyperhdr'], hyperhdr: { host: '192.168.1.50', port: 5004, multicast: false, multicastTtl: 4, sapAnnounce: true }, hyperhdrViz: { host: '', port: 19400, priority: 150, width: 64, height: 36, fps: 30, mode: 'spectrum', saturation: 1.0, minBrightness: 0.02, }, hyperhdrAdjust: { host: '', port: 19444, minBrightness: 20, maxBrightness: 100, level: 'rms', restrictToApp: '', }, udp: { host: '', port: 4010, multicastTtl: 4 }, tcp: { port: 4011, maxClients: 4 }, http: { port: 4012, maxClients: 4 }, }; var running = false; var subscribers = []; var startedAt = 0; function merge(base, patch) { Object.keys(patch).forEach(function (k) { if (patch[k] && typeof patch[k] === 'object' && !Array.isArray(patch[k]) && base[k] && typeof base[k] === 'object' && !Array.isArray(base[k])) { merge(base[k], patch[k]); } else { base[k] = patch[k]; } }); } function status(subscribed) { var t = Date.now() / 1000; var bands = []; for (var i = 0; i < 16; i++) { var v = running ? Math.abs(Math.sin(t * (1 + i * 0.25) + i)) * (1 - i / 24) : 0; bands.push(Math.max(0, Math.min(1, v))); } var peak = running ? 0.4 + 0.4 * Math.abs(Math.sin(t * 2)) : 0; return { returnValue: true, subscribed: !!subscribed, state: running ? 'running' : 'stopped', running: running, error: null, capture: { backend: running ? 'pulse' : null, backendName: running ? 'PulseAudio (mock)' : null, device: '@DEFAULT_MONITOR@', rate: 48000, channels: 2, frames: running ? Math.round((Date.now() - startedAt) * 48) : 0, blocks: running ? Math.round((Date.now() - startedAt) / 10.7) : 0, timeouts: 0, uptimeMs: running ? Date.now() - startedAt : 0, }, levels: { peak: peak, rms: peak * 0.6, peakDb: peak > 0 ? 20 * Math.log(peak) / Math.LN10 : -90, rmsDb: peak > 0 ? 20 * Math.log(peak * 0.6) / Math.LN10 : -90, clipping: peak > 0.98, bands: bands, }, sinks: running ? settings.sinks.map(function (id) { return { id: id, ok: true, name: id, error: null, target: settings.hyperhdr.host, port: settings.hyperhdr.port, packetsSent: 1234 }; }) : [], configPath: '/var/lib/webosbrew/audiocap/config.json', configPersistent: true, }; } setInterval(function () { subscribers.forEach(function (s) { if (!s.cancelled) { s.onReply(status(true)); } }); }, 100); function respond(onReply, payload) { setTimeout(function () { onReply(payload); }, 30); return { cancel: function () {} }; } var LunaMock = { call: function (uri, params, onReply, onError) { var method = uri.split('/').pop(); switch (method) { case 'start': if (params && Object.keys(params).length) { merge(settings, params); } running = true; startedAt = Date.now(); return respond(onReply, status(false)); case 'stop': running = false; return respond(onReply, status(false)); case 'getStatus': { var sub = { cancelled: false, onReply: onReply }; if (params && params.subscribe) { subscribers.push(sub); } setTimeout(function () { onReply(status(!!(params && params.subscribe))); }, 30); return { cancel: function () { sub.cancelled = true; } }; } case 'getConfig': return respond(onReply, { returnValue: true, path: '/var/lib/webosbrew/audiocap/config.json', persistent: true, settings: JSON.parse(JSON.stringify(settings)), }); case 'setConfig': merge(settings, params.settings || params); return respond(onReply, { returnValue: true, saved: true, restartRequired: running, settings: JSON.parse(JSON.stringify(settings)), }); case 'listBackends': return respond(onReply, { returnValue: true, backends: [ { id: 'pulse', name: 'PulseAudio', description: 'Records a PulseAudio monitor source.', available: true }, { id: 'alsa', name: 'ALSA', description: 'Records from an ALSA capture PCM.', available: true }, { id: 'exec', name: 'External command', description: 'Reads raw PCM from a command you supply.', available: true }, { id: 'tone', name: 'Test tone', description: 'Synthesised sweep, for testing the transport.', available: true }, ], }); case 'listSinks': return respond(onReply, { returnValue: true, sinks: [ { id: 'hyperhdr', name: 'HyperHDR audio (RTP)', description: 'RTP/L16 audio to the HyperHDR host.' }, { id: 'hyperhdrViz', name: 'HyperHDR visualiser', description: 'Renders on the TV, sends images. No host setup.' }, { id: 'hyperhdrAdjust', name: 'HyperHDR brightness (JSON-RPC)', description: 'Only adjusts brightness; colour stays with HyperHDR\'s own grabber.' }, { id: 'udp', name: 'Raw PCM over UDP', description: 'Fire-and-forget S16LE datagrams.' }, { id: 'tcp', name: 'Raw PCM over TCP', description: 'The TV listens; connect to pull audio.' }, { id: 'http', name: 'HTTP WAV stream', description: 'Open the URL in VLC.' }, ], }); 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': return respond(onReply, { returnValue: true, backends: [ { id: 'pulse', name: 'PulseAudio monitor', available: true }, { id: 'alsa', name: 'ALSA PCM', available: true }, { id: 'exec', name: 'External command', available: true }, { id: 'tone', name: 'Test tone', available: true }, ], system: { root: true, uid: 0, libraries: { 'libpulse.so.0': '/usr/lib/libpulse.so.0', 'libpulse-simple.so.0': null, 'libasound.so.2': '/usr/lib/libasound.so.2', }, binaries: { parec: false, pactl: true, pacat: false, arecord: true }, pulseSockets: ['/var/run/pulse/native'], // Realistic shape: a TV that mixes several per-app sinks down to // one common output, the case the device picker exists for. // Space-padded, not tab-separated — some TVs' own pactl-alike // formats it that way, and the parser has to tolerate both. pactlSources: [ '0 tpcm_output.monitor module-combine-sink.c s16le 2ch 48000Hz RUNNING', '1 tpmedia.monitor module-alsa-card.c s16le 2ch 48000Hz IDLE', '2 tpeffects.monitor module-alsa-card.c s16le 2ch 48000Hz IDLE', '3 tptts.monitor module-alsa-card.c s16le 2ch 48000Hz SUSPENDED', ].join('\n'), alsaCards: ['0 [Loopback]: Loopback - Loopback'], alsaCapturePcms: ['00-01: Loopback PCM : playback 1 : capture 1'], }, }); case 'getLogs': return respond(onReply, { returnValue: true, logs: '[info] running in the browser mock\n[info] no TV attached\n', }); case 'quit': running = false; return respond(onReply, { returnValue: true }); case 'exec': return respond(onReply, { returnValue: true, stdoutString: '' }); default: if (onError) { onError('mock: unknown method ' + method); } return { cancel: function () {} }; } }, }; global.LunaMock = LunaMock; })(window);