Compare commits

...

12 Commits
v2.1 ... v2.1.2

Author SHA1 Message Date
0870065a81 Bump version to 2.1.2 2023-12-19 18:10:50 +07:00
e0f0617b12 Update translations 2023-12-19 18:10:30 +07:00
38623cc086 Fix Remote Play (#188)
* Try to fix remote play not working when not using local network

* Fix Server badge reporting incorrect info

* Refactor remote play requests in window.fetch

* Fix exceptions

* Stop overriding WebSocket class

* Fix not connecting to IPv6 server as expected
2023-12-19 18:08:14 +07:00
bbce49791f Bump version to 2.1.1 2023-12-14 17:52:40 +07:00
d719f0c2b5 Update translations 2023-12-14 17:52:19 +07:00
622057980d Update README.md 2023-12-14 17:50:47 +07:00
24d608bc3e Redesign Quick Settings sidebar (#186)
* Redesign Quick Settings bar

* Move stream stats settings to quick settings bar

* Add "for" attributes to labels

* Minor optimization

* Stop rendering Toast

* Don't render UI elements when not playing
2023-12-14 17:44:12 +07:00
f55344b4cb Update README.md 2023-12-14 09:41:00 +07:00
6139fb386b Update README.md 2023-12-14 09:14:37 +07:00
f7c46c5ef3 Update README.md 2023-12-14 08:03:27 +07:00
f5b495efa8 Update README.md 2023-12-13 08:33:21 +07:00
eb4803492e Update README.md 2023-12-13 08:07:39 +07:00
3 changed files with 337 additions and 281 deletions

View File

@ -1,9 +1,10 @@
# Better xCloud # Better xCloud
Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play). Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play). It also allows you to use Remote Play on the xCloud website.
The main target of this script is mobile users, but it should work great on desktop too.
Supported platforms: **Supported platforms:**
- Windows, macOS, Linux - Windows
- macOS
- Linux, SteamOS (Steam Deck)
- Android, Android TV - Android, Android TV
- iOS, iPadOS - iOS, iPadOS
@ -35,8 +36,8 @@ If you like this project please give it a 🌟. Thank you 🙏.
- [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js) - [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
<!-- - [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)--> <!-- - [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)-->
I only distribute **Better xCloud** on GitHub, *DO NOT* download it on other websites or from unknown sources. I only distribute **Better xCloud** on GitHub, *DO NOT* download it on other websites or from unknown sources.
4. Refresh [xCloud web page](https://www.xbox.com/play/). 4. Refresh the [xCloud web page](https://www.xbox.com/play/).
5. Click on the new "SERVER NAME" button next to your profile picture to adjust settings. 5. Click on the new *\<SERVER NAME\>* button next to your profile picture to adjust settings.
To update manually, just install the script again (you won't lose your settings). To update manually, just install the script again (you won't lose your settings).
@ -49,12 +50,12 @@ To update manually, just install the script again (you won't lose your settings)
- = unavailable - = unavailable
- 🗒️ = see custom notes - 🗒️ = see custom notes
| | Windows/Linux | macOS | Android/Android TV | iOS | | | Windows/Linux/SteamOS | macOS | Android/Android TV | iOS |
|-----------------------------------------|:-----------------|:-----------------|:-------------------|:-----------------| |-----------------------------------------|:----------------------|:-----------------|:-------------------|:-----------------|
| Chrome/Edge/Chromium variants | 👍 | 👍 | ❌ | ❌ | | Chrome/Edge/Chromium... | 👍 | 👍 | ❌ | ❌ |
| Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ | | Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ |
| Safari | | ✅<sup>(2)</sup> | | ✅<sup>(3)</sup> | | Safari | | ✅<sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [Kiwi Browser](https://kiwibrowser.com) | | | 👍 | | | [Kiwi Browser](https://kiwibrowser.com) | | | 👍 | |
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**. Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
@ -73,8 +74,7 @@ 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/f7df312c-e6bc-49a1-8239-24c280ed7fa6"> <img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/ed513cb3-6e6c-4e8e-9e06-c62e71e41c90">
&nbsp; &nbsp;
@ -88,7 +88,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
- **🔥 Capture screenshot** - **🔥 Capture screenshot**
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info. > Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
- **🔥 Hold the "Quit game" button for one second to refresh the stream** - **🔥 Hold the "Quit game" button for one second to refresh the stream**
> Sometimes you can fix the bad connection to the stream simply by refreshing the page. > Sometimes you can fix the bad connection to the stream or low FPS simply by refreshing the page.
> Useful on mobile where the pull-to-refresh feature doesn't work while playing. > Useful on mobile where the pull-to-refresh feature doesn't work while playing.
- **🔥 Touch controller** - **🔥 Touch controller**
> Enable touch controller support for all games. > Enable touch controller support for all games.
@ -217,13 +217,12 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
<sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on. <sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on.
## Stream stats ## Stream stats
<img alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/9fb51941-85a9-47c4-8d48-331456b9ce73">
![stats](https://github.com/redphx/better-xcloud/assets/96280/736548db-316d-4bb3-a0f8-467766ae810b) <img width="418" alt="Stream stats settings" src="https://github.com/redphx/better-xcloud/assets/96280/6313a0c6-03bf-4325-b60d-18a23c681933">
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/142625ea-20ab-4392-a111-0c5bc08bae09">
- While playing > `...` > `Stream Stats`. - While playing > `...` > `Stream Stats`.
- Double-click on the stats bar to show the Settings dialog. - Change settings by opening `Stream settings` while playing.
- This bar is updated every second. - This bar is updated every second.
- **Quick glance** feature: only show the stats bar when the System menu is expanded. The 👀 emoji at the beginning indicates that the stats bar is in the quick glance mode. - **Quick glance** feature: only show the stats bar when the System menu is expanded. The 👀 emoji at the beginning indicates that the stats bar is in the quick glance mode.
- ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream. - ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream.
@ -302,7 +301,7 @@ It's a reference to an Userscript called "better360" that I created many years a
- **Korean**: [@rightones](https://github.com/rightones) - **Korean**: [@rightones](https://github.com/rightones)
- **Italian**: Greenylie, Rakan129, Carza-104, graziequalcuno, DioCannabinoide - **Italian**: Greenylie, Rakan129, Carza-104, graziequalcuno, DioCannabinoide
- **Japanese**: Tak_attack, udonshi - **Japanese**: Tak_attack, udonshi
- **Portuguese (Brazilian)**: [@ricardo404](https://github.com/ricardo404), [@Haisom](https://github.com/Haisom), italorafael22062009, PotatoPTT, guilhermecursi - **Portuguese (Brazilian)**: [@ricardo404](https://github.com/ricardo404), [@Haisom](https://github.com/Haisom), italorafael22062009, PotatoPTT, guilhermecursi, renatomaster01
- **Polish**: [@aleksishere](https://github.com/aleksishere) - **Polish**: [@aleksishere](https://github.com/aleksishere)
- **Russian**: anpom6, soophik - **Russian**: anpom6, soophik
- **Spanish**: [@PabloSebas](https://github.com/PabloSebas), csvnchzn - **Spanish**: [@PabloSebas](https://github.com/PabloSebas), csvnchzn

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.1 // @version 2.1.2
// ==/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.1 // @version 2.1.2
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -13,11 +13,12 @@
// ==/UserScript== // ==/UserScript==
'use strict'; 'use strict';
const SCRIPT_VERSION = '2.1'; const SCRIPT_VERSION = '2.1.2';
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; const ENABLE_XCLOUD_LOGGER = false;
const ENABLE_PRELOAD_BX_UI = false;
console.log(`[Better xCloud] readyState: ${document.readyState}`); console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -305,7 +306,7 @@ const Translations = {
"ru-RU": "Яркость", "ru-RU": "Яркость",
"tr-TR": "Aydınlık", "tr-TR": "Aydınlık",
"uk-UA": "Яскравість", "uk-UA": "Яскравість",
"vi-VN": ộ sáng", "vi-VN": ộ sáng",
"zh-CN": "亮度", "zh-CN": "亮度",
}, },
"browser-unsupported-feature": { "browser-unsupported-feature": {
@ -474,22 +475,12 @@ const Translations = {
"vi-VN": "Bộ điều khiển", "vi-VN": "Bộ điều khiển",
"zh-CN": "手柄", "zh-CN": "手柄",
}, },
"controller-polling-rate": {
"de-DE": "Controller-Abfragerate",
"en-US": "Controller polling rate",
"es-ES": "Tasa de sondeo del Joystick",
"ja-JP": "コントローラーポーリングレート",
"pl-PL": "Częstotliwości raportowania kontrolera",
"pt-BR": "Taxa de consulta do controle",
"ru-RU": "Частота опроса контроллера",
"tr-TR": "Oyun kumandası işlem hızı",
"uk-UA": "Частота опитувань контролера",
"vi-VN": "Tần suất cập nhật của bộ điều khiển",
},
"controller-vibration": { "controller-vibration": {
"de-DE": "Vibration des Controllers", "de-DE": "Vibration des Controllers",
"en-US": "Controller vibration", "en-US": "Controller vibration",
"ja-JP": "コントローラーの振動", "ja-JP": "コントローラーの振動",
"pt-BR": "Vibração do controle",
"ru-RU": "Вибрация контроллера",
"tr-TR": "Oyun kumandası titreşimi", "tr-TR": "Oyun kumandası titreşimi",
"vi-VN": "Rung bộ điều khiển", "vi-VN": "Rung bộ điều khiển",
}, },
@ -545,14 +536,18 @@ const Translations = {
"de-DE": "Vibration des Geräts", "de-DE": "Vibration des Geräts",
"en-US": "Device vibration", "en-US": "Device vibration",
"ja-JP": "デバイスの振動", "ja-JP": "デバイスの振動",
"pt-BR": "Vibração do dispositivo",
"ru-RU": "Вибрация устройства",
"tr-TR": "Cihaz titreşimi", "tr-TR": "Cihaz titreşimi",
"uk-UA": "Вібрація пристрою", "uk-UA": "Вібрація пристрою",
"vi-VN": "Rung thiết bị", "vi-VN": "Rung thiết bị",
}, },
"device-vibration-not-using-gamepad": { "device-vibration-not-using-gamepad": {
"de-DE": "Aktiviert, wenn kein Gamepad verwendet wird", "de-DE": "An, wenn kein Gamepad verbunden",
"en-US": "On when not using gamepad", "en-US": "On when not using gamepad",
"ja-JP": "ゲームパッド未使用時にオン", "ja-JP": "ゲームパッド未使用時にオン",
"pt-BR": "Ativar quando não estiver usando o dispositivo",
"ru-RU": "Включить когда не используется геймпад",
"tr-TR": "Oyun kumandası bağlanmadan titreşim", "tr-TR": "Oyun kumandası bağlanmadan titreşim",
"uk-UA": "Увімкнена, коли не використовується геймпад", "uk-UA": "Увімкнена, коли не використовується геймпад",
"vi-VN": "Bật khi không dùng tay cầm", "vi-VN": "Bật khi không dùng tay cầm",
@ -1623,22 +1618,6 @@ const Translations = {
"vi-VN": "Stream", "vi-VN": "Stream",
"zh-CN": "串流", "zh-CN": "串流",
}, },
"stream-stats-settings": {
"de-DE": "Stream Statistik Einstellungen",
"en-US": "Stream stats settings",
"es-ES": "Ajustes de estadísticas de stream",
"fr-FR": "Paramètres des statistiques du stream",
"it-IT": "Impostazioni statistiche dello streaming",
"ja-JP": "ストリーミング統計の設定",
"ko-KR": "스트리밍 통계 설정",
"pl-PL": "Ustawienia statystyk strumienia",
"pt-BR": "Ajustes de estatísticas",
"ru-RU": "Настройки потоковой передачи",
"tr-TR": "Yayın durumu ayarları",
"uk-UA": "Налаштування статистики трансляції",
"vi-VN": "Cấu hình thông số của stream",
"zh-CN": "串流统计信息设置",
},
"stretch": { "stretch": {
"de-DE": "Strecken", "de-DE": "Strecken",
"en-US": "Stretch", "en-US": "Stretch",
@ -1660,6 +1639,7 @@ const Translations = {
"en-US": "Swap buttons", "en-US": "Swap buttons",
"ja-JP": "ボタン入れ替え", "ja-JP": "ボタン入れ替え",
"pt-BR": "Trocar botões", "pt-BR": "Trocar botões",
"ru-RU": "Поменять кнопки",
"tr-TR": "Düğme düzenini ters çevir", "tr-TR": "Düğme düzenini ters çevir",
"uk-UA": "Поміняти кнопки місцями", "uk-UA": "Поміняти кнопки місцями",
"vi-VN": "Hoán đổi nút", "vi-VN": "Hoán đổi nút",
@ -1961,6 +1941,8 @@ const Translations = {
"de-DE": "Vibrationsstärke", "de-DE": "Vibrationsstärke",
"en-US": "Vibration intensity", "en-US": "Vibration intensity",
"ja-JP": "振動の強さ", "ja-JP": "振動の強さ",
"pt-BR": "Intensidade da vibração",
"ru-RU": "Сила вибрации",
"tr-TR": "Titreşim gücü", "tr-TR": "Titreşim gücü",
"vi-VN": "Cường độ rung", "vi-VN": "Cường độ rung",
}, },
@ -3224,7 +3206,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.packetsReceived > 0 && stat.state === 'succeeded') {
totalIn += stat.bytesReceived; totalIn += stat.bytesReceived;
totalOut += stat.bytesSent; totalOut += stat.bytesSent;
} }
@ -3365,8 +3347,6 @@ class StreamStats {
static #$fl; static #$fl;
static #$br; static #$br;
static #$dialog;
static #lastStat; static #lastStat;
static #quickGlanceObserver; static #quickGlanceObserver;
@ -3391,8 +3371,10 @@ class StreamStats {
StreamStats.#interval = null; StreamStats.#interval = null;
StreamStats.#lastStat = null; StreamStats.#lastStat = null;
StreamStats.#$container.removeAttribute('data-display'); if (StreamStats.#$container) {
StreamStats.#$container.classList.add('bx-gone'); StreamStats.#$container.removeAttribute('data-display');
StreamStats.#$container.classList.add('bx-gone');
}
} }
static toggle() { static toggle() {
@ -3409,8 +3391,8 @@ class StreamStats {
StreamStats.hideSettingsUi(); StreamStats.hideSettingsUi();
} }
static isHidden = () => StreamStats.#$container.classList.contains('bx-gone'); static isHidden = () => StreamStats.#$container && StreamStats.#$container.classList.contains('bx-gone');
static isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing'; static isGlancing = () => StreamStats.#$container && StreamStats.#$container.getAttribute('data-display') === 'glancing';
static quickGlanceSetup() { static quickGlanceSetup() {
if (StreamStats.#quickGlanceObserver) { if (StreamStats.#quickGlanceObserver) {
@ -3489,7 +3471,7 @@ class StreamStats {
} }
StreamStats.#lastStat = stat; StreamStats.#lastStat = stat;
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') { } else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') {
// Round Trip Time // Round Trip Time
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???'; const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
StreamStats.#$ping.textContent = roundTripTime; StreamStats.#$ping.textContent = roundTripTime;
@ -3503,18 +3485,19 @@ class StreamStats {
}); });
} }
static #refreshStyles() { static refreshStyles() {
const PREF_ITEMS = PREFS.get(Preferences.STATS_ITEMS); const PREF_ITEMS = PREFS.get(Preferences.STATS_ITEMS);
const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION); const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION);
const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT); const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT);
const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY); const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY);
const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE); const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE);
StreamStats.#$container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']'); const $container = StreamStats.#$container;
StreamStats.#$container.setAttribute('data-position', PREF_POSITION); $container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']');
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT); $container.setAttribute('data-position', PREF_POSITION);
StreamStats.#$container.style.opacity = PREF_OPACITY + '%'; $container.setAttribute('data-transparent', PREF_TRANSPARENT);
StreamStats.#$container.style.fontSize = PREF_TEXT_SIZE; $container.style.opacity = PREF_OPACITY + '%';
$container.style.fontSize = PREF_TEXT_SIZE;
} }
static hideSettingsUi() { static hideSettingsUi() {
@ -3523,10 +3506,6 @@ class StreamStats {
} }
} }
static #toggleSettingsUi() {
StreamStats.#$dialog.toggle();
}
static render() { static render() {
if (StreamStats.#$container) { if (StreamStats.#$container) {
return; return;
@ -3549,79 +3528,9 @@ class StreamStats {
} }
StreamStats.#$container = CE('div', {'class': 'bx-stats-bar bx-gone'}, $barFragment); StreamStats.#$container = CE('div', {'class': 'bx-stats-bar bx-gone'}, $barFragment);
let clickTimeout;
StreamStats.#$container.addEventListener('mousedown', e => {
clearTimeout(clickTimeout);
if (clickTimeout) {
// Double-clicked
clickTimeout = null;
StreamStats.#toggleSettingsUi();
return;
}
clickTimeout = setTimeout(() => {
clickTimeout = null;
}, 400);
});
document.documentElement.appendChild(StreamStats.#$container); document.documentElement.appendChild(StreamStats.#$container);
const refreshFunc = e => { StreamStats.refreshStyles();
StreamStats.#refreshStyles()
};
let $close;
const STATS_UI = {
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
'label': __('show-stats-on-startup'),
},
[Preferences.STATS_QUICK_GLANCE]: {
'label': __('enable-quick-glance-mode'),
'onChange': e => {
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
},
},
[Preferences.STATS_ITEMS]: {
'label': __('stats'),
'onChange': refreshFunc,
},
[Preferences.STATS_POSITION]: {
'label': __('position'),
'onChange': refreshFunc,
},
[Preferences.STATS_TEXT_SIZE]: {
'label': __('text-size'),
'onChange': refreshFunc,
},
[Preferences.STATS_OPACITY]: {
'label': `${__('opacity')} (50-100%)`,
'onChange': refreshFunc,
},
[Preferences.STATS_TRANSPARENT]: {
'label': __('transparent-background'),
'onChange': refreshFunc,
},
[Preferences.STATS_CONDITIONAL_FORMATTING]: {
'label': __('conditional-formatting'),
'onChange': refreshFunc,
},
};
const $fragment = document.createDocumentFragment();
for (let settingKey in STATS_UI) {
const setting = STATS_UI[settingKey];
$fragment.appendChild(CE('div', {},
CE('label', {'for': `xcloud_setting_${settingKey}`}, setting.label),
PREFS.toElement(settingKey, setting.onChange)
));
}
StreamStats.#$dialog = new Dialog(__('stream-stats-settings'), 'bx-stats-settings-dialog', $fragment, StreamStats.hideSettingsUi);
StreamStats.#refreshStyles();
} }
} }
@ -4278,7 +4187,7 @@ class Preferences {
let $control; let $control;
if ('options' in setting) { if ('options' in setting) {
$control = CE('select', {'id': 'xcloud_setting_' + key}); $control = CE('select', {'id': `bx_setting_${key}`});
for (let value in setting.options) { for (let value in setting.options) {
const label = setting.options[value]; const label = setting.options[value];
@ -4293,7 +4202,7 @@ class Preferences {
onChange && onChange(e); onChange && onChange(e);
}); });
} else if ('multiple_options' in setting) { } else if ('multiple_options' in setting) {
$control = CE('select', {'id': 'xcloud_setting_' + key, 'multiple': true}); $control = CE('select', {'id': `bx_setting_${key}`, 'multiple': true});
for (let value in setting.multiple_options) { for (let value in setting.multiple_options) {
const label = setting.multiple_options[value]; const label = setting.multiple_options[value];
@ -4348,7 +4257,7 @@ class Preferences {
}); });
} }
$control.id = `xcloud_setting_${key}`; $control.id = `bx_setting_${key}`;
return $control; return $control;
} }
@ -4877,11 +4786,11 @@ function addCss() {
--bx-monospaced-font: Consolas, "Courier New", Courier, monospace; --bx-monospaced-font: Consolas, "Courier New", Courier, monospace;
--bx-wait-time-box-z-index: 9999; --bx-wait-time-box-z-index: 9999;
--bx-stream-settings-z-index: 9999; --bx-stats-bar-z-index: 9001;
--bx-stream-settings-z-index: 9000;
--bx-screenshot-z-index: 8888; --bx-screenshot-z-index: 8888;
--bx-touch-controller-bar-z-index: 5555; --bx-touch-controller-bar-z-index: 5555;
--bx-dialog-z-index: 1010; --bx-dialog-z-index: 1010;
--bx-stats-bar-z-index: 1000;
--bx-dialog-overlay-z-index: 900; --bx-dialog-overlay-z-index: 900;
} }
@ -5345,20 +5254,19 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
-webkit-user-select: none; -webkit-user-select: none;
position: fixed; position: fixed;
right: 0; right: 0;
top: 20px; top: 0;
bottom: 20px; bottom: 0;
z-index: var(--bx-stream-settings-z-index); z-index: var(--bx-stream-settings-z-index);
padding: 8px; padding: 16px;
width: 320px; width: 420px;
background: #1a1b1e; background: #1a1b1e;
color: #fff; color: #fff;
border-radius: 8px 0 0 8px;
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
font-family: var(--bx-title-font); font-family: var(--bx-title-font);
text-align: center; text-align: center;
box-shadow: 0px 0px 6px #000; box-shadow: 0px 0px 6px #000;
opacity: 0.95; opacity: 0.98;
overflow: overlay; overflow: overlay;
} }
@ -5385,27 +5293,34 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.bx-quick-settings-bar > div { .bx-quick-settings-bar > div {
display: flex;
border-bottom: 1px solid #40404080;
margin-bottom: 16px; margin-bottom: 16px;
padding-bottom: 16px;
} }
.bx-quick-settings-bar h2 { .bx-quick-settings-bar h2 {
font-size: 32px; font-size: 28px;
font-weight: bold; font-weight: bold;
margin-bottom: 8px; margin-bottom: 8px;
text-transform: uppercase;
text-align: left;
} }
.bx-quick-settings-bar input[type="range"] { .bx-quick-settings-bar input[type="range"] {
display: block; display: block;
margin: 12px auto; margin: 12px auto 2px;
width: 80%; width: 180px;
color: #959595 !important; color: #959595 !important;
} }
.bx-quick-settings-bar label { .bx-quick-settings-bar label {
font-size: 16px; font-size: 16px;
font-weight: bold;
display: block; display: block;
margin-bottom: 8px; text-align: left;
flex: 1;
align-self: center;
margin-bottom: 0 !important;
} }
.bx-quick-settings-bar button { .bx-quick-settings-bar button {
@ -5423,9 +5338,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.bx-quick-settings-bar-note { .bx-quick-settings-bar-note {
display: block;
text-align: center;
font-size: 12px; font-size: 12px;
font-weight: lighter; font-weight: lighter;
font-style: italic; font-style: italic;
padding-top: 16px;
} }
.bx-toast { .bx-toast {
@ -5774,7 +5692,7 @@ function getPreferredServerRegion() {
} }
function updateIceCandidates(candidates) { function updateIceCandidates(candidates, options) {
const pattern = new RegExp(/a=candidate:(?<foundation>\d+) (?<component>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<the_rest>.*)/); const pattern = new RegExp(/a=candidate:(?<foundation>\d+) (?<component>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<the_rest>.*)/);
const lst = []; const lst = [];
@ -5787,13 +5705,15 @@ function updateIceCandidates(candidates) {
lst.push(groups); lst.push(groups);
} }
lst.sort((a, b) => (a.ip.includes(':') || a.ip > b.ip) ? -1 : 1); if (options.preferIpv6Server) {
lst.sort((a, b) => (!a.ip.includes(':') && b.ip.includes(':')) ? 1 : -1);
}
const newCandidates = []; const newCandidates = [];
let foundation = 1; let foundation = 1;
lst.forEach(item => { lst.forEach(item => {
item.foundation = foundation; item.foundation = foundation;
item.priority = (foundation == 1) ? 100 : 1; item.priority = (foundation == 1) ? 10000 : 1;
newCandidates.push({ newCandidates.push({
'candidate': `a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`, 'candidate': `a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`,
@ -5805,6 +5725,15 @@ function updateIceCandidates(candidates) {
++foundation; ++foundation;
}); });
if (options.consoleIp) {
newCandidates.push({
'candidate': `a=candidate:${newCandidates.length + 1} 1 UDP 1 ${options.consoleIp} 9002 typ host`,
'messageType': 'iceCandidate',
'sdpMLineIndex': '0',
'sdpMid': '0',
});
}
newCandidates.push({ newCandidates.push({
'candidate': 'a=end-of-candidates', 'candidate': 'a=end-of-candidates',
'messageType': 'iceCandidate', 'messageType': 'iceCandidate',
@ -5812,6 +5741,7 @@ function updateIceCandidates(candidates) {
'sdpMid': '0', 'sdpMid': '0',
}); });
console.log(newCandidates);
return newCandidates; return newCandidates;
} }
@ -5837,11 +5767,6 @@ function interceptHttpRequests() {
} }
if (PREFS.get(Preferences.BLOCK_SOCIAL_FEATURES)) { if (PREFS.get(Preferences.BLOCK_SOCIAL_FEATURES)) {
// Disable WebSocket
WebSocket = {
CLOSING: 2,
};
BLOCKED_URLS = BLOCKED_URLS.concat([ BLOCKED_URLS = BLOCKED_URLS.concat([
'https://peoplehub.xboxlive.com/users/me', 'https://peoplehub.xboxlive.com/users/me',
'https://accounts.xboxlive.com/family/memberXuid', 'https://accounts.xboxlive.com/family/memberXuid',
@ -5883,13 +5808,15 @@ function interceptHttpRequests() {
const PREF_OVERRIDE_CONFIGURATION = PREF_AUDIO_MIC_ON_PLAYING || PREF_STREAM_TOUCH_CONTROLLER === 'all'; const PREF_OVERRIDE_CONFIGURATION = PREF_AUDIO_MIC_ON_PLAYING || PREF_STREAM_TOUCH_CONTROLLER === 'all';
const orgFetch = window.fetch; const orgFetch = window.fetch;
let consoleIp;
let consolePort;
const patchIpv6 = function(...arg) { const patchIceCandidates = function(...arg) {
// ICE server candidates // ICE server candidates
const request = arg[0]; const request = arg[0];
const url = (typeof request === 'string') ? request : request.url; const url = (typeof request === 'string') ? request : request.url;
if (PREF_PREFER_IPV6_SERVER && url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') { if (url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') {
const promise = orgFetch(...arg); const promise = orgFetch(...arg);
return promise.then(response => { return promise.then(response => {
@ -5898,9 +5825,14 @@ function interceptHttpRequests() {
return response; return response;
} }
const options = {
preferIpv6Server: PREF_PREFER_IPV6_SERVER,
consoleIp: consoleIp,
};
const obj = JSON.parse(text); const obj = JSON.parse(text);
let exchangeResponse = JSON.parse(obj.exchangeResponse); let exchangeResponse = JSON.parse(obj.exchangeResponse);
exchangeResponse = updateIceCandidates(exchangeResponse) exchangeResponse = updateIceCandidates(exchangeResponse, options)
obj.exchangeResponse = JSON.stringify(exchangeResponse); obj.exchangeResponse = JSON.stringify(exchangeResponse);
response.json = () => Promise.resolve(obj); response.json = () => Promise.resolve(obj);
@ -5916,19 +5848,21 @@ function interceptHttpRequests() {
window.fetch = async (...arg) => { window.fetch = async (...arg) => {
let request = arg[0]; let request = arg[0];
const url = (typeof request === 'string') ? request : request.url; let url = (typeof request === 'string') ? request : request.url;
// Remote Play if (url.endsWith('/play')) {
if (IS_REMOTE_PLAYING && url.includes('/home/play')) { // Setup UI
setupBxUi();
}
if (IS_REMOTE_PLAYING && url.includes('/sessions/home')) {
const clone = request.clone(); const clone = request.clone();
const cloneBody = await clone.json();
cloneBody.settings.osName = 'windows';
// Clone headers
const headers = {}; const headers = {};
for (const pair of clone.headers.entries()) { for (const pair of clone.headers.entries()) {
headers[pair[0]] = pair[1]; headers[pair[0]] = pair[1];
} }
headers['authorization'] = `Bearer ${RemotePlay.XHOME_TOKEN}`;
const deviceInfo = RemotePlay.BASE_DEVICE_INFO; const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
if (PREFS.get(Preferences.REMOTE_PLAY_RESOLUTION) === '720p') { if (PREFS.get(Preferences.REMOTE_PLAY_RESOLUTION) === '720p') {
@ -5936,16 +5870,41 @@ function interceptHttpRequests() {
} }
headers['x-ms-device-info'] = JSON.stringify(deviceInfo); headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
headers['authorization'] = `Bearer ${RemotePlay.XHOME_TOKEN}`;
request = new Request('https://wus2.gssv-play-prodxhome.xboxlive.com/v5/sessions/home/play', { const opts = {
method: 'POST', method: clone.method,
body: JSON.stringify(cloneBody),
headers: headers, headers: headers,
}); };
arg[0] = request;
return orgFetch(...arg); if (clone.method === 'POST') {
opts.body = await clone.text();
}
const index = request.url.indexOf('.xboxlive.com');
let newUrl = 'https://wus2.gssv-play-prodxhome' + request.url.substring(index);
request = new Request(newUrl, opts);
arg[0] = request;
url = (typeof request === 'string') ? request : request.url;
// Get console IP
if (url.includes('/configuration')) {
const promise = orgFetch(...arg);
return promise.then(response => {
return response.clone().json().then(obj => {
console.log(obj);
consoleIp = obj.serverDetails.ipAddress;
consolePort = obj.serverDetails.port;
response.json = () => Promise.resolve(obj);
return response;
});
});
}
return patchIceCandidates(...arg) || orgFetch(...arg);
} }
if (IS_REMOTE_PLAYING && url.includes('/login/user')) { if (IS_REMOTE_PLAYING && url.includes('/login/user')) {
@ -5992,38 +5951,10 @@ function interceptHttpRequests() {
return orgFetch(...arg); return orgFetch(...arg);
} }
if (url.includes('/sessions/home')) {
const clone = request.clone();
const headers = {};
for (const pair of clone.headers.entries()) {
headers[pair[0]] = pair[1];
}
headers['authorization'] = `Bearer ${RemotePlay.XHOME_TOKEN}`;
const opts = {
method: clone.method,
headers: headers,
};
if (clone.method === 'POST') {
opts.body = await clone.text();
}
const index = request.url.indexOf('.xboxlive.com');
request = new Request('https://wus2.gssv-play-prodxhome' + request.url.substring(index), opts);
arg[0] = request;
return patchIpv6(...arg) || orgFetch(...arg);
}
// ICE server candidates // ICE server candidates
if (!IS_REMOTE_PLAYING) { const patchedIpv6 = patchIceCandidates(...arg);
const patchedIpv6 = patchIpv6(...arg); if (patchedIpv6) {
if (patchedIpv6) { return patchedIpv6;
return patchedIpv6;
}
} }
// Server list // Server list
@ -6406,7 +6337,7 @@ function injectSettingsButton($parent) {
} else if (settingId === Preferences.SERVER_REGION) { } else if (settingId === Preferences.SERVER_REGION) {
let selectedValue; let selectedValue;
$control = CE('select', {id: 'xcloud_setting_' + settingId}); $control = CE('select', {id: `bx_setting_${settingId}`});
$control.addEventListener('change', e => { $control.addEventListener('change', e => {
PREFS.set(settingId, e.target.value); PREFS.set(settingId, e.target.value);
}); });
@ -6849,65 +6780,178 @@ function patchRtcCodecs() {
} }
function setupVideoSettingsBar() { function setupQuickSettingsBar() {
const CE = createElement; const CE = createElement;
const isSafari = UserAgent.isSafari(); const isSafari = UserAgent.isSafari();
const onVideoChange = e => {
updateVideoPlayerCss(); const SETTINGS_UI = [
{
group: 'controller',
label: __('controller'),
items: {
[Preferences.CONTROLLER_ENABLE_VIBRATION]: {
label: __('controller-vibration'),
unsupported: !VibrationManager.supportControllerVibration(),
onChange: VibrationManager.updateGlobalVars,
},
[Preferences.CONTROLLER_DEVICE_VIBRATION]: {
label: __('device-vibration'),
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars,
},
[Preferences.CONTROLLER_VIBRATION_INTENSITY]: (VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
label: __('vibration-intensity'),
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars,
type: 'number-stepper',
params: {
suffix: '%',
ticks: 50,
},
},
},
},
{
group: 'audio',
label: __('audio'),
items: {
[Preferences.AUDIO_VOLUME]: {
label: __('volume'),
onChange: (e, value) => {
STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
},
type: 'number-stepper',
params: {
suffix: '%',
ticks: 100,
disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL),
},
},
},
},
{
group: 'video',
label: __('video'),
note: CE('div', {'class': 'bx-quick-settings-bar-note bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
items: {
[Preferences.VIDEO_RATIO]: {
label: __('ratio'),
onChange: updateVideoPlayerCss,
},
[Preferences.VIDEO_CLARITY]: {
label: __('clarity'),
onChange: updateVideoPlayerCss,
type: 'number-stepper',
unsupported: isSafari,
params: {
hideSlider: true,
},
},
[Preferences.VIDEO_SATURATION]: {
label: __('saturation'),
onChange: updateVideoPlayerCss,
type: 'number-stepper',
params: {
suffix: '%',
ticks: 25,
},
},
[Preferences.VIDEO_CONTRAST]: {
label: __('contrast'),
onChange: updateVideoPlayerCss,
type: 'number-stepper',
params: {
suffix: '%',
ticks: 25,
},
},
[Preferences.VIDEO_BRIGHTNESS]: {
label: __('brightness'),
onChange: updateVideoPlayerCss,
type: 'number-stepper',
params: {
suffix: '%',
ticks: 25,
},
},
},
},
{
group: 'stats',
label: __('menu-stream-stats'),
items: {
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
label: __('show-stats-on-startup'),
},
[Preferences.STATS_QUICK_GLANCE]: {
label: __('enable-quick-glance-mode'),
onChange: e => {
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
},
},
[Preferences.STATS_ITEMS]: {
label: __('stats'),
onChange: StreamStats.refreshStyles,
},
[Preferences.STATS_POSITION]: {
label: __('position'),
onChange: StreamStats.refreshStyles,
},
[Preferences.STATS_TEXT_SIZE]: {
label: __('text-size'),
onChange: StreamStats.refreshStyles,
},
[Preferences.STATS_OPACITY]: {
label: `${__('opacity')} (50-100%)`,
onChange: StreamStats.refreshStyles,
},
[Preferences.STATS_TRANSPARENT]: {
label: __('transparent-background'),
onChange: StreamStats.refreshStyles,
},
[Preferences.STATS_CONDITIONAL_FORMATTING]: {
label: __('conditional-formatting'),
onChange: StreamStats.refreshStyles,
},
},
},
];
const $wrapper = CE('div', {'class': 'bx-quick-settings-bar'});
for (const settingGroup of SETTINGS_UI) {
$wrapper.appendChild(CE('h2', {}, settingGroup.label));
if (settingGroup.note) {
if (typeof settingGroup.note === 'string') {
settingGroup.note = document.createTextNode(settingGroup.note);
}
$wrapper.appendChild(settingGroup.note);
}
for (const pref in settingGroup.items) {
const setting = settingGroup.items[pref];
if (!setting) {
continue;
}
$wrapper.appendChild(CE('div', {'data-type': settingGroup.group},
CE('label', {for: `bx_setting_${pref}`},
setting.label,
setting.unsupported && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
!setting.unsupported && (setting.type === 'number-stepper' ? PREFS.toNumberStepper(pref, setting.onChange, setting.params) : PREFS.toElement(pref, setting.onChange)),
));
}
} }
let $stretchInp;
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('div', {},
CE('label', {}, __('volume')),
PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
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)}),
),
CE('h2', {}, __('video')),
CE('div', {'class': 'bx-quick-settings-bar-note bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'data-type': 'video'},
CE('label', {}, __('ratio')),
PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('clarity')),
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true}), // disable this feature in Safari
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('saturation')),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('contrast')),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('brightness')),
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}),
),
);
document.documentElement.appendChild($wrapper); document.documentElement.appendChild($wrapper);
} }
@ -7000,7 +7044,7 @@ function patchHistoryMethod(type) {
function onHistoryChanged(e) { function onHistoryChanged(e) {
if (e.arguments[0] && e.arguments[0].origin === 'better-xcloud') { if (e.arguments && e.arguments[0] && e.arguments[0].origin === 'better-xcloud') {
return; return;
} }
@ -7020,7 +7064,11 @@ function onHistoryChanged(e) {
STREAM_AUDIO_GAIN_NODE = null; STREAM_AUDIO_GAIN_NODE = null;
$STREAM_VIDEO = null; $STREAM_VIDEO = null;
StreamStats.onStoppedPlaying(); StreamStats.onStoppedPlaying();
document.querySelector('.bx-screenshot-button').style = '';
const $screenshotBtn = document.querySelector('.bx-screenshot-button');
if ($screenshotBtn) {
$screenshotBtn.style = '';
}
MouseCursorHider.stop(); MouseCursorHider.stop();
TouchController.reset(); TouchController.reset();
@ -7102,7 +7150,7 @@ 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') { } else if (stat.type === 'candidate-pair' && stat.packetsReceived > 0 && stat.state === 'succeeded') {
candidateId = stat.remoteCandidateId; candidateId = stat.remoteCandidateId;
} else if (stat.type === 'remote-candidate') { } else if (stat.type === 'remote-candidate') {
allCandidates[stat.id] = stat.address; allCandidates[stat.id] = stat.address;
@ -7173,6 +7221,21 @@ function disablePwa() {
} }
} }
function setupBxUi() {
updateVideoPlayerCss();
// Prevent initializing multiple times
if (document.querySelector('.bx-quick-settings-bar')) {
return;
}
window.addEventListener('resize', updateVideoPlayerCss);
setupQuickSettingsBar();
setupScreenshotButton();
StreamStats.render();
}
// Hide Settings UI when navigate to another page // Hide Settings UI when navigate to another page
window.addEventListener('xcloud_popstate', onHistoryChanged); window.addEventListener('xcloud_popstate', onHistoryChanged);
@ -7255,13 +7318,7 @@ patchVideoApi();
// Setup UI // Setup UI
addCss(); addCss();
updateVideoPlayerCss(); ENABLE_PRELOAD_BX_UI && setupBxUi();
window.addEventListener('resize', updateVideoPlayerCss);
Toast.setup();
setupVideoSettingsBar();
setupScreenshotButton();
StreamStats.render();
disablePwa(); disablePwa();