diff --git a/locales/en.json b/locales/en.json index 3c203d139..7e1690749 100644 --- a/locales/en.json +++ b/locales/en.json @@ -3453,7 +3453,7 @@ "s_onboarding_51085de55c": "You're set.", "s_onboarding_b8e418c4eb": "Your preferences will be saved when you finish setup. You can revisit them in Settings.", "s_onboarding_113143c308": "macOS ONLY", - "s_onboarding_23a5a729cc": "%1 / 200 Mbps", + "s_onboarding_23a5a729cc": "%1 / 100 Mbps", "s_onboarding_6b89662ed2": "%1 Mbps", "s_onboarding_e9037adc0f": "%1 Mbps maximum", "s_onboarding_a77247ed92": "%1 is a beta. We will say more about that right after you sign in.", diff --git a/native/opennow-core/src/settings.rs b/native/opennow-core/src/settings.rs index 077872974..4044c7672 100644 --- a/native/opennow-core/src/settings.rs +++ b/native/opennow-core/src/settings.rs @@ -789,13 +789,13 @@ fn clamp_integer( fn clamp_bitrate_mbps(values: &mut Map) { // 0.22 Mbps is 220 kbps. Whole numbers stay integers so existing settings - // and the 10–200 Mbps slider keep their previous JSON shape. + // and the bitrate slider keep their previous JSON shape. let raw = values .get("maxBitrateMbps") .and_then(Value::as_f64) .filter(|value| value.is_finite()) .unwrap_or(75.0); - let value = (raw.clamp(0.22, 200.0) * 100.0).round() / 100.0; + let value = (raw.clamp(0.22, 100.0) * 100.0).round() / 100.0; let stored = if (value - value.round()).abs() < 1e-9 { Value::from(value.round() as i64) } else { @@ -2417,6 +2417,31 @@ mod tests { fs::remove_dir_all(directory).unwrap(); } + #[test] + fn legacy_bitrate_above_limit_is_clamped_on_load_and_preserved_on_save() { + let directory = tempfile::tempdir().unwrap(); + fs::write( + directory.path().join("settings.json"), + json!({ + "maxBitrateMbps": 200, + "onboardingCompleted": true, + "qtConsoleModePolicyVersion": 1, + "legacySetting": "retained" + }) + .to_string(), + ) + .unwrap(); + + let mut store = SettingsStore::load(Some(directory.path().to_path_buf())).unwrap(); + assert_eq!(store.all()["maxBitrateMbps"], json!(100)); + assert_eq!(store.set("maxBitrateMbps", json!(100)).unwrap(), json!(100)); + let persisted: Value = + serde_json::from_slice(&fs::read(directory.path().join("settings.json")).unwrap()) + .unwrap(); + assert_eq!(persisted["maxBitrateMbps"], json!(100)); + assert_eq!(persisted["legacySetting"], json!("retained")); + } + #[test] fn persists_and_normalizes_settings() { let unique = SystemTime::now() @@ -2481,13 +2506,13 @@ mod tests { assert_eq!(store.set("fps", json!(999)).unwrap(), json!(360)); assert_eq!(store.set("fps", json!(360)).unwrap(), json!(360)); assert_eq!(store.set("fps", json!(240)).unwrap(), json!(240)); - assert_eq!(store.set("maxBitrateMbps", json!(200)).unwrap(), json!(200)); + assert_eq!(store.set("maxBitrateMbps", json!(100)).unwrap(), json!(100)); let low_bitrate = store.set("maxBitrateMbps", json!(0.22)).unwrap(); assert!((low_bitrate.as_f64().unwrap() - 0.22).abs() < 0.001); let clamped_bitrate = store.set("maxBitrateMbps", json!(0.1)).unwrap(); assert!((clamped_bitrate.as_f64().unwrap() - 0.22).abs() < 0.001); assert_eq!(store.set("maxBitrateMbps", json!(27)).unwrap(), json!(27)); - assert_eq!(store.set("maxBitrateMbps", json!(200)).unwrap(), json!(200)); + assert_eq!(store.set("maxBitrateMbps", json!(200)).unwrap(), json!(100)); assert_eq!( store.set("launchInConsoleMode", json!(false)).unwrap(), json!(false) @@ -2510,7 +2535,7 @@ mod tests { ); let loaded = SettingsStore::load(Some(directory.clone())).unwrap(); assert_eq!(loaded.all()["fps"], json!(240)); - assert_eq!(loaded.all()["maxBitrateMbps"], json!(200)); + assert_eq!(loaded.all()["maxBitrateMbps"], json!(100)); assert_eq!(loaded.all()["saveBandwidth"], json!(true)); assert_eq!(loaded.all()["launchInConsoleMode"], json!(false)); assert_eq!(loaded.all()["reducedMotion"], json!(true)); diff --git a/native/opennow-streamer/crates/opennow-streamer-core/src/nvst_rtsp.rs b/native/opennow-streamer/crates/opennow-streamer-core/src/nvst_rtsp.rs index a52d6ed29..5a1027d51 100644 --- a/native/opennow-streamer/crates/opennow-streamer-core/src/nvst_rtsp.rs +++ b/native/opennow-streamer/crates/opennow-streamer-core/src/nvst_rtsp.rs @@ -37,7 +37,7 @@ const CONTROL_PING_EXPIRY: Duration = Duration::from_secs(5); const CONTROL_IO_TIMEOUT: Duration = Duration::from_millis(100); const MAX_REQUEST_RESPONSE_BYTES: usize = 4 * 1024 * 1024; const MAX_CONTROL_RESPONSE_BYTES: usize = 64 * 1024; -const MAX_STREAM_BITRATE_MBPS: u64 = 200; +const MAX_STREAM_BITRATE_MBPS: u64 = 100; // GeForce NOW 2.0.87.131 reports video[0].timeoutLengthMs=8000 and // video[0].sendFrameTimeoutMs=7000. Waiting sixty seconds left a dead Mjolnir media leg on screen // while audio/control remained alive; use the official receiver timeout so the existing bounded @@ -2755,7 +2755,7 @@ mod tests { } #[test] - fn owned_announce_preserves_the_configured_200_mbps_ceiling() { + fn owned_announce_clamps_legacy_200_mbps_request_to_100_mbps() { let mut value = context(); value.settings["maxBitrateMbps"] = json!(200); let sdp = build_announce( @@ -2776,9 +2776,9 @@ mod tests { qos_timings_v5: false, }, ); - assert!(sdp.contains("a=x-nv-video[0].initialBitrateKbps:200000")); - assert!(sdp.contains("a=x-nv-video[0].initialPeakBitrateKbps:200000")); - assert!(sdp.contains("a=x-nv-vqos[0].bw.maximumBitrateKbps:200000")); + assert!(sdp.contains("a=x-nv-video[0].initialBitrateKbps:100000")); + assert!(sdp.contains("a=x-nv-video[0].initialPeakBitrateKbps:100000")); + assert!(sdp.contains("a=x-nv-vqos[0].bw.maximumBitrateKbps:100000")); assert!(sdp.contains("a=x-nv-vqos[0].bw.minimumBitrateKbps:1000")); } diff --git a/opennow-qt/qml/desktop/onboarding/DesktopOnboardingPicture.qml b/opennow-qt/qml/desktop/onboarding/DesktopOnboardingPicture.qml index 79ea267fe..5c78260b2 100644 --- a/opennow-qt/qml/desktop/onboarding/DesktopOnboardingPicture.qml +++ b/opennow-qt/qml/desktop/onboarding/DesktopOnboardingPicture.qml @@ -246,7 +246,7 @@ Column { BitrateSlider { objectName: "onboardingBitrate"; accessibleName: qsTr("Bitrate") trackWidth: Math.min(DesktopTokens.px(220), Math.max(DesktopTokens.px(100), bitrateRow.width - DesktopTokens.px(470))) - from: 0.22; to: 200; stepSize: 0.01; suffix: qsTr(" Mbps") + from: 0.22; to: 100; stepSize: 0.01; suffix: qsTr(" Mbps") value: Number(root.settings.maxBitrateMbps ?? 75) onMoved: value => root.store.setOnboardingSetting("maxBitrateMbps", Math.round(value * 100) / 100) } @@ -322,11 +322,11 @@ Column { RowLayout { width: parent.width Copy { text: qsTr("Bitrate limit"); color: Theme.label; font.pixelSize: DesktopTokens.px(13); font.weight: Font.ExtraBold; Layout.fillWidth: true } - Copy { text: qsTr("%1 / 200 Mbps").arg(root.settings.maxBitrateMbps ?? 75); font.family: Theme.monoFont; font.pixelSize: DesktopTokens.px(11) } + Copy { text: qsTr("%1 / 100 Mbps").arg(root.settings.maxBitrateMbps ?? 75); font.family: Theme.monoFont; font.pixelSize: DesktopTokens.px(11) } } Rectangle { width: parent.width; height: DesktopTokens.px(4); radius: height / 2; color: DesktopTokens.raised - Rectangle { width: parent.width * Math.max(0, Math.min(1, Number(root.settings.maxBitrateMbps ?? 75) / 200)); height: parent.height; radius: height / 2; color: root.mint } + Rectangle { width: parent.width * Math.max(0, Math.min(1, Number(root.settings.maxBitrateMbps ?? 75) / 100)); height: parent.height; radius: height / 2; color: root.mint } } Copy { width: parent.width; text: qsTr("Requested maximum, not a network test. Actual quality depends on your membership, device and connection.") } } diff --git a/opennow-qt/qml/desktop/settings/pages/DesktopSettingsStreamPage.qml b/opennow-qt/qml/desktop/settings/pages/DesktopSettingsStreamPage.qml index 8cbbf97b8..f60dc17e9 100644 --- a/opennow-qt/qml/desktop/settings/pages/DesktopSettingsStreamPage.qml +++ b/opennow-qt/qml/desktop/settings/pages/DesktopSettingsStreamPage.qml @@ -34,7 +34,7 @@ Column { DesktopSettingsRow { width: parent.width; paperStyle: true; glyph: "wave"; title: qsTr("Bitrate"); description: qsTr("Maximum requested bitrate") DesktopSettingsSlider { - from: 0.22; to: 200; stepSize: 0.01; decimals: 2 + from: 0.22; to: 100; stepSize: 0.01; decimals: 2 value: Number(page.settingsScreen.valueSetting("maxBitrateMbps",75)); suffix: " Mbps" onCommitted: value => page.settingsScreen.setSetting("maxBitrateMbps", Math.round(value * 100) / 100) } diff --git a/opennow-qt/qml/overlays/QuickSettingsOverlay.qml b/opennow-qt/qml/overlays/QuickSettingsOverlay.qml index 4ddf08e45..e779d108b 100644 --- a/opennow-qt/qml/overlays/QuickSettingsOverlay.qml +++ b/opennow-qt/qml/overlays/QuickSettingsOverlay.qml @@ -12,7 +12,7 @@ FocusScope { Accessible.role: Accessible.Pane Accessible.name: qsTr("Quick settings") - readonly property var bitrates: [25, 50, 75, 100, 150, 200] + readonly property var bitrates: [25, 50, 75, 100] readonly property var controllers: ControllerInput.controllers || [] readonly property string tier: String(ShellStore.subscription && ShellStore.subscription.membershipTier || qsTr("Ready")) @@ -234,7 +234,7 @@ FocusScope { title: qsTr("Max bitrate") value: Number(ShellStore.settings.maxBitrateMbps || 75) + " Mbps" sliderVisible: true - sliderProgress: Number(ShellStore.settings.maxBitrateMbps || 75) / 200 + sliderProgress: Number(ShellStore.settings.maxBitrateMbps || 75) / 100 KeyNavigation.up: regionRow; KeyNavigation.down: statsRow onTriggered: root.nextBitrate() } diff --git a/opennow-qt/qml/screens/SettingsScreen.qml b/opennow-qt/qml/screens/SettingsScreen.qml index 5daca1b36..0999d28d1 100644 --- a/opennow-qt/qml/screens/SettingsScreen.qml +++ b/opennow-qt/qml/screens/SettingsScreen.qml @@ -305,7 +305,7 @@ FocusScope { choice("Fallback codec", "Used when the preferred codec isn't offered by the rig", "fallbackCodec", ["auto","h264","h265"], ["Auto","H.264","H.265"], "dropdown", disabledCodecs), descriptorChoice(qsTr("Color quality"), ShellStore.settingsOwnerState.colorDescription, "colorQuality", ShellStore.settingsOwnerState.colorQualityItems), {t:qsTr("HDR"), d:hdrDescription, v:Boolean(settings.enableHdr) ? qsTr("On") : qsTr("Off"), key:"enableHdr", values:[false,true], labels:[qsTr("Off"),qsTr("On")], control:"segments", selectedIndex:Boolean(settings.enableHdr) ? 1 : 0, disabledValues:(hdrAvailable && hdrTierOk) ? [] : [true]}, - {t:"Max bitrate", d:"Maximum requested stream bitrate", v:Number(settings.maxBitrateMbps || 75) + " Mbps", key:"maxBitrateMbps", values:[0.22,1,5,10,25,50,75,100,150,200], labels:["0.22 Mbps","1 Mbps","5 Mbps","10 Mbps","25 Mbps","50 Mbps","75 Mbps","100 Mbps","150 Mbps","200 Mbps"], control:"slider", sliderPercent:Number(settings.maxBitrateMbps || 75) / 200}, + {t:"Max bitrate", d:"Maximum requested stream bitrate", v:Number(settings.maxBitrateMbps || 75) + " Mbps", key:"maxBitrateMbps", values:[0.22,1,5,10,25,50,75,100], labels:["0.22 Mbps","1 Mbps","5 Mbps","10 Mbps","25 Mbps","50 Mbps","75 Mbps","100 Mbps"], control:"slider", sliderPercent:Number(settings.maxBitrateMbps || 75) / 100}, toggle(qsTr("Save bandwidth"), qsTr("Lets the server trade resolution and image quality for a steadier frame rate when your connection cannot sustain the selected profile. Off requests no dynamic adjustment. Applies to new sessions."), "saveBandwidth"), {t:qsTr("Frame generation (Experimental)"), d:qsTr("Targets 120 displayed FPS from a 60 FPS stream. Requires a fast GPU and 120 Hz display; adds latency and artifacts."), v:frameGeneration ? qsTr("2×") : qsTr("Off"), key:"frameGeneration", values:["off","2x"], labels:[qsTr("Off"),qsTr("2×")], control:"segments", selectedIndex:frameGeneration ? 1 : 0}, choice(qsTr("Upscaling"), Qt.platform.os === "osx"