Compare commits

...

16 Commits
v1.8 ... v1.8.2

Author SHA1 Message Date
8ee28d92d9 Bump version to 1.8.2 2023-08-02 12:45:23 +07:00
1f94058b99 Bump version to 1.8.2 2023-08-02 12:45:08 +07:00
95e94242aa Update README.md 2023-08-02 12:44:40 +07:00
91aa28450d Fix battery status 2023-08-02 11:58:18 +07:00
8eb8bbf598 Add In/Out badges (#48)
* Show In/Out badges

* Cache DOMs of Stream badges

* Refresh badges every 3s

* Shorten Video badge: "1920x1080" -> "1080p"

* Fix 404 error when spoofing User-Agent (#34)
2023-08-02 11:51:46 +07:00
47817d9d36 Update color of "Video" badge 2023-08-01 21:17:27 +07:00
b770a4c9d3 Bump version to 1.8.1 2023-08-01 08:32:29 +07:00
a27c0ed8f6 Bump version to 1.8.1 2023-08-01 08:31:56 +07:00
8ac37754e6 Combine "Resolution" and "Video" badges into one 2023-08-01 08:31:01 +07:00
d9288a322b Update battery stat 2023-08-01 08:23:48 +07:00
5facfd2348 Fix inaccurate percentages of PL & FL stats 2023-08-01 08:18:03 +07:00
889717be7d Show confirm dialog to refresh the stream after holding for 1s 2023-08-01 08:07:57 +07:00
4b0f0784ae Fix stats bar not showing sometimes 2023-08-01 08:05:18 +07:00
31217d01bb Update bug_report.md 2023-08-01 07:40:50 +07:00
2f6176e906 Update README.md 2023-07-31 14:49:03 +07:00
fe011fd0f2 Update README.md 2023-07-31 08:28:37 +07:00
4 changed files with 162 additions and 60 deletions

View File

@ -24,10 +24,11 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Platform (please complete the following information):**
- OS: [e.g. Android]
- Browser: [e.g. chrome, firefox]
- Browser Version: [e.g. 100]
- Better xCloud Version: [e.g. 1.4]
- Device: [e.g. Phone/Laptop/Desktop/TV]
- OS: [e.g. Android]
- Browser: [e.g. Chrome, Kiwi]
- Browser Version: [e.g. 100]
- Better xCloud Version: [e.g. 1.4]
**Additional context**
Add any other context about the problem here.

View File

@ -16,7 +16,7 @@ Give this project a 🌟 if you like it. Thank you 🙏.
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/f4187ef8-fa10-4eab-b085-ef3c3aed3201">
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/68f91041-10b2-4fd9-adc1-e2a21db7e36f">
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/7e0fe3e1-b826-4a69-9843-a3acb866a2f9">
&nbsp;
@ -64,7 +64,8 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **Display stream's statuses**
> Region/Server/Codecs/Resolution...
> Current playtime of the session.
> Current battery level. Only visible on battery-powered devices.
> Current battery level.
> Estimated total data sent/received.
- **Disable social features**
> Features like friends, chat... Disable these will make the page load faster.
- **Disable xCloud analytics**
@ -81,7 +82,7 @@ Give this project a 🌟 if you like it. Thank you 🙏.
<sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on.
## How to use
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers.
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887).
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)
@ -129,14 +130,14 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
- This bar is updated every second.
- ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream.
| Abbr. | Full name | Explain |
|------:|:-------------------|:-----------------------------------------------------------------------------------------------------------------------------------|
| FPS | Frames per Seconds | The number of decoded frames in the last second of the stream (equal to or lower than the FPS of the game) |
| DT | Decode Time | The average time it took to decode one frame in the last second (might be bugged [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| 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) |
| 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 |
| Abbr. | Full name | Explain |
|------:|:-------------------|:-------------------------------------------------------------------------------------------------------------------------------------------|
| 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) |
| DT | Decode Time | The average time it took to decode one frame in the last second (might be bugged [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| 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) |
| 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 |
This info is provided by WebRTC API. You can use browser's built-in tool to see more info:
- Chrome/Edge/Chromium variants: `chrome://webrtc-internals`

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.8
// @version 1.8.2
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,7 +13,7 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.8';
const SCRIPT_VERSION = '1.8.2';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const SERVER_REGIONS = {};
@ -64,6 +64,18 @@ class MouseCursorHider {
class StreamBadges {
static get BADGE_PLAYTIME() { return 'playtime'; };
static get BADGE_BATTERY() { return 'battery'; };
static get BADGE_IN() { return 'in'; };
static get BADGE_OUT() { return 'out'; };
static get BADGE_REGION() { return 'region'; };
static get BADGE_SERVER() { return 'server'; };
static get BADGE_VIDEO() { return 'video'; };
static get BADGE_AUDIO() { return 'audio'; };
static get BADGE_BREAK() { return 'break'; };
static ipv6 = false;
static resolution = null;
static video = null;
@ -74,15 +86,92 @@ class StreamBadges {
static startBatteryLevel = 100;
static startTimestamp = 0;
static #cachedDoms = {};
static #interval;
static get #REFRESH_INTERVAL() { return 3000; };
static #renderBadge(name, value, color) {
const CE = createElement;
const $badge = CE('div', {'class': 'better-xcloud-badge'},
CE('span', {'class': 'better-xcloud-badge-name'}, name),
CE('span', {'class': 'better-xcloud-badge-value', 'style': `background-color: ${color}`}, value));
if (name === StreamBadges.BADGE_BREAK) {
return CE('div', {'style': 'display: block'});
}
let $badge;
if (StreamBadges.#cachedDoms[name]) {
$badge = StreamBadges.#cachedDoms[name];
$badge.lastElementChild.textContent = value;
return $badge;
}
$badge = CE('div', {'class': 'better-xcloud-badge'},
CE('span', {'class': 'better-xcloud-badge-name'}, name),
CE('span', {'class': 'better-xcloud-badge-value', 'style': `background-color: ${color}`}, value));
StreamBadges.#cachedDoms[name] = $badge;
return $badge;
}
static async #updateBadges(forceUpdate) {
if (!forceUpdate && !document.querySelector('.better-xcloud-badges')) {
StreamBadges.#stop();
return;
}
// Playtime
let now = +new Date;
const diffSeconds = Math.ceil((now - StreamBadges.startTimestamp) / 1000);
const playtime = StreamBadges.#secondsToHm(diffSeconds);
// Battery
let batteryLevel = '100%';
if (navigator.getBattery) {
try {
const currentLevel = (await navigator.getBattery()).level * 100;
batteryLevel = `${currentLevel}%`;
if (currentLevel != StreamBadges.startBatteryLevel) {
const diffLevel = currentLevel - StreamBadges.startBatteryLevel;
const sign = diffLevel > 0 ? '+' : '';
batteryLevel += ` (${sign}${diffLevel}%)`;
}
} catch(e) {}
}
const stats = await STREAM_WEBRTC.getStats();
let totalIn = 0;
let totalOut = 0;
stats.forEach(stat => {
if (stat.type === 'candidate-pair' && stat.state == 'succeeded') {
totalIn += stat.bytesReceived;
totalOut += stat.bytesSent;
}
});
const badges = {
[StreamBadges.BADGE_IN]: totalIn ? StreamBadges.#humanFileSize(totalIn) : null,
[StreamBadges.BADGE_OUT]: totalOut ? StreamBadges.#humanFileSize(totalOut) : null,
[StreamBadges.BADGE_PLAYTIME]: playtime,
[StreamBadges.BADGE_BATTERY]: batteryLevel,
};
for (let name in badges) {
const value = badges[name];
if (value === null) {
continue;
}
const $elm = StreamBadges.#cachedDoms[name];
$elm && ($elm.lastElementChild.textContent = value);
}
}
static #stop() {
StreamBadges.#interval && clearInterval(StreamBadges.#interval);
StreamBadges.#interval = null;
}
static #secondsToHm(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor(seconds % 3600 / 60) + 1;
@ -92,10 +181,22 @@ class StreamBadges {
return hDisplay + mDisplay;
}
// https://stackoverflow.com/a/20732091
static #humanFileSize(size) {
let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
static async render() {
let video;
// Video
let video = '';
if (StreamBadges.resolution) {
video = `${StreamBadges.resolution.height}p`;
}
if (StreamBadges.video) {
video = StreamBadges.video.codec;
video && (video += '/');
video += StreamBadges.video.codec;
if (StreamBadges.video.profile) {
let profile = StreamBadges.video.profile;
profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile);
@ -103,6 +204,7 @@ class StreamBadges {
}
}
// Audio
let audio;
if (StreamBadges.audio) {
audio = StreamBadges.audio.codec;
@ -110,36 +212,31 @@ class StreamBadges {
audio += ` (${bitrate} kHz)`;
}
// Battery
let batteryLevel = '';
if (navigator.getBattery && StreamBadges.startBatteryLevel < 100) {
try {
const currentLevel = (await navigator.getBattery()).level * 100;
batteryLevel = `${currentLevel}%`;
if (currentLevel < StreamBadges.startBatteryLevel) {
const diffLevel = StreamBadges.startBatteryLevel - currentLevel;
batteryLevel += ` (-${diffLevel}%)`;
}
} catch(e) {}
if (navigator.getBattery) {
batteryLevel = '100%';
}
let now = +new Date;
const diffSeconds = Math.ceil((now - StreamBadges.startTimestamp) / 1000);
const playtime = StreamBadges.#secondsToHm(diffSeconds);
const BADGES = [
playtime ? ['playtime', playtime, '#ff004d'] : null,
batteryLevel ? ['battery', batteryLevel, '#008751'] : null,
['region', StreamBadges.region, '#ff6c24'],
['server', StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#065ab5'],
StreamBadges.resolution && ['resolution', `${StreamBadges.resolution.width}x${StreamBadges.resolution.height}`, '#7e2553'],
video ? ['video', video, '#065AB5'] : null,
audio ? ['audio', audio, '#5f574f'] : null,
[StreamBadges.BADGE_PLAYTIME, '1m', '#ff004d'],
[StreamBadges.BADGE_BATTERY, batteryLevel, '#00b543'],
[StreamBadges.BADGE_IN, StreamBadges.#humanFileSize(0), '#29adff'],
[StreamBadges.BADGE_OUT, StreamBadges.#humanFileSize(0), '#ff77a8'],
[StreamBadges.BADGE_BREAK],
[StreamBadges.BADGE_REGION, StreamBadges.region, '#ff6c24'],
[StreamBadges.BADGE_SERVER, StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#065ab5'],
video ? [StreamBadges.BADGE_VIDEO, video, '#754665'] : null,
audio ? [StreamBadges.BADGE_AUDIO, audio, '#5f574f'] : null,
];
const $wrapper = createElement('div', {'class': 'better-xcloud-badges'});
BADGES.forEach(item => item && $wrapper.appendChild(StreamBadges.#renderBadge(...item)));
await StreamBadges.#updateBadges(true);
StreamBadges.#stop();
StreamBadges.#interval = setInterval(StreamBadges.#updateBadges, StreamBadges.#REFRESH_INTERVAL);
return $wrapper;
}
}
@ -162,14 +259,14 @@ class StreamStats {
static #lastStat;
static start() {
StreamStats.#$container.style.display = 'block';
StreamStats.#$container.classList.remove('better-xcloud-gone');
StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval);
}
static stop() {
clearInterval(StreamStats.#interval);
StreamStats.#$container.style.display = 'none';
StreamStats.#$container.classList.add('better-xcloud-gone');
StreamStats.#interval = null;
StreamStats.#lastStat = null;
}
@ -178,7 +275,7 @@ class StreamStats {
StreamStats.#isHidden() ? StreamStats.start() : StreamStats.stop();
}
static #isHidden = () => StreamStats.#$container.style.display === 'none';
static #isHidden = () => StreamStats.#$container.classList.contains('better-xcloud-gone');
static update() {
if (StreamStats.#isHidden() || !STREAM_WEBRTC) {
@ -196,13 +293,15 @@ class StreamStats {
// Packets Lost
const packetsLost = stat.packetsLost;
const packetsReceived = stat.packetsReceived || 1;
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`;
const packetsReceived = stat.packetsReceived;
const packetsLostPercentage = (packetsLost * 100 / ((packetsLost + packetsReceived) || 1)).toFixed(2);
StreamStats.#$pl.textContent = `${packetsLost} (${packetsLostPercentage}%)`;
// Frames Dropped
const framesDropped = stat.framesDropped;
const framesReceived = stat.framesReceived || 1;
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`;
const framesReceived = stat.framesReceived;
const framesDroppedPercentage = (framesDropped * 100 / ((framesDropped + framesReceived) || 1)).toFixed(2);
StreamStats.#$fl.textContent = `${framesDropped} (${framesDroppedPercentage}%)`;
if (StreamStats.#lastStat) {
const lastStat = StreamStats.#lastStat;
@ -265,7 +364,7 @@ class StreamStats {
}
const CE = createElement;
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar'},
StreamStats.#$container = CE('div', {'class': 'better-xcloud-stats-bar better-xcloud-gone'},
CE('label', {}, 'FPS'),
StreamStats.#$fps = CE('span', {}, 0),
CE('label', {}, 'RTT'),
@ -387,6 +486,7 @@ class UserAgent {
get: () => this._state,
set: (state) => {
state.appContext.requestInfo.userAgent = userAgent;
state.appContext.requestInfo.origin = 'https://www.xbox.com';
this._state = state;
}
});
@ -768,8 +868,8 @@ function addCss() {
font-family: "Segoe UI", Arial, Helvetica, sans-serif
}
.better-xcloud-settings-gone {
display: none;
.better-xcloud-gone {
display: none !important;
}
.better-xcloud-settings-wrapper {
@ -951,7 +1051,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.better-xcloud-stats-bar {
display: none;
display: block;
user-select: none;
position: fixed;
top: 0;
@ -1503,7 +1603,7 @@ function injectSettingsButton($parent) {
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
$button.addEventListener('click', e => {
const $settings = document.querySelector('.better_xcloud_settings');
$settings.classList.toggle('better-xcloud-settings-gone');
$settings.classList.toggle('better-xcloud-gone');
$settings.scrollIntoView();
});
@ -1514,7 +1614,7 @@ function injectSettingsButton($parent) {
$parent.appendChild($button);
const $container = CE('div', {
'class': 'better_xcloud_settings better-xcloud-settings-gone',
'class': 'better_xcloud_settings better-xcloud-gone',
});
let $updateAvailable;
@ -1835,7 +1935,8 @@ function injectVideoSettingsButton() {
isHolding = false;
holdTimeout = setTimeout(() => {
isHolding = true;
}, 750);
confirm('Do you want to refresh the stream?') && window.location.reload();
}, 1000);
};
const onMouseUp = e => {
holdTimeout && clearTimeout(holdTimeout);
@ -1843,7 +1944,6 @@ function injectVideoSettingsButton() {
if (isHolding) {
e.preventDefault();
e.stopPropagation();
confirm('Do you want to refresh the stream?') && window.location.reload();
}
isHolding = false;
};
@ -2181,7 +2281,7 @@ function patchHistoryMethod(type) {
function onHistoryChange() {
const $settings = document.querySelector('.better_xcloud_settings');
if ($settings) {
$settings.classList.add('better-xcloud-settings-gone');
$settings.classList.add('better-xcloud-gone');
}
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');