voice: deep stability audit — FFmpeg crash recovery, activity timeout, reconnect refresh

Gateway (transmitter.ts):
- Auto-stop on FFmpeg crash: non-zero exit triggers stop() to prevent
  silent audio loss and resource leaks
- Voice activity timeout (10s): auto-stops transmitter when no PCM
  received, preventing dead-air CPU waste on backgrounded tabs
- Stderr cap (4KB): prevents unbounded memory growth in long sessions

Gateway (voice.handler.ts):
- Double-check voiceController.getStatus().connected before starting
  transmitter — detects stale player state after gateway disconnect

Frontend (context.tsx):
- Force-refetch voice status on WS reconnect — UI converges in <1s
  instead of waiting up to 4s for SWR poll interval

All: tsc clean, biome clean
This commit is contained in:
asepharyana
2026-08-26 23:48:36 +07:00
parent 7e0d0d5123
commit a6e2c1fa4f
4 changed files with 68 additions and 4 deletions
+6 -1
View File
@@ -9,6 +9,7 @@ import {
useRef,
useState,
} from "react";
import { useSWRConfig } from "swr";
import { toast } from "@/components/primitives";
import { WsConnection } from "./connection";
import type { PcmChunk, WsEventHandler, WsEventType, WsStatus } from "./types";
@@ -39,6 +40,7 @@ export function WsProvider({
}) {
const connRef = useRef<WsConnection | null>(null);
const [status, setStatus] = useState<WsStatus>("disconnected");
const { mutate } = useSWRConfig();
// Tracks whether we've ever been connected — used to suppress the
// "reconnecting" toast on initial page load.
const wasConnected = useRef(false);
@@ -95,6 +97,9 @@ export function WsProvider({
wasConnected.current = false;
} else if (s === "connected") {
wasConnected.current = true;
// After reconnect, force-refetch voice status immediately so
// the UI converges faster instead of waiting up to 4s for SWR poll.
void mutate(["voice-status"]);
} else if (s === "error" && !wasConnected.current) {
toast({
title: "Connection error",
@@ -121,7 +126,7 @@ export function WsProvider({
unsubEvent();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url, handleBinaryEvent, handleJsonEvent]);
}, [url, handleBinaryEvent, handleJsonEvent, mutate]);
const subscribe = useCallback(
<E extends WsEventType>(_eventType: E, handler: WsEventHandler<E>) => {