Compare commits

..

20 Commits

Author SHA1 Message Date
cc9a644a5e Bump version to 1.15.1 2023-09-08 17:24:05 +07:00
a77db68afb Bump version to 1.15.1 2023-09-08 17:23:46 +07:00
cd7a7c92c7 Validate settings when getting its values 2023-09-08 17:16:38 +07:00
651402a6b4 Restore stretch to full screen feature 2023-09-08 17:15:45 +07:00
6cd2648325 Update README.md 2023-09-04 10:40:14 +07:00
fa0d761d24 Bump version to 1.15 2023-09-04 10:37:31 +07:00
f01d7a3b0b Bump version to 1.15 2023-09-04 10:37:12 +07:00
b520e8173e Update README.md 2023-09-04 10:36:43 +07:00
f15f43faf7 Remove "Bitrate" & "Decode Time" stats from default items 2023-09-04 10:31:52 +07:00
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
d1882046e2 Update README.md 2023-08-29 08:42:04 +07:00
fb7bd2da0d Update README.md 2023-08-28 14:31:13 +07:00
d9a14f9d83 Bump version to 1.14.1 2023-08-27 15:04:41 +07:00
18dd006aad Bump version to 1.14.1 2023-08-27 15:04:25 +07:00
f74de11e10 Fix Clarity Boost mode detection 2023-08-27 15:03:24 +07:00
2a71e17d2d Fix the stats selection box not working on mobile 2023-08-27 14:39:35 +07:00
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
9692286f1e Show a warning when the Clarity Boost mode is ON 2023-08-26 18:14:19 +07:00
26498efa7c Fix Settings button not showing when after playing (#112) 2023-08-26 18:02:53 +07:00
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
3 changed files with 369 additions and 225 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.1 // @version 1.15.1
// ==/UserScript== // ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 1.13.1 // @version 1.15.1
// @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.1'; const SCRIPT_VERSION = '1.15.1';
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}`);
@ -735,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;
@ -873,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 + '%';
@ -915,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 => {
@ -949,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());
@ -1126,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'; }
@ -1289,27 +1313,48 @@ class Preferences {
'min': 0, 'min': 0,
'max': 5, 'max': 5,
}, },
[Preferences.VIDEO_FILL_FULL_SCREEN]: { [Preferences.VIDEO_RATIO]: {
'default': false, 'default': '16:9',
'options': {
'16:9': '16:9',
'21:9': '21:9',
'16:10': '16:10',
'4:3': '4:3',
'fill': 'Stretch',
'cover': 'Cover',
},
}, },
[Preferences.VIDEO_SATURATION]: { [Preferences.VIDEO_SATURATION]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[Preferences.VIDEO_CONTRAST]: { [Preferences.VIDEO_CONTRAST]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[Preferences.VIDEO_BRIGHTNESS]: { [Preferences.VIDEO_BRIGHTNESS]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[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,
}, },
@ -1372,7 +1417,43 @@ class Preferences {
} }
} }
get(key, defaultValue=null) { #validateValue(key, value) {
const config = Preferences.SETTINGS[key];
if (!config) {
return value;
}
if (typeof value === 'undefined' || value === null) {
value = config.default;
}
if ('min' in config) {
value = Math.max(config.min, value);
}
if ('max' in config) {
value = Math.min(config.max, value);
}
if ('options' in config && !(value in config.options)) {
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;
}
}
return value;
}
get(key) {
if (typeof key === 'undefined') { if (typeof key === 'undefined') {
debugger; debugger;
return; return;
@ -1383,35 +1464,14 @@ class Preferences {
return 'default'; return 'default';
} }
const value = this._prefs[key]; let value = this._prefs[key];
value = this.#validateValue(key, value);
if (typeof value !== 'undefined' && value !== null && value !== '') { return value;
return value;
}
if (defaultValue !== null) {
return defaultValue;
}
// Return default value
return Preferences.SETTINGS[key].default;
} }
set(key, value) { set(key, value) {
const config = Preferences.SETTINGS[key]; value = this.#validateValue(key, value);
if (config) {
if ('min' in config) {
value = Math.max(config.min, value);
}
if ('max' in config) {
value = Math.min(config.max, value);
}
if ('options' in config && !(value in config.options)) {
value = config.default;
}
}
this._prefs[key] = value; this._prefs[key] = value;
this._update_storage(); this._update_storage();
@ -1428,7 +1488,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];
@ -1439,6 +1499,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') {
@ -1465,6 +1560,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;
}
} }
@ -1474,8 +1653,8 @@ const PREFS = new Preferences();
function checkForUpdate() { function checkForUpdate() {
const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION, ''); const currentVersion = PREFS.get(Preferences.CURRENT_VERSION);
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK, 0); const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK);
const now = Math.round((+new Date) / 1000); const now = Math.round((+new Date) / 1000);
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) { if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
@ -1644,7 +1823,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;
} }
@ -1746,6 +1925,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;
@ -1753,15 +1958,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] {
@ -1781,9 +1989,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;
} }
@ -1803,11 +2008,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;
@ -1912,22 +2112,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;
@ -1950,6 +2146,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 {
@ -2476,7 +2673,7 @@ function injectSettingsButton($parent) {
const CE = createElement; const CE = createElement;
const PREF_PREFERRED_REGION = getPreferredServerRegion(); const PREF_PREFERRED_REGION = getPreferredServerRegion();
const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION, null); const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION);
// Setup Settings button // Setup Settings button
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION); const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
@ -2731,17 +2928,42 @@ 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 && PREF_RATIO !== '16:9') {
if (PREF_RATIO.includes(':')) {
videoCss += `aspect-ratio: ${PREF_RATIO.replace(':', '/')}; object-fit: unset !important;`;
const tmp = PREF_RATIO.split(':');
const ratio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
const maxRatio = window.innerWidth / window.innerHeight;
if (ratio < maxRatio) {
videoCss += 'width: fit-content !important;'
} else {
videoCss += 'height: fit-content !important;'
}
} else {
videoCss += `object-fit: ${PREF_RATIO} !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;
align-self: center;
${videoCss}
}
`;
} }
$elm.textContent = css; $elm.textContent = css;
@ -2842,6 +3064,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();
@ -3023,117 +3251,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.toElement(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);
} }
@ -3239,6 +3382,8 @@ function onHistoryChanged() {
TouchController.reset(); TouchController.reset();
LoadingScreen.reset(); LoadingScreen.reset();
setTimeout(checkHeader, 2000);
} }
@ -3403,6 +3548,8 @@ patchVideoApi();
// Setup UI // Setup UI
addCss(); addCss();
updateVideoPlayerCss(); updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
setupVideoSettingsBar(); setupVideoSettingsBar();
setupScreenshotButton(); setupScreenshotButton();
StreamStats.render(); StreamStats.render();