diff --git a/locales/en.json b/locales/en.json index 4394639a5..1a5a17c8f 100644 --- a/locales/en.json +++ b/locales/en.json @@ -403,6 +403,9 @@ "encoder": "Encoder", "colorDepth": "Color Depth", "maxBitrate": "Max Bitrate", + "recordingBitrate": "Recording Bitrate", + "recordingBitrateHint": "Controls local recording video bitrate. Auto lets the browser choose.", + "customBitrate": "Custom", "appliesAfterRestart": "Applies after app restart.", "requiresH265OrAv1": "This mode requires H265 or AV1. Codec will be auto-switched.", "testing": "Testing...", diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts index 04e081b6c..f62c92fb1 100644 --- a/opennow-stable/src/main/settings.ts +++ b/opennow-stable/src/main/settings.ts @@ -34,6 +34,8 @@ export interface Settings { fps: number; /** Maximum bitrate in Mbps (cap at 150) */ maxBitrateMbps: number; + /** Recording video bitrate in Mbps (null = MediaRecorder auto, cap at 200) */ + recordingBitrateMbps: number | null; /** Stream client implementation to use for new sessions */ streamClientMode: StreamClientMode; /** Native streamer backend preference for new native sessions */ @@ -155,12 +157,24 @@ function normalizeAppAccentColor(raw: unknown): AppAccentColor { return APP_ACCENT_COLORS.has(raw as AppAccentColor) ? (raw as AppAccentColor) : "green"; } +function normalizeRecordingBitrateMbps(raw: unknown): number | null { + if (raw === null || raw === undefined) { + return null; + } + const value = Number(raw); + if (!Number.isFinite(value)) { + return null; + } + return Math.max(1, Math.min(200, Math.round(value))); +} + const DEFAULT_SETTINGS: Settings = { resolution: "1920x1080", aspectRatio: "16:9", posterSizeScale: 1, fps: 60, maxBitrateMbps: 75, + recordingBitrateMbps: null, streamClientMode: "web", nativeStreamerBackend: "gstreamer", nativeVideoBackend: "auto", @@ -271,6 +285,11 @@ export class SettingsManager { } merged.mouseAcceleration = Math.max(1, Math.min(150, Math.round(merged.mouseAcceleration))); + const recordingBitrateBefore = merged.recordingBitrateMbps; + merged.recordingBitrateMbps = normalizeRecordingBitrateMbps(merged.recordingBitrateMbps); + if (merged.recordingBitrateMbps !== recordingBitrateBefore) { + migrated = true; + } if (migrated) { writeFileSync(this.settingsPath, JSON.stringify(merged, null, 2), "utf-8"); } @@ -321,6 +340,12 @@ export class SettingsManager { migrated = true; } + const recordingBitrate = normalizeRecordingBitrateMbps(settings.recordingBitrateMbps); + if (settings.recordingBitrateMbps !== recordingBitrate) { + settings.recordingBitrateMbps = recordingBitrate; + migrated = true; + } + return migrated; } diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 239576833..407988b23 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -243,6 +243,7 @@ export function App(): JSX.Element { posterSizeScale: 1, fps: 60, maxBitrateMbps: 75, + recordingBitrateMbps: null, streamClientMode: "web", nativeStreamerBackend: "gstreamer", nativeVideoBackend: "auto", @@ -3479,6 +3480,7 @@ export function App(): JSX.Element { isFullscreen={sessionFullscreen || !!document.fullscreenElement} isConnecting={streamStatus === "connecting"} isStreaming={isStreaming} + recordingBitrateMbps={settings.recordingBitrateMbps} gameTitle={streamingGame?.title ?? t("app.labels.game")} platformStore={streamingStore ?? undefined} onToggleFullscreen={() => { diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx index 9247ed0dd..7f71ee7a7 100644 --- a/opennow-stable/src/renderer/src/components/SettingsPage.tsx +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -2262,6 +2262,44 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, /> +
+
+ + + {settings.recordingBitrateMbps === null + ? t("app.labels.auto") + : `${settings.recordingBitrateMbps} Mbps`} + +
+
+ + +
+ handleChange("recordingBitrateMbps", parseInt(e.target.value, 10))} + /> + {t("settings.video.recordingBitrateHint")} +
+