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
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.
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.
Supported platforms:
- Windows, macOS, Linux
**Supported platforms:**
- Windows
- macOS
- Linux, SteamOS (Steam Deck)
- Android, Android TV
- 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)
<!-- - [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.
4. Refresh [xCloud web page](https://www.xbox.com/play/).
5. Click on the new "SERVER NAME" button next to your profile picture to adjust settings.
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.
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
- 🗒️ = see custom notes
| | Windows/Linux | macOS | Android/Android TV | iOS |
|-----------------------------------------|:-----------------|:-----------------|:-------------------|:-----------------|
| Chrome/Edge/Chromium variants | 👍 | 👍 | ❌ | ❌ |
| Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ |
| Safari | | ✅<sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [Kiwi Browser](https://kiwibrowser.com) | | | 👍 | |
| | Windows/Linux/SteamOS | macOS | Android/Android TV | iOS |
|-----------------------------------------|:----------------------|:-----------------|:-------------------|:-----------------|
| Chrome/Edge/Chromium... | 👍 | 👍 | ❌ | ❌ |
| Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ |
| Safari | | ✅<sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [Kiwi Browser](https://kiwibrowser.com) | | | 👍 | |
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>
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/51bdb96c-79ab-402f-902a-a9e6229973b2">
<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;
@ -88,7 +88,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
- **🔥 Capture screenshot**
> 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**
> 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.
- **🔥 Touch controller**
> 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.
## Stream stats
<img alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/9fb51941-85a9-47c4-8d48-331456b9ce73">
<img width="418" alt="Stream stats settings" src="https://github.com/redphx/better-xcloud/assets/96280/6313a0c6-03bf-4325-b60d-18a23c681933">
![stats](https://github.com/redphx/better-xcloud/assets/96280/736548db-316d-4bb3-a0f8-467766ae810b)
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/142625ea-20ab-4392-a111-0c5bc08bae09">
- While playing > `...` > `Stream Stats`.
- Double-click on the stats bar to show the Settings dialog.
- While playing > `...` > `Stream Stats`.
- Change settings by opening `Stream settings` while playing.
- 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.
- ⚠️ 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)
- **Italian**: Greenylie, Rakan129, Carza-104, graziequalcuno, DioCannabinoide
- **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)
- **Russian**: anpom6, soophik
- **Spanish**: [@PabloSebas](https://github.com/PabloSebas), csvnchzn

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 2.1
// @version 2.1.2
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,11 +13,12 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '2.1';
const SCRIPT_VERSION = '2.1.2';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const ENABLE_MKB = false;
const ENABLE_XCLOUD_LOGGER = false;
const ENABLE_PRELOAD_BX_UI = false;
console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -305,7 +306,7 @@ const Translations = {
"ru-RU": "Яркость",
"tr-TR": "Aydınlık",
"uk-UA": "Яскравість",
"vi-VN": ộ sáng",
"vi-VN": ộ sáng",
"zh-CN": "亮度",
},
"browser-unsupported-feature": {
@ -474,22 +475,12 @@ const Translations = {
"vi-VN": "Bộ điều khiển",
"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": {
"de-DE": "Vibration des Controllers",
"en-US": "Controller vibration",
"ja-JP": "コントローラーの振動",
"pt-BR": "Vibração do controle",
"ru-RU": "Вибрация контроллера",
"tr-TR": "Oyun kumandası titreşimi",
"vi-VN": "Rung bộ điều khiển",
},
@ -545,14 +536,18 @@ const Translations = {
"de-DE": "Vibration des Geräts",
"en-US": "Device vibration",
"ja-JP": "デバイスの振動",
"pt-BR": "Vibração do dispositivo",
"ru-RU": "Вибрация устройства",
"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",
"de-DE": "An, wenn kein Gamepad verbunden",
"en-US": "On when not using gamepad",
"ja-JP": "ゲームパッド未使用時にオン",
"pt-BR": "Ativar quando não estiver usando o dispositivo",
"ru-RU": "Включить когда не используется геймпад",
"tr-TR": "Oyun kumandası bağlanmadan titreşim",
"uk-UA": "Увімкнена, коли не використовується геймпад",
"vi-VN": "Bật khi không dùng tay cầm",
@ -1623,22 +1618,6 @@ const Translations = {
"vi-VN": "Stream",
"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": {
"de-DE": "Strecken",
"en-US": "Stretch",
@ -1660,6 +1639,7 @@ const Translations = {
"en-US": "Swap buttons",
"ja-JP": "ボタン入れ替え",
"pt-BR": "Trocar botões",
"ru-RU": "Поменять кнопки",
"tr-TR": "Düğme düzenini ters çevir",
"uk-UA": "Поміняти кнопки місцями",
"vi-VN": "Hoán đổi nút",
@ -1961,6 +1941,8 @@ const Translations = {
"de-DE": "Vibrationsstärke",
"en-US": "Vibration intensity",
"ja-JP": "振動の強さ",
"pt-BR": "Intensidade da vibração",
"ru-RU": "Сила вибрации",
"tr-TR": "Titreşim gücü",
"vi-VN": "Cường độ rung",
},
@ -3224,7 +3206,7 @@ class StreamBadges {
let totalIn = 0;
let totalOut = 0;
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;
totalOut += stat.bytesSent;
}
@ -3365,8 +3347,6 @@ class StreamStats {
static #$fl;
static #$br;
static #$dialog;
static #lastStat;
static #quickGlanceObserver;
@ -3391,8 +3371,10 @@ class StreamStats {
StreamStats.#interval = null;
StreamStats.#lastStat = null;
StreamStats.#$container.removeAttribute('data-display');
StreamStats.#$container.classList.add('bx-gone');
if (StreamStats.#$container) {
StreamStats.#$container.removeAttribute('data-display');
StreamStats.#$container.classList.add('bx-gone');
}
}
static toggle() {
@ -3409,8 +3391,8 @@ class StreamStats {
StreamStats.hideSettingsUi();
}
static isHidden = () => StreamStats.#$container.classList.contains('bx-gone');
static isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing';
static isHidden = () => StreamStats.#$container && StreamStats.#$container.classList.contains('bx-gone');
static isGlancing = () => StreamStats.#$container && StreamStats.#$container.getAttribute('data-display') === 'glancing';
static quickGlanceSetup() {
if (StreamStats.#quickGlanceObserver) {
@ -3489,7 +3471,7 @@ class StreamStats {
}
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
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
StreamStats.#$ping.textContent = roundTripTime;
@ -3503,18 +3485,19 @@ class StreamStats {
});
}
static #refreshStyles() {
static refreshStyles() {
const PREF_ITEMS = PREFS.get(Preferences.STATS_ITEMS);
const PREF_POSITION = PREFS.get(Preferences.STATS_POSITION);
const PREF_TRANSPARENT = PREFS.get(Preferences.STATS_TRANSPARENT);
const PREF_OPACITY = PREFS.get(Preferences.STATS_OPACITY);
const PREF_TEXT_SIZE = PREFS.get(Preferences.STATS_TEXT_SIZE);
StreamStats.#$container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']');
StreamStats.#$container.setAttribute('data-position', PREF_POSITION);
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT);
StreamStats.#$container.style.opacity = PREF_OPACITY + '%';
StreamStats.#$container.style.fontSize = PREF_TEXT_SIZE;
const $container = StreamStats.#$container;
$container.setAttribute('data-stats', '[' + PREF_ITEMS.join('][') + ']');
$container.setAttribute('data-position', PREF_POSITION);
$container.setAttribute('data-transparent', PREF_TRANSPARENT);
$container.style.opacity = PREF_OPACITY + '%';
$container.style.fontSize = PREF_TEXT_SIZE;
}
static hideSettingsUi() {
@ -3523,10 +3506,6 @@ class StreamStats {
}
}
static #toggleSettingsUi() {
StreamStats.#$dialog.toggle();
}
static render() {
if (StreamStats.#$container) {
return;
@ -3549,79 +3528,9 @@ class StreamStats {
}
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);
const refreshFunc = e => {
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();
StreamStats.refreshStyles();
}
}
@ -4278,7 +4187,7 @@ class Preferences {
let $control;
if ('options' in setting) {
$control = CE('select', {'id': 'xcloud_setting_' + key});
$control = CE('select', {'id': `bx_setting_${key}`});
for (let value in setting.options) {
const label = setting.options[value];
@ -4293,7 +4202,7 @@ class Preferences {
onChange && onChange(e);
});
} 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) {
const label = setting.multiple_options[value];
@ -4348,7 +4257,7 @@ class Preferences {
});
}
$control.id = `xcloud_setting_${key}`;
$control.id = `bx_setting_${key}`;
return $control;
}
@ -4877,11 +4786,11 @@ function addCss() {
--bx-monospaced-font: Consolas, "Courier New", Courier, monospace;
--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-touch-controller-bar-z-index: 5555;
--bx-dialog-z-index: 1010;
--bx-stats-bar-z-index: 1000;
--bx-dialog-overlay-z-index: 900;
}
@ -5345,20 +5254,19 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
-webkit-user-select: none;
position: fixed;
right: 0;
top: 20px;
bottom: 20px;
top: 0;
bottom: 0;
z-index: var(--bx-stream-settings-z-index);
padding: 8px;
width: 320px;
padding: 16px;
width: 420px;
background: #1a1b1e;
color: #fff;
border-radius: 8px 0 0 8px;
font-weight: 400;
font-size: 16px;
font-family: var(--bx-title-font);
text-align: center;
box-shadow: 0px 0px 6px #000;
opacity: 0.95;
opacity: 0.98;
overflow: overlay;
}
@ -5385,27 +5293,34 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.bx-quick-settings-bar > div {
display: flex;
border-bottom: 1px solid #40404080;
margin-bottom: 16px;
padding-bottom: 16px;
}
.bx-quick-settings-bar h2 {
font-size: 32px;
font-size: 28px;
font-weight: bold;
margin-bottom: 8px;
text-transform: uppercase;
text-align: left;
}
.bx-quick-settings-bar input[type="range"] {
display: block;
margin: 12px auto;
width: 80%;
margin: 12px auto 2px;
width: 180px;
color: #959595 !important;
}
.bx-quick-settings-bar label {
font-size: 16px;
font-weight: bold;
display: block;
margin-bottom: 8px;
text-align: left;
flex: 1;
align-self: center;
margin-bottom: 0 !important;
}
.bx-quick-settings-bar button {
@ -5423,9 +5338,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.bx-quick-settings-bar-note {
display: block;
text-align: center;
font-size: 12px;
font-weight: lighter;
font-style: italic;
padding-top: 16px;
}
.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 lst = [];
@ -5787,13 +5705,15 @@ function updateIceCandidates(candidates) {
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 = [];
let foundation = 1;
lst.forEach(item => {
item.foundation = foundation;
item.priority = (foundation == 1) ? 100 : 1;
item.priority = (foundation == 1) ? 10000 : 1;
newCandidates.push({
'candidate': `a=candidate:${item.foundation} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`,
@ -5805,6 +5725,15 @@ function updateIceCandidates(candidates) {
++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({
'candidate': 'a=end-of-candidates',
'messageType': 'iceCandidate',
@ -5812,6 +5741,7 @@ function updateIceCandidates(candidates) {
'sdpMid': '0',
});
console.log(newCandidates);
return newCandidates;
}
@ -5837,11 +5767,6 @@ function interceptHttpRequests() {
}
if (PREFS.get(Preferences.BLOCK_SOCIAL_FEATURES)) {
// Disable WebSocket
WebSocket = {
CLOSING: 2,
};
BLOCKED_URLS = BLOCKED_URLS.concat([
'https://peoplehub.xboxlive.com/users/me',
'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 orgFetch = window.fetch;
let consoleIp;
let consolePort;
const patchIpv6 = function(...arg) {
const patchIceCandidates = function(...arg) {
// ICE server candidates
const request = arg[0];
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);
return promise.then(response => {
@ -5898,9 +5825,14 @@ function interceptHttpRequests() {
return response;
}
const options = {
preferIpv6Server: PREF_PREFER_IPV6_SERVER,
consoleIp: consoleIp,
};
const obj = JSON.parse(text);
let exchangeResponse = JSON.parse(obj.exchangeResponse);
exchangeResponse = updateIceCandidates(exchangeResponse)
exchangeResponse = updateIceCandidates(exchangeResponse, options)
obj.exchangeResponse = JSON.stringify(exchangeResponse);
response.json = () => Promise.resolve(obj);
@ -5916,19 +5848,21 @@ function interceptHttpRequests() {
window.fetch = async (...arg) => {
let request = arg[0];
const url = (typeof request === 'string') ? request : request.url;
let url = (typeof request === 'string') ? request : request.url;
// Remote Play
if (IS_REMOTE_PLAYING && url.includes('/home/play')) {
if (url.endsWith('/play')) {
// Setup UI
setupBxUi();
}
if (IS_REMOTE_PLAYING && url.includes('/sessions/home')) {
const clone = request.clone();
const cloneBody = await clone.json();
cloneBody.settings.osName = 'windows';
// Clone headers
const headers = {};
for (const pair of clone.headers.entries()) {
headers[pair[0]] = pair[1];
}
headers['authorization'] = `Bearer ${RemotePlay.XHOME_TOKEN}`;
const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
if (PREFS.get(Preferences.REMOTE_PLAY_RESOLUTION) === '720p') {
@ -5936,16 +5870,41 @@ function interceptHttpRequests() {
}
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', {
method: 'POST',
body: JSON.stringify(cloneBody),
const opts = {
method: clone.method,
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')) {
@ -5992,38 +5951,10 @@ function interceptHttpRequests() {
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
if (!IS_REMOTE_PLAYING) {
const patchedIpv6 = patchIpv6(...arg);
if (patchedIpv6) {
return patchedIpv6;
}
const patchedIpv6 = patchIceCandidates(...arg);
if (patchedIpv6) {
return patchedIpv6;
}
// Server list
@ -6406,7 +6337,7 @@ function injectSettingsButton($parent) {
} else if (settingId === Preferences.SERVER_REGION) {
let selectedValue;
$control = CE('select', {id: 'xcloud_setting_' + settingId});
$control = CE('select', {id: `bx_setting_${settingId}`});
$control.addEventListener('change', e => {
PREFS.set(settingId, e.target.value);
});
@ -6849,65 +6780,178 @@ function patchRtcCodecs() {
}
function setupVideoSettingsBar() {
function setupQuickSettingsBar() {
const CE = createElement;
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);
}
@ -7000,7 +7044,7 @@ function patchHistoryMethod(type) {
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;
}
@ -7020,7 +7064,11 @@ function onHistoryChanged(e) {
STREAM_AUDIO_GAIN_NODE = null;
$STREAM_VIDEO = null;
StreamStats.onStoppedPlaying();
document.querySelector('.bx-screenshot-button').style = '';
const $screenshotBtn = document.querySelector('.bx-screenshot-button');
if ($screenshotBtn) {
$screenshotBtn.style = '';
}
MouseCursorHider.stop();
TouchController.reset();
@ -7102,7 +7150,7 @@ function onStreamStarted($video) {
} else if (stat.kind === 'audio') {
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;
} else if (stat.type === 'remote-candidate') {
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
window.addEventListener('xcloud_popstate', onHistoryChanged);
@ -7255,13 +7318,7 @@ patchVideoApi();
// Setup UI
addCss();
updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
Toast.setup();
setupVideoSettingsBar();
setupScreenshotButton();
StreamStats.render();
ENABLE_PRELOAD_BX_UI && setupBxUi();
disablePwa();