Compare commits

...

19 Commits
v1.13 ... v1.15

Author SHA1 Message Date
redphx
6cd2648325 Update README.md 2023-09-04 10:40:14 +07:00
redphx
fa0d761d24 Bump version to 1.15 2023-09-04 10:37:31 +07:00
redphx
f01d7a3b0b Bump version to 1.15 2023-09-04 10:37:12 +07:00
redphx
b520e8173e Update README.md 2023-09-04 10:36:43 +07:00
redphx
f15f43faf7 Remove "Bitrate" & "Decode Time" stats from default items 2023-09-04 10:31:52 +07:00
redphx
e470cb20a3 Replace "Stretch Video" setting with "Video Ratio" (#121)
* Make Game Pass' app version easier to read

* Replace numberPicker with NumberStepper

* Replace "Stretch Video" setting with "Video Ratio"

* Make number stepper's buttons a little bit bigger
2023-09-04 09:11:08 +07:00
redphx
d1882046e2 Update README.md 2023-08-29 08:42:04 +07:00
redphx
fb7bd2da0d Update README.md 2023-08-28 14:31:13 +07:00
redphx
d9a14f9d83 Bump version to 1.14.1 2023-08-27 15:04:41 +07:00
redphx
18dd006aad Bump version to 1.14.1 2023-08-27 15:04:25 +07:00
redphx
f74de11e10 Fix Clarity Boost mode detection 2023-08-27 15:03:24 +07:00
redphx
2a71e17d2d Fix the stats selection box not working on mobile 2023-08-27 14:39:35 +07:00
redphx
2a85dd574e Version 1.14 (#116)
* Update README.md

* Update better-xcloud.meta.js

* Bump version to 1.14
2023-08-26 18:21:05 +07:00
redphx
9692286f1e Show a warning when the Clarity Boost mode is ON 2023-08-26 18:14:19 +07:00
redphx
26498efa7c Fix Settings button not showing when after playing (#112) 2023-08-26 18:02:53 +07:00
redphx
d1c724ff2c Improve Stats bar (#115)
* Rename "RTT" to "PNG"

* Show select box to select stats items

* Toggle stats

* Prevent select box from jumping around

* Put Ping before FPS & remove "ms" suffix from Ping
2023-08-26 18:01:31 +07:00
redphx
eec41c58b6 Bump version to 1.13.1 2023-08-18 17:44:01 +07:00
redphx
38cc78e0da Bump version to 1.13.1 2023-08-18 17:43:43 +07:00
redphx
3cf029818e Fix crashing when not skipping splash video 2023-08-18 17:07:43 +07:00
3 changed files with 333 additions and 203 deletions

View File

@@ -6,10 +6,9 @@ This script makes me spend more time with xCloud, and I hope the same thing happ
If you like this project please give it a 🌟. Thank you 🙏. If you like this project please give it a 🌟. Thank you 🙏.
[![Latest version](https://img.shields.io/github/v/release/redphx/better-xcloud?label=latest)](https://github.com/redphx/better-xcloud/releases) [![Latest version](https://img.shields.io/github/v/release/redphx/better-xcloud?label=latest)](https://github.com/redphx/better-xcloud/releases)
[![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers)
<!--
[![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases) [![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases)
--> [![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers)
## Table of Contents ## Table of Contents
- [**Features**](#features) - [**Features**](#features)
@@ -29,8 +28,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
<br> <br>
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956"> <img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956">
<br> <br>
<img width="600" alt="Video settings" src="https://github.com/redphx/better-xcloud/assets/96280/a8614693-7f56-4a49-82ad-c1fd7e2e00a5"> <img width="600" alt="Video settings" src="https://github.com/redphx/better-xcloud/assets/96280/20756157-917b-4a43-9985-df7bfdc24aa3">
&nbsp; &nbsp;
@@ -93,14 +91,13 @@ If you like this project please give it a 🌟. Thank you 🙏.
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062"> > <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
### Loading screen ### Loading screen
- Show game art - **Show game art**
> Replace the black background with game art if it's available. > Replace the black background with game art if it's available.
- Show the estimated wait time - **Show the estimated wait time**
> The time is estimated by the server. > The time is estimated by the server.
> It's not 100% correct: you might get in the game sooner or later. > It's not 100% correct: you might get in the game sooner or later.
> Don't be mad when the estimated time is inaccurate.
> Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info. > Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info.
- Show/hide the rocket animation - **Show/hide the rocket animation**
> Always show/Hide when queuing/Always hide. > Always show/Hide when queuing/Always hide.
> Hide this animation might save some battery life while queuing. > Hide this animation might save some battery life while queuing.
@@ -135,7 +132,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
> ![clarity](https://github.com/redphx/better-xcloud/assets/96280/ed63bbb0-fcbf-43e2-8e51-ac2733e697b8) > ![clarity](https://github.com/redphx/better-xcloud/assets/96280/ed63bbb0-fcbf-43e2-8e51-ac2733e697b8)
> *(click to enlarge)* > *(click to enlarge)*
- **Stretch video to full sctreen** - **Change video's ratio**
> Useful when you don't have a 16:9 screen > Useful when you don't have a 16:9 screen
- **Adjust video filters** - **Adjust video filters**
> Brightness/Contrast/Saturation. > Brightness/Contrast/Saturation.
@@ -159,7 +156,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use the [Userscripts extension](https://apps.apple.com/us/app/userscripts/id1463298887) (check [this page](https://github.com/redphx/better-xcloud/wiki/Using-with-Safari) before using). 1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use the [Userscripts extension](https://apps.apple.com/us/app/userscripts/id1463298887) (check [this page](https://github.com/redphx/better-xcloud/wiki/Using-with-Safari) before using).
2. Install **Better xCloud**: 2. Install **Better xCloud**:
- [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.
3. Refresh [xCloud web page](https://www.xbox.com/play/). 3. Refresh [xCloud web page](https://www.xbox.com/play/).
4. Click on the new "SERVER NAME" button next to your profile picture to adjust settings. 4. Click on the new "SERVER NAME" button next to your profile picture to adjust settings.
@@ -203,7 +200,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
![stats](https://github.com/redphx/better-xcloud/assets/96280/736548db-316d-4bb3-a0f8-467766ae810b) ![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/0d4abb6b-49ab-4c9a-a52d-df7e396d2145"> <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. - Double-click on the stats bar to show the Settings dialog.
@@ -213,9 +210,9 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
| Abbr. | Full name | Explain | | Abbr. | Full name | Explain |
|------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------| |------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
| PING | Ping | The number of seconds it takes for data to be sent from your device to the server and back over (the correct term is "Round Trip Time") |
| FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (may not be the same as the FPS of the game) | | FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (may not be the same as the FPS of the game) |
| RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (similar to ping, lower is better) | | DT | Decode Time | The average time it took to decode one frame in the last second (bugged on Android [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| DT | Decode Time | The average time it took to decode one frame in the last second (bugged in Kiwi Browser [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| BR | Bitrate | The amount of data the server sent to your device in the last second | | BR | Bitrate | The amount of data the server sent to your device in the last second |
| PL | Packets Lost | The total number of packets lost | | PL | Packets Lost | The total number of packets lost |
| FL | Frames Lost | The total number of frames dropped prior to decode or dropped because the frame missed its display deadline | | FL | Frames Lost | The total number of frames dropped prior to decode or dropped because the frame missed its display deadline |

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 1.13 // @version 1.15
// ==/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 1.13 // @version 1.15
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@@ -13,7 +13,7 @@
// ==/UserScript== // ==/UserScript==
'use strict'; 'use strict';
const SCRIPT_VERSION = '1.13'; const SCRIPT_VERSION = '1.15';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud'; const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
console.log(`[Better xCloud] readyState: ${document.readyState}`); console.log(`[Better xCloud] readyState: ${document.readyState}`);
@@ -325,7 +325,8 @@ class LoadingScreen {
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle); LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle);
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone'); LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone');
document.querySelector('#game-stream rect[width="800"]').addEventListener('transitionend', e => { const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
LoadingScreen.#$bgStyle.textContent += ` LoadingScreen.#$bgStyle.textContent += `
#game-stream { #game-stream {
background: #000 !important; background: #000 !important;
@@ -734,12 +735,19 @@ class StreamBadges {
class StreamStats { class StreamStats {
static get PING() { return 'ping'; }
static get FPS() { return 'fps'; }
static get BITRATE() { return 'btr'; }
static get DECODE_TIME() { return 'dt'; }
static get PACKETS_LOST() { return 'pl'; }
static get FRAMES_LOST() { return 'fl'; }
static #interval; static #interval;
static #updateInterval = 1000; static #updateInterval = 1000;
static #$container; static #$container;
static #$fps; static #$fps;
static #$rtt; static #$ping;
static #$dt; static #$dt;
static #$pl; static #$pl;
static #$fl; static #$fl;
@@ -872,23 +880,25 @@ class StreamStats {
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') { } else if (stat.type === 'candidate-pair' && 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.#$rtt.textContent = `${roundTripTime}ms`; StreamStats.#$ping.textContent = roundTripTime;
if (PREF_STATS_CONDITIONAL_FORMATTING) { if (PREF_STATS_CONDITIONAL_FORMATTING) {
grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : ''; grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : '';
} }
StreamStats.#$rtt.setAttribute('data-grade', grade); StreamStats.#$ping.setAttribute('data-grade', grade);
} }
}); });
}); });
} }
static #refreshStyles() { static #refreshStyles() {
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('][') + ']');
StreamStats.#$container.setAttribute('data-position', PREF_POSITION); StreamStats.#$container.setAttribute('data-position', PREF_POSITION);
StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT); StreamStats.#$container.setAttribute('data-transparent', PREF_TRANSPARENT);
StreamStats.#$container.style.opacity = PREF_OPACITY + '%'; StreamStats.#$container.style.opacity = PREF_OPACITY + '%';
@@ -914,19 +924,22 @@ class StreamStats {
} }
const CE = createElement; const CE = createElement;
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, const STATS = {
CE('label', {}, 'FPS'), [StreamStats.PING]: (StreamStats.#$ping = CE('span', {}, '0')),
StreamStats.#$fps = CE('span', {}, 0), [StreamStats.FPS]: (StreamStats.#$fps = CE('span', {}, '0')),
CE('label', {}, 'RTT'), [StreamStats.BITRATE]: (StreamStats.#$br = CE('span', {}, '0 Mbps')),
StreamStats.#$rtt = CE('span', {}, '0ms'), [StreamStats.DECODE_TIME]: (StreamStats.#$dt = CE('span', {}, '0ms')),
CE('label', {}, 'DT'), [StreamStats.PACKETS_LOST]: (StreamStats.#$pl = CE('span', {}, '0 (0.00%)')),
StreamStats.#$dt = CE('span', {}, '0ms'), [StreamStats.FRAMES_LOST]: (StreamStats.#$fl = CE('span', {}, '0 (0.00%)')),
CE('label', {}, 'BR'), };
StreamStats.#$br = CE('span', {}, '0 Mbps'),
CE('label', {}, 'PL'), const $barFragment = document.createDocumentFragment();
StreamStats.#$pl = CE('span', {}, '0 (0.00%)'), for (let statKey in STATS) {
CE('label', {}, 'FL'), const $div = CE('div', {'class': `better-xcloud-stat-${statKey}`}, CE('label', {}, statKey.toUpperCase()), STATS[statKey]);
StreamStats.#$fl = CE('span', {}, '0 (0.00%)')); $barFragment.appendChild($div);
}
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, $barFragment);
let clickTimeout; let clickTimeout;
StreamStats.#$container.addEventListener('mousedown', e => { StreamStats.#$container.addEventListener('mousedown', e => {
@@ -948,48 +961,59 @@ class StreamStats {
const refreshFunc = e => { const refreshFunc = e => {
StreamStats.#refreshStyles() StreamStats.#refreshStyles()
}; };
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
let $close; let $close;
const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING);
const $quickGlance = PREFS.toElement(Preferences.STATS_QUICK_GLANCE, e => {
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop(); const STATS_UI = {
}); [Preferences.STATS_SHOW_WHEN_PLAYING]: {
const $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc); 'label': 'Show stats when starting the game',
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc); },
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc); [Preferences.STATS_QUICK_GLANCE]: {
const $textSize = PREFS.toElement(Preferences.STATS_TEXT_SIZE, refreshFunc); '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 text color',
'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.#$settings = CE('div', {'class': 'better-xcloud-stats-settings'}, StreamStats.#$settings = CE('div', {'class': 'better-xcloud-stats-settings'},
CE('b', {}, 'Stream Stats Settings'), CE('b', {}, 'Stream Stats Settings'),
CE('div', {}, $fragment,
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
$showStartup
),
CE('div', {},
CE('label', {'for': `xcloud_setting_${Preferences.STATS_QUICK_GLANCE}`}, 'Enable "Quick Glance" mode'),
$quickGlance
),
CE('div', {},
CE('label', {}, 'Position'),
$position
),
CE('div', {},
CE('label', {}, 'Text size'),
$textSize
),
CE('div', {},
CE('label', {'for': `xcloud_setting_${Preferences.STATS_OPACITY}`}, 'Opacity (50-100%)'),
$opacity
),
CE('div', {},
CE('label', {'for': `xcloud_setting_${Preferences.STATS_TRANSPARENT}`}, 'Transparent background'),
$transparent
),
CE('div', {},
CE('label', {'for': `xcloud_setting_${Preferences.STATS_CONDITIONAL_FORMATTING}`}, 'Conditional formatting text color'),
$formatting
),
$close = CE('button', {}, 'Close')); $close = CE('button', {}, 'Close'));
$close.addEventListener('click', e => StreamStats.hideSettingsUi()); $close.addEventListener('click', e => StreamStats.hideSettingsUi());
@@ -1125,13 +1149,14 @@ class Preferences {
static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; } static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; }
static get VIDEO_CLARITY() { return 'video_clarity'; } static get VIDEO_CLARITY() { return 'video_clarity'; }
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; } static get VIDEO_RATIO() { return 'video_ratio' }
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; } static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
static get VIDEO_CONTRAST() { return 'video_contrast'; } static get VIDEO_CONTRAST() { return 'video_contrast'; }
static get VIDEO_SATURATION() { return 'video_saturation'; } static get VIDEO_SATURATION() { return 'video_saturation'; }
static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; } static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; }
static get STATS_ITEMS() { return 'stats_items'; };
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; } static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; } static get STATS_QUICK_GLANCE() { return 'stats_quick_glance'; }
static get STATS_POSITION() { return 'stats_position'; } static get STATS_POSITION() { return 'stats_position'; }
@@ -1288,8 +1313,11 @@ class Preferences {
'min': 0, 'min': 0,
'max': 5, 'max': 5,
}, },
[Preferences.VIDEO_FILL_FULL_SCREEN]: { [Preferences.VIDEO_RATIO]: {
'default': false, 'default': 16,
'min': 16,
'max': 21,
'steps': 1,
}, },
[Preferences.VIDEO_SATURATION]: { [Preferences.VIDEO_SATURATION]: {
'default': 100, 'default': 100,
@@ -1309,6 +1337,18 @@ class Preferences {
[Preferences.AUDIO_MIC_ON_PLAYING]: { [Preferences.AUDIO_MIC_ON_PLAYING]: {
'default': false, 'default': false,
}, },
[Preferences.STATS_ITEMS]: {
'default': [StreamStats.PING, StreamStats.FPS, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST],
'multiple_options': {
[StreamStats.PING]: 'Ping',
[StreamStats.FPS]: 'FPS',
[StreamStats.BITRATE]: 'Bitrate',
[StreamStats.DECODE_TIME]: 'Decode time',
[StreamStats.PACKETS_LOST]: 'Packets lost',
[StreamStats.FRAMES_LOST]: 'Frames lost',
},
},
[Preferences.STATS_SHOW_WHEN_PLAYING]: { [Preferences.STATS_SHOW_WHEN_PLAYING]: {
'default': false, 'default': false,
}, },
@@ -1409,6 +1449,17 @@ class Preferences {
if ('options' in config && !(value in config.options)) { if ('options' in config && !(value in config.options)) {
value = config.default; value = config.default;
} else if ('multiple_options' in config) {
if (value.length) {
const validOptions = Object.keys(config.multiple_options);
value.forEach((item, idx) => {
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
});
}
if (!value.length) {
value = config.default;
}
} }
} }
@@ -1427,7 +1478,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': 'xcloud_setting_' + key});
for (let value in setting.options) { for (let value in setting.options) {
const label = setting.options[value]; const label = setting.options[value];
@@ -1438,6 +1489,41 @@ class Preferences {
$control.value = currentValue; $control.value = currentValue;
$control.addEventListener('change', e => { $control.addEventListener('change', e => {
PREFS.set(key, e.target.value); PREFS.set(key, e.target.value);
onChange && onChange(e);
});
} else if ('multiple_options' in setting) {
$control = CE('select', {'id': 'xcloud_setting_' + key, 'multiple': true});
for (let value in setting.multiple_options) {
const label = setting.multiple_options[value];
const $option = CE('option', {value: value}, label);
$option.selected = currentValue.indexOf(value) > -1;
$option.addEventListener('mousedown', function(e) {
e.preventDefault();
e.target.selected = !e.target.selected;
const $parent = e.target.parentElement;
$parent.focus();
$parent.dispatchEvent(new Event('change'));
});
$control.appendChild($option);
}
$control.addEventListener('mousedown', e => {
const $this = this;
const orgScrollTop = $this.scrollTop;
setTimeout(() => ($this.scrollTop = orgScrollTop), 0);
});
$control.addEventListener('mousemove', e => e.preventDefault());
// $control.value = currentValue;
$control.addEventListener('change', e => {
const values = Array.from(e.target.selectedOptions).map(e => e.value);
PREFS.set(key, values);
onChange && onChange(e); onChange && onChange(e);
}); });
} else if (typeof setting.default === 'number') { } else if (typeof setting.default === 'number') {
@@ -1464,6 +1550,90 @@ class Preferences {
$control.id = `xcloud_setting_${key}`; $control.id = `xcloud_setting_${key}`;
return $control; return $control;
} }
toNumberStepper(key, onChange, suffix='', disabled=false) {
const setting = Preferences.SETTINGS[key]
let value = PREFS.get(key);
let $text, $decBtn, $incBtn;
const MIN = setting.min;
const MAX= setting.max;
const STEPS = Math.max(setting.steps || 1, 1);
const CE = createElement;
const $wrapper = CE('div', {},
$decBtn = CE('button', {'data-type': 'dec'}, '-'),
$text = CE('span', {}, value + suffix),
$incBtn = CE('button', {'data-type': 'inc'}, '+'),
);
if (disabled) {
$incBtn.disabled = true;
$incBtn.classList.add('better-xcloud-hidden');
$decBtn.disabled = true;
$decBtn.classList.add('better-xcloud-hidden');
return $wrapper;
}
let interval;
let isHolding = false;
const onClick = e => {
if (isHolding) {
e.preventDefault();
isHolding = false;
return;
}
const btnType = e.target.getAttribute('data-type');
if (btnType === 'dec') {
value = Math.max(MIN, value - STEPS);
} else {
value = Math.min(MAX, value + STEPS);
}
$text.textContent = value + suffix;
PREFS.set(key, value);
isHolding = false;
onChange && onChange();
}
const onMouseDown = e => {
isHolding = true;
const args = arguments;
interval = setInterval(() => {
const event = new Event('click');
event.arguments = args;
e.target.dispatchEvent(event);
}, 200);
};
const onMouseUp = e => {
clearInterval(interval);
isHolding = false;
};
$decBtn.addEventListener('click', onClick);
$decBtn.addEventListener('mousedown', onMouseDown);
$decBtn.addEventListener('mouseup', onMouseUp);
$decBtn.addEventListener('touchstart', onMouseDown);
$decBtn.addEventListener('touchend', onMouseUp);
$incBtn.addEventListener('click', onClick);
$incBtn.addEventListener('mousedown', onMouseDown);
$incBtn.addEventListener('mouseup', onMouseUp);
$incBtn.addEventListener('touchstart', onMouseDown);
$incBtn.addEventListener('touchend', onMouseUp);
return $wrapper;
}
} }
@@ -1643,7 +1813,7 @@ function addCss() {
.better-xcloud-settings-app-version { .better-xcloud-settings-app-version {
margin-top: 10px; margin-top: 10px;
text-align: center; text-align: center;
color: #484848; color: #747474;
font-size: 12px; font-size: 12px;
} }
@@ -1745,6 +1915,32 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
text-wrap: nowrap; text-wrap: nowrap;
} }
.better-xcloud-stats-bar > div {
display: none;
margin-right: 8px;
border-right: 2px solid #fff;
padding-right: 8px;
}
.better-xcloud-stats-bar[data-stats*="[fps]"] > .better-xcloud-stat-fps,
.better-xcloud-stats-bar[data-stats*="[ping]"] > .better-xcloud-stat-ping,
.better-xcloud-stats-bar[data-stats*="[btr]"] > .better-xcloud-stat-btr,
.better-xcloud-stats-bar[data-stats*="[dt]"] > .better-xcloud-stat-dt,
.better-xcloud-stats-bar[data-stats*="[pl]"] > .better-xcloud-stat-pl,
.better-xcloud-stats-bar[data-stats*="[fl]"] > .better-xcloud-stat-fl {
display: inline-block;
}
.better-xcloud-stats-bar[data-stats$="[fps]"] > .better-xcloud-stat-fps,
.better-xcloud-stats-bar[data-stats$="[ping]"] > .better-xcloud-stat-ping,
.better-xcloud-stats-bar[data-stats$="[btr]"] > .better-xcloud-stat-btr,
.better-xcloud-stats-bar[data-stats$="[dt]"] > .better-xcloud-stat-dt,
.better-xcloud-stats-bar[data-stats$="[pl]"] > .better-xcloud-stat-pl,
.better-xcloud-stats-bar[data-stats$="[fl]"] > .better-xcloud-stat-fl {
margin-right: 0;
border-right: none;
}
.better-xcloud-stats-bar[data-display=glancing]::before { .better-xcloud-stats-bar[data-display=glancing]::before {
content: '👀 '; content: '👀 ';
vertical-align: middle; vertical-align: middle;
@@ -1752,15 +1948,18 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-stats-bar[data-position=top-left] { .better-xcloud-stats-bar[data-position=top-left] {
left: 0; left: 0;
border-radius: 0 0 4px 0;
} }
.better-xcloud-stats-bar[data-position=top-right] { .better-xcloud-stats-bar[data-position=top-right] {
right: 0; right: 0;
border-radius: 0 0 0 4px;
} }
.better-xcloud-stats-bar[data-position=top-center] { .better-xcloud-stats-bar[data-position=top-center] {
transform: translate(-50%, 0); transform: translate(-50%, 0);
left: 50%; left: 50%;
border-radius: 0 0 4px 4px;
} }
.better-xcloud-stats-bar[data-transparent=true] { .better-xcloud-stats-bar[data-transparent=true] {
@@ -1780,9 +1979,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
min-width: 60px; min-width: 60px;
display: inline-block; display: inline-block;
text-align: right; text-align: right;
padding-right: 8px;
margin-right: 8px;
border-right: 2px solid #fff;
vertical-align: middle; vertical-align: middle;
} }
@@ -1802,11 +1998,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
min-width: 30px; min-width: 30px;
} }
.better-xcloud-stats-bar span:last-of-type {
border: 0;
margin-right: 0;
}
.better-xcloud-stats-settings { .better-xcloud-stats-settings {
display: none; display: none;
position: fixed; position: fixed;
@@ -1911,22 +2102,18 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.better-xcloud-quick-settings-bar label { .better-xcloud-quick-settings-bar label {
font-size: 16px; font-size: 18px;
font-weight: bold;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
.better-xcloud-quick-settings-bar input {
width: 22px;
height: 22px;
}
.better-xcloud-quick-settings-bar button { .better-xcloud-quick-settings-bar button {
border: none; border: none;
width: 22px; width: 24px;
height: 22px; height: 24px;
margin: 0 4px; margin: 0 4px;
line-height: 22px; line-height: 24px;
background-color: #515151; background-color: #515151;
color: #fff; color: #fff;
border-radius: 4px; border-radius: 4px;
@@ -1949,6 +2136,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
width: 40px; width: 40px;
font-weight: bold; font-weight: bold;
font-family: Consolas, "Courier New", Courier, monospace; font-family: Consolas, "Courier New", Courier, monospace;
font-size: 16px;
} }
.better-xcloud-stream-menu-button-on { .better-xcloud-stream-menu-button-on {
@@ -2016,7 +2204,7 @@ div[class*=NotFocusedDialog] {
height: 0px !important; height: 0px !important;
} }
#game-stream video { #game-stream video:not([src]) {
visibility: hidden; visibility: hidden;
} }
`; `;
@@ -2344,7 +2532,7 @@ function interceptHttpRequests() {
} }
if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') { if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
LoadingScreen.hide(); PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.hide();
const promise = orgFetch(...arg); const promise = orgFetch(...arg);
if (!PREF_OVERRIDE_CONFIGURATION) { if (!PREF_OVERRIDE_CONFIGURATION) {
@@ -2730,17 +2918,33 @@ function updateVideoPlayerCss() {
} }
let filters = getVideoPlayerFilterStyle(); let filters = getVideoPlayerFilterStyle();
let css = ''; let videoCss = '';
if (filters) { if (filters) {
css += `filter: ${filters} !important;`; videoCss += `filter: ${filters} !important;`;
} }
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) { const PREF_RATIO = PREFS.get(Preferences.VIDEO_RATIO);
css += 'object-fit: fill !important;'; if (PREF_RATIO) {
const minRatio = 16 / 9;
let maxRatio = window.innerWidth / window.innerHeight;
const ratio = Math.min(maxRatio, PREF_RATIO / 9);
if (ratio > minRatio) {
videoCss += `aspect-ratio: ${ratio}; width: auto !important; object-fit: unset !important;`;
}
} }
if (css) { let css = '';
css = `#game-stream video {${css}}`; if (videoCss) {
css = `
div[data-testid="media-container"] {
display: flex;
}
#game-stream video {
margin: 0 auto;
${videoCss}
}
`;
} }
$elm.textContent = css; $elm.textContent = css;
@@ -2841,6 +3045,12 @@ function injectStreamMenuButtons() {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const msVideoProcessing = $STREAM_VIDEO.msVideoProcessing;
if (msVideoProcessing && msVideoProcessing !== 'default') {
alert('This feature doesn\'t work when the Clarity Boost mode is ON');
return;
}
// Close HUD // Close HUD
$btnCloseHud.click(); $btnCloseHud.click();
@@ -2940,14 +3150,18 @@ function patchVideoApi() {
HTMLMediaElement.prototype.play = function() { HTMLMediaElement.prototype.play = function() {
LoadingScreen.reset(); LoadingScreen.reset();
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) { if (this.className && this.className.startsWith('XboxSplashVideo')) {
this.volume = 0; if (PREF_SKIP_SPLASH_VIDEO) {
this.style.display = 'none'; this.volume = 0;
this.dispatchEvent(new Event('ended')); this.style.display = 'none';
this.dispatchEvent(new Event('ended'));
return { return {
catch: () => {}, catch: () => {},
}; };
}
return this.orgPlay.apply(this);
} }
this.addEventListener('playing', showFunc); this.addEventListener('playing', showFunc);
@@ -3018,117 +3232,32 @@ function patchRtcCodecs() {
} }
function numberPicker(key, suffix='', disabled=false) {
const setting = Preferences.SETTINGS[key]
let value = PREFS.get(key);
let $text, $decBtn, $incBtn;
const MIN = setting.min;
const MAX= setting.max;
const CE = createElement;
const $wrapper = CE('div', {},
$decBtn = CE('button', {'data-type': 'dec'}, '-'),
$text = CE('span', {}, value + suffix),
$incBtn = CE('button', {'data-type': 'inc'}, '+'),
);
if (disabled) {
$incBtn.disabled = true;
$incBtn.classList.add('better-xcloud-hidden');
$decBtn.disabled = true;
$decBtn.classList.add('better-xcloud-hidden');
return $wrapper;
}
let interval;
let isHolding = false;
const onClick = e => {
if (isHolding) {
e.preventDefault();
isHolding = false;
return;
}
const btnType = e.target.getAttribute('data-type');
if (btnType === 'dec') {
value = (value <= MIN) ? MIN : value - 1;
} else {
value = (value >= MAX) ? MAX : value + 1;
}
$text.textContent = value + suffix;
PREFS.set(key, value);
updateVideoPlayerCss();
isHolding = false;
}
const onMouseDown = e => {
isHolding = true;
const args = arguments;
interval = setInterval(() => {
const event = new Event('click');
event.arguments = args;
e.target.dispatchEvent(event);
}, 200);
};
const onMouseUp = e => {
clearInterval(interval);
isHolding = false;
};
$decBtn.addEventListener('click', onClick);
$decBtn.addEventListener('mousedown', onMouseDown);
$decBtn.addEventListener('mouseup', onMouseUp);
$decBtn.addEventListener('touchstart', onMouseDown);
$decBtn.addEventListener('touchend', onMouseUp);
$incBtn.addEventListener('click', onClick);
$incBtn.addEventListener('mousedown', onMouseDown);
$incBtn.addEventListener('mouseup', onMouseUp);
$incBtn.addEventListener('touchstart', onMouseDown);
$incBtn.addEventListener('touchend', onMouseUp);
return $wrapper;
}
function setupVideoSettingsBar() { function setupVideoSettingsBar() {
const CE = createElement; const CE = createElement;
const isSafari = UserAgent.isSafari(); const isSafari = UserAgent.isSafari();
const onChange = e => {
updateVideoPlayerCss();
}
let $stretchInp; let $stretchInp;
const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'}, const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'},
CE('div', {}, CE('div', {},
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Stretch Video'), CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Video Ratio'),
$stretchInp = CE('input', {'id': 'better-xcloud-quick-setting-stretch', 'type': 'checkbox'})), PREFS.toNumberStepper(Preferences.VIDEO_RATIO, onChange, ':9')),
CE('div', {}, CE('div', {},
CE('label', {}, 'Clarity'), CE('label', {}, 'Clarity'),
numberPicker(Preferences.VIDEO_CLARITY, '', isSafari)), // disable this feature in Safari PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onChange, '', isSafari)), // disable this feature in Safari
CE('div', {}, CE('div', {},
CE('label', {}, 'Saturation'), CE('label', {}, 'Saturation'),
numberPicker(Preferences.VIDEO_SATURATION, '%')), PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onChange, '%')),
CE('div', {}, CE('div', {},
CE('label', {}, 'Contrast'), CE('label', {}, 'Contrast'),
numberPicker(Preferences.VIDEO_CONTRAST, '%')), PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onChange, '%')),
CE('div', {}, CE('div', {},
CE('label', {}, 'Brightness'), CE('label', {}, 'Brightness'),
numberPicker(Preferences.VIDEO_BRIGHTNESS, '%')) PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onChange, '%'))
); );
$stretchInp.checked = PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN);
$stretchInp.addEventListener('change', e => {
PREFS.set(Preferences.VIDEO_FILL_FULL_SCREEN, e.target.checked);
updateVideoPlayerCss();
});
document.documentElement.appendChild($wrapper); document.documentElement.appendChild($wrapper);
} }
@@ -3234,6 +3363,8 @@ function onHistoryChanged() {
TouchController.reset(); TouchController.reset();
LoadingScreen.reset(); LoadingScreen.reset();
setTimeout(checkHeader, 2000);
} }
@@ -3398,6 +3529,8 @@ patchVideoApi();
// Setup UI // Setup UI
addCss(); addCss();
updateVideoPlayerCss(); updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
setupVideoSettingsBar(); setupVideoSettingsBar();
setupScreenshotButton(); setupScreenshotButton();
StreamStats.render(); StreamStats.render();