Compare commits

...

13 Commits
v2.0.3 ... v2.1

Author SHA1 Message Date
47ef7da37b Bump version to 2.1 2023-12-12 17:55:41 +07:00
63896469e2 Update README.md 2023-12-12 17:52:19 +07:00
4ab265e370 Controller & device vibration (#184)
* Test vibration

* Modify vibration patterns

* Try another vibration patterns

* Update vibration patterns

* Add setting to toggle device vibration

* Disable device vibration based on setting

* Bug fixes

* Test PWM

* Rename GamepadVibration to VibrationManager

* Add setting to toggle controller vibration

* Add vibration intensity setting

* Move Controller settings to the top

* Fix device vibration intensity

* No longer parse "delayMs" and "repeat"

* Fix device vibration intensity

* Disable vibration features on unsupported browsers

* Add "step" property to Vibration intensity slider

* Disable PWA prompt & Stream Gate dialog

* Update translations
2023-12-12 17:37:04 +07:00
584509a53d Improve IPv6 server detection 2023-12-09 11:09:08 +07:00
f3b9ebdb22 Bump version to 2.0.5 2023-12-08 07:43:30 +07:00
abd1aae57a Add "enableConsoleLogging" patch 2023-12-08 07:35:45 +07:00
ccdb944b99 Fix the Settings button not showing for some users 2023-12-08 07:23:44 +07:00
b4149e718b Bump version to 2.0.4 2023-12-06 20:06:41 +07:00
7c22685e95 Update README.md 2023-12-05 14:50:32 +07:00
ad98eb60e1 Only call eval() once per patch group 2023-12-05 06:38:35 +07:00
049e65429a Fix not applying patches correctly 2023-12-03 16:36:02 +07:00
a5b77ae8c0 Improve Patcher class 2023-12-03 10:38:10 +07:00
49550eed0a Add "enableXcloudLogger" patch 2023-12-02 17:31:51 +07:00
3 changed files with 443 additions and 56 deletions

View File

@ -1,5 +1,5 @@
# Better xCloud # Better xCloud
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience on web browser. Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play).
The main target of this script is mobile users, but it should work great on desktop too. The main target of this script is mobile users, but it should work great on desktop too.
Supported platforms: Supported platforms:
@ -73,7 +73,8 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
<br> <br>
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/51bdb96c-79ab-402f-902a-a9e6229973b2"> <img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/51bdb96c-79ab-402f-902a-a9e6229973b2">
<br> <br>
<img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/18ed4598-4eca-4626-9434-5f74266b00e7"> <img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/f7df312c-e6bc-49a1-8239-24c280ed7fa6">
&nbsp; &nbsp;
@ -179,6 +180,9 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future. > The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
### In-game settings ### In-game settings
- **🔥 Controller & device vibrations**
> Control vibration settings
> Adjust vibration intensity
- **Volume control** - **Volume control**
> Increase stream's volume up to 600% > Increase stream's volume up to 600%
> Can be disabled in the Main Settings > Can be disabled in the Main Settings

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 2.0.3 // @version 2.1
// ==/UserScript== // ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 2.0.3 // @version 2.1
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -13,10 +13,11 @@
// ==/UserScript== // ==/UserScript==
'use strict'; 'use strict';
const SCRIPT_VERSION = '2.0.3'; const SCRIPT_VERSION = '2.1';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud'; const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const ENABLE_MKB = false; const ENABLE_MKB = false;
const ENABLE_XCLOUD_LOGGER = false;
console.log(`[Better xCloud] readyState: ${document.readyState}`); console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -53,7 +54,7 @@ function createElement(elmName, props = {}) {
const argType = typeof arg; const argType = typeof arg;
if (argType === 'string' || argType === 'number') { if (argType === 'string' || argType === 'number') {
$elm.textContent = arg; $elm.appendChild(document.createTextNode(arg));
} else if (arg) { } else if (arg) {
$elm.appendChild(arg); $elm.appendChild(arg);
} }
@ -485,6 +486,13 @@ const Translations = {
"uk-UA": "Частота опитувань контролера", "uk-UA": "Частота опитувань контролера",
"vi-VN": "Tần suất cập nhật của bộ điều khiển", "vi-VN": "Tần suất cập nhật của bộ điều khiển",
}, },
"controller-vibration": {
"de-DE": "Vibration des Controllers",
"en-US": "Controller vibration",
"ja-JP": "コントローラーの振動",
"tr-TR": "Oyun kumandası titreşimi",
"vi-VN": "Rung bộ điều khiển",
},
"custom": { "custom": {
"de-DE": "Benutzerdefiniert", "de-DE": "Benutzerdefiniert",
"en-US": "Custom", "en-US": "Custom",
@ -533,6 +541,22 @@ const Translations = {
"vi-VN": "Thiết bị này không hỗ trợ cảm ứng", "vi-VN": "Thiết bị này không hỗ trợ cảm ứng",
"zh-CN": "您的设备不支持触摸", "zh-CN": "您的设备不支持触摸",
}, },
"device-vibration": {
"de-DE": "Vibration des Geräts",
"en-US": "Device vibration",
"ja-JP": "デバイスの振動",
"tr-TR": "Cihaz titreşimi",
"uk-UA": "Вібрація пристрою",
"vi-VN": "Rung thiết bị",
},
"device-vibration-not-using-gamepad": {
"de-DE": "Aktiviert, wenn kein Gamepad verwendet wird",
"en-US": "On when not using gamepad",
"ja-JP": "ゲームパッド未使用時にオン",
"tr-TR": "Oyun kumandası bağlanmadan titreşim",
"uk-UA": "Увімкнена, коли не використовується геймпад",
"vi-VN": "Bật khi không dùng tay cầm",
},
"disable": { "disable": {
"de-DE": "Deaktiviert", "de-DE": "Deaktiviert",
"en-US": "Disable", "en-US": "Disable",
@ -646,6 +670,7 @@ const Translations = {
"de-DE": "Maus- und Tastaturunterstützung aktivieren", "de-DE": "Maus- und Tastaturunterstützung aktivieren",
"en-US": "Enable Mouse & Keyboard support", "en-US": "Enable Mouse & Keyboard support",
"es-ES": "Habilitar soporte para ratón y teclado", "es-ES": "Habilitar soporte para ratón y teclado",
"it-IT": "Abilitare il supporto di mouse e tastiera",
"ja-JP": "マウス&キーボードのサポートを有効化", "ja-JP": "マウス&キーボードのサポートを有効化",
"pl-PL": "Włącz obsługę myszy i klawiatury", "pl-PL": "Włącz obsługę myszy i klawiatury",
"pt-BR": "Habilitar suporte ao Mouse & Teclado", "pt-BR": "Habilitar suporte ao Mouse & Teclado",
@ -674,6 +699,7 @@ const Translations = {
"de-DE": "\"Remote Play\" Funktion aktivieren", "de-DE": "\"Remote Play\" Funktion aktivieren",
"en-US": "Enable the \"Remote Play\" feature", "en-US": "Enable the \"Remote Play\" feature",
"es-ES": "Activar la función \"Reproducción remota\"", "es-ES": "Activar la función \"Reproducción remota\"",
"it-IT": "Abilitare la funzione \"Riproduzione remota\"",
"ja-JP": "リモートプレイ機能を有効化", "ja-JP": "リモートプレイ機能を有効化",
"pl-PL": "Włącz funkcję \"Gra zdalna\"", "pl-PL": "Włącz funkcję \"Gra zdalna\"",
"pt-BR": "Ativar o recurso \"Reprodução Remota\"", "pt-BR": "Ativar o recurso \"Reprodução Remota\"",
@ -702,6 +728,7 @@ const Translations = {
"de-DE": "Schnell", "de-DE": "Schnell",
"en-US": "Fast", "en-US": "Fast",
"es-ES": "Rápido", "es-ES": "Rápido",
"it-IT": "Veloce",
"ja-JP": "高速", "ja-JP": "高速",
"pl-PL": "Szybko", "pl-PL": "Szybko",
"pt-BR": "Rápido", "pt-BR": "Rápido",
@ -791,6 +818,7 @@ const Translations = {
"de-DE": "Layout", "de-DE": "Layout",
"en-US": "Layout", "en-US": "Layout",
"es-ES": "Diseño", "es-ES": "Diseño",
"it-IT": "Layout",
"ja-JP": "レイアウト", "ja-JP": "レイアウト",
"pl-PL": "Układ", "pl-PL": "Układ",
"pt-BR": "Layout", "pt-BR": "Layout",
@ -819,6 +847,7 @@ const Translations = {
"de-DE": "Max. Bitrate", "de-DE": "Max. Bitrate",
"en-US": "Max bitrate", "en-US": "Max bitrate",
"es-ES": "Tasa de bits máxima", "es-ES": "Tasa de bits máxima",
"it-IT": "Bitrate massimo",
"ja-JP": "最大ビットレート", "ja-JP": "最大ビットレート",
"pl-PL": "Maksymalny bitrate", "pl-PL": "Maksymalny bitrate",
"pt-BR": "Taxa máxima dos bits", "pt-BR": "Taxa máxima dos bits",
@ -831,6 +860,7 @@ const Translations = {
"de-DE": "Funktioniert evtl. nicht fehlerfrei!", "de-DE": "Funktioniert evtl. nicht fehlerfrei!",
"en-US": "May not work properly!", "en-US": "May not work properly!",
"es-ES": "¡Puede que no funcione correctamente!", "es-ES": "¡Puede que no funcione correctamente!",
"it-IT": "Potrebbe non funzionare correttamente!",
"ja-JP": "正常に動作しない場合があります!", "ja-JP": "正常に動作しない場合があります!",
"pl-PL": "Może nie działać poprawnie!", "pl-PL": "Może nie działać poprawnie!",
"pt-BR": "Pode não funcionar corretamente!", "pt-BR": "Pode não funcionar corretamente!",
@ -889,6 +919,7 @@ const Translations = {
"de-DE": "Maus & Tastatur", "de-DE": "Maus & Tastatur",
"en-US": "Mouse & Keyboard", "en-US": "Mouse & Keyboard",
"es-ES": "Ratón y teclado", "es-ES": "Ratón y teclado",
"it-IT": "Mouse e tastiera",
"ja-JP": "マウス&キーボード", "ja-JP": "マウス&キーボード",
"pl-PL": "Mysz i klawiatura", "pl-PL": "Mysz i klawiatura",
"pt-BR": "Mouse e Teclado", "pt-BR": "Mouse e Teclado",
@ -975,6 +1006,7 @@ const Translations = {
"de-DE": "Unterstützt nur einige Spiele", "de-DE": "Unterstützt nur einige Spiele",
"en-US": "Only supports some games", "en-US": "Only supports some games",
"es-ES": "Sólo soporta algunos juegos", "es-ES": "Sólo soporta algunos juegos",
"it-IT": "Supporta solo alcuni giochi",
"ja-JP": "一部のゲームのみサポート", "ja-JP": "一部のゲームのみサポート",
"pl-PL": "Wspiera tylko niektóre gry", "pl-PL": "Wspiera tylko niektóre gry",
"pt-BR": "Suporta apenas alguns jogos", "pt-BR": "Suporta apenas alguns jogos",
@ -1397,6 +1429,7 @@ const Translations = {
"de-DE": "Langsam", "de-DE": "Langsam",
"en-US": "Slow", "en-US": "Slow",
"es-ES": "Lento", "es-ES": "Lento",
"it-IT": "Lento",
"ja-JP": "低速", "ja-JP": "低速",
"pl-PL": "Wolno", "pl-PL": "Wolno",
"pt-BR": "Lento", "pt-BR": "Lento",
@ -1425,6 +1458,7 @@ const Translations = {
"de-DE": "Smart TV", "de-DE": "Smart TV",
"en-US": "Smart TV", "en-US": "Smart TV",
"es-ES": "Smart TV", "es-ES": "Smart TV",
"it-IT": "Smart TV",
"ja-JP": "スマートTV", "ja-JP": "スマートTV",
"pl-PL": "Smart TV", "pl-PL": "Smart TV",
"pt-BR": "Smart TV", "pt-BR": "Smart TV",
@ -1621,6 +1655,15 @@ const Translations = {
"vi-VN": "Kéo giãn", "vi-VN": "Kéo giãn",
"zh-CN": "拉伸", "zh-CN": "拉伸",
}, },
"swap-buttons": {
"de-DE": "Tasten tauschen",
"en-US": "Swap buttons",
"ja-JP": "ボタン入れ替え",
"pt-BR": "Trocar botões",
"tr-TR": "Düğme düzenini ters çevir",
"uk-UA": "Поміняти кнопки місцями",
"vi-VN": "Hoán đổi nút",
},
"target-resolution": { "target-resolution": {
"de-DE": "Festgelegte Auflösung", "de-DE": "Festgelegte Auflösung",
"en-US": "Target resolution", "en-US": "Target resolution",
@ -1863,6 +1906,7 @@ const Translations = {
"de-DE": "Unbegrenzt", "de-DE": "Unbegrenzt",
"en-US": "Unlimited", "en-US": "Unlimited",
"es-ES": "Ilimitado", "es-ES": "Ilimitado",
"it-IT": "Illimitato",
"ja-JP": "無制限", "ja-JP": "無制限",
"pl-PL": "Bez ograniczeń", "pl-PL": "Bez ograniczeń",
"pt-BR": "Ilimitado", "pt-BR": "Ilimitado",
@ -1913,6 +1957,13 @@ const Translations = {
"vi-VN": "User-Agent", "vi-VN": "User-Agent",
"zh-CN": "浏览器UA伪装", "zh-CN": "浏览器UA伪装",
}, },
"vibration-intensity": {
"de-DE": "Vibrationsstärke",
"en-US": "Vibration intensity",
"ja-JP": "振動の強さ",
"tr-TR": "Titreşim gücü",
"vi-VN": "Cường độ rung",
},
"video": { "video": {
"de-DE": "Video", "de-DE": "Video",
"en-US": "Video", "en-US": "Video",
@ -2909,6 +2960,147 @@ class GamepadHandler {
} }
} }
class VibrationManager {
static #playDeviceVibration(data) {
// console.log(+new Date, data);
const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY;
if (intensity === 0 || intensity === 100) {
// Stop vibration
window.navigator.vibrate(intensity ? data.durationMs : 0);
return;
}
const pulseDuration = 200;
const onDuration = Math.floor(pulseDuration * intensity / 100);
const offDuration = pulseDuration - onDuration;
const repeats = Math.ceil(data.durationMs / pulseDuration);
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
// console.log(pulses);
window.navigator.vibrate(pulses);
}
static supportControllerVibration() {
return Gamepad.prototype.hasOwnProperty('vibrationActuator');
}
static supportDeviceVibration() {
return !!window.navigator.vibrate;
}
static updateGlobalVars() {
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? PREFS.get(Preferences.CONTROLLER_ENABLE_VIBRATION) : false;
window.BX_VIBRATION_INTENSITY = PREFS.get(Preferences.CONTROLLER_VIBRATION_INTENSITY) / 100;
if (!VibrationManager.supportDeviceVibration()) {
window.BX_ENABLE_DEVICE_VIBRATION = false;
return;
}
// Stop vibration
window.navigator.vibrate(0);
const value = PREFS.get(Preferences.CONTROLLER_DEVICE_VIBRATION);
let enabled;
if (value === 'on') {
enabled = true;
} else if (value === 'auto') {
enabled = true;
const gamepads = window.navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
enabled = false;
break;
}
}
} else {
enabled = false;
}
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
}
static initialSetup() {
window.addEventListener('gamepadconnected', VibrationManager.updateGlobalVars);
window.addEventListener('gamepaddisconnected', VibrationManager.updateGlobalVars);
VibrationManager.updateGlobalVars();
const orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = orgCreateDataChannel.apply(this, arguments);
if (dataChannel.label !== 'input') {
return dataChannel;
}
const VIBRATION_DATA_MAP = {
'gamepadIndex': 8,
'leftMotorPercent': 8,
'rightMotorPercent': 8,
'leftTriggerMotorPercent': 8,
'rightTriggerMotorPercent': 8,
'durationMs': 16,
// 'delayMs': 16,
// 'repeat': 8,
};
dataChannel.addEventListener('message', e => {
if (!window.BX_ENABLE_DEVICE_VIBRATION) {
return;
}
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
return;
}
const dataView = new DataView(e.data);
let offset = 0;
let messageType;
if (dataView.byteLength === 13) { // version >= 8
messageType = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
messageType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
if (!(messageType & 128)) { // Vibration
return;
}
const vibrationType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
if (vibrationType !== 0) { // FourMotorRumble
return;
}
const data = {};
for (const key in VIBRATION_DATA_MAP) {
if (VIBRATION_DATA_MAP[key] === 16) {
data[key] = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
data[key] = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
}
VibrationManager.#playDeviceVibration(data);
});
return dataChannel;
};
}
}
class MouseCursorHider { class MouseCursorHider {
static #timeout; static #timeout;
static #cursorVisible = true; static #cursorVisible = true;
@ -3032,7 +3224,7 @@ class StreamBadges {
let totalIn = 0; let totalIn = 0;
let totalOut = 0; let totalOut = 0;
stats.forEach(stat => { stats.forEach(stat => {
if (stat.type === 'candidate-pair' && stat.state == 'succeeded') { if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
totalIn += stat.bytesReceived; totalIn += stat.bytesReceived;
totalOut += stat.bytesSent; totalOut += stat.bytesSent;
} }
@ -3552,6 +3744,9 @@ class Preferences {
static get STREAM_DISABLE_FEEDBACK_DIALOG() { return 'stream_disable_feedback_dialog'; } static get STREAM_DISABLE_FEEDBACK_DIALOG() { return 'stream_disable_feedback_dialog'; }
static get CONTROLLER_ENABLE_SHORTCUTS() { return 'controller_enable_shortcuts'; } static get CONTROLLER_ENABLE_SHORTCUTS() { return 'controller_enable_shortcuts'; }
static get CONTROLLER_ENABLE_VIBRATION() { return 'controller_enable_vibration'; }
static get CONTROLLER_DEVICE_VIBRATION() { return 'controller_device_vibration'; }
static get CONTROLLER_VIBRATION_INTENSITY() { return 'controller_vibration_intensity'; }
static get MKB_ENABLED() { return 'mkb_enabled'; } static get MKB_ENABLED() { return 'mkb_enabled'; }
static get MKB_ABSOLUTE_MOUSE() { return 'mkb_absolute_mouse'; } static get MKB_ABSOLUTE_MOUSE() { return 'mkb_absolute_mouse'; }
@ -3785,6 +3980,26 @@ class Preferences {
'default': false, 'default': false,
}, },
[Preferences.CONTROLLER_ENABLE_VIBRATION]: {
'default': true,
},
[Preferences.CONTROLLER_DEVICE_VIBRATION]: {
'default': 'off',
'options': {
'on': __('on'),
'auto': __('device-vibration-not-using-gamepad'),
'off': __('off'),
},
},
[Preferences.CONTROLLER_VIBRATION_INTENSITY]: {
'default': 100,
'min': 0,
'max': 100,
'steps': 10,
},
[Preferences.MKB_ENABLED]: { [Preferences.MKB_ENABLED]: {
'default': false, 'default': false,
}, },
@ -4163,7 +4378,7 @@ class Preferences {
); );
if (!options.disabled && !options.hideSlider) { if (!options.disabled && !options.hideSlider) {
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value}); $range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value, 'step': STEPS});
$range.addEventListener('input', e => { $range.addEventListener('input', e => {
value = parseInt(e.target.value); value = parseInt(e.target.value);
@ -4304,30 +4519,21 @@ class Patcher {
return funcStr.replace(funcStr.substring(index - 9, index + 15), 'https://www.xbox.com/play'); return funcStr.replace(funcStr.substring(index - 9, index + 15), 'https://www.xbox.com/play');
}, },
// Disable trackEvent() function
disableTrackEvent: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.trackEvent=';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, 'this.trackEvent=e=>{},this.uwuwu=');
},
remotePlayKeepAlive: PREFS.get(Preferences.REMOTE_PLAY_ENABLED) && function(funcStr) { remotePlayKeepAlive: PREFS.get(Preferences.REMOTE_PLAY_ENABLED) && function(funcStr) {
if (!funcStr.includes('onServerDisconnectMessage(e){')) { if (!funcStr.includes('onServerDisconnectMessage(e){')) {
return false; return false;
} }
funcStr = funcStr.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage (e) { funcStr = funcStr.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage(e) {
const msg = JSON.parse(e); const msg = JSON.parse(e);
if (msg.reason === 'WarningForBeingIdle') { if (msg.reason === 'WarningForBeingIdle') {
try { try {
this.sendKeepAlive(); this.sendKeepAlive();
return; return;
} catch (ex) {} } catch (ex) { console.log(ex); }
} }
`); `);
return funcStr; return funcStr;
}, },
@ -4341,6 +4547,16 @@ class Patcher {
return funcStr.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`); return funcStr.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`);
}, },
// Disable trackEvent() function
disableTrackEvent: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.trackEvent=';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, 'this.trackEvent=e=>{},this.uwuwu=');
},
// Block WebRTC stats collector // Block WebRTC stats collector
blockWebRtcStatsCollector: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) { blockWebRtcStatsCollector: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.intervalMs=0,'; const text = 'this.intervalMs=0,';
@ -4351,6 +4567,68 @@ class Patcher {
return funcStr.replace(text, 'false,' + text); return funcStr.replace(text, 'false,' + text);
}, },
enableXcloudLogger: ENABLE_XCLOUD_LOGGER && function(funcStr) {
const text = '}log(e,t,n){';
if (!funcStr.includes(text)) {
return false;
}
funcStr = funcStr.replaceAll(text, text + 'console.log(arguments);');
return funcStr;
},
enableConsoleLogging: ENABLE_XCLOUD_LOGGER && function(funcStr) {
const text = 'static isConsoleLoggingAllowed(){';
if (!funcStr.includes(text)) {
return false;
}
funcStr = funcStr.replaceAll(text, text + 'return true;');
return funcStr;
},
// Control controller vibration
playVibration: function(funcStr) {
const text = '}playVibration(e){';
if (!funcStr.includes(text)) {
return false;
}
const newCode = `
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
return void(0);
}
if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
}
`;
VibrationManager.updateGlobalVars();
funcStr = funcStr.replaceAll(text, text + newCode);
return funcStr;
},
// Override website's settings
overrideSettings: function(funcStr) {
const index = funcStr.indexOf(',EnableStreamGate:');
if (index === -1) {
return false;
}
// Find the next "},"
const endIndex = funcStr.indexOf('},', index);
const newCode = `
EnableStreamGate: false,
PwaPrompt: false,
`;
funcStr = funcStr.substring(0, endIndex) + ',' + newCode + funcStr.substring(endIndex);
return funcStr;
},
// Enable Mouse and Keyboard support // Enable Mouse and Keyboard support
enableMouseAndKeyboard: PREFS.get(Preferences.MKB_ENABLED) && function(funcStr) { enableMouseAndKeyboard: PREFS.get(Preferences.MKB_ENABLED) && function(funcStr) {
if (!funcStr.includes('EnableMouseAndKeyboard:')) { if (!funcStr.includes('EnableMouseAndKeyboard:')) {
@ -4366,6 +4644,32 @@ class Patcher {
}, },
}; };
static #PATCH_ORDERS = [
[
'disableAiTrack',
'disableTelemetry',
],
['tvLayout'],
['enableXcloudLogger'],
[
// 'enableMouseAndKeyboard',
'overrideSettings',
'remotePlayDirectConnectUrl',
'disableTrackEvent',
'enableConsoleLogging',
'remotePlayKeepAlive',
'blockWebRtcStatsCollector',
],
// Only when playing
['remotePlayConnectMode'],
['playVibration'],
['enableConsoleLogging'],
];
static #patchFunctionBind() { static #patchFunctionBind() {
Function.prototype.nativeBind = Function.prototype.bind; Function.prototype.nativeBind = Function.prototype.bind;
Function.prototype.bind = function() { Function.prototype.bind = function() {
@ -4400,39 +4704,82 @@ class Patcher {
}; };
} }
static length() { return Object.keys(Patcher.#PATCHES).length }; static length() { return Patcher.#PATCH_ORDERS.length; };
static patch(item) { static patch(item) {
let patchName; let patchName;
let appliedPatches;
for (let id in item[1]) { for (let id in item[1]) {
if (Patcher.length() <= 0) { if (Patcher.#PATCH_ORDERS.length <= 0) {
return; return;
} }
appliedPatches = [];
const func = item[1][id]; const func = item[1][id];
const funcStr = func.toString(); let funcStr = func.toString();
// Only check the first patch for (let groupIndex = 0; groupIndex < Patcher.#PATCH_ORDERS.length; groupIndex++) {
if (!patchName) { const group = Patcher.#PATCH_ORDERS[groupIndex];
patchName = Object.keys(Patcher.#PATCHES)[0]; let modified = false;
}
const patchedFuncStr = Patcher.#PATCHES[patchName].call(null, funcStr); for (let patchIndex = 0; patchIndex < group.length; patchIndex++) {
if (patchedFuncStr) { const patchName = group[patchIndex];
console.log(`[Better xCloud] Applied "${patchName}" patch`); if (appliedPatches.indexOf(patchName) > -1) {
continue;
}
item[1][id] = eval(patchedFuncStr); const patchedFuncStr = Patcher.#PATCHES[patchName].call(null, funcStr);
delete Patcher.#PATCHES[patchName]; if (!patchedFuncStr) {
patchName = null; // Only stop if the first patch is failed
if (patchIndex === 0) {
break;
} else {
continue;
}
}
modified = true;
funcStr = patchedFuncStr;
console.log(`[Better xCloud] Applied "${patchName}" patch`);
appliedPatches.push(patchName);
// Remove patch from group
group.splice(patchIndex, 1);
patchIndex--;
}
// Apply patched functions
if (modified) {
item[1][id] = eval(funcStr);
}
// Remove empty group
if (!group.length) {
Patcher.#PATCH_ORDERS.splice(groupIndex, 1);
groupIndex--;
}
} }
} }
} }
static initialize() { static initialize() {
// Remove disabled patches // Remove disabled patches
for (const patchName in Patcher.#PATCHES) { for (let groupIndex = Patcher.#PATCH_ORDERS.length - 1; groupIndex >= 0; groupIndex--) {
if (!Patcher.#PATCHES[patchName]) { const group = Patcher.#PATCH_ORDERS[groupIndex];
delete Patcher.#PATCHES[patchName];
for (let patchIndex = group.length - 1; patchIndex >= 0; patchIndex--) {
const patchName = group[patchIndex];
if (!Patcher.#PATCHES[patchName]) {
// Remove disabled patch
group.splice(patchIndex, 1);
}
}
// Remove empty group
if (!group.length) {
Patcher.#PATCH_ORDERS.splice(groupIndex, 1);
} }
} }
@ -5002,7 +5349,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
bottom: 20px; bottom: 20px;
z-index: var(--bx-stream-settings-z-index); z-index: var(--bx-stream-settings-z-index);
padding: 8px; padding: 8px;
width: 220px; width: 320px;
background: #1a1b1e; background: #1a1b1e;
color: #fff; color: #fff;
border-radius: 8px 0 0 8px; border-radius: 8px 0 0 8px;
@ -5075,6 +5422,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: var(--bx-monospaced-font); font-family: var(--bx-monospaced-font);
} }
.bx-quick-settings-bar-note {
font-size: 12px;
font-weight: lighter;
font-style: italic;
}
.bx-toast { .bx-toast {
position: fixed; position: fixed;
left: 50%; left: 50%;
@ -5689,7 +6042,7 @@ function interceptHttpRequests() {
} }
// Start rendering UI // Start rendering UI
if (!document.getElementById('gamepass-root')) { if (document.querySelector('div[class^=UnsupportedMarketPage]')) {
setTimeout(watchHeader, 2000); setTimeout(watchHeader, 2000);
} else { } else {
watchHeader(); watchHeader();
@ -5807,6 +6160,7 @@ function interceptHttpRequests() {
// Enable touch controller // Enable touch controller
if (TouchController.isEnabled()) { if (TouchController.isEnabled()) {
overrides.inputConfiguration = overrides.inputConfiguration || {}; overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.enableVibration = true;
overrides.inputConfiguration.enableTouchInput = true; overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10; overrides.inputConfiguration.maxTouchPoints = 10;
} }
@ -6504,30 +6858,54 @@ function setupVideoSettingsBar() {
let $stretchInp; let $stretchInp;
const $wrapper = CE('div', {'class': 'bx-quick-settings-bar'}, const $wrapper = CE('div', {'class': 'bx-quick-settings-bar'},
CE('h2', {}, __('controller')),
CE('div', {},
CE('label', {}, __('controller-vibration')),
VibrationManager.supportControllerVibration() && PREFS.toElement(Preferences.CONTROLLER_ENABLE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportControllerVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
CE('div', {},
CE('label', {}, __('device-vibration')),
VibrationManager.supportDeviceVibration() && PREFS.toElement(Preferences.CONTROLLER_DEVICE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportDeviceVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) &&
CE('div', {},
CE('label', {}, __('vibration-intensity')),
PREFS.toNumberStepper(Preferences.CONTROLLER_VIBRATION_INTENSITY, VibrationManager.updateGlobalVars, {suffix: '%', ticks: 50}),
),
CE('h2', {}, __('audio')), CE('h2', {}, __('audio')),
CE('div', {}, CE('div', {},
CE('label', {}, __('volume')), CE('label', {}, __('volume')),
PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => { PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2)); STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
}, {suffix: '%', ticks: 100, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)})), }, {suffix: '%', ticks: 100, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)}),
),
CE('h2', {}, __('video')), CE('h2', {}, __('video')),
CE('div', {'class': 'bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`), CE('div', {'class': 'bx-quick-settings-bar-note bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'data-type': 'video'}, CE('div', {'data-type': 'video'},
CE('label', {'for': 'bx-quick-setting-stretch'}, __('ratio')), CE('label', {}, __('ratio')),
PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange)), PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange),
),
CE('div', {'data-type': 'video'}, CE('div', {'data-type': 'video'},
CE('label', {}, __('clarity')), CE('label', {}, __('clarity')),
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true})), // disable this feature in Safari PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true}), // disable this feature in Safari
),
CE('div', {'data-type': 'video'}, CE('div', {'data-type': 'video'},
CE('label', {}, __('saturation')), CE('label', {}, __('saturation')),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25})), PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'}, CE('div', {'data-type': 'video'},
CE('label', {}, __('contrast')), CE('label', {}, __('contrast')),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25})), PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'}, CE('div', {'data-type': 'video'},
CE('label', {}, __('brightness')), CE('label', {}, __('brightness')),
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25})) PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}),
),
); );
document.documentElement.appendChild($wrapper); document.documentElement.appendChild($wrapper);
@ -6704,6 +7082,9 @@ function onStreamStarted($video) {
const allAudioCodecs = {}; const allAudioCodecs = {};
let audioCodecId; let audioCodecId;
const allCandidates = {};
let candidateId;
stats.forEach(stat => { stats.forEach(stat => {
if (stat.type == 'codec') { if (stat.type == 'codec') {
const mimeType = stat.mimeType.split('/'); const mimeType = stat.mimeType.split('/');
@ -6721,6 +7102,10 @@ function onStreamStarted($video) {
} else if (stat.kind === 'audio') { } else if (stat.kind === 'audio') {
audioCodecId = stat.codecId; audioCodecId = stat.codecId;
} }
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
candidateId = stat.remoteCandidateId;
} else if (stat.type === 'remote-candidate') {
allCandidates[stat.id] = stat.address;
} }
}); });
@ -6748,6 +7133,12 @@ function onStreamStarted($video) {
} }
} }
// Get server type
if (candidateId) {
console.log(candidateId, allCandidates);
StreamBadges.ipv6 = allCandidates[candidateId].includes(':');
}
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) { if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
StreamStats.start(); StreamStats.start();
} }
@ -6845,20 +7236,12 @@ if (PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)) {
} }
} }
RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate;
RTCPeerConnection.prototype.addIceCandidate = function(...args) {
const candidate = args[0].candidate;
if (candidate && candidate.startsWith('a=candidate:1 ')) {
StreamBadges.ipv6 = candidate.substring(20).includes(':');
}
return this.orgAddIceCandidate.apply(this, args);
}
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') { if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.setup(); TouchController.setup();
} }
VibrationManager.initialSetup();
const OrgRTCPeerConnection = window.RTCPeerConnection; const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() { window.RTCPeerConnection = function() {
const peer = new OrgRTCPeerConnection(); const peer = new OrgRTCPeerConnection();