From 34f33f2508f95cec1f94d6ae31a4a72634a919da Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sun, 6 Aug 2023 05:48:38 +0700 Subject: [PATCH] Add setting to enable touch controller for all games (#66) * Add "Enable touch controller for all games" feature * Minor fixes * Disable STREAM_ENABLE_TOUCH_CONTROLLER when STREAM_ENABLE_TOUCH_CONTROLLER is enabled * Combine STREAM_ENABLE_TOUCH_CONTROLLER & STREAM_HIDE_TOUCH_CONTROLLER into STREAM_TOUCH_CONTROLLER --- better-xcloud.user.js | 166 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 144 insertions(+), 22 deletions(-) diff --git a/better-xcloud.user.js b/better-xcloud.user.js index 9acf45c..e6a46e9 100644 --- a/better-xcloud.user.js +++ b/better-xcloud.user.js @@ -22,6 +22,9 @@ var $STREAM_VIDEO; var $SCREENSHOT_CANVAS; var GAME_TITLE_ID; +const TOUCH_SUPPORTED_GAME_IDS = new Set(); +var SHOW_GENERIC_TOUCH_CONTROLLER = false; + // Credit: https://phosphoricons.com const ICON_VIDEO_SETTINGS = ''; const ICON_STREAM_STATS = ''; @@ -560,12 +563,39 @@ class UserAgent { value: userAgent, }); + return userAgent; + } +} + + +class PreloadedState { + static override() { Object.defineProperty(window, '__PRELOADED_STATE__', { configurable: true, get: () => this._state, set: (state) => { - state.appContext.requestInfo.userAgent = userAgent; - state.appContext.requestInfo.origin = 'https://www.xbox.com'; + // Override User-Agent + const userAgent = UserAgent.spoof(); + if (userAgent) { + state.appContext.requestInfo.userAgent = userAgent; + state.appContext.requestInfo.origin = 'https://www.xbox.com'; + } + + // Get a list of touch-supported games + if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') { + let titles = {}; + try { + titles = state.xcloud.titles.data.titles; + } catch (e) {} + + for (let id in titles) { + const details = titles[id].data.details; + // Has move than one input type -> must have touch support + if (details.supportedInputTypes.length > 1) { + TOUCH_SUPPORTED_GAME_IDS.add(details.productId); + } + } + } this._state = state; } }); @@ -586,7 +616,7 @@ class Preferences { static get USER_AGENT_PROFILE() { return 'user_agent_profile'; } static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; } static get STREAM_HIDE_IDLE_CURSOR() { return 'stream_hide_idle_cursor';} - static get STREAM_HIDE_TOUCH_CONTROLLER() { return 'stream_hide_touch_controller'; } + static get STREAM_TOUCH_CONTROLLER() { return 'stream_touch_controller'; } static get STREAM_SIMPLIFY_MENU() { return 'stream_simplify_menu'; } static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; } @@ -700,9 +730,14 @@ class Preferences { 'label': 'Hide System menu\'s icon while playing', 'default': false, }, - [Preferences.STREAM_HIDE_TOUCH_CONTROLLER]: { - 'label': 'Disable touch controller', - 'default': false, + [Preferences.STREAM_TOUCH_CONTROLLER]: { + 'label': 'Touch controller', + 'default': 'default', + 'options': { + 'default': 'Default', + 'all': 'All games', + 'off': 'Off', + }, }, [Preferences.STREAM_SIMPLIFY_MENU]: { 'label': 'Simplify Stream\'s menu', @@ -835,6 +870,11 @@ class Preferences { } get(key, defaultValue=null) { + if (typeof key === 'undefined') { + debugger; + return; + } + const value = this._prefs[key]; if (typeof value !== 'undefined' && value !== null && value !== '') { @@ -1429,7 +1469,7 @@ div[class*=StreamHUD-module__buttonsContainer] { } // Hide touch controller - if (PREFS.get(Preferences.STREAM_HIDE_TOUCH_CONTROLLER)) { + if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'off') { css += ` #MultiTouchSurface, #BabylonCanvasContainer-main { display: none !important; @@ -1613,6 +1653,7 @@ function interceptHttpRequests() { const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER); const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION); const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE); + const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER); const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC); const orgFetch = window.fetch; @@ -1694,6 +1735,58 @@ function interceptHttpRequests() { return orgFetch(...arg); } + if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') { + SHOW_GENERIC_TOUCH_CONTROLLER = false; + // Get game ID from window.location + const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/); + // Check touch support + if (match && !TOUCH_SUPPORTED_GAME_IDS.has(match[1])) { + SHOW_GENERIC_TOUCH_CONTROLLER = true; + } + + const promise = orgFetch(...arg); + if (!SHOW_GENERIC_TOUCH_CONTROLLER) { + return promise; + } + + // Intercept result to make xCloud show the touch controller + return promise.then(response => { + return response.clone().text().then(text => { + if (!text.length) { + return response; + } + + const obj = JSON.parse(text); + let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {}; + overrides.inputConfiguration = { + enableTouchInput: true, + maxTouchPoints: 10, + }; + obj.clientStreamingConfigOverrides = JSON.stringify(overrides); + + response.json = () => Promise.resolve(obj); + response.text = () => Promise.resolve(JSON.stringify(obj)); + + return response; + }); + }); + } + + if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && (url.endsWith('/titles') || url.endsWith('/mru'))) { + const promise = orgFetch(...arg); + return promise.then(response => { + return response.clone().json().then(json => { + for (let game of json.results) { + if (game.details.supportedInputTypes.length > 1) { + TOUCH_SUPPORTED_GAME_IDS.add(game.details.productId); + } + } + + return response; + }); + }); + } + // ICE server candidates if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/') && request.method === 'GET') { const promise = orgFetch(...arg); @@ -1887,22 +1980,20 @@ function injectSettingsButton($parent) { $control.checked = setting.value; labelAttrs = {'for': 'xcloud_setting_' + settingId, 'tabindex': 0}; + } - if (settingId === Preferences.USE_DESKTOP_CODEC && !hasHighQualityCodecSupport()) { - $control.checked = false; + if (settingId === Preferences.USE_DESKTOP_CODEC && !hasHighQualityCodecSupport()) { + $control.disabled = true; + $control.checked = false; + $control.title = 'Your browser doesn\'t support this feature'; + } else if (settingId === Preferences.STREAM_TOUCH_CONTROLLER) { + // Disable this setting for non-touchable devices + if (!('ontouchstart'in window) && navigator.maxTouchPoints === 0) { $control.disabled = true; - $control.title = 'Your browser doesn\'t support this feature'; - $control.style.cursor = 'help'; - } else if (settingId === Preferences.STREAM_HIDE_TOUCH_CONTROLLER) { - // Disable this setting for non-touchable devices - if (!('ontouchstart'in window) && navigator.maxTouchPoints === 0) { - $control.checked = false; - $control.disabled = true; - $control.title = 'Your device doesn\'t have touch support'; - $control.style.cursor = 'help'; - } + $control.title = 'Your device doesn\'t have touch support'; } } + $control.disabled && ($control.style.cursor = 'help'); const $elm = CE('div', {'class': 'setting_row'}, CE('label', labelAttrs, setting.label), @@ -2030,7 +2121,7 @@ function injectVideoSettingsButton() { const $parent = $screen.parentElement; const hideQuickBarFunc = e => { e.stopPropagation(); - if (e.target != $parent && e.target.id !== 'MultiTouchSurface' && !e.target.getElementById('BabylonCanvasContainer-main')) { + if (e.target != $parent && e.target.id !== 'MultiTouchSurface' && !e.target.querySelector('#BabylonCanvasContainer-main')) { return; } @@ -2540,7 +2631,7 @@ window.addEventListener('popstate', onHistoryChanged); window.history.pushState = patchHistoryMethod('pushState'); window.history.replaceState = patchHistoryMethod('replaceState'); -UserAgent.spoof(); +PreloadedState.override(); // Disable bandwidth checking if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) { @@ -2557,13 +2648,44 @@ RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.add RTCPeerConnection.prototype.addIceCandidate = function(...args) { const candidate = args[0].candidate; if (candidate && candidate.startsWith('a=candidate:1 ')) { + STREAM_WEBRTC = this; StreamBadges.ipv6 = candidate.substring(20).includes(':'); } - STREAM_WEBRTC = this; return this.orgAddIceCandidate.apply(this, args); } +if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') { + RTCPeerConnection.prototype.orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; + RTCPeerConnection.prototype.createDataChannel = function() { + const dataChannel = this.orgCreateDataChannel.apply(this, arguments); + if (!SHOW_GENERIC_TOUCH_CONTROLLER) { + return dataChannel; + } + + const dispatchLayout = () => { + // Dispatch a message to display generic touch controller + dataChannel.dispatchEvent(new MessageEvent('message', { + data: '{"content":"{\\"layoutId\\":\\"\\"}","target":"/streaming/touchcontrols/showlayoutv2","type":"Message"}', + origin: 'better-xcloud', + })); + } + + dataChannel.addEventListener('message', msg => { + if (msg.origin === 'better-xcloud' || typeof msg.data !== 'string') { + return; + } + + if (msg.data.includes('touchcontrols/showtitledefault')) { + setTimeout(dispatchLayout, 10); + } + }); + + return dataChannel; + }; +} + + patchRtcCodecs(); interceptHttpRequests(); patchVideoApi();