cliamp music player integration with browser audio streaming

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 17:23:52 +00:00
co-authored by Claude Opus 4.6
parent fd4d77a389
commit 30f06e91e4
16 changed files with 1051 additions and 18 deletions
+52 -3
View File
@@ -6,6 +6,8 @@ import { verify } from './servers/jwt';
import { isTokenBlacklisted } from 'officerdb';
import { terminalWebsocket, initTerminalSidecars } from './servers/api/terminal/websocket';
import { piWebsocket } from './servers/api/pi/websocket';
import { cliampWebsocket } from './servers/api/cliamp/websocket';
import { cliampAudioWebsocket } from './servers/api/cliamp/audio-ws';
import { findEntryByProxyId, touchEntry } from './servers/api/dev-server/router';
import officerWeb from './apps/officer-web/index.html';
import { startBrowserRelay } from './servers/api/browser/relay';
@@ -17,13 +19,14 @@ type WSData = {
email: string;
username: string;
role: string;
provider: 'terminal' | 'pi' | 'dev-server';
provider: 'terminal' | 'pi' | 'dev-server' | 'cliamp' | 'cliamp-audio';
sandboxed: boolean;
sessionId?: string;
cwd?: string;
command?: string;
cols?: number;
rows?: number;
files?: string;
devServerPort?: number;
devServerSlug?: string;
wsProxyPath?: string;
@@ -33,6 +36,8 @@ type WSData = {
const handlers: Record<string, any> = {
terminal: terminalWebsocket,
pi: piWebsocket,
cliamp: cliampWebsocket,
'cliamp-audio': cliampAudioWebsocket,
};
// Dev-server WebSocket proxy: bridges client WS ↔ upstream dev server WS (for HMR etc.)
@@ -100,7 +105,7 @@ const devServerWebsocket = {
};
handlers['dev-server'] = devServerWebsocket;
async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi') {
async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi' | 'cliamp' | 'cliamp-audio') {
const token = new URL(req.url).searchParams.get('token');
if (!token) return new Response('Unauthorized', { status: 401 });
@@ -119,8 +124,9 @@ async function upgradeWs(req: Request, server: any, provider: 'terminal' | 'pi')
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, email: user.email, username: user.username || user.email.split('@')[0]!, role: user.role, provider, sandboxed, sessionId, cwd, command, cols, rows },
data: { userId: user.id, email: user.email, username: user.username || user.email.split('@')[0]!, role: user.role, provider, sandboxed, sessionId, cwd, command, cols, rows, files },
});
if (!ok) return new Response('Upgrade failed', { status: 500 });
} catch {
@@ -171,6 +177,8 @@ const server = serve({
},
'/api/terminal/ws': (req, server) => upgradeWs(req, server, 'terminal'),
'/api/pi/chat/ws': (req, server) => upgradeWs(req, server, 'pi'),
'/api/cliamp/ws': (req, server) => upgradeWs(req, server, 'cliamp'),
'/api/cliamp/audio/ws': (req, server) => upgradeWs(req, server, 'cliamp-audio'),
'/': officerWeb,
'/*': officerWeb,
'/api': honoServer.fetch,
@@ -211,6 +219,47 @@ try {
void initTerminalSidecars();
// 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');
}
})();
// Ensure pi is installed
(async () => {
try {