Skip to content
Open
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
50 changes: 46 additions & 4 deletions opennow-stable/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import type {
IceCandidatePayload,
Settings,
VideoAccelerationPreference,
VideoDecodeBackend,
SubscriptionFetchRequest,
SessionConflictChoice,
} from "@shared/gfn";
Expand All @@ -53,16 +54,22 @@ const __dirname = dirname(__filename);
interface BootstrapVideoPreferences {
decoderPreference: VideoAccelerationPreference;
encoderPreference: VideoAccelerationPreference;
videoDecodeBackend: VideoDecodeBackend;
}

function isAccelerationPreference(value: unknown): value is VideoAccelerationPreference {
return value === "auto" || value === "hardware" || value === "software";
}

function isVideoDecodeBackend(value: unknown): value is VideoDecodeBackend {
return value === "auto" || value === "vaapi" || value === "v4l2" || value === "software";
}

function loadBootstrapVideoPreferences(): BootstrapVideoPreferences {
const defaults: BootstrapVideoPreferences = {
decoderPreference: "auto",
encoderPreference: "auto",
videoDecodeBackend: "auto",
};
try {
const settingsPath = join(app.getPath("userData"), "settings.json");
Expand All @@ -77,6 +84,9 @@ function loadBootstrapVideoPreferences(): BootstrapVideoPreferences {
encoderPreference: isAccelerationPreference(parsed.encoderPreference)
? parsed.encoderPreference
: defaults.encoderPreference,
videoDecodeBackend: isVideoDecodeBackend(parsed.videoDecodeBackend)
? parsed.videoDecodeBackend
: defaults.videoDecodeBackend,
};
} catch {
return defaults;
Expand All @@ -85,13 +95,18 @@ function loadBootstrapVideoPreferences(): BootstrapVideoPreferences {

const bootstrapVideoPrefs = loadBootstrapVideoPreferences();
console.log(
`[Main] Video acceleration preference: decode=${bootstrapVideoPrefs.decoderPreference}, encode=${bootstrapVideoPrefs.encoderPreference}`,
`[Main] Video acceleration preference: decode=${bootstrapVideoPrefs.decoderPreference}, encode=${bootstrapVideoPrefs.encoderPreference}, videoDecodeBackend=${bootstrapVideoPrefs.videoDecodeBackend}`,
);

// --- Platform-specific HW video decode features ---
const platformFeatures: string[] = [];
const isLinuxArm = process.platform === "linux" && (process.arch === "arm64" || process.arch === "arm");
const isLinuxArm = process.platform === "linux" && (process.arch === "arm64" || process.arch === "arm");

const platformFeatures: string[] = [];
const disableFeatures: string[] = [
"WebRtcHideLocalIpsWithMdns",
];
if (process.platform === "win32") {
// Windows: D3D11 + Media Foundation path for HW decode/encode acceleration
if (bootstrapVideoPrefs.decoderPreference !== "software") {
Expand Down Expand Up @@ -123,7 +138,33 @@ if (process.platform === "win32") {
) {
platformFeatures.push("VaapiIgnoreDriverChecks");
}
const pref = bootstrapVideoPrefs.videoDecodeBackend;

if (pref === "software") {
disableFeatures.push("UseChromeOSDirectVideoDecoder");
} else if (pref === "vaapi") {
if (bootstrapVideoPrefs.decoderPreference !== "software") {
platformFeatures.push("VaapiVideoDecoder");
platformFeatures.push("VaapiIgnoreDriverChecks");
}
disableFeatures.push("UseChromeOSDirectVideoDecoder");
} else if (pref === "v4l2") {
platformFeatures.push("UseChromeOSDirectVideoDecoder");
} else {
// auto: select based on architecture
if (isLinuxArm) {
platformFeatures.push("UseChromeOSDirectVideoDecoder");
} else {
if (bootstrapVideoPrefs.decoderPreference !== "software") {
platformFeatures.push("VaapiVideoDecoder");
platformFeatures.push("VaapiIgnoreDriverChecks");
}
disableFeatures.push("UseChromeOSDirectVideoDecoder");
}
}

if (bootstrapVideoPrefs.encoderPreference !== "software") {
platformFeatures.push("VaapiVideoEncoder"); }
}
// macOS: VideoToolbox handles HW acceleration natively, no extra feature flags needed

Expand All @@ -145,8 +186,7 @@ const disableFeatures: string[] = [
if (process.platform === "linux" && !isLinuxArm) {
// ChromeOS-only direct video decoder path interferes on regular Linux
disableFeatures.push("UseChromeOSDirectVideoDecoder");
}
app.commandLine.appendSwitch("disable-features", disableFeatures.join(","));
}app.commandLine.appendSwitch("disable-features", disableFeatures.join(","));

app.commandLine.appendSwitch("force-fieldtrials",
[
Expand Down Expand Up @@ -501,7 +541,9 @@ function registerIpcHandlers(): void {
});

// Save window size when it changes
mainWindow?.on("resize", () => {
ipcMain.handle(IPC_CHANNELS.GET_PLATFORM_INFO, () => {
return { platform: process.platform, arch: process.arch };
}); mainWindow?.on("resize", () => {
if (mainWindow && !mainWindow.isDestroyed()) {
const [width, height] = mainWindow.getSize();
settingsManager.set("windowWidth", width);
Expand Down
56 changes: 53 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 } from "@shared/gfn";
import { defaultFlightSlots } from "@shared/gfn";
export interface Settings {
/** Video resolution (e.g., "1920x1080") */
resolution: string;
Expand Down Expand Up @@ -48,7 +49,40 @@ 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;}

const defaultStopShortcut = "Ctrl+Shift+Q";
const defaultAntiAfkShortcut = "Ctrl+Shift+K";
Expand All @@ -75,7 +109,23 @@ const DEFAULT_SETTINGS: Settings = {
microphoneMode: "disabled",
microphoneDeviceId: "",
hideStreamButtons: false,
sessionClockShowEveryMinutes: 60,
windowWidth: 1400,
windowHeight: 900,
discordPresenceEnabled: false,
discordClientId: "",
flightControlsEnabled: false,
flightControlsSlot: 3,
flightSlots: defaultFlightSlots(),
hdrStreaming: "off",
micMode: "off",
micDeviceId: "",
micGain: 1.0,
micNoiseSuppression: true,
micAutoGainControl: true,
micEchoCancellation: true,
shortcutToggleMic: "Ctrl+Shift+M",
hevcCompatMode: "auto",
videoDecodeBackend: "auto", sessionClockShowEveryMinutes: 60,
sessionClockShowDurationSeconds: 30,
windowWidth: 1400,
windowHeight: 900,
Expand Down
39 changes: 37 additions & 2 deletions opennow-stable/src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import type {
IceCandidatePayload,
Settings,
SubscriptionFetchRequest,
} from "@shared/gfn";
DiscordPresencePayload,
FlightProfile,
MicDeviceInfo,
PlatformInfo,} from "@shared/gfn";

// Extend the OpenNowApi interface for internal preload use
type PreloadApi = OpenNowApi;
Expand Down Expand Up @@ -74,6 +77,38 @@ const api: PreloadApi = {
ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_SET, key, value),
resetSettings: () => ipcRenderer.invoke(IPC_CHANNELS.SETTINGS_RESET),
exportLogs: (format?: "text" | "json") => ipcRenderer.invoke(IPC_CHANNELS.LOGS_EXPORT, format),
};
updateDiscordPresence: (state: DiscordPresencePayload) =>
ipcRenderer.invoke(IPC_CHANNELS.DISCORD_UPDATE_PRESENCE, state),
clearDiscordPresence: () => ipcRenderer.invoke(IPC_CHANNELS.DISCORD_CLEAR_PRESENCE),
flightGetProfile: (vidPid: string, gameId?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.FLIGHT_GET_PROFILE, vidPid, gameId),
flightSetProfile: (profile: FlightProfile) =>
ipcRenderer.invoke(IPC_CHANNELS.FLIGHT_SET_PROFILE, profile),
flightDeleteProfile: (vidPid: string, gameId?: string) =>
ipcRenderer.invoke(IPC_CHANNELS.FLIGHT_DELETE_PROFILE, vidPid, gameId),
flightGetAllProfiles: () => ipcRenderer.invoke(IPC_CHANNELS.FLIGHT_GET_ALL_PROFILES),
flightResetProfile: (vidPid: string) => ipcRenderer.invoke(IPC_CHANNELS.FLIGHT_RESET_PROFILE, vidPid),
getOsHdrInfo: () => ipcRenderer.invoke(IPC_CHANNELS.HDR_GET_OS_INFO),
relaunchApp: () => ipcRenderer.invoke(IPC_CHANNELS.APP_RELAUNCH),
micEnumerateDevices: () => ipcRenderer.invoke(IPC_CHANNELS.MIC_ENUMERATE_DEVICES),
onMicDevicesChanged: (listener: (devices: MicDeviceInfo[]) => void) => {
const wrapped = (_event: Electron.IpcRendererEvent, devices: MicDeviceInfo[]) => {
listener(devices);
};
ipcRenderer.on(IPC_CHANNELS.MIC_DEVICES_CHANGED, wrapped);
return () => {
ipcRenderer.off(IPC_CHANNELS.MIC_DEVICES_CHANGED, wrapped);
};
},
onSessionExpired: (listener: (reason: string) => void) => {
const wrapped = (_event: Electron.IpcRendererEvent, reason: string) => {
listener(reason);
};
ipcRenderer.on(IPC_CHANNELS.AUTH_SESSION_EXPIRED, wrapped);
return () => {
ipcRenderer.off(IPC_CHANNELS.AUTH_SESSION_EXPIRED, wrapped);
};
},
getPlatformInfo: (): Promise<PlatformInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_PLATFORM_INFO),};

contextBridge.exposeInMainWorld("openNow", api);
35 changes: 31 additions & 4 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ import type {
SubscriptionInfo,
StreamRegion,
VideoCodec,
} from "@shared/gfn";
DiscordPresencePayload,
FlightSlotConfig,
HdrCapability,
HdrStreamState,
PlatformInfo,} from "@shared/gfn";

import {
GfnWebRtcClient,
Expand Down Expand Up @@ -282,7 +286,23 @@ export function App(): JSX.Element {
microphoneMode: "disabled",
microphoneDeviceId: "",
hideStreamButtons: false,
sessionClockShowEveryMinutes: 60,
windowWidth: 1400,
windowHeight: 900,
discordPresenceEnabled: false,
discordClientId: "",
flightControlsEnabled: false,
flightControlsSlot: 3,
flightSlots: defaultFlightSlots(),
hdrStreaming: "off",
micMode: "off",
micDeviceId: "",
micGain: 1.0,
micNoiseSuppression: true,
micAutoGainControl: true,
micEchoCancellation: true,
shortcutToggleMic: DEFAULT_SHORTCUTS.shortcutToggleMic,
hevcCompatMode: "auto",
videoDecodeBackend: "auto", sessionClockShowEveryMinutes: 60,
sessionClockShowDurationSeconds: 30,
windowWidth: 1400,
windowHeight: 900,
Expand All @@ -291,6 +311,9 @@ export function App(): JSX.Element {
const [regions, setRegions] = useState<StreamRegion[]>([]);
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo | null>(null);

// Platform info (from main process)
const [platformInfo, setPlatformInfo] = useState<PlatformInfo | null>(null);

// Stream State
const [session, setSession] = useState<SessionInfo | null>(null);
const [streamStatus, setStreamStatus] = useState<StreamStatus>("idle");
Expand Down Expand Up @@ -359,7 +382,8 @@ export function App(): JSX.Element {
document.body.classList.remove("controller-mode");
};
}, [controllerConnected]);

void window.openNow.getPlatformInfo().then(setPlatformInfo).catch(() => {});
}, []);
// Derived state
const selectedProvider = useMemo(() => {
return providers.find((p) => p.idpId === providerIdpId) ?? authSession?.provider ?? null;
Expand Down Expand Up @@ -722,7 +746,10 @@ export function App(): JSX.Element {
resolution: settings.resolution,
fps: settings.fps,
maxBitrateKbps: settings.maxBitrateMbps * 1000,
});
hdrEnabled: hdrEnabledForStream,
hevcCompatMode: settings.hevcCompatMode,
platformInfo,
videoDecodeBackend: settings.videoDecodeBackend, });
setLaunchError(null);
setStreamStatus("streaming");
setSessionStartedAtMs((current) => current ?? Date.now());
Expand Down
Loading