Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions skills/meet-join/bot/src/browser/xvfb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,11 @@ function isProcessAlive(pid: number): boolean {
try {
process.kill(pid, 0);
return true;
} catch {
} catch (err) {
// EPERM means the process exists but is owned by another user — still
// alive from our perspective, and we must not clobber its lock file.
// Only ESRCH ("no such process") is a reliable liveness signal.
if ((err as NodeJS.ErrnoException)?.code === "EPERM") return true;
return false;
}
}
Expand All @@ -93,13 +97,14 @@ async function sleep(ms: number): Promise<void> {
export async function startXvfb(display = ":99"): Promise<XvfbHandle> {
const displayIndex = parseDisplayIndex(display);
const lockPath = lockFilePath(displayIndex);
const canonicalDisplay = `:${displayIndex}`;

if (existsSync(lockPath)) {
// Verify the lock holder is still alive. If Xvfb died uncleanly its
// lock file lingers and prevents respawning.
const pid = parseLockPid(lockPath);
if (pid !== null && isProcessAlive(pid)) {
return { display, process: null };
return { display: canonicalDisplay, process: null };
}
// Stale lock — remove it so we can respawn.
try {
Expand All @@ -109,7 +114,6 @@ export async function startXvfb(display = ":99"): Promise<XvfbHandle> {
}
}

const canonicalDisplay = `:${displayIndex}`;
const proc = Bun.spawn(
["Xvfb", canonicalDisplay, "-screen", "0", "1280x720x24"],
{
Expand Down
35 changes: 24 additions & 11 deletions skills/meet-join/bot/src/media/audio-capture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,17 @@ export const DEFAULT_FRAME_BYTES = 320;
const MAX_RECONNECT_ATTEMPTS = 3;
const RECONNECT_BACKOFF_MS = 500;

/**
* Minimum number of frames that must flow through the socket in a single
* attempt before we consider the pipeline "stable" and reset the reconnect
* budget. At the default 20ms/frame this is ~2s of audio — enough that a
* pathologically flapping PulseAudio (crashing shortly after each startup
* and emitting a single frame in between) can no longer keep the budget
* perpetually topped up. Below this threshold a single-frame attempt still
* counts as a failure.
*/
const MIN_FRAMES_TO_RESET_BUDGET = 100;
Comment thread
siddseethepalli marked this conversation as resolved.

export interface AudioCaptureOptions {
/**
* Absolute path to the Unix socket the daemon is listening on. The daemon
Expand Down Expand Up @@ -247,7 +258,7 @@ export async function startAudioCapture(
async function runOneAttempt(): Promise<{
outcome: AttemptOutcome;
error?: Error;
hadData: boolean;
framesWritten: number;
}> {
let attemptError: Error | undefined;

Expand All @@ -259,7 +270,7 @@ export async function startAudioCapture(
return {
outcome: "parec",
error: err instanceof Error ? err : new Error(String(err)),
hadData: false,
framesWritten: 0,
};
}
currentProc = proc;
Expand All @@ -278,7 +289,7 @@ export async function startAudioCapture(
return {
outcome: "socket",
error: err instanceof Error ? err : new Error(String(err)),
hadData: false,
framesWritten: 0,
};
}
currentSocket = sock;
Expand Down Expand Up @@ -308,14 +319,14 @@ export async function startAudioCapture(
// 4. Pipe parec.stdout through the frame chunker into the socket.
// We deliberately don't `await` the pump — it races against the three
// promises above and terminates when any of them settles.
let framesWritten = false;
let framesWritten = 0;
const pumpDone = pumpFrames(
proc.stdout,
sock,
frameBytes,
() => stopping,
() => {
framesWritten = true;
framesWritten += 1;
},
);

Expand Down Expand Up @@ -366,9 +377,9 @@ export async function startAudioCapture(
currentSocket = null;

if (outcome === "stopped") {
return { outcome: "stopped", hadData: framesWritten };
return { outcome: "stopped", framesWritten };
}
return { outcome, error: attemptError, hadData: framesWritten };
return { outcome, error: attemptError, framesWritten };
}

/**
Expand All @@ -380,15 +391,17 @@ export async function startAudioCapture(
let consecutiveFailures = 0;

while (!stopping) {
const { outcome, error, hadData } = await runOneAttempt();
const { outcome, error, framesWritten } = await runOneAttempt();

if (outcome === "stopped") {
break;
}

// Reset the failure counter if this attempt successfully transferred
// data — the pipeline was healthy for a while before it broke.
if (hadData) {
// Reset the failure counter only if this attempt streamed enough data
// to look genuinely healthy. A single 320-byte frame would otherwise
// let pathological flapping (e.g. PulseAudio crashing moments after
// each startup) keep the reconnect budget perpetually topped up.
if (framesWritten >= MIN_FRAMES_TO_RESET_BUDGET) {
consecutiveFailures = 0;
}

Expand Down
Loading