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
47 changes: 44 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 } 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,34 @@ 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;}

const defaultStopShortcut = "Ctrl+Shift+Q";
const defaultAntiAfkShortcut = "Ctrl+Shift+K";
Expand Down Expand Up @@ -79,7 +107,20 @@ const DEFAULT_SETTINGS: Settings = {
sessionClockShowDurationSeconds: 30,
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",};

export class SettingsManager {
private settings: Settings;
Expand Down
18 changes: 16 additions & 2 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,20 @@ export function App(): JSX.Element {
sessionClockShowDurationSeconds: 30,
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", });
const [settingsLoaded, setSettingsLoaded] = useState(false);
const [regions, setRegions] = useState<StreamRegion[]>([]);
const [subscriptionInfo, setSubscriptionInfo] = useState<SubscriptionInfo | null>(null);
Expand Down Expand Up @@ -722,7 +735,8 @@ export function App(): JSX.Element {
resolution: settings.resolution,
fps: settings.fps,
maxBitrateKbps: settings.maxBitrateMbps * 1000,
});
hdrEnabled: hdrEnabledForStream,
hevcCompatMode: settings.hevcCompatMode, });
setLaunchError(null);
setStreamStatus("streaming");
setSessionStartedAtMs((current) => current ?? Date.now());
Expand Down
24 changes: 24 additions & 0 deletions opennow-stable/src/renderer/src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -840,6 +840,30 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag
</div>
</div>

{/* HEVC Compatibility Mode */}
<div className="settings-row settings-row--column">
<label className="settings-label">HEVC Compatibility</label>
<div className="settings-chip-row">
{([
{ value: "auto" as const, label: "Auto" },
{ value: "force_h264" as const, label: "Force H.264" },
{ value: "force_hevc" as const, label: "Force HEVC" },
{ value: "hevc_software" as const, label: "HEVC Software" },
] as const).map((option) => (
<button
key={`hevc-compat-${option.value}`}
className={`settings-chip ${settings.hevcCompatMode === option.value ? "active" : ""}`}
onClick={() => handleChange("hevcCompatMode", option.value)}
>
{option.label}
</button>
))}
</div>
<span className="settings-subtle-hint">
Auto disables HEVC on AMD Polaris/Vega GPUs (RX 550, Vega iGPU) to prevent green screen.
</span>
</div>

{/* Decoder preference */}
<div className="settings-row settings-row--column">
<label className="settings-label">Decoder</label>
Expand Down
144 changes: 144 additions & 0 deletions opennow-stable/src/renderer/src/gfn/gpuDetect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import type { HevcCompatMode, VideoCodec } from "@shared/gfn";

export interface GpuInfo {
vendor: "amd" | "nvidia" | "intel" | "unknown";
renderer: string;
unmaskedRenderer: string;
isAmdPolarisOrVega: boolean;
}

const AMD_HEVC_PROBLEM_PATTERNS = [
/RX\s*550/i,
/RX\s*560/i,
/RX\s*570/i,
/RX\s*580/i,
/RX\s*590/i,
/RX\s*5[0-9]{2}\b/i,
/Polaris/i,
/Vega/i,
/Ryzen.*Vega/i,
/Radeon.*Vega/i,
/Radeon\(TM\).*Graphics/i,
/Radeon\s+Graphics/i,
/gfx80[0-3]/i,
/gfx90[0-2]/i,
];

function detectVendor(renderer: string): "amd" | "nvidia" | "intel" | "unknown" {
const r = renderer.toLowerCase();
if (r.includes("amd") || r.includes("radeon") || r.includes("ati")) return "amd";
if (r.includes("nvidia") || r.includes("geforce") || r.includes("quadro") || r.includes("rtx") || r.includes("gtx")) return "nvidia";
if (r.includes("intel") || r.includes("iris") || r.includes("uhd")) return "intel";
return "unknown";
}

function isProblematicAmdGpu(renderer: string): boolean {
if (detectVendor(renderer) !== "amd") return false;
return AMD_HEVC_PROBLEM_PATTERNS.some((pattern) => pattern.test(renderer));
}

let cachedGpuInfo: GpuInfo | null = null;

export function detectGpu(): GpuInfo {
if (cachedGpuInfo) return cachedGpuInfo;

let renderer = "";
let unmaskedRenderer = "";

try {
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl") ?? canvas.getContext("webgl2");
if (gl) {
renderer = gl.getParameter(gl.RENDERER) as string;
const ext = gl.getExtension("WEBGL_debug_renderer_info");
if (ext) {
unmaskedRenderer = gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) as string;
}
const loseCtx = gl.getExtension("WEBGL_lose_context");
if (loseCtx) loseCtx.loseContext();
}
} catch {
// WebGL not available
}

const effectiveRenderer = unmaskedRenderer || renderer;
const vendor = detectVendor(effectiveRenderer);
const isAmdPolarisOrVega = isProblematicAmdGpu(effectiveRenderer);

cachedGpuInfo = { vendor, renderer, unmaskedRenderer, isAmdPolarisOrVega };
return cachedGpuInfo;
}

export interface HevcCodecDecision {
effectiveCodec: VideoCodec;
reason: string;
gpuInfo: GpuInfo;
wasOverridden: boolean;
}

export function resolveHevcCompat(
requestedCodec: VideoCodec,
compatMode: HevcCompatMode,
): HevcCodecDecision {
const gpuInfo = detectGpu();

if (requestedCodec !== "H265") {
return {
effectiveCodec: requestedCodec,
reason: `Codec is ${requestedCodec}, HEVC compat not applicable`,
gpuInfo,
wasOverridden: false,
};
}

switch (compatMode) {
case "force_h264":
return {
effectiveCodec: "H264",
reason: "HEVC Compatibility Mode: Force H.264",
gpuInfo,
wasOverridden: true,
};

case "force_hevc":
return {
effectiveCodec: "H265",
reason: "HEVC Compatibility Mode: Force HEVC (user override)",
gpuInfo,
wasOverridden: false,
};

case "hevc_software":
return {
effectiveCodec: "H265",
reason: "HEVC Compatibility Mode: HEVC Software Decode requested",
gpuInfo,
wasOverridden: false,
};

case "auto":
default: {
if (gpuInfo.isAmdPolarisOrVega) {
return {
effectiveCodec: "H264",
reason:
`HEVC auto-disabled: AMD Polaris/Vega GPU detected (${gpuInfo.unmaskedRenderer || gpuInfo.renderer}). ` +
`Known HEVC hardware decode issue (D3D11/DXVA green screen). Falling back to H.264.`,
gpuInfo,
wasOverridden: true,
};
}

return {
effectiveCodec: "H265",
reason: "HEVC Compatibility Mode: Auto — no problematic GPU detected",
gpuInfo,
wasOverridden: false,
};
}
}
}

export function shouldRequestSoftwareDecode(compatMode: HevcCompatMode, codec: VideoCodec): boolean {
return compatMode === "hevc_software" && codec === "H265";
}
Loading