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();