move the whole local-audio pipeline into the music sidecar
cliamp playback was implemented entirely in officer: it located the cliamp binary, validated the requested path against the owner's home, faked a PTY with `script`, injected PULSE_SINK and an ALSA config shipped inside the API tree, spawned parec to capture the sink, and set up the pulseaudio daemon and the virtual_out null sink at every boot — about 356 lines of audio-pipeline knowledge in a process that is meant to be a proxy, and none of it owned by the sidecar whose whole job is music. all of it now lives in sidecar/music: cliamp-ws.ts serves both sockets (/cliamp/ws for the player, /cliamp/audio/ws for the PCM capture) on the loopback server it already runs, pulse-audio.ts does the daemon + sink setup at sidecar startup instead of at officer's, and the asoundrc moved next to the code that passes it. officer keeps the part that is actually its job — authenticating the browser — and relays frames both ways without reading them (api/cliamp/relay.ts, same dumb-pipe shape as the vault notifications relay). the browser's frame contract is unchanged, so the frontend is not touched. two things fixed on the way: the traversal check now requires a separator after the home path, so a sibling directory whose name merely starts with it can no longer pass; and the music proxy no longer special-cases /reindex and /reindex/stream by name to extend the idle timeout — it extends the whole prefix, because a proxy should not know which of the sidecar's routes are slow. the music-specific `files` query param is out of the shared WS envelope too: upgradeWs now carries the raw query string, which any relayed provider can use. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+6
-51
@@ -8,8 +8,7 @@ import { terminalWebsocket } from './servers/api/terminal/websocket';
|
||||
import { chatWebsocket } from './servers/api/chat/websocket';
|
||||
import { taskRunnerWebsocket } from './servers/api/tasks/task-executor';
|
||||
import { pipelineWebsocket } from './servers/api/tasks/pipeline-executor';
|
||||
import { cliampWebsocket } from './servers/api/cliamp/websocket';
|
||||
import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws';
|
||||
import { cliampWebsocket, cliampAudioWebsocket } from './servers/api/cliamp/relay';
|
||||
import { desktopWebsocket } from './servers/api/desktop/websocket';
|
||||
import { vaultWebsocket, upgradeVaultWs } from './servers/api/vault/websocket';
|
||||
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
|
||||
@@ -50,7 +49,7 @@ type WSData = {
|
||||
command?: string;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
files?: string;
|
||||
search?: string; // raw query string, for providers that relay it to a sidecar
|
||||
devServerPort?: number;
|
||||
devServerSlug?: string;
|
||||
wsProxyPath?: string;
|
||||
@@ -250,7 +249,6 @@ async function upgradeWs(
|
||||
const command = url.searchParams.get('command') ?? undefined;
|
||||
const cols = url.searchParams.get('cols') ? Number(url.searchParams.get('cols')) : undefined;
|
||||
const rows = url.searchParams.get('rows') ? Number(url.searchParams.get('rows')) : undefined;
|
||||
const files = url.searchParams.get('files') ?? undefined;
|
||||
const ok = server.upgrade(req, {
|
||||
data: {
|
||||
userId: user.id,
|
||||
@@ -262,7 +260,7 @@ async function upgradeWs(
|
||||
command,
|
||||
cols,
|
||||
rows,
|
||||
files,
|
||||
search: url.search,
|
||||
},
|
||||
});
|
||||
if (!ok) return new Response('Upgrade failed', { status: 500 });
|
||||
@@ -388,51 +386,8 @@ initQueue().catch((err) => console.error('[queue] failed to initialize:', err));
|
||||
import { cleanupOnStartup } from './servers/api/tasks/pipeline-job-manager';
|
||||
cleanupOnStartup().catch((err) => console.error('[pipeline-jobs] startup cleanup failed:', err));
|
||||
|
||||
// Ensure PulseAudio is running with virtual sink for cliamp audio streaming
|
||||
(async () => {
|
||||
const pulseaudio = Bun.which('pulseaudio');
|
||||
const pactl = Bun.which('pactl');
|
||||
if (!pulseaudio || !pactl) {
|
||||
console.log('[cliamp] pulseaudio not installed, skipping audio setup');
|
||||
return;
|
||||
}
|
||||
|
||||
// Start PulseAudio daemon if not running
|
||||
const check = Bun.spawnSync({ cmd: [pulseaudio, '--check'], stdout: 'ignore', stderr: 'ignore' });
|
||||
if (check.exitCode !== 0) {
|
||||
const start = Bun.spawnSync({ cmd: [pulseaudio, '--start', '-D'], stdout: 'ignore', stderr: 'ignore' });
|
||||
if (start.exitCode !== 0) {
|
||||
console.error('[cliamp] failed to start pulseaudio');
|
||||
return;
|
||||
}
|
||||
console.log('[cliamp] pulseaudio started');
|
||||
} else {
|
||||
console.log('[cliamp] pulseaudio already running');
|
||||
}
|
||||
|
||||
// Load null sink if not already loaded
|
||||
const sinks = Bun.spawnSync({ cmd: [pactl, 'list', 'short', 'sinks'], stdout: 'pipe', stderr: 'ignore' });
|
||||
const sinkList = sinks.stdout.toString();
|
||||
if (!sinkList.includes('virtual_out')) {
|
||||
const load = Bun.spawnSync({
|
||||
cmd: [
|
||||
pactl,
|
||||
'load-module',
|
||||
'module-null-sink',
|
||||
'sink_name=virtual_out',
|
||||
'sink_properties=device.description=Virtual_Output',
|
||||
],
|
||||
stdout: 'pipe',
|
||||
stderr: 'pipe',
|
||||
});
|
||||
if (load.exitCode !== 0) {
|
||||
console.error('[cliamp] failed to load null sink:', load.stderr.toString().trim());
|
||||
} else {
|
||||
console.log('[cliamp] virtual_out null sink loaded');
|
||||
}
|
||||
} else {
|
||||
console.log('[cliamp] virtual_out sink already exists');
|
||||
}
|
||||
})();
|
||||
// PulseAudio and the `virtual_out` sink used to be set up here, at every boot of a process that has no
|
||||
// audio responsibilities. They belong to the music sidecar, which owns both cliamp halves now
|
||||
// (sidecar/music/pulse-audio.ts).
|
||||
|
||||
// Pi check/install is handled by bootstrap.ts (imported above)
|
||||
|
||||
Reference in New Issue
Block a user