From 7f701702008a845c6283fb3d9ab56c71935cdc22 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 21:49:12 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(opennow):=20route=20optimization=20?= =?UTF-8?q?=E2=80=94=20region=20wiring,=20ICE=20policy,=20richer=20pings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use Settings.region as CloudMatch streaming base URL when set (was unused). - Add Connection settings: WebRTC ICE transport policy (all vs relay-only). - Region latency probe: 5 TCP samples with min/max/jitter; show jitter in UI. - Clarify that region choice optimizes data center routing, not a VPN booster. Co-authored-by: Zortos --- opennow-stable/package-lock.json | 65 +++++++--- opennow-stable/src/main/index.ts | 55 +++++---- opennow-stable/src/main/settings.ts | 13 +- opennow-stable/src/renderer/src/App.tsx | 33 ++++- .../renderer/src/components/SettingsPage.tsx | 114 +++++++++++++++--- .../src/renderer/src/gfn/webrtcClient.ts | 15 +++ opennow-stable/src/renderer/src/styles.css | 6 + opennow-stable/src/shared/gfn.ts | 15 +++ 8 files changed, 254 insertions(+), 62 deletions(-) diff --git a/opennow-stable/package-lock.json b/opennow-stable/package-lock.json index 1afd9256..3234c1c1 100644 --- a/opennow-stable/package-lock.json +++ b/opennow-stable/package-lock.json @@ -1,12 +1,12 @@ { "name": "opennow-stable", - "version": "0.2.4", + "version": "0.3.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "opennow-stable", - "version": "0.2.4", + "version": "0.3.1", "dependencies": { "lucide-react": "^0.563.0", "react": "^19.2.4", @@ -58,7 +58,6 @@ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1831,7 +1830,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -1973,7 +1971,6 @@ "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -2140,6 +2137,7 @@ "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^2.1.0", "async": "^3.2.4", @@ -2159,6 +2157,7 @@ "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.1.4", "graceful-fs": "^4.2.0", @@ -2181,6 +2180,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2196,7 +2196,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/archiver-utils/node_modules/string_decoder": { "version": "1.1.1", @@ -2204,6 +2205,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -2401,7 +2403,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -2911,6 +2912,7 @@ "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "buffer-crc32": "^0.2.13", "crc32-stream": "^4.0.2", @@ -3161,6 +3163,7 @@ "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "crc32": "bin/crc32.njs" }, @@ -3174,6 +3177,7 @@ "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "crc-32": "^1.2.0", "readable-stream": "^3.4.0" @@ -3414,7 +3418,6 @@ "integrity": "sha512-NoXo6Liy2heSklTI5OIZbCgXC1RzrDQsZkeEwXhdOro3FT1VBOvbubvscdPnjVuQ4AMwwv61oaH96AbiYg9EnQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "app-builder-lib": "25.1.8", "builder-util": "25.1.7", @@ -3610,6 +3613,7 @@ "integrity": "sha512-2ntkJ+9+0GFP6nAISiMabKt6eqBB0kX1QqHNWFWAXgi0VULKGisM46luRFpIBiU3u/TDmhZMM8tzvo2Abn3ayg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "app-builder-lib": "25.1.8", "archiver": "^5.3.1", @@ -3623,6 +3627,7 @@ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3638,6 +3643,7 @@ "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "universalify": "^2.0.0" }, @@ -3651,6 +3657,7 @@ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">= 10.0.0" } @@ -4121,7 +4128,8 @@ "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/fs-extra": { "version": "8.1.0", @@ -4760,7 +4768,8 @@ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/isbinaryfile": { "version": "5.0.7", @@ -4917,6 +4926,7 @@ "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "readable-stream": "^2.0.5" }, @@ -4930,6 +4940,7 @@ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -4945,7 +4956,8 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lazystream/node_modules/string_decoder": { "version": "1.1.1", @@ -4953,6 +4965,7 @@ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.1.0" } @@ -4969,35 +4982,40 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.difference": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.flatten": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/lodash.union": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/log-symbols": { "version": "4.1.0", @@ -5526,6 +5544,7 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -5802,7 +5821,8 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/progress": { "version": "2.0.3", @@ -5874,7 +5894,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -5935,6 +5954,7 @@ "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", "dev": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "minimatch": "^5.1.0" } @@ -5944,7 +5964,8 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/readdir-glob/node_modules/brace-expansion": { "version": "2.0.2", @@ -5952,6 +5973,7 @@ "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -5962,6 +5984,7 @@ "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, "license": "ISC", + "peer": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -6525,6 +6548,7 @@ "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -6780,7 +6804,6 @@ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -7022,6 +7045,7 @@ "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "archiver-utils": "^3.0.4", "compress-commons": "^4.1.2", @@ -7037,6 +7061,7 @@ "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "glob": "^7.2.3", "graceful-fs": "^4.2.0", diff --git a/opennow-stable/src/main/index.ts b/opennow-stable/src/main/index.ts index b3c3e407..3ff0edf0 100644 --- a/opennow-stable/src/main/index.ts +++ b/opennow-stable/src/main/index.ts @@ -1091,45 +1091,58 @@ function registerIpcHandlers(): void { }); } - // Ping regions IPC handler - uses TCP connection timing for accurate latency measurement - // Runs 3 tests and averages the results + // Ping regions IPC handler — TCP connect timing (like ExitLag-style route probes: latency + stability) ipcMain.handle(IPC_CHANNELS.PING_REGIONS, async (_event, regions: StreamRegion[]): Promise => { const pingPromises = regions.map(async (region) => { try { const url = new URL(region.url); const hostname = url.hostname; - const port = url.protocol === 'https:' ? 443 : 80; - + const port = url.protocol === "https:" ? 443 : 80; + const validPings: number[] = []; - - // Run 3 ping tests - for (let i = 0; i < 3; i++) { + const sampleCount = 5; + for (let i = 0; i < sampleCount; i++) { const pingMs = await tcpPing(hostname, port, 3000); if (pingMs !== null) { validPings.push(pingMs); } } - - // Calculate average of successful pings + if (validPings.length > 0) { - const avgPing = Math.round(validPings.reduce((a, b) => a + b, 0) / validPings.length); - return { url: region.url, pingMs: avgPing }; - } else { - return { - url: region.url, - pingMs: null, - error: 'All ping tests failed' + const sum = validPings.reduce((a, b) => a + b, 0); + const avgPing = Math.round(sum / validPings.length); + const minMs = Math.min(...validPings); + const maxMs = Math.max(...validPings); + let jitterMs: number | undefined; + if (validPings.length >= 2) { + const mean = sum / validPings.length; + const variance = + validPings.reduce((acc, v) => acc + (v - mean) * (v - mean), 0) / validPings.length; + jitterMs = Math.round(Math.sqrt(variance)); + } + return { + url: region.url, + pingMs: avgPing, + minMs, + maxMs, + jitterMs, + samples: validPings.length, }; } + return { + url: region.url, + pingMs: null, + error: "All ping tests failed", + }; } catch { - return { - url: region.url, - pingMs: null, - error: 'Invalid URL' + return { + url: region.url, + pingMs: null, + error: "Invalid URL", }; } }); - + return Promise.all(pingPromises); }); diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts index c45aff82..fd6d3f6b 100644 --- a/opennow-stable/src/main/settings.ts +++ b/opennow-stable/src/main/settings.ts @@ -1,7 +1,15 @@ import { app } from "electron"; import { join } from "node:path"; import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs"; -import type { VideoCodec, ColorQuality, VideoAccelerationPreference, MicrophoneMode, GameLanguage, AspectRatio } from "@shared/gfn"; +import type { + VideoCodec, + ColorQuality, + VideoAccelerationPreference, + MicrophoneMode, + GameLanguage, + AspectRatio, + IceTransportPolicy, +} from "@shared/gfn"; export interface Settings { /** Video resolution (e.g., "1920x1080") */ @@ -67,6 +75,8 @@ export interface Settings { gameLanguage: GameLanguage; /** Experimental request for Low Latency, Low Loss, Scalable throughput on new sessions */ enableL4S: boolean; + /** WebRTC ICE policy: "all" (default) or "relay" (TURN-only) */ + iceTransportPolicy: IceTransportPolicy; } const defaultStopShortcut = "Ctrl+Shift+Q"; @@ -108,6 +118,7 @@ const DEFAULT_SETTINGS: Settings = { windowHeight: 900, gameLanguage: "en_US", enableL4S: false, + iceTransportPolicy: "all", }; export class SettingsManager { diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index 2032ab1c..a0c4078f 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -426,6 +426,7 @@ export function App(): JSX.Element { windowHeight: 900, gameLanguage: "en_US", enableL4S: false, + iceTransportPolicy: "all", }); const [settingsLoaded, setSettingsLoaded] = useState(false); const [regions, setRegions] = useState([]); @@ -793,8 +794,13 @@ export function App(): JSX.Element { }, [providers, providerIdpId, authSession]); const effectiveStreamingBaseUrl = useMemo(() => { - return selectedProvider?.streamingServiceUrl ?? ""; - }, [selectedProvider]); + const base = selectedProvider?.streamingServiceUrl?.trim() ?? ""; + const preferred = settings.region?.trim() ?? ""; + if (preferred && preferred.startsWith("https://")) { + return preferred.endsWith("/") ? preferred : `${preferred}/`; + } + return base; + }, [selectedProvider, settings.region]); const loadSubscriptionInfo = useCallback( async (session: AuthSession): Promise => { @@ -1239,6 +1245,7 @@ export function App(): JSX.Element { microphoneDeviceId: settings.microphoneDeviceId || undefined, mouseSensitivity: settings.mouseSensitivity, mouseAcceleration: settings.mouseAcceleration, + iceTransportPolicy: settings.iceTransportPolicy, onLog: (line: string) => console.log(`[WebRTC] ${line}`), onStats: (stats) => diagnosticsStore.set(stats), onEscHoldProgress: (visible, progress) => { @@ -1298,7 +1305,20 @@ export function App(): JSX.Element { }); return () => unsubscribe(); - }, [resetLaunchRuntime, settings]); + }, [ + resetLaunchRuntime, + settings.microphoneMode, + settings.microphoneDeviceId, + settings.mouseSensitivity, + settings.mouseAcceleration, + settings.codec, + settings.colorQuality, + settings.resolution, + settings.fps, + settings.maxBitrateMbps, + settings.iceTransportPolicy, + (settings as { autoFullScreen?: boolean }).autoFullScreen, + ]); // Save settings when changed const updateSetting = useCallback(async (key: K, value: Settings[K]) => { @@ -1328,6 +1348,13 @@ export function App(): JSX.Element { // ignore } } + if (key === "iceTransportPolicy") { + try { + (clientRef.current as any)?.setIceTransportPolicy?.(value); + } catch { + // ignore + } + } }, [settingsLoaded]); const handleMouseSensitivityChange = useCallback((value: number) => { diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx index cc8df624..a8d93c09 100644 --- a/opennow-stable/src/renderer/src/components/SettingsPage.tsx +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -307,17 +307,30 @@ function loadStoredCodecResults(): CodecTestResult[] | null { interface PingCacheEntry { url: string; pingMs: number | null; + minMs?: number; + maxMs?: number; + jitterMs?: number; + samples?: number; + error?: string; } -function loadStoredPingResults(): Map | null { +function loadStoredPingResults(): Map | null { try { const raw = window.sessionStorage.getItem(PING_RESULTS_STORAGE_KEY); if (!raw) return null; const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return null; - const results = new Map(); + const results = new Map(); for (const entry of parsed as PingCacheEntry[]) { - results.set(entry.url, entry.pingMs); + results.set(entry.url, { + url: entry.url, + pingMs: entry.pingMs, + minMs: entry.minMs, + maxMs: entry.maxMs, + jitterMs: entry.jitterMs, + samples: entry.samples, + error: entry.error, + }); } return results; } catch { @@ -325,11 +338,19 @@ function loadStoredPingResults(): Map | null { } } -function saveStoredPingResults(results: Map): void { +function saveStoredPingResults(results: Map): void { try { const entries: PingCacheEntry[] = []; - results.forEach((pingMs, url) => { - entries.push({ url, pingMs }); + results.forEach((r, url) => { + entries.push({ + url, + pingMs: r.pingMs, + minMs: r.minMs, + maxMs: r.maxMs, + jitterMs: r.jitterMs, + samples: r.samples, + error: r.error, + }); }); window.sessionStorage.setItem(PING_RESULTS_STORAGE_KEY, JSON.stringify(entries)); } catch { @@ -512,6 +533,10 @@ async function testCodecSupport(): Promise { return results; } +function getRegionPingMs(map: Map, url: string): number | null | undefined { + return map.get(url)?.pingMs; +} + /* ── Component ────────────────────────────────────────────────────── */ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPageProps): JSX.Element { @@ -527,13 +552,14 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag // Region ping state const initialPingResults = useMemo(() => loadStoredPingResults(), []); - const [pingResults, setPingResults] = useState>(initialPingResults ?? new Map()); + const [pingResults, setPingResults] = useState>(initialPingResults ?? new Map()); const [isPinging, setIsPinging] = useState(false); const [bestRegionUrl, setBestRegionUrl] = useState(() => { if (!initialPingResults) return null; let bestUrl: string | null = null; let bestPing = Infinity; - initialPingResults.forEach((pingMs, url) => { + initialPingResults.forEach((entry, url) => { + const pingMs = entry.pingMs; if (pingMs !== null && pingMs < bestPing) { bestPing = pingMs; bestUrl = url; @@ -547,12 +573,12 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag setIsPinging(true); try { const results = await window.openNow.pingRegions(regions); - const pingMap = new Map(); + const pingMap = new Map(); let bestUrl: string | null = null; let bestPing = Infinity; for (const result of results) { - pingMap.set(result.url, result.pingMs); + pingMap.set(result.url, result); if (result.pingMs !== null && result.pingMs < bestPing) { bestPing = result.pingMs; bestUrl = result.url; @@ -795,8 +821,8 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag // Sort by ping (best first), then by name filtered.sort((a, b) => { - const pingA = pingResults.get(a.url); - const pingB = pingResults.get(b.url); + const pingA = getRegionPingMs(pingResults, a.url); + const pingB = getRegionPingMs(pingResults, b.url); // If both have ping results, sort by ping if (pingA !== undefined && pingB !== undefined && pingA !== null && pingB !== null) { @@ -958,7 +984,7 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag {!settings.region && bestRegionUrl && ( (() => { const bestRegion = regions.find(r => r.url === bestRegionUrl); - const pingValue = pingResults.get(bestRegionUrl); + const pingValue = getRegionPingMs(pingResults, bestRegionUrl); if (bestRegion && pingValue !== undefined && pingValue !== null) { return ( @@ -971,7 +997,7 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag )} {settings.region && ( (() => { - const pingValue = pingResults.get(settings.region); + const pingValue = getRegionPingMs(pingResults, settings.region); if (pingValue !== undefined && pingValue !== null) { return ( @@ -1040,7 +1066,7 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag Auto (Best) {bestRegionUrl && (() => { const bestRegion = regions.find(r => r.url === bestRegionUrl); - const bestPing = pingResults.get(bestRegionUrl); + const bestPing = getRegionPingMs(pingResults, bestRegionUrl); if (bestRegion && bestPing !== undefined && bestPing !== null) { return ( @@ -1077,15 +1103,31 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag ... ) : ( (() => { - const pingValue = pingResults.get(region.url); + const pr = pingResults.get(region.url); + const pingValue = pr?.pingMs; + const jitterHint = + pr?.jitterMs != null || pr?.minMs != null || pr?.maxMs != null + ? [ + pr.jitterMs != null ? `Jitter ~${pr.jitterMs}ms` : null, + pr.minMs != null && pr.maxMs != null ? `${pr.minMs}–${pr.maxMs}ms` : null, + ] + .filter(Boolean) + .join(" · ") + : undefined; if (pingValue === undefined) { return -; } else if (pingValue === null) { return Failed; } else { return ( - + {pingValue}ms + {pr?.jitterMs != null && pr.jitterMs > 0 ? ( + ±{pr.jitterMs} + ) : null} ); } @@ -1103,6 +1145,44 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag )} + + Choosing a region (or Auto) sets which GeForce NOW data center CloudMatch uses. This is the main in-app way to optimize latency; it is not a VPN or custom routing product like ExitLag. + + + + + {/* ── Connection (WebRTC) ───────────────────────── */} +
+
+

Connection

+
+
+
+ +
+ {( + [ + { value: "all" as const, label: "Default", title: "Try direct and relay candidates (typical home networks)" }, + { value: "relay" as const, label: "Relay only", title: "Force TURN — can help with VPNs or strict NAT (may add latency)" }, + ] as const + ).map((opt) => ( + + ))} +
+ + Controls WebRTC candidate gathering. This is not a full “game booster” VPN; it only affects how OpenNOW connects to GeForce NOW. + +
diff --git a/opennow-stable/src/renderer/src/gfn/webrtcClient.ts b/opennow-stable/src/renderer/src/gfn/webrtcClient.ts index 5c037f31..e1d8ef2c 100644 --- a/opennow-stable/src/renderer/src/gfn/webrtcClient.ts +++ b/opennow-stable/src/renderer/src/gfn/webrtcClient.ts @@ -5,6 +5,7 @@ import type { SessionInfo, VideoCodec, MicrophoneMode, + IceTransportPolicy, } from "@shared/gfn"; import { @@ -208,6 +209,8 @@ interface ClientOptions { onEscHoldProgress?: (visible: boolean, progress: number) => void; onTimeWarning?: (warning: StreamTimeWarning) => void; onMicStateChange?: (state: MicStateChange) => void; + /** ICE gathering policy — "relay" forces TURN-only (VPN / strict NAT). Default "all". */ + iceTransportPolicy?: IceTransportPolicy; } function timestampUs(sourceTimestampMs?: number): bigint { @@ -565,6 +568,7 @@ export class GfnWebRtcClient { private videoDecodeStallWarningSent = false; private serverRegion = ""; private gpuType = ""; + private iceTransportPolicy: IceTransportPolicy = "all"; private diagnostics: StreamDiagnostics = { connectionState: "closed", @@ -608,6 +612,7 @@ export class GfnWebRtcClient { options.audioElement.muted = true; this.mouseSensitivity = options.mouseSensitivity ?? 1; this.mouseAccelerationPercent = Math.max(1, Math.min(150, Math.round(options.mouseAcceleration ?? 1))); + this.iceTransportPolicy = options.iceTransportPolicy === "relay" ? "relay" : "all"; // Configure video element for lowest latency playback this.configureVideoElementForLowLatency(options.videoElement); @@ -657,6 +662,15 @@ export class GfnWebRtcClient { } /** Update mouse sensitivity multiplier at runtime. */ + public setIceTransportPolicy(policy: IceTransportPolicy): void { + const next = policy === "relay" ? "relay" : "all"; + if (this.iceTransportPolicy === next) { + return; + } + this.iceTransportPolicy = next; + this.log(`ICE transport policy updated to ${next} (applies on next stream)`); + } + public setMouseSensitivity(value: number): void { const v = Number.isFinite(value) ? value : 1; this.mouseSensitivity = Math.max(0.01, v); @@ -3291,6 +3305,7 @@ export class GfnWebRtcClient { iceServers: toRtcIceServers(session.iceServers), bundlePolicy: "max-bundle", rtcpMuxPolicy: "require", + iceTransportPolicy: this.iceTransportPolicy, }; const pc = new RTCPeerConnection(rtcConfig); diff --git a/opennow-stable/src/renderer/src/styles.css b/opennow-stable/src/renderer/src/styles.css index 05501695..3927bacc 100644 --- a/opennow-stable/src/renderer/src/styles.css +++ b/opennow-stable/src/renderer/src/styles.css @@ -2805,6 +2805,12 @@ button.game-card-store-chip.active:hover { background: rgba(239, 68, 68, 0.15); } +.region-ping-jitter { + font-size: 0.72em; + opacity: 0.85; + font-weight: 500; +} + .region-ping-loading { color: var(--ink-muted); font-style: italic; diff --git a/opennow-stable/src/shared/gfn.ts b/opennow-stable/src/shared/gfn.ts index 66465e9a..38be44db 100644 --- a/opennow-stable/src/shared/gfn.ts +++ b/opennow-stable/src/shared/gfn.ts @@ -35,6 +35,9 @@ export function colorQualityIs10Bit(cq: ColorQuality): boolean { export type MicrophoneMode = "disabled" | "push-to-talk" | "voice-activity"; export type AspectRatio = "16:9" | "16:10" | "21:9" | "32:9"; +/** WebRTC ICE candidate gathering policy — "relay" can help when only TURN works reliably (e.g. VPN / strict NAT). */ +export type IceTransportPolicy = "all" | "relay"; + export interface Settings { resolution: string; aspectRatio: AspectRatio; @@ -72,6 +75,10 @@ export interface Settings { gameLanguage: GameLanguage; /** Experimental request for Low Latency, Low Loss, Scalable throughput on new sessions */ enableL4S: boolean; + /** + * WebRTC ICE transport policy for streaming. "relay" uses TURN only (often needed with VPNs or strict NAT). + */ + iceTransportPolicy: IceTransportPolicy; } export interface LoginProvider { @@ -180,6 +187,14 @@ export interface PingResult { url: string; pingMs: number | null; error?: string; + /** Lowest TCP connect time in the sample (ms), when measured */ + minMs?: number; + /** Highest TCP connect time in the sample (ms), when measured */ + maxMs?: number; + /** Approximate jitter: standard deviation of samples (ms), when measured */ + jitterMs?: number; + /** Number of successful connect samples (e.g. 5) */ + samples?: number; } export interface GamesFetchRequest { From 64014326c765c2fdff979c870f618d4e3a03d210 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 31 Mar 2026 21:54:29 +0000 Subject: [PATCH 2/2] fix(settings): move ICE path under Video next to L4S, remove Connection section Co-authored-by: Zortos --- .../renderer/src/components/SettingsPage.tsx | 62 ++++++++----------- 1 file changed, 27 insertions(+), 35 deletions(-) diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx index a8d93c09..228e8989 100644 --- a/opennow-stable/src/renderer/src/components/SettingsPage.tsx +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -1151,41 +1151,6 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag - {/* ── Connection (WebRTC) ───────────────────────── */} -
-
-

Connection

-
-
-
- -
- {( - [ - { value: "all" as const, label: "Default", title: "Try direct and relay candidates (typical home networks)" }, - { value: "relay" as const, label: "Relay only", title: "Force TURN — can help with VPNs or strict NAT (may add latency)" }, - ] as const - ).map((opt) => ( - - ))} -
- - Controls WebRTC candidate gathering. This is not a full “game booster” VPN; it only affects how OpenNOW connects to GeForce NOW. - -
-
-
- {/* ── Game ───────────────────────────────────────── */}
@@ -1400,6 +1365,33 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag
+
+ +
+ {( + [ + { value: "all" as const, label: "Default", title: "Try direct and relay candidates (typical home networks)" }, + { value: "relay" as const, label: "Relay only", title: "Force TURN — can help with VPNs or strict NAT (may add latency)" }, + ] as const + ).map((opt) => ( + + ))} +
+ + Controls WebRTC candidate gathering. This is not a full “game booster” VPN; it only affects how OpenNOW connects to GeForce NOW. + +
+