Skip to content
43 changes: 40 additions & 3 deletions opennow-stable/src/main/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { app } from "electron";
import { join } from "node:path";
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
import type { VideoCodec, ColorQuality, VideoAccelerationPreference, MicrophoneMode } from "@shared/gfn";

import type { VideoCodec, ColorQuality, VideoAccelerationPreference, FlightSlotConfig, HdrStreamingMode, MicMode, HevcCompatMode, VideoDecodeBackend, KeyboardLayout } from "@shared/gfn";
import type { VideoCodec, ColorQuality, VideoAccelerationPreference, FlightSlotConfig, HdrStreamingMode, MicMode, HevcCompatMode, VideoDecodeBackend } from "@shared/gfn";import { defaultFlightSlots } from "@shared/gfn";
export interface Settings {
/** Video resolution (e.g., "1920x1080") */
resolution: string;
Expand Down Expand Up @@ -48,8 +49,44 @@ export interface Settings {
windowWidth: number;
/** Window height */
windowHeight: number;
}
/** Enable Discord Rich Presence */
discordPresenceEnabled: boolean;
/** Discord Application Client ID */
discordClientId: string;
/** Enable flight controls (HOTAS/joystick) */
flightControlsEnabled: boolean;
/** Controller slot for flight controls (0-3) — legacy, kept for compat */
flightControlsSlot: number;
/** Per-slot flight configurations */
flightSlots: FlightSlotConfig[];
/** HDR streaming mode: off, auto, on */
hdrStreaming: HdrStreamingMode;
/** Microphone mode: off, on, push-to-talk */
micMode: MicMode;
/** Selected microphone device ID (empty = default) */
micDeviceId: string;
/** Microphone input gain 0.0 - 2.0 */
micGain: number;
/** Enable noise suppression */
micNoiseSuppression: boolean;
/** Enable automatic gain control */
micAutoGainControl: boolean;
/** Enable echo cancellation */
micEchoCancellation: boolean;
/** Toggle mic on/off shortcut (works in-stream) */
shortcutToggleMic: string;
/** HEVC compatibility mode: auto, force_h264, force_hevc, hevc_software */
hevcCompatMode: HevcCompatMode;
/** Linux video decode backend override: auto, vaapi, v4l2, software */
videoDecodeBackend: VideoDecodeBackend;
/** Show session clock every N minutes (0 = always visible) */
sessionClockShowEveryMinutes: number;
/** Duration in seconds to show session clock when periodically revealed */
sessionClockShowDurationSeconds: number;
/** Keyboard layout: auto, qwerty, azerty, qwertz */
keyboardLayout: KeyboardLayout;}

}
const defaultStopShortcut = "Ctrl+Shift+Q";
const defaultAntiAfkShortcut = "Ctrl+Shift+K";
const defaultMicShortcut = "Ctrl+Shift+M";
Expand Down Expand Up @@ -79,8 +116,8 @@ const DEFAULT_SETTINGS: Settings = {
sessionClockShowDurationSeconds: 30,
windowWidth: 1400,
windowHeight: 900,
keyboardLayout: "auto",};
};

export class SettingsManager {
private settings: Settings;
private readonly settingsPath: string;
Expand Down
92 changes: 85 additions & 7 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,15 @@ import {
} from "./gfn/webrtcClient";
import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts";
import { useControllerNavigation } from "./controllerNavigation";

import { MicAudioService } from "./gfn/micAudioService";
import type { MicAudioState } from "./gfn/micAudioService";
import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts";
import { getFlightHidService } from "./flight/FlightHidService";
import {
probeHdrCapability,
shouldEnableHdr,
buildInitialHdrState,
} from "./gfn/hdrCapability";
// UI Components
import { LoginScreen } from "./components/LoginScreen";
import { Navbar } from "./components/Navbar";
Expand Down Expand Up @@ -123,8 +131,9 @@ function defaultDiagnostics(): StreamDiagnostics {
serverRegion: "",
micState: "uninitialized",
micEnabled: false,
};
}
keyboardLayout: "qwerty",
detectedKeyboardLayout: "unknown", };
};}

function isSessionLimitError(error: unknown): boolean {
if (error && typeof error === "object" && "gfnErrorCode" in error) {
Expand Down Expand Up @@ -286,8 +295,8 @@ export function App(): JSX.Element {
sessionClockShowDurationSeconds: 30,
windowWidth: 1400,
windowHeight: 900,
});
const [settingsLoaded, setSettingsLoaded] = useState(false);
keyboardLayout: "auto", });
}); const [settingsLoaded, setSettingsLoaded] = useState(false);
const [regions, setRegions] = useState<StreamRegion[]>([]);
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo | null>(null);

Expand Down Expand Up @@ -338,6 +347,10 @@ export function App(): JSX.Element {
onBackAction: handleControllerBackAction,
});


// Mic state for StreamView indicator
const [micAudioState, setMicAudioState] = useState<MicAudioState | null>(null);

// Refs
const videoRef = useRef<HTMLVideoElement | null>(null);
const audioRef = useRef<HTMLAudioElement | null>(null);
Expand Down Expand Up @@ -713,8 +726,15 @@ export function App(): JSX.Element {
if (settings.microphoneMode !== "disabled") {
void clientRef.current.startMicrophone();
}
// Set keyboard layout on freshly created client
const cached = getCachedDetection();
const effective = resolveEffectiveLayout(
settings.keyboardLayout,
cached?.detected ?? "unknown",
);
clientRef.current.setKeyboardLayout(effective);
clientRef.current.setDetectedKeyboardLayout(cached?.detected ?? "unknown"); }
}

if (clientRef.current) {
await clientRef.current.handleOffer(event.sdp, activeSession, {
codec: settings.codec,
Expand Down Expand Up @@ -1344,7 +1364,64 @@ export function App(): JSX.Element {
streamStatus,
]);

// Filter games by search
useEffect(() => {
if (streamStatus === "streaming") {
if (!micServiceRef.current) {
micServiceRef.current = new MicAudioService();
}
const svc = micServiceRef.current;

if (clientRef.current) {
clientRef.current.setMicService(svc);
}

void svc.configure({
mode: settings.micMode,
deviceId: settings.micDeviceId,
gain: settings.micGain,
noiseSuppression: settings.micNoiseSuppression,
autoGainControl: settings.micAutoGainControl,
echoCancellation: settings.micEchoCancellation,
});
} else {
if (micServiceRef.current) {
micServiceRef.current.dispose();
micServiceRef.current = null;
}
}
}, [
streamStatus,
settings.micMode,
settings.micDeviceId,
settings.micGain,
settings.micNoiseSuppression,
settings.micAutoGainControl,
settings.micEchoCancellation,
]);

useEffect(() => {
return () => {
if (micServiceRef.current) {
micServiceRef.current.dispose();
micServiceRef.current = null;
}
};
}, []);


// Subscribe to mic audio state for StreamView indicator
useEffect(() => {
const svc = micServiceRef.current;
if (!svc) {
setMicAudioState(null);
return;
}
setMicAudioState(svc.getState());
const unsub = svc.onStateChange((state) => {
setMicAudioState(state);
});
return unsub;
}, [streamStatus, settings.micMode]); // Filter games by search
const filteredGames = useMemo(() => {
const query = searchQuery.trim().toLowerCase();
if (!query) return games;
Expand Down Expand Up @@ -1451,6 +1528,7 @@ export function App(): JSX.Element {
streamWarning={streamWarning}
isConnecting={streamStatus === "connecting"}
gameTitle={streamingGame?.title ?? "Game"}
micStatus={micAudioState?.status ?? null}
onToggleFullscreen={() => {
if (document.fullscreenElement) {
document.exitFullscreen().catch(() => {});
Expand Down
Loading