mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 10:51:44 +02:00
Compare commits
23 Commits
Author | SHA1 | Date | |
---|---|---|---|
cc9a644a5e | |||
a77db68afb | |||
cd7a7c92c7 | |||
651402a6b4 | |||
6cd2648325 | |||
fa0d761d24 | |||
f01d7a3b0b | |||
b520e8173e | |||
f15f43faf7 | |||
e470cb20a3 | |||
d1882046e2 | |||
fb7bd2da0d | |||
d9a14f9d83 | |||
18dd006aad | |||
f74de11e10 | |||
2a71e17d2d | |||
2a85dd574e | |||
9692286f1e | |||
26498efa7c | |||
d1c724ff2c | |||
eec41c58b6 | |||
38cc78e0da | |||
3cf029818e |
27
README.md
27
README.md
@ -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 🙏.
|
||||
|
||||
[](https://github.com/redphx/better-xcloud/releases)
|
||||
[](https://github.com/redphx/better-xcloud/stargazers)
|
||||
<!--
|
||||
[](https://github.com/redphx/better-xcloud/releases)
|
||||
-->
|
||||
[](https://github.com/redphx/better-xcloud/stargazers)
|
||||
|
||||
|
||||
## Table of Contents
|
||||
- [**Features**](#features)
|
||||
@ -29,8 +28,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
<br>
|
||||
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956">
|
||||
<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">
|
||||
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
### Loading screen
|
||||
- Show game art
|
||||
- **Show game art**
|
||||
> 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.
|
||||
> It's not 100% correct: you might get in the game sooner or later.
|
||||
> Don't be mad when the estimated time is inaccurate.
|
||||
> It's not 100% correct: you might get in the game sooner or later.
|
||||
> 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.
|
||||
> 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 🙏.
|
||||
> 
|
||||
> *(click to enlarge)*
|
||||
|
||||
- **Stretch video to full sctreen**
|
||||
- **Change video's ratio**
|
||||
> Useful when you don't have a 16:9 screen
|
||||
- **Adjust video filters**
|
||||
> 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).
|
||||
2. Install **Better xCloud**:
|
||||
- [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.
|
||||
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.
|
||||
@ -203,7 +200,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
||||
|
||||

|
||||
|
||||
<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`.
|
||||
- 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 |
|
||||
|------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| 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) |
|
||||
| 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 in Kiwi Browser [#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 on Android [#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 |
|
||||
| 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 |
|
||||
|
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.13
|
||||
// @version 1.15.1
|
||||
// ==/UserScript==
|
||||
|
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.13
|
||||
// @version 1.15.1
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
@ -13,7 +13,7 @@
|
||||
// ==/UserScript==
|
||||
'use strict';
|
||||
|
||||
const SCRIPT_VERSION = '1.13';
|
||||
const SCRIPT_VERSION = '1.15.1';
|
||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||
|
||||
console.log(`[Better xCloud] readyState: ${document.readyState}`);
|
||||
@ -325,7 +325,8 @@ class LoadingScreen {
|
||||
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle);
|
||||
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 += `
|
||||
#game-stream {
|
||||
background: #000 !important;
|
||||
@ -734,12 +735,19 @@ class StreamBadges {
|
||||
|
||||
|
||||
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 #updateInterval = 1000;
|
||||
|
||||
static #$container;
|
||||
static #$fps;
|
||||
static #$rtt;
|
||||
static #$ping;
|
||||
static #$dt;
|
||||
static #$pl;
|
||||
static #$fl;
|
||||
@ -872,23 +880,25 @@ class StreamStats {
|
||||
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
|
||||
// Round Trip Time
|
||||
const roundTripTime = typeof stat.currentRoundTripTime !== 'undefined' ? stat.currentRoundTripTime * 1000 : '???';
|
||||
StreamStats.#$rtt.textContent = `${roundTripTime}ms`;
|
||||
StreamStats.#$ping.textContent = roundTripTime;
|
||||
|
||||
if (PREF_STATS_CONDITIONAL_FORMATTING) {
|
||||
grade = (roundTripTime > 100) ? 'bad' : (roundTripTime > 75) ? 'ok' : (roundTripTime > 40) ? 'good' : '';
|
||||
}
|
||||
StreamStats.#$rtt.setAttribute('data-grade', grade);
|
||||
StreamStats.#$ping.setAttribute('data-grade', grade);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
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 + '%';
|
||||
@ -914,19 +924,22 @@ class StreamStats {
|
||||
}
|
||||
|
||||
const CE = createElement;
|
||||
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'},
|
||||
CE('label', {}, 'FPS'),
|
||||
StreamStats.#$fps = CE('span', {}, 0),
|
||||
CE('label', {}, 'RTT'),
|
||||
StreamStats.#$rtt = CE('span', {}, '0ms'),
|
||||
CE('label', {}, 'DT'),
|
||||
StreamStats.#$dt = CE('span', {}, '0ms'),
|
||||
CE('label', {}, 'BR'),
|
||||
StreamStats.#$br = CE('span', {}, '0 Mbps'),
|
||||
CE('label', {}, 'PL'),
|
||||
StreamStats.#$pl = CE('span', {}, '0 (0.00%)'),
|
||||
CE('label', {}, 'FL'),
|
||||
StreamStats.#$fl = CE('span', {}, '0 (0.00%)'));
|
||||
const STATS = {
|
||||
[StreamStats.PING]: (StreamStats.#$ping = CE('span', {}, '0')),
|
||||
[StreamStats.FPS]: (StreamStats.#$fps = CE('span', {}, '0')),
|
||||
[StreamStats.BITRATE]: (StreamStats.#$br = CE('span', {}, '0 Mbps')),
|
||||
[StreamStats.DECODE_TIME]: (StreamStats.#$dt = CE('span', {}, '0ms')),
|
||||
[StreamStats.PACKETS_LOST]: (StreamStats.#$pl = CE('span', {}, '0 (0.00%)')),
|
||||
[StreamStats.FRAMES_LOST]: (StreamStats.#$fl = CE('span', {}, '0 (0.00%)')),
|
||||
};
|
||||
|
||||
const $barFragment = document.createDocumentFragment();
|
||||
for (let statKey in STATS) {
|
||||
const $div = CE('div', {'class': `better-xcloud-stat-${statKey}`}, CE('label', {}, statKey.toUpperCase()), STATS[statKey]);
|
||||
$barFragment.appendChild($div);
|
||||
}
|
||||
|
||||
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'}, $barFragment);
|
||||
|
||||
let clickTimeout;
|
||||
StreamStats.#$container.addEventListener('mousedown', e => {
|
||||
@ -948,48 +961,59 @@ class StreamStats {
|
||||
const refreshFunc = e => {
|
||||
StreamStats.#refreshStyles()
|
||||
};
|
||||
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
|
||||
|
||||
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 $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc);
|
||||
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
|
||||
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
|
||||
const $textSize = PREFS.toElement(Preferences.STATS_TEXT_SIZE, refreshFunc);
|
||||
|
||||
|
||||
const STATS_UI = {
|
||||
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
|
||||
'label': 'Show stats when starting the game',
|
||||
},
|
||||
[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 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'},
|
||||
CE('b', {}, 'Stream Stats Settings'),
|
||||
CE('div', {},
|
||||
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
|
||||
),
|
||||
$fragment,
|
||||
$close = CE('button', {}, 'Close'));
|
||||
|
||||
$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 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_CONTRAST() { return 'video_contrast'; }
|
||||
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
||||
|
||||
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_QUICK_GLANCE() { return 'stats_quick_glance'; }
|
||||
static get STATS_POSITION() { return 'stats_position'; }
|
||||
@ -1288,27 +1313,48 @@ class Preferences {
|
||||
'min': 0,
|
||||
'max': 5,
|
||||
},
|
||||
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
|
||||
'default': false,
|
||||
[Preferences.VIDEO_RATIO]: {
|
||||
'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]: {
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'min': 50,
|
||||
'max': 150,
|
||||
},
|
||||
[Preferences.VIDEO_CONTRAST]: {
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'min': 50,
|
||||
'max': 150,
|
||||
},
|
||||
[Preferences.VIDEO_BRIGHTNESS]: {
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'min': 50,
|
||||
'max': 150,
|
||||
},
|
||||
[Preferences.AUDIO_MIC_ON_PLAYING]: {
|
||||
'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]: {
|
||||
'default': false,
|
||||
},
|
||||
@ -1371,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') {
|
||||
debugger;
|
||||
return;
|
||||
@ -1382,35 +1464,14 @@ class Preferences {
|
||||
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;
|
||||
}
|
||||
|
||||
if (defaultValue !== null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Return default value
|
||||
return Preferences.SETTINGS[key].default;
|
||||
return value;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
const config = Preferences.SETTINGS[key];
|
||||
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;
|
||||
}
|
||||
}
|
||||
value = this.#validateValue(key, value);
|
||||
|
||||
this._prefs[key] = value;
|
||||
this._update_storage();
|
||||
@ -1427,7 +1488,7 @@ class Preferences {
|
||||
|
||||
let $control;
|
||||
if ('options' in setting) {
|
||||
$control = CE('select', {id: 'xcloud_setting_' + key});
|
||||
$control = CE('select', {'id': 'xcloud_setting_' + key});
|
||||
for (let value in setting.options) {
|
||||
const label = setting.options[value];
|
||||
|
||||
@ -1438,6 +1499,41 @@ class Preferences {
|
||||
$control.value = currentValue;
|
||||
$control.addEventListener('change', e => {
|
||||
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);
|
||||
});
|
||||
} else if (typeof setting.default === 'number') {
|
||||
@ -1464,6 +1560,90 @@ class Preferences {
|
||||
$control.id = `xcloud_setting_${key}`;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1473,8 +1653,8 @@ const PREFS = new Preferences();
|
||||
function checkForUpdate() {
|
||||
const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours
|
||||
|
||||
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION, '');
|
||||
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK, 0);
|
||||
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION);
|
||||
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK);
|
||||
const now = Math.round((+new Date) / 1000);
|
||||
|
||||
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
|
||||
@ -1643,7 +1823,7 @@ function addCss() {
|
||||
.better-xcloud-settings-app-version {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: #484848;
|
||||
color: #747474;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@ -1745,6 +1925,32 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
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 {
|
||||
content: '👀 ';
|
||||
vertical-align: middle;
|
||||
@ -1752,15 +1958,18 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
|
||||
.better-xcloud-stats-bar[data-position=top-left] {
|
||||
left: 0;
|
||||
border-radius: 0 0 4px 0;
|
||||
}
|
||||
|
||||
.better-xcloud-stats-bar[data-position=top-right] {
|
||||
right: 0;
|
||||
border-radius: 0 0 0 4px;
|
||||
}
|
||||
|
||||
.better-xcloud-stats-bar[data-position=top-center] {
|
||||
transform: translate(-50%, 0);
|
||||
left: 50%;
|
||||
border-radius: 0 0 4px 4px;
|
||||
}
|
||||
|
||||
.better-xcloud-stats-bar[data-transparent=true] {
|
||||
@ -1780,9 +1989,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
min-width: 60px;
|
||||
display: inline-block;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
margin-right: 8px;
|
||||
border-right: 2px solid #fff;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@ -1802,11 +2008,6 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
.better-xcloud-stats-bar span:last-of-type {
|
||||
border: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.better-xcloud-stats-settings {
|
||||
display: none;
|
||||
position: fixed;
|
||||
@ -1911,22 +2112,18 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar label {
|
||||
font-size: 16px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar input {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar button {
|
||||
border: none;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 4px;
|
||||
line-height: 22px;
|
||||
line-height: 24px;
|
||||
background-color: #515151;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
@ -1949,6 +2146,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
width: 40px;
|
||||
font-weight: bold;
|
||||
font-family: Consolas, "Courier New", Courier, monospace;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.better-xcloud-stream-menu-button-on {
|
||||
@ -2016,7 +2214,7 @@ div[class*=NotFocusedDialog] {
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
#game-stream video {
|
||||
#game-stream video:not([src]) {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
@ -2344,7 +2542,7 @@ function interceptHttpRequests() {
|
||||
}
|
||||
|
||||
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);
|
||||
if (!PREF_OVERRIDE_CONFIGURATION) {
|
||||
@ -2475,7 +2673,7 @@ function injectSettingsButton($parent) {
|
||||
|
||||
const CE = createElement;
|
||||
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
|
||||
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
|
||||
@ -2730,17 +2928,42 @@ function updateVideoPlayerCss() {
|
||||
}
|
||||
|
||||
let filters = getVideoPlayerFilterStyle();
|
||||
let css = '';
|
||||
let videoCss = '';
|
||||
if (filters) {
|
||||
css += `filter: ${filters} !important;`;
|
||||
videoCss += `filter: ${filters} !important;`;
|
||||
}
|
||||
|
||||
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) {
|
||||
css += 'object-fit: fill !important;';
|
||||
const PREF_RATIO = PREFS.get(Preferences.VIDEO_RATIO);
|
||||
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) {
|
||||
css = `#game-stream video {${css}}`;
|
||||
let css = '';
|
||||
if (videoCss) {
|
||||
css = `
|
||||
div[data-testid="media-container"] {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#game-stream video {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
${videoCss}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
$elm.textContent = css;
|
||||
@ -2841,6 +3064,12 @@ function injectStreamMenuButtons() {
|
||||
e.preventDefault();
|
||||
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
|
||||
$btnCloseHud.click();
|
||||
|
||||
@ -2940,14 +3169,18 @@ function patchVideoApi() {
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
LoadingScreen.reset();
|
||||
|
||||
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
|
||||
this.volume = 0;
|
||||
this.style.display = 'none';
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
if (this.className && this.className.startsWith('XboxSplashVideo')) {
|
||||
if (PREF_SKIP_SPLASH_VIDEO) {
|
||||
this.volume = 0;
|
||||
this.style.display = 'none';
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
|
||||
return {
|
||||
catch: () => {},
|
||||
};
|
||||
return {
|
||||
catch: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
return this.orgPlay.apply(this);
|
||||
}
|
||||
|
||||
this.addEventListener('playing', showFunc);
|
||||
@ -3018,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() {
|
||||
const CE = createElement;
|
||||
const isSafari = UserAgent.isSafari();
|
||||
const onChange = e => {
|
||||
updateVideoPlayerCss();
|
||||
}
|
||||
|
||||
let $stretchInp;
|
||||
const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'},
|
||||
CE('div', {},
|
||||
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Stretch Video'),
|
||||
$stretchInp = CE('input', {'id': 'better-xcloud-quick-setting-stretch', 'type': 'checkbox'})),
|
||||
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Video Ratio'),
|
||||
PREFS.toElement(Preferences.VIDEO_RATIO, onChange, ':9')),
|
||||
CE('div', {},
|
||||
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('label', {}, 'Saturation'),
|
||||
numberPicker(Preferences.VIDEO_SATURATION, '%')),
|
||||
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onChange, '%')),
|
||||
CE('div', {},
|
||||
CE('label', {}, 'Contrast'),
|
||||
numberPicker(Preferences.VIDEO_CONTRAST, '%')),
|
||||
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onChange, '%')),
|
||||
CE('div', {},
|
||||
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);
|
||||
}
|
||||
|
||||
@ -3234,6 +3382,8 @@ function onHistoryChanged() {
|
||||
TouchController.reset();
|
||||
|
||||
LoadingScreen.reset();
|
||||
|
||||
setTimeout(checkHeader, 2000);
|
||||
}
|
||||
|
||||
|
||||
@ -3398,6 +3548,8 @@ patchVideoApi();
|
||||
// Setup UI
|
||||
addCss();
|
||||
updateVideoPlayerCss();
|
||||
window.addEventListener('resize', updateVideoPlayerCss);
|
||||
|
||||
setupVideoSettingsBar();
|
||||
setupScreenshotButton();
|
||||
StreamStats.render();
|
||||
|
Reference in New Issue
Block a user