Compare commits

...

11 Commits

Author SHA1 Message Date
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
eec41c58b6 Bump version to 1.13.1 2023-08-18 17:44:01 +07:00
38cc78e0da Bump version to 1.13.1 2023-08-18 17:43:43 +07:00
3cf029818e Fix crashing when not skipping splash video 2023-08-18 17:07:43 +07:00
3 changed files with 193 additions and 77 deletions

View File

@ -203,7 +203,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)
<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,8 +213,8 @@ 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)) |
| 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 |

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.13
// @version 1.14.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.14.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());
@ -1132,6 +1156,7 @@ class Preferences {
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'; }
@ -1309,6 +1334,18 @@ class Preferences {
[Preferences.AUDIO_MIC_ON_PLAYING]: {
'default': false,
},
[Preferences.STATS_ITEMS]: {
'default': [StreamStats.PING, StreamStats.FPS, StreamStats.BITRATE, StreamStats.DECODE_TIME, 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,
},
@ -1409,6 +1446,17 @@ class Preferences {
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;
}
}
}
@ -1427,7 +1475,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 +1486,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') {
@ -1745,6 +1828,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 +1861,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 +1892,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 +1911,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;
@ -2016,7 +2120,7 @@ div[class*=NotFocusedDialog] {
height: 0px !important;
}
#game-stream video {
#game-stream video:not([src]) {
visibility: hidden;
}
`;
@ -2344,7 +2448,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) {
@ -2841,6 +2945,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 +3050,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);
@ -3234,6 +3348,8 @@ function onHistoryChanged() {
TouchController.reset();
LoadingScreen.reset();
setTimeout(checkHeader, 2000);
}