Skip to content
Merged
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,15 @@
> **Warning**
> OpenNOW is under active development. Bugs and performance issues are expected while features are finalized.

> **Trademark & Affiliation Notice**
> OpenNOW is an independent community project and is **not affiliated with, endorsed by, or sponsored by NVIDIA Corporation**.
> **NVIDIA** and **GeForce NOW** are trademarks of NVIDIA Corporation. You must use your own GeForce NOW account.

---

## What is OpenNOW?

OpenNOW is a community-built desktop client for [NVIDIA GeForce NOW](https://www.nvidia.com/en-us/geforce-now/), built with Electron and TypeScript. It gives you a fully open-source, cross-platform alternative to the official app with zero telemetry, full transparency, and features the official client doesn't have.
OpenNOW is an independent, community-built desktop client for [NVIDIA GeForce NOW](https://www.nvidia.com/en-us/geforce-now/), built with Electron and TypeScript. It provides a fully open-source, cross-platform alternative to the official app with zero telemetry, full transparency, and power-user features.

- 🔓 **Fully open source** — audit every line, fork it, improve it
- 🚫 **No telemetry** — OpenNOW collects nothing
Expand Down Expand Up @@ -201,7 +205,7 @@ opennow-stable/src/
## FAQ

**Is this the official GeForce NOW client?**
No. OpenNOW is a community-built alternative. It uses the same NVIDIA streaming infrastructure but is not affiliated with or endorsed by NVIDIA.
No. OpenNOW is an independent third-party client and is not affiliated with, endorsed by, or sponsored by NVIDIA. NVIDIA and GeForce NOW are trademarks of NVIDIA Corporation.

**Was this project built in Rust before?**
Yes. OpenNOW originally used Rust/Tauri but switched to Electron for better cross-platform compatibility and faster development.
Expand Down
23 changes: 11 additions & 12 deletions opennow-stable/src/main/gfn/errorCodes.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/**
* GFN CloudMatch Error Codes
* CloudMatch error codes.
*
* Error code mappings extracted from the official GFN web client.
* These provide user-friendly error messages for session failures.
* These mappings provide user-friendly messages for session failures.
*/

/** GFN Session Error Codes from official client */
/** Session error code constants. */
export enum GfnErrorCode {
// Success codes
Success = 15859712,
Expand Down Expand Up @@ -296,7 +295,7 @@ export const ERROR_MESSAGES: Map<number, ErrorMessageEntry> = new Map([
3237093656,
{
title: "Under Maintenance",
description: "GeForce NOW is currently under maintenance. Please try again later.",
description: "The service is currently under maintenance. Please try again later.",
},
],
[
Expand Down Expand Up @@ -394,14 +393,14 @@ export const ERROR_MESSAGES: Map<number, ErrorMessageEntry> = new Map([
3237093684,
{
title: "Region Not Supported",
description: "GeForce NOW is not available in your region.",
description: "The service is not available in your region.",
},
],
[
3237093685,
{
title: "Region Banned",
description: "GeForce NOW is not available in your region.",
description: "The service is not available in your region.",
},
],
[
Expand Down Expand Up @@ -534,7 +533,7 @@ export const ERROR_MESSAGES: Map<number, ErrorMessageEntry> = new Map([
3237093722,
{
title: "Storage Error",
description: "GFN storage is not available.",
description: "Service storage is not available.",
},
],

Expand Down Expand Up @@ -646,7 +645,7 @@ export interface SessionErrorInfo {
unifiedErrorCode?: number;
/** Session error code from session.errorCode */
sessionErrorCode?: number;
/** Computed GFN error code */
/** Computed service error code */
gfnErrorCode: number;
/** User-friendly title */
title: string;
Expand Down Expand Up @@ -679,7 +678,7 @@ export class SessionError extends Error {
public readonly unifiedErrorCode?: number;
/** Session error code from session.errorCode */
public readonly sessionErrorCode?: number;
/** Computed GFN error code */
/** Computed service error code */
public readonly gfnErrorCode: number;
/** User-friendly title */
public readonly title: string;
Expand Down Expand Up @@ -733,7 +732,7 @@ export class SessionError extends Error {
const unifiedErrorCode = json.requestStatus?.unifiedErrorCode;
const sessionErrorCode = json.session?.errorCode;

// Compute GFN error code using official client logic
// Compute normalized service error code
const gfnErrorCode = SessionError.computeErrorCode(statusCode, unifiedErrorCode);

// Get user-friendly message
Expand All @@ -756,7 +755,7 @@ export class SessionError extends Error {
}

/**
* Compute GFN error code from CloudMatch response (matching official client logic)
* Compute service error code from CloudMatch response
*/
private static computeErrorCode(statusCode: number, unifiedErrorCode?: number): number {
// Base error code
Expand Down
9 changes: 9 additions & 0 deletions opennow-stable/src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -692,6 +692,7 @@ export function App(): JSX.Element {
audioElement: audioRef.current,
microphoneMode: settings.microphoneMode,
microphoneDeviceId: settings.microphoneDeviceId || undefined,
mouseSensitivity: settings.mouseSensitivity,
onLog: (line: string) => console.log(`[WebRTC] ${line}`),
onStats: (stats) => setDiagnostics(stats),
onEscHoldProgress: (visible, progress) => {
Expand Down Expand Up @@ -760,6 +761,14 @@ export function App(): JSX.Element {
if (settingsLoaded) {
await window.openNow.setSetting(key, value);
}
// If a running client exists, push certain settings live
if (key === "mouseSensitivity") {
try {
(clientRef.current as any)?.setMouseSensitivity?.(value as number);
} catch {
// ignore
}
}
}, [settingsLoaded]);

// Login handler
Expand Down
30 changes: 30 additions & 0 deletions opennow-stable/src/renderer/src/components/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1416,6 +1416,36 @@ export function SettingsPage({ settings, regions, onSettingChange }: SettingsPag
Export Logs
</button>
</div>

{/* Mouse Sensitivity */}
<div className="settings-row">
<label className="settings-label">Mouse Sensitivity</label>
<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>
<input
type="range"
className="settings-slider"
min={0.1}
max={4}
step={0.01}
value={settings.mouseSensitivity}
onChange={(e) => handleChange("mouseSensitivity", parseFloat(e.target.value))}
/>
<input
type="number"
className="settings-number-input"
style={{ width: 80 }}
min={0.1}
max={4}
step={0.01}
value={Number(settings.mouseSensitivity.toFixed(2))}
onChange={(e) => {
const v = parseFloat(e.target.value || "0");
if (Number.isFinite(v)) handleChange("mouseSensitivity", Math.max(0.1, Math.min(4, v)));
}}
/>
<span className="settings-subtle-hint">Multiplier applied to mouse movement (1.00 = default)</span>
</div>
</div>
</div>
</section>
</div>
Expand Down
16 changes: 14 additions & 2 deletions opennow-stable/src/renderer/src/gfn/webrtcClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ interface ClientOptions {
microphoneMode?: MicrophoneMode;
/** Preferred microphone device ID */
microphoneDeviceId?: string;
/** Mouse sensitivity multiplier (1.0 = default) */
mouseSensitivity?: number;
onLog: (line: string) => void;
onStats?: (stats: StreamDiagnostics) => void;
onEscHoldProgress?: (visible: boolean, progress: number) => void;
Expand Down Expand Up @@ -503,6 +505,7 @@ export class GfnWebRtcClient {
private mouseFlushLastTickMs = 0;
private pendingMouseTimestampUs: bigint | null = null;
private mouseDeltaFilter = new MouseDeltaFilter();
private mouseSensitivity = 1;

private partialReliableThresholdMs = GfnWebRtcClient.DEFAULT_PARTIAL_RELIABLE_THRESHOLD_MS;
private inputQueuePeakBufferedBytesWindow = 0;
Expand Down Expand Up @@ -557,6 +560,7 @@ export class GfnWebRtcClient {
options.videoElement.srcObject = this.videoStream;
options.audioElement.srcObject = this.audioStream;
options.audioElement.muted = true;
this.mouseSensitivity = options.mouseSensitivity ?? 1;

// Configure video element for lowest latency playback
this.configureVideoElementForLowLatency(options.videoElement);
Expand Down Expand Up @@ -605,6 +609,13 @@ export class GfnWebRtcClient {
this.log("Video element configured for low-latency playback");
}

/** Update mouse sensitivity multiplier at runtime. */
public setMouseSensitivity(value: number): void {
const v = Number.isFinite(value) ? value : 1;
this.mouseSensitivity = Math.max(0.01, v);
this.log(`Mouse sensitivity set to ${this.mouseSensitivity}`);
}

/**
* Configure an RTCRtpReceiver for minimum jitter buffer delay.
*
Expand Down Expand Up @@ -1840,8 +1851,9 @@ export class GfnWebRtcClient {
return;
}

this.pendingMouseDx += Math.round(this.mouseDeltaFilter.getX());
this.pendingMouseDy += Math.round(this.mouseDeltaFilter.getY());
// Apply user-configured mouse sensitivity multiplier before queuing
this.pendingMouseDx += Math.round(this.mouseDeltaFilter.getX() * this.mouseSensitivity);
this.pendingMouseDy += Math.round(this.mouseDeltaFilter.getY() * this.mouseSensitivity);
this.pendingMouseTimestampUs = timestampUs(eventTimestampMs);
};

Expand Down