diff --git a/opennow-stable/src/main/gfn/cloudmatch.ts b/opennow-stable/src/main/gfn/cloudmatch.ts index 405111624..6ba0f525f 100644 --- a/opennow-stable/src/main/gfn/cloudmatch.ts +++ b/opennow-stable/src/main/gfn/cloudmatch.ts @@ -497,6 +497,7 @@ function buildSessionRequestBody(input: SessionCreateRequest, deviceHashId: stri const bitDepth = colorQualityBitDepth(cq); const chromaFormat = colorQualityChromaFormat(cq); const accountLinked = input.accountLinked ?? true; + const sonyHidEnabled = input.settings.experimentalGamepadGyro === true; return { sessionRequestData: { @@ -550,7 +551,7 @@ function buildSessionRequestBody(input: SessionCreateRequest, deviceHashId: stri } : null, surroundAudioInfo: 0, - remoteControllersBitmap: 0, + remoteControllersBitmap: sonyHidEnabled ? 1 : 0, clientTimezoneOffset: timezoneOffsetMs(), enhancedStreamMode: 1, appLaunchMode: 1, @@ -566,7 +567,7 @@ function buildSessionRequestBody(input: SessionCreateRequest, deviceHashId: stri enabledL4S: input.settings.enableL4S, mouseMovementFlags: 0, trueHdr: hdrEnabled, - supportedHidDevices: 0, + supportedHidDevices: sonyHidEnabled ? 3 : 0, profile: 0, fallbackToLogicalResolution: false, hidDevices: null, diff --git a/opennow-stable/src/main/index.ts b/opennow-stable/src/main/index.ts index 204af8f32..82378ab71 100644 --- a/opennow-stable/src/main/index.ts +++ b/opennow-stable/src/main/index.ts @@ -1622,7 +1622,7 @@ function registerIpcHandlers(): void { }); ipcMain.handle(IPC_CHANNELS.SETTINGS_SET, async (_event: Electron.IpcMainInvokeEvent, key: K, value: Settings[K]) => { - settingsManager.set(key, value); + settingsManager.set(key as keyof import("./settings").Settings, value as import("./settings").Settings[keyof import("./settings").Settings]); // React to certain setting changes immediately in main process try { if (key === "autoCheckForUpdates") { @@ -2272,18 +2272,24 @@ app.whenReady().then(async () => { } // Set up permission handlers for getUserMedia, fullscreen, pointer lock + const isExperimentalGamepadGyroEnabled = (): boolean => settingsManager.get("experimentalGamepadGyro") === true; + const allowedNonHidPermissions = new Set([ + "media", + "microphone", + "fullscreen", + "automatic-fullscreen", + "pointerLock", + "keyboardLock", + "speaker-selection", + ]); + session.defaultSession.setPermissionRequestHandler((webContents, permission, callback) => { - const allowedPermissions = new Set([ - "media", - "microphone", - "fullscreen", - "automatic-fullscreen", - "pointerLock", - "keyboardLock", - "speaker-selection", - ]); - - if (allowedPermissions.has(permission)) { + if ((permission as string) === "hid") { + callback(isExperimentalGamepadGyroEnabled()); + return; + } + + if (allowedNonHidPermissions.has(permission)) { callback(true); return; } @@ -2292,17 +2298,26 @@ app.whenReady().then(async () => { }); session.defaultSession.setPermissionCheckHandler((webContents, permission, requestingOrigin) => { - const allowedPermissions = new Set([ - "media", - "microphone", - "fullscreen", - "automatic-fullscreen", - "pointerLock", - "keyboardLock", - "speaker-selection", - ]); - - return allowedPermissions.has(permission); + if ((permission as string) === "hid") { + return isExperimentalGamepadGyroEnabled(); + } + + return allowedNonHidPermissions.has(permission); + }); + + session.defaultSession.setDevicePermissionHandler((details) => { + return isExperimentalGamepadGyroEnabled() && details.deviceType === "hid" && details.device.vendorId === 0x054c; + }); + + session.defaultSession.on("select-hid-device", (event, details, callback) => { + if (!isExperimentalGamepadGyroEnabled()) { + return; + } + const sonyDevice = details.deviceList.find((device) => device.vendorId === 0x054c); + if (sonyDevice) { + event.preventDefault(); + callback(sonyDevice.deviceId); + } }); registerOpenNowMediaProtocol(); diff --git a/opennow-stable/src/main/settings.ts b/opennow-stable/src/main/settings.ts index 20c17ddc5..c7c48e6a9 100644 --- a/opennow-stable/src/main/settings.ts +++ b/opennow-stable/src/main/settings.ts @@ -104,6 +104,8 @@ export interface Settings { enableL4S: boolean; /** Request Cloud G-Sync / Variable Refresh Rate on new sessions */ enableCloudGsync: boolean; + /** Experimental Sony controller gyro support over WebHID */ + experimentalGamepadGyro: boolean; /** Show the currently streaming game as Discord Rich Presence activity */ discordRichPresence: boolean; /** Automatically check GitHub Releases for app updates in the background */ @@ -187,6 +189,7 @@ const DEFAULT_SETTINGS: Settings = { gameLanguage: "en_US", enableL4S: false, enableCloudGsync: false, + experimentalGamepadGyro: false, discordRichPresence: false, autoCheckForUpdates: true, allowEscapeToExitFullscreen: false, diff --git a/opennow-stable/src/renderer/src/App.tsx b/opennow-stable/src/renderer/src/App.tsx index a05fe3fd0..67f072c12 100644 --- a/opennow-stable/src/renderer/src/App.tsx +++ b/opennow-stable/src/renderer/src/App.tsx @@ -44,6 +44,7 @@ import { type StreamDiagnostics, type StreamTimeWarning, } from "./gfn/webrtcClient"; +import { requestSonyGamepadHidAccess } from "./gfn/gamepadMotion"; import { formatShortcutForDisplay, isShortcutMatch, normalizeShortcut } from "./shortcuts"; import { useControllerNavigation } from "./controllerNavigation"; import { useElapsedSeconds } from "./utils/useElapsedSeconds"; @@ -935,6 +936,7 @@ export function App(): JSX.Element { gameLanguage: "en_US", enableL4S: false, enableCloudGsync: false, + experimentalGamepadGyro: false, discordRichPresence: false, autoCheckForUpdates: true, }); @@ -2444,6 +2446,13 @@ export function App(): JSX.Element { // Save settings when changed const updateSetting = useCallback(async (key: K, value: Settings[K]) => { setSettings((prev) => ({ ...prev, [key]: value })); + if (key === "experimentalGamepadGyro" && value) { + try { + void requestSonyGamepadHidAccess((line) => console.log(`[WebHID] ${line}`)); + } catch { + // ignore + } + } if (settingsLoaded) { await window.openNow.setSetting(key, value); } @@ -2476,6 +2485,13 @@ export function App(): JSX.Element { // ignore } } + if (key === "experimentalGamepadGyro") { + try { + clientRef.current?.setExperimentalGamepadGyroEnabled(Boolean(value)); + } catch { + // ignore + } + } }, [settingsLoaded]); const handleStreamVolumeChange = useCallback((v: number) => { @@ -3025,6 +3041,7 @@ export function App(): JSX.Element { gameLanguage: settings.gameLanguage, enableL4S: settings.enableL4S, enableCloudGsync: settings.enableCloudGsync, + experimentalGamepadGyro: settings.experimentalGamepadGyro, }, }); @@ -3185,6 +3202,7 @@ export function App(): JSX.Element { gameLanguage: settings.gameLanguage, enableL4S: settings.enableL4S, enableCloudGsync: settings.enableCloudGsync, + experimentalGamepadGyro: settings.experimentalGamepadGyro, }, }); if (!isRecoveryGenerationCurrent(recoveryGeneration)) { @@ -3301,6 +3319,7 @@ export function App(): JSX.Element { microphoneDeviceId: settings.microphoneDeviceId || undefined, mouseSensitivity: settings.mouseSensitivity, mouseAcceleration: settings.mouseAcceleration, + experimentalGamepadGyro: settings.experimentalGamepadGyro, onLog: (line: string) => console.log(`[WebRTC] ${line}`), onStats: (stats) => diagnosticsStore.set(stats), onTimeWarning: (warning) => { @@ -3639,6 +3658,7 @@ export function App(): JSX.Element { gameLanguage: settings.gameLanguage, enableL4S: settings.enableL4S, enableCloudGsync: settings.enableCloudGsync, + experimentalGamepadGyro: settings.experimentalGamepadGyro, }, }); diff --git a/opennow-stable/src/renderer/src/components/SettingsPage.tsx b/opennow-stable/src/renderer/src/components/SettingsPage.tsx index 822e141a3..89dc39660 100644 --- a/opennow-stable/src/renderer/src/components/SettingsPage.tsx +++ b/opennow-stable/src/renderer/src/components/SettingsPage.tsx @@ -2157,6 +2157,24 @@ export function SettingsPage({ settings, regions, onSettingChange, codecResults, +
+ + +
+