added support for microphone

This commit is contained in:
2026-06-02 21:05:02 +01:00
parent f456c587a8
commit 0f98ae862f
6 changed files with 170 additions and 6 deletions
+1
View File
@@ -229,6 +229,7 @@ async def run_signaling(session_id, video_track_name, audio_track_name):
'sessionId': session_id,
'videoTrackName': video_track_name,
'audioTrackName': audio_track_name,
'containerName': os.environ.get('CONTAINER_NAME', 'css-test'),
}))
log.info('Registered with signaling server')
+80 -2
View File
@@ -2,6 +2,7 @@
* Signaling Server
*
* Handles two tracks: video and game audio.
* Also relays browser microphone audio to container PulseAudio via pacat.
* All Cloudflare API calls happen server-side.
*
* Run: CF_APP_ID=xxx CF_APP_TOKEN=xxx node signaling-server.js
@@ -14,12 +15,24 @@ const http = require('http');
const WebSocket = require('ws');
const fetch = require('node-fetch');
const path = require('path');
const { spawn } = require('child_process');
const PORT = process.env.PORT || 3000;
const CF_APP_ID = process.env.CF_APP_ID || '';
const CF_APP_TOKEN = process.env.CF_APP_TOKEN || '';
const CF_BASE = `https://rtc.live.cloudflare.com/v1/apps/${CF_APP_ID}`;
// Container's PulseAudio sink to write mic audio into
// pacat writes raw PCM to the null sink which virtual_mic reads from
const PACAT_ARGS = [
'--playback',
'--device=mic_sink',
'--format=s16le',
'--rate=48000',
'--channels=1',
'--latency-msec=20',
];
if (!CF_APP_ID || !CF_APP_TOKEN) {
console.error('[sig] CF_APP_ID and CF_APP_TOKEN must be set');
process.exit(1);
@@ -57,12 +70,68 @@ app.get('/', (req, res) => res.sendFile(path.join(__dirname, '..', 'client.html'
const agentSessions = new Map();
const browserToAgent = new Map();
// Per-browser pacat process for mic audio
// browserSessionId -> pacat child process
const pacatProcesses = new Map();
function startPacat(browserSessId) {
// Find the container ID for this browser session
// pacat runs inside the Docker container via docker exec
const agentId = browserToAgent.get(browserSessId);
const entry = agentSessions.get(agentId);
if (!entry) return null;
// Get container name from agent — agent sends it on register
const containerName = entry.containerName || 'css-test';
const pacat = spawn('docker', [
'exec', '-i', containerName,
'pacat', ...PACAT_ARGS,
]);
pacat.stderr.on('data', d => {
console.error(`[pacat:${browserSessId}]`, d.toString().trim());
});
pacat.on('close', code => {
console.log(`[pacat:${browserSessId}] exited with code ${code}`);
pacatProcesses.delete(browserSessId);
});
pacatProcesses.set(browserSessId, pacat);
console.log(`[sig] pacat started for browser session ${browserSessId}`);
return pacat;
}
function stopPacat(browserSessId) {
const pacat = pacatProcesses.get(browserSessId);
if (pacat) {
pacat.kill();
pacatProcesses.delete(browserSessId);
console.log(`[sig] pacat stopped for browser session ${browserSessId}`);
}
}
wss.on('connection', ws => {
let role = null;
let agentSessionId = null;
let browserSessId = null;
ws.on('message', async raw => {
ws.on('message', async (raw, isBinary) => {
// Binary messages are raw PCM audio from browser microphone
if (isBinary) {
if (browserSessId) {
let pacat = pacatProcesses.get(browserSessId);
if (!pacat) {
pacat = startPacat(browserSessId);
}
if (pacat && pacat.stdin.writable) {
pacat.stdin.write(raw);
}
}
return;
}
let msg;
try { msg = JSON.parse(raw); } catch { return; }
@@ -73,9 +142,17 @@ wss.on('connection', ws => {
agentSessionId: msg.sessionId,
videoTrackName: msg.videoTrackName,
audioTrackName: msg.audioTrackName,
containerName: msg.containerName || 'css-test',
agentWs: ws,
});
console.log(`[sig] agent registered — sessionId=${agentSessionId} video=${msg.videoTrackName} audio=${msg.audioTrackName}`);
console.log(`[sig] agent registered — sessionId=${agentSessionId} container=${msg.containerName || 'css-test'}`);
}
else if (msg.type === 'mic_stop') {
// Browser released PTT key — stop pacat
if (browserSessId) {
stopPacat(browserSessId);
}
}
else if (msg.type === 'browser_connect') {
@@ -147,6 +224,7 @@ wss.on('connection', ws => {
console.log(`[sig] agent disconnected — sessionId=${agentSessionId}`);
}
if (role === 'browser' && browserSessId) {
stopPacat(browserSessId);
browserToAgent.delete(browserSessId);
}
});