mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 02:41:44 +02:00
Compare commits
16 Commits
Author | SHA1 | Date | |
---|---|---|---|
8ee28d92d9 | |||
1f94058b99 | |||
95e94242aa | |||
91aa28450d | |||
8eb8bbf598 | |||
47817d9d36 | |||
b770a4c9d3 | |||
a27c0ed8f6 | |||
8ac37754e6 | |||
d9288a322b | |||
5facfd2348 | |||
889717be7d | |||
4b0f0784ae | |||
31217d01bb | |||
2f6176e906 | |||
fe011fd0f2 |
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
9
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -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.
|
||||
|
23
README.md
23
README.md
@ -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">
|
||||
|
||||
|
||||
|
||||
@ -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`
|
||||
|
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.8
|
||||
// @version 1.8.2
|
||||
// ==/UserScript==
|
||||
|
@ -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');
|
||||
|
Reference in New Issue
Block a user