From d338f8dd00683f19558da51fa6db1aed88f62d85 Mon Sep 17 00:00:00 2001 From: zortos293 <65777760+zortos293@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:14:51 +0000 Subject: [PATCH] Optimize stream performance with adaptive polling and lag classification --- opennow-stable/src/renderer/src/App.tsx | 40 +++-- .../renderer/src/components/StatsOverlay.tsx | 7 + .../renderer/src/components/StreamView.tsx | 84 ++++++--- .../src/renderer/src/gfn/webrtcClient.ts | 161 ++++++++++++++++-- .../src/utils/streamDiagnosticsStore.ts | 41 +++++ 5 files changed, 282 insertions(+), 51 deletions(-) create mode 100644 opennow-stable/src/renderer/src/utils/streamDiagnosticsStore.ts diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 2f4d4171..96acf5a3 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -24,6 +24,7 @@ import { import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts"; import { useControllerNavigation } from "./controllerNavigation"; import { usePlaytime } from "./utils/usePlaytime"; +import { createStreamDiagnosticsStore } from "./utils/streamDiagnosticsStore"; // UI Components import { LoginScreen } from "./components/LoginScreen"; @@ -196,6 +197,8 @@ function defaultDiagnostics(): StreamDiagnostics { inputQueuePeakBufferedBytes: 0, inputQueueDropCount: 0, inputQueueMaxSchedulingDelayMs: 0, + lagReason: "unknown", + lagReasonDetail: "Waiting for stream stats", gpuType: "", serverRegion: "", decoderPressureActive: false, @@ -403,11 +406,13 @@ export function App(): JSX.Element { const [settingsLoaded, setSettingsLoaded] = useState(false); const [regions, setRegions] = useState([]); const [subscriptionInfo, setSubscriptionInfo] = useState(null); + const diagnosticsStoreRef = useRef | null>(null); + const diagnosticsStore = + diagnosticsStoreRef.current ?? (diagnosticsStoreRef.current = createStreamDiagnosticsStore(defaultDiagnostics())); // Stream State const [session, setSession] = useState(null); const [streamStatus, setStreamStatus] = useState("idle"); - const [diagnostics, setDiagnostics] = useState(defaultDiagnostics()); const [showStatsOverlay, setShowStatsOverlay] = useState(true); const [antiAfkEnabled, setAntiAfkEnabled] = useState(false); const [escHoldReleaseIndicator, setEscHoldReleaseIndicator] = useState<{ visible: boolean; progress: number }>({ @@ -668,7 +673,7 @@ export function App(): JSX.Element { setSessionElapsedSeconds(0); setStreamWarning(null); setEscHoldReleaseIndicator({ visible: false, progress: 0 }); - setDiagnostics(defaultDiagnostics()); + diagnosticsStore.set(defaultDiagnostics()); if (!options?.keepStreamingContext) { setStreamingGame(null); @@ -678,7 +683,7 @@ export function App(): JSX.Element { if (!options?.keepLaunchError) { setLaunchError(null); } - }, []); + }, [diagnosticsStore]); // Session ref sync useEffect(() => { @@ -1121,19 +1126,19 @@ export function App(): JSX.Element { return; } - const hasLiveFrames = diagnostics.framesDecoded > 0 || diagnostics.framesReceived > 0 || diagnostics.renderFps > 0; - if (!hasLiveFrames) { - return; - } + const evaluate = () => { + const snapshot = diagnosticsStore.getSnapshot(); + const hasLiveFrames = + snapshot.framesDecoded > 0 || snapshot.framesReceived > 0 || snapshot.renderFps > 0; + if (hasLiveFrames) { + setSessionStartedAtMs(Date.now()); + } + }; - setSessionStartedAtMs(Date.now()); - }, [ - diagnostics.framesDecoded, - diagnostics.framesReceived, - diagnostics.renderFps, - sessionStartedAtMs, - streamStatus, - ]); + evaluate(); + const unsubscribe = diagnosticsStore.subscribe(evaluate); + return unsubscribe; + }, [sessionStartedAtMs, streamStatus]); useEffect(() => { if (!streamWarning) return; @@ -1172,7 +1177,7 @@ export function App(): JSX.Element { mouseSensitivity: settings.mouseSensitivity, mouseAcceleration: settings.mouseAcceleration, onLog: (line: string) => console.log(`[WebRTC] ${line}`), - onStats: (stats) => setDiagnostics(stats), + onStats: (stats) => diagnosticsStore.set(stats), onEscHoldProgress: (visible, progress) => { setEscHoldReleaseIndicator({ visible, progress }); }, @@ -2019,7 +2024,7 @@ export function App(): JSX.Element { className={isSwitchingGame ? "sv--switching" : undefined} videoRef={videoRef} audioRef={audioRef} - stats={diagnostics} + diagnosticsStore={diagnosticsStore} showStats={showStatsOverlay} shortcuts={{ toggleStats: formatShortcutForDisplay(settings.shortcutToggleStats, isMac), @@ -2031,7 +2036,6 @@ export function App(): JSX.Element { }} hideStreamButtons={settings.hideStreamButtons} serverRegion={session?.serverIp} - connectedControllers={diagnostics.connectedGamepads} antiAfkEnabled={antiAfkEnabled} escHoldReleaseIndicator={escHoldReleaseIndicator} exitPrompt={exitPrompt} diff --git a/opennow-stable/src/renderer/src/components/StatsOverlay.tsx b/opennow-stable/src/renderer/src/components/StatsOverlay.tsx index eac695a4..16987bd2 100644 --- a/opennow-stable/src/renderer/src/components/StatsOverlay.tsx +++ b/opennow-stable/src/renderer/src/components/StatsOverlay.tsx @@ -82,6 +82,13 @@ export function StatsOverlay({ )} + {stats.lagReason !== "stable" && stats.lagReason !== "unknown" && ( +
+ + {stats.lagReason} +
+ )} + {/* Controller Status */} {connectedControllers > 0 && (
diff --git a/opennow-stable/src/renderer/src/components/StreamView.tsx b/opennow-stable/src/renderer/src/components/StreamView.tsx index 5962b522..247a300a 100644 --- a/opennow-stable/src/renderer/src/components/StreamView.tsx +++ b/opennow-stable/src/renderer/src/components/StreamView.tsx @@ -3,7 +3,9 @@ import { createPortal } from "react-dom"; import type { JSX } from "react"; import { Maximize, Minimize, Gamepad2, Loader2, LogOut, Clock3, AlertTriangle, Mic, MicOff, Camera, ChevronLeft, ChevronRight, Save, Trash2, X, Circle, Square, Video, FolderOpen } from "lucide-react"; import SideBar from "./SideBar"; -import type { StreamDiagnostics } from "../gfn/webrtcClient"; +import type { StreamDiagnosticsStore } from "../utils/streamDiagnosticsStore"; +import { useStreamDiagnosticsStore } from "../utils/streamDiagnosticsStore"; +import type { StreamLagReason } from "../gfn/webrtcClient"; import { getStoreDisplayName, getStoreIconComponent } from "./GameCard"; import type { MicrophoneMode, ScreenshotEntry, RecordingEntry } from "@shared/gfn"; import { isShortcutMatch, normalizeShortcut } from "../shortcuts"; @@ -11,7 +13,7 @@ import { isShortcutMatch, normalizeShortcut } from "../shortcuts"; interface StreamViewProps { videoRef: React.Ref; audioRef: React.Ref; - stats: StreamDiagnostics; + diagnosticsStore: StreamDiagnosticsStore; showStats: boolean; shortcuts: { toggleStats: string; @@ -23,7 +25,6 @@ interface StreamViewProps { }; hideStreamButtons?: boolean; serverRegion?: string; - connectedControllers: number; antiAfkEnabled: boolean; escHoldReleaseIndicator: { visible: boolean; @@ -91,6 +92,38 @@ function getInputQueueColor(bufferedBytes: number, dropCount: number): string { return "var(--success)"; } +function getLagReasonLabel(reason: StreamLagReason): string { + switch (reason) { + case "network": + return "Network"; + case "decoder": + return "Decode"; + case "input_backpressure": + return "Input"; + case "render": + return "Render"; + case "stable": + return "Stable"; + default: + return "Unknown"; + } +} + +function getLagReasonColor(reason: StreamLagReason): string { + switch (reason) { + case "network": + case "decoder": + return "var(--error)"; + case "input_backpressure": + case "render": + return "var(--warning)"; + case "stable": + return "var(--success)"; + default: + return "var(--ink-muted)"; + } +} + function formatElapsed(totalSeconds: number): string { const safe = Math.max(0, Math.floor(totalSeconds)); const hours = Math.floor(safe / 3600); @@ -121,12 +154,6 @@ function formatWarningSeconds(value: number | undefined): string | null { return `${seconds}s`; } -/** - * Drives a canvas-based segmented level meter from a live MediaStreamTrack. - * Uses the Web Audio API AnalyserNode as a read-only tap — audio is never - * routed to the speaker. Runs a requestAnimationFrame loop while active; - * tears down fully (rAF cancelled, AudioContext closed) on deactivation. - */ function useMicMeter( canvasRef: React.RefObject, track: MediaStreamTrack | null, @@ -153,7 +180,7 @@ function useMicMeter( let audioCtx: AudioContext | null = null; let source: MediaStreamAudioSourceNode | null = null; let analyser: AnalyserNode | null = null; - let raf = 0; + let tickTimer: number | null = null; let dead = false; const start = async () => { @@ -176,21 +203,21 @@ function useMicMeter( } analyser = audioCtx.createAnalyser(); - analyser.fftSize = 512; + analyser.fftSize = 256; analyser.smoothingTimeConstant = 0.65; source = audioCtx.createMediaStreamSource(new MediaStream([track])); source.connect(analyser); - // NOT connected to destination — monitoring only, no loopback const buf = new Uint8Array(analyser.frequencyBinCount); const SEG = 20; const GAP = Math.round(2 * dpr); const bw = (W - GAP * (SEG - 1)) / SEG; const radius = Math.min(3 * dpr, bw / 2); + const frameIntervalMs = 33; const frame = () => { if (dead || !analyser) return; - raf = requestAnimationFrame(frame); + tickTimer = window.setTimeout(frame, frameIntervalMs); analyser.getByteTimeDomainData(buf); let sum = 0; @@ -227,7 +254,9 @@ function useMicMeter( return () => { dead = true; - cancelAnimationFrame(raf); + if (tickTimer !== null) { + window.clearTimeout(tickTimer); + } source?.disconnect(); analyser?.disconnect(); if (audioCtx && audioCtx.state !== "closed") { @@ -243,11 +272,10 @@ function useMicMeter( export function StreamView({ videoRef, audioRef, - stats, + diagnosticsStore, showStats, shortcuts, serverRegion, - connectedControllers, antiAfkEnabled, escHoldReleaseIndicator, exitPrompt, @@ -278,6 +306,7 @@ export function StreamView({ hideStreamButtons = false, className, }: StreamViewProps): JSX.Element { + const stats = useStreamDiagnosticsStore(diagnosticsStore); const [isFullscreen, setIsFullscreen] = useState(false); const [showHints, setShowHints] = useState(true); const [showSessionClock, setShowSessionClock] = useState(false); @@ -1505,6 +1534,11 @@ export function StreamView({ IQ {inputQueueText} + {stats.lagReason !== "stable" && stats.lagReason !== "unknown" && ( + + Lag {getLagReasonLabel(stats.lagReason)} + + )}
@@ -1522,14 +1556,20 @@ export function StreamView({ {[stats.gpuType, regionLabel].filter(Boolean).join(" · ")}
)} + + {stats.lagReason !== "stable" && stats.lagReason !== "unknown" && ( +
+ Lag source {getLagReasonLabel(stats.lagReason).toLowerCase()} · {stats.lagReasonDetail} +
+ )} )} {/* Controller indicator (top-left) */} - {connectedControllers > 0 && !isConnecting && ( -
+ {stats.connectedGamepads > 0 && !isConnecting && ( +
- {connectedControllers > 1 && {connectedControllers}} + {stats.connectedGamepads > 1 && {stats.connectedGamepads}}
)} @@ -1537,7 +1577,7 @@ export function StreamView({ {showMicIndicator && onToggleMicrophone && (