import type { ServerWebSocket } from 'bun'; import { spawn, type Subprocess } from 'bun'; type WSData = { userId: number; email: string; username: string; role: string; }; type AudioSession = { proc: Subprocess; closed: boolean; }; const sessions = new Map, AudioSession>(); export const cliampAudioWebsocket = { async open(ws: ServerWebSocket) { const parecPath = Bun.which('parec'); if (!parecPath) { ws.close(4000, 'parec not found on host'); return; } let proc: Subprocess<'ignore', 'pipe', 'ignore'>; try { proc = spawn({ cmd: [parecPath, '--format=s16le', '--rate=44100', '--channels=2', '-d', 'virtual_out.monitor'], stdout: 'pipe', stderr: 'ignore', stdin: 'ignore', }); } catch { ws.close(4000, 'Failed to start audio capture'); return; } const session: AudioSession = { proc, closed: false }; sessions.set(ws, session); console.log('[cliamp-audio] parec started, streaming to WS'); // Stream stdout chunks as binary WS frames const reader = proc.stdout.getReader(); let totalBytes = 0; const pump = async () => { try { while (!session.closed) { const { done, value } = await reader.read(); if (done) break; if (value && !session.closed) { totalBytes += value.byteLength; if (totalBytes <= value.byteLength) { console.log(`[cliamp-audio] first chunk: ${value.byteLength} bytes`); } try { ws.sendBinary(value); } catch { break; } } } } catch { // stream ended or error } finally { if (!session.closed) { session.closed = true; try { ws.close(); } catch { /* ignore */ } } } }; pump(); }, message() { // No client-to-server messages expected }, close(ws: ServerWebSocket) { console.log('[cliamp-audio] WS closed'); const session = sessions.get(ws); if (session) { session.closed = true; try { session.proc.kill(); } catch { /* ignore */ } sessions.delete(ws); } }, drain() {}, };