cliamp music player integration with browser audio streaming
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+52
-3
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user