mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-01 11:51:43 +02:00
Compare commits
35 Commits
Author | SHA1 | Date | |
---|---|---|---|
ecad1dc51b | |||
e1c1d74a22 | |||
b1881678b1 | |||
9d8d9680d3 | |||
4d2b6c5ef7 | |||
0c80e3ab1d | |||
6f326e8f2a | |||
92fe3756cf | |||
8ee28d92d9 | |||
1f94058b99 | |||
95e94242aa | |||
91aa28450d | |||
8eb8bbf598 | |||
47817d9d36 | |||
b770a4c9d3 | |||
a27c0ed8f6 | |||
8ac37754e6 | |||
d9288a322b | |||
5facfd2348 | |||
889717be7d | |||
4b0f0784ae | |||
31217d01bb | |||
2f6176e906 | |||
fe011fd0f2 | |||
c23f55ee6b | |||
0c85770ed1 | |||
8233192b8d | |||
6c5fa3c061 | |||
e47c6d9103 | |||
563ad65580 | |||
7933d8d22c | |||
b0e23ca335 | |||
c9f3990173 | |||
5d301b6588 | |||
32123a7891 |
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
3
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -24,8 +24,9 @@ A clear and concise description of what you expected to happen.
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Platform (please complete the following information):**
|
**Platform (please complete the following information):**
|
||||||
|
- Device: [e.g. Phone/Laptop/Desktop/TV]
|
||||||
- OS: [e.g. Android]
|
- OS: [e.g. Android]
|
||||||
- Browser: [e.g. chrome, firefox]
|
- Browser: [e.g. Chrome, Kiwi]
|
||||||
- Browser Version: [e.g. 100]
|
- Browser Version: [e.g. 100]
|
||||||
- Better xCloud Version: [e.g. 1.4]
|
- Better xCloud Version: [e.g. 1.4]
|
||||||
|
|
||||||
|
48
README.md
48
README.md
@ -14,10 +14,9 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/ef689e3b-0235-4635-8623-ebcc332774b2">
|
<img width="475" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/575d566a-7759-4cce-962d-7e5f55a70d9e">
|
||||||
|
|
||||||
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/905750b2-5d02-41e0-af36-2e8b590d81a0">
|
|
||||||
|
|
||||||
|
<img width="475" alt="Stream HUD UI" src="https://github.com/redphx/better-xcloud/assets/96280/b4f943f1-d0b4-4401-a8cb-0fd677a5c6f0">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -25,6 +24,11 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
|
|
||||||
- **🔥 Show stream stats**
|
- **🔥 Show stream stats**
|
||||||
> Check [Stream stats section](#stream-stats) for more info.
|
> Check [Stream stats section](#stream-stats) for more info.
|
||||||
|
- **🔥 Capture screenshot**
|
||||||
|
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
|
||||||
|
- **🔥 Hold the "Quit game" button for one second to refresh the stream**
|
||||||
|
> Sometimes you can fix the bad connection to the stream simply by refreshing the page.
|
||||||
|
> Useful on mobile where the pull-to-refresh feature doesn't work while playing.
|
||||||
- **Switch region of streaming server**
|
- **Switch region of streaming server**
|
||||||
> Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes.
|
> Connect to another server instead of the default one. Check the [**FAQ** section](#faq) for some notes.
|
||||||
- **Preferred game's language**
|
- **Preferred game's language**
|
||||||
@ -43,36 +47,42 @@ Give this project a 🌟 if you like it. Thank you 🙏.
|
|||||||
> Might reduce latency.
|
> Might reduce latency.
|
||||||
- **Disable bandwidth checking**
|
- **Disable bandwidth checking**
|
||||||
> xCloud won't warn about slow connection speed.
|
> xCloud won't warn about slow connection speed.
|
||||||
- **🔥 Capture screenshot**
|
|
||||||
> Exclusive to **Better xCloud**. Check the [**Capture screenshot** section](#capture-screenshot) for more info.
|
|
||||||
- **Skip Xbox splash video**
|
- **Skip Xbox splash video**
|
||||||
> Save 3 seconds.
|
> Save 3 seconds.
|
||||||
- **Hide Dots icon while playing**
|
- **Hide Dots icon while playing**
|
||||||
> You can still click on it, but it doesn't block the screen anymore.
|
> You can still click on it, but it doesn't block the screen anymore.
|
||||||
|
- **Disable touch controller**
|
||||||
|
> Stop the touch controller from showing when touching the screen.
|
||||||
|
> Useful when you play on a device with a built-in controller like Logitech G Cloud, Steam Deck, Retroid, etc.
|
||||||
|
- **Simplify Stream's menu**
|
||||||
|
> Hide the labels of the menu buttons.
|
||||||
- **Hide mouse cursor while playing**
|
- **Hide mouse cursor while playing**
|
||||||
> Hide the mouse cursor after 3 seconds of not moving.
|
> Hide the mouse cursor after 3 seconds of not moving.
|
||||||
- **Reduce UI animations**
|
|
||||||
> Disable `transition` CSS property in some elements. The smooth scrolling cannot be disabled.
|
|
||||||
- **Stretch video to full sctreen**
|
- **Stretch video to full sctreen**
|
||||||
> 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.
|
||||||
- **Display stream's statuses**
|
- **Display stream's statuses**
|
||||||
> Region/Server/Quality/Resolution...
|
> Region/Server/Codecs/Resolution...
|
||||||
|
> Current playtime of the session.
|
||||||
|
> Current battery level.
|
||||||
|
> Estimated total data sent/received.
|
||||||
- **Disable social features**
|
- **Disable social features**
|
||||||
> Features like friends, chat... Disable these will make the page load faster.
|
> Features like friends, chat... Disable these will make the page load faster.
|
||||||
- **Disable xCloud analytics**
|
- **Disable xCloud analytics**
|
||||||
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
|
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
|
||||||
- **Change User-Agent**
|
- **Change User-Agent**
|
||||||
> Useful when you're using unsupported browsers.
|
> Useful when you're using unsupported browsers.
|
||||||
> If you're on Safari, changing User-Agent to "Edge on Windows" will allow you to use Mic feature.
|
|
||||||
> This setting only affects xCloud, and it doesn't change browser's global User-Agent.
|
> This setting only affects xCloud, and it doesn't change browser's global User-Agent.
|
||||||
|
> 📝 If you get 404 error after using this feature, try refreshing the page a few times. See [#34](https://github.com/redphx/better-xcloud/issues/34).
|
||||||
|
- **Reduce UI animations**
|
||||||
|
> Disable `transition` CSS property in some elements. The smooth scrolling cannot be disabled.
|
||||||
- **Hide footer and other UI elements**
|
- **Hide footer and other UI elements**
|
||||||
|
|
||||||
<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.
|
<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
|
## 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**:
|
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)
|
||||||
@ -113,16 +123,17 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
|||||||
- **Better xCloud** also works on Android TV, but you'll have to sideload the browser APK and need a Bluetooth mouse if you want to interact with the Settings.
|
- **Better xCloud** also works on Android TV, but you'll have to sideload the browser APK and need a Bluetooth mouse if you want to interact with the Settings.
|
||||||
|
|
||||||
## Stream stats
|
## Stream stats
|
||||||
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/70f4b1bb-4e3d-4f27-9b2f-afcfe1b8b261">
|
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/0d4abb6b-49ab-4c9a-a52d-df7e396d2145">
|
||||||
|
|
||||||
- While playing > `...` > `Stream Stats`.
|
- While playing > `...` > `Stream Stats` (the one with the eye icon).
|
||||||
- Double-click on the stats bar to show Settings dialog.
|
- Double-click on the stats bar to show the Settings dialog.
|
||||||
- This bar is updated every second.
|
- This bar is updated every second.
|
||||||
|
- **Quick glance** feature: only show the stats bar when the System buttons bar is expanded. The 👀 emoji at the beginning indicates that the stats bar is in the quick glance mode.
|
||||||
- ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream.
|
- ⚠️ Using **Better xCloud** or showing the stats bar also affects the performance of the stream.
|
||||||
|
|
||||||
| Abbr. | Full name | Explain |
|
| 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) |
|
| 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)) |
|
| 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) |
|
| 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 |
|
| BR | Bitrate | The amount of data the server sent to your device in the last second |
|
||||||
@ -139,8 +150,7 @@ Colors:
|
|||||||
- Green = Good
|
- Green = Good
|
||||||
- White = Great
|
- White = Great
|
||||||
|
|
||||||
📝 Having this info on all the time might reduce your enjoyment, so I'd recommend only using it when having network problems.
|
⚠️ Having this info on all the time will drain the battery faster, so I'd recommend only using it when having network problems.
|
||||||
|
|
||||||
|
|
||||||
## Capture screenshot
|
## Capture screenshot
|
||||||
- This feature is only available in **Better xCloud**.
|
- This feature is only available in **Better xCloud**.
|
||||||
@ -169,8 +179,8 @@ I think it's very unlikely that you'll get banned for using this. Most of the fe
|
|||||||
2. **Why is it an Userscript and not an extension?**
|
2. **Why is it an Userscript and not an extension?**
|
||||||
It's because not many browsers on Android support installing extensions (and not all extensions can be installed).
|
It's because not many browsers on Android support installing extensions (and not all extensions can be installed).
|
||||||
|
|
||||||
3. **I see "???" button instead of the server's name**
|
3. **Why doesn't the xCloud website implement *this* or *that* feature from Better xCloud?**
|
||||||
That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser.
|
For being an unofficial tool, **Better xCloud** has the luxury to implement anything on the xCloud website. On the xCloud's side, they have a lot more users and devices to support, so it's more difficult for them to implement a new feature. Also it's not easy to explain some of the features of **Better xCloud** to normal xCloud users.
|
||||||
|
|
||||||
4. **Can I use this with the Xbox Android app?**
|
4. **Can I use this with the Xbox Android app?**
|
||||||
No, you can't. You'll have to modify the app.
|
No, you can't. You'll have to modify the app.
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.7
|
// @version 1.9
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.7
|
// @version 1.9
|
||||||
// @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.7';
|
const SCRIPT_VERSION = '1.9';
|
||||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||||
|
|
||||||
const SERVER_REGIONS = {};
|
const SERVER_REGIONS = {};
|
||||||
@ -22,6 +22,10 @@ var $STREAM_VIDEO;
|
|||||||
var $SCREENSHOT_CANVAS;
|
var $SCREENSHOT_CANVAS;
|
||||||
var GAME_TITLE_ID;
|
var GAME_TITLE_ID;
|
||||||
|
|
||||||
|
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
|
||||||
|
const ICON_VIDEO_SETTINGS = '<path d="M8 2c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.186C5.602 7.158 6.706 8 8 8s2.395-.843 2.813-2h10.188a1 1 0 1 0 0-2H10.813C10.395 2.843 9.293 2 8 2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h9.186c.417 1.158 1.521 2 2.814 2s2.395-.843 2.813-2H21a1 1 0 1 0 0-2h-3.187c-.418-1.157-1.52-2-2.813-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm-7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.188c.417 1.157 1.519 2 2.813 2s2.398-.842 2.814-2H21a1 1 0 1 0 0-2H10.812c-.417-1.157-1.519-2-2.812-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
|
||||||
|
const ICON_STREAM_STATS = '<path d="M12.005 5C9.184 5 6.749 6.416 5.009 7.903c-.87.743-1.571 1.51-2.074 2.18-.251.335-.452.644-.605.934-.434.733-.389 1.314-.004 1.98a6.98 6.98 0 0 0 .609.949 13.62 13.62 0 0 0 2.076 2.182C6.753 17.606 9.188 19 12.005 19s5.252-1.394 6.994-2.873a13.62 13.62 0 0 0 2.076-2.182 6.98 6.98 0 0 0 .609-.949c.425-.737.364-1.343-.004-1.98-.154-.29-.354-.599-.605-.934-.503-.669-1.204-1.436-2.074-2.18C17.261 6.416 14.826 5 12.005 5zm0 2c2.135 0 4.189 1.135 5.697 2.424.754.644 1.368 1.32 1.773 1.859.203.27.354.509.351.733s-.151.462-.353.732c-.404.541-1.016 1.214-1.77 1.854C16.198 15.881 14.145 17 12.005 17s-4.193-1.12-5.699-2.398a11.8 11.8 0 0 1-1.77-1.854c-.202-.27-.351-.508-.353-.732s.149-.463.351-.733c.406-.54 1.019-1.215 1.773-1.859C7.816 8.135 9.87 7 12.005 7zm.025 1.975c-1.645 0-3 1.355-3 3s1.355 3 3 3 3-1.355 3-3-1.355-3-3-3zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
|
||||||
|
|
||||||
|
|
||||||
class MouseCursorHider {
|
class MouseCursorHider {
|
||||||
static #timeout;
|
static #timeout;
|
||||||
@ -60,6 +64,17 @@ class MouseCursorHider {
|
|||||||
|
|
||||||
|
|
||||||
class StreamBadges {
|
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_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 ipv6 = false;
|
||||||
static resolution = null;
|
static resolution = null;
|
||||||
static video = null;
|
static video = null;
|
||||||
@ -67,19 +82,129 @@ class StreamBadges {
|
|||||||
static fps = 0;
|
static fps = 0;
|
||||||
static region = '';
|
static region = '';
|
||||||
|
|
||||||
|
static startBatteryLevel = 100;
|
||||||
|
static startTimestamp = 0;
|
||||||
|
|
||||||
|
static #cachedDoms = {};
|
||||||
|
|
||||||
|
static #interval;
|
||||||
|
static get #REFRESH_INTERVAL() { return 3000; };
|
||||||
|
|
||||||
static #renderBadge(name, value, color) {
|
static #renderBadge(name, value, color) {
|
||||||
const CE = createElement;
|
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;
|
return $badge;
|
||||||
}
|
}
|
||||||
|
|
||||||
static render() {
|
$badge = CE('div', {'class': 'better-xcloud-badge'},
|
||||||
let video;
|
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%';
|
||||||
|
let batteryLevelInt = 100;
|
||||||
|
if (navigator.getBattery) {
|
||||||
|
try {
|
||||||
|
batteryLevelInt = Math.round((await navigator.getBattery()).level * 100);
|
||||||
|
batteryLevel = `${batteryLevelInt}%`;
|
||||||
|
|
||||||
|
if (batteryLevelInt != StreamBadges.startBatteryLevel) {
|
||||||
|
const diffLevel = Math.round(batteryLevelInt - 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);
|
||||||
|
|
||||||
|
if (name === StreamBadges.BADGE_BATTERY) {
|
||||||
|
if (StreamBadges.startBatteryLevel === 100 && batteryLevelInt === 100) {
|
||||||
|
$elm.style.display = 'none';
|
||||||
|
} else {
|
||||||
|
$elm.style = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const hDisplay = h > 0 ? `${h}h`: '';
|
||||||
|
const mDisplay = m > 0 ? `${m}m`: '';
|
||||||
|
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() {
|
||||||
|
// Video
|
||||||
|
let video = '';
|
||||||
|
if (StreamBadges.resolution) {
|
||||||
|
video = `${StreamBadges.resolution.height}p`;
|
||||||
|
}
|
||||||
|
|
||||||
if (StreamBadges.video) {
|
if (StreamBadges.video) {
|
||||||
video = StreamBadges.video.codec;
|
video && (video += '/');
|
||||||
|
video += StreamBadges.video.codec;
|
||||||
if (StreamBadges.video.profile) {
|
if (StreamBadges.video.profile) {
|
||||||
let profile = StreamBadges.video.profile;
|
let profile = StreamBadges.video.profile;
|
||||||
profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile);
|
profile = profile.startsWith('4d') ? 'High' : (profile.startsWith('42') ? 'Normal' : profile);
|
||||||
@ -87,6 +212,7 @@ class StreamBadges {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio
|
||||||
let audio;
|
let audio;
|
||||||
if (StreamBadges.audio) {
|
if (StreamBadges.audio) {
|
||||||
audio = StreamBadges.audio.codec;
|
audio = StreamBadges.audio.codec;
|
||||||
@ -94,17 +220,34 @@ class StreamBadges {
|
|||||||
audio += ` (${bitrate} kHz)`;
|
audio += ` (${bitrate} kHz)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Battery
|
||||||
|
let batteryLevel = '';
|
||||||
|
if (navigator.getBattery) {
|
||||||
|
batteryLevel = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server + Region
|
||||||
|
let server = StreamBadges.region;
|
||||||
|
server += '@' + (StreamBadges.ipv6 ? 'IPv6' : 'IPv4');
|
||||||
|
|
||||||
const BADGES = [
|
const BADGES = [
|
||||||
['region', StreamBadges.region, '#d7450b'],
|
[StreamBadges.BADGE_PLAYTIME, '1m', '#ff004d'],
|
||||||
['server', StreamBadges.ipv6 ? 'IPv6' : 'IPv4', '#008746'],
|
[StreamBadges.BADGE_BATTERY, batteryLevel, '#00b543'],
|
||||||
video ? ['video', video, '#007c8f'] : null,
|
[StreamBadges.BADGE_IN, StreamBadges.#humanFileSize(0), '#29adff'],
|
||||||
audio ? ['audio', audio, '#007c8f'] : null,
|
[StreamBadges.BADGE_OUT, StreamBadges.#humanFileSize(0), '#ff77a8'],
|
||||||
StreamBadges.resolution && ['resolution', `${StreamBadges.resolution.width}x${StreamBadges.resolution.height}`, '#ff3977'],
|
[StreamBadges.BADGE_BREAK],
|
||||||
|
[StreamBadges.BADGE_SERVER, server, '#ff6c24'],
|
||||||
|
video ? [StreamBadges.BADGE_VIDEO, video, '#742f29'] : null,
|
||||||
|
audio ? [StreamBadges.BADGE_AUDIO, audio, '#5f574f'] : null,
|
||||||
];
|
];
|
||||||
|
|
||||||
const $wrapper = createElement('div', {'class': 'better-xcloud-badges'});
|
const $wrapper = createElement('div', {'class': 'better-xcloud-badges'});
|
||||||
BADGES.forEach(item => item && $wrapper.appendChild(StreamBadges.#renderBadge(...item)));
|
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;
|
return $wrapper;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -126,28 +269,83 @@ class StreamStats {
|
|||||||
|
|
||||||
static #lastStat;
|
static #lastStat;
|
||||||
|
|
||||||
static start() {
|
static #quickGlanceObserver;
|
||||||
StreamStats.#$container.style.display = 'block';
|
|
||||||
|
static start(glancing=false) {
|
||||||
|
if (!StreamStats.isHidden() || (glancing && StreamStats.#isGlancing())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StreamStats.#$container.classList.remove('better-xcloud-gone');
|
||||||
|
StreamStats.#$container.setAttribute('data-display', glancing ? 'glancing' : 'fixed');
|
||||||
|
|
||||||
StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval);
|
StreamStats.#interval = setInterval(StreamStats.update, StreamStats.#updateInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
static stop() {
|
static stop(glancing=false) {
|
||||||
clearInterval(StreamStats.#interval);
|
if (glancing && !StreamStats.#isGlancing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
StreamStats.#$container.style.display = 'none';
|
clearInterval(StreamStats.#interval);
|
||||||
StreamStats.#interval = null;
|
StreamStats.#interval = null;
|
||||||
StreamStats.#lastStat = null;
|
StreamStats.#lastStat = null;
|
||||||
|
|
||||||
|
StreamStats.#$container.removeAttribute('data-display');
|
||||||
|
StreamStats.#$container.classList.add('better-xcloud-gone');
|
||||||
}
|
}
|
||||||
|
|
||||||
static toggle() {
|
static toggle() {
|
||||||
StreamStats.#isHidden() ? StreamStats.start() : StreamStats.stop();
|
if (StreamStats.#isGlancing()) {
|
||||||
|
StreamStats.#$container.setAttribute('data-display', 'fixed');
|
||||||
|
} else {
|
||||||
|
StreamStats.isHidden() ? StreamStats.start() : StreamStats.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #isHidden = () => StreamStats.#$container.style.display === 'none';
|
static onStoppedPlaying() {
|
||||||
|
StreamStats.stop();
|
||||||
|
StreamStats.quickGlanceStop();
|
||||||
|
StreamStats.hideSettingsUi();
|
||||||
|
}
|
||||||
|
|
||||||
|
static isHidden = () => StreamStats.#$container.classList.contains('better-xcloud-gone');
|
||||||
|
static #isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing';
|
||||||
|
|
||||||
|
static quickGlanceSetup() {
|
||||||
|
if (StreamStats.#quickGlanceObserver) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $uiContainer = document.querySelector('div[data-testid=ui-container]');
|
||||||
|
StreamStats.#quickGlanceObserver = new MutationObserver((mutationList, observer) => {
|
||||||
|
for (let record of mutationList) {
|
||||||
|
if (record.attributeName && record.attributeName === 'aria-expanded') {
|
||||||
|
const expanded = record.target.ariaExpanded;
|
||||||
|
if (expanded === 'true') {
|
||||||
|
StreamStats.isHidden() && StreamStats.start(true);
|
||||||
|
} else {
|
||||||
|
StreamStats.stop(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
StreamStats.#quickGlanceObserver.observe($uiContainer, {
|
||||||
|
attributes: true,
|
||||||
|
attributeFilter: ['aria-expanded'],
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static quickGlanceStop() {
|
||||||
|
StreamStats.#quickGlanceObserver && StreamStats.#quickGlanceObserver.disconnect();
|
||||||
|
StreamStats.#quickGlanceObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
static update() {
|
static update() {
|
||||||
if (StreamStats.#isHidden() || !STREAM_WEBRTC) {
|
if (StreamStats.isHidden() || !STREAM_WEBRTC) {
|
||||||
StreamStats.stop();
|
StreamStats.onStoppedPlaying();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,13 +359,15 @@ class StreamStats {
|
|||||||
|
|
||||||
// Packets Lost
|
// Packets Lost
|
||||||
const packetsLost = stat.packetsLost;
|
const packetsLost = stat.packetsLost;
|
||||||
const packetsReceived = stat.packetsReceived || 1;
|
const packetsReceived = stat.packetsReceived;
|
||||||
StreamStats.#$pl.textContent = `${packetsLost} (${(packetsLost * 100 / packetsReceived).toFixed(2)}%)`;
|
const packetsLostPercentage = (packetsLost * 100 / ((packetsLost + packetsReceived) || 1)).toFixed(2);
|
||||||
|
StreamStats.#$pl.textContent = `${packetsLost} (${packetsLostPercentage}%)`;
|
||||||
|
|
||||||
// Frames Dropped
|
// Frames Dropped
|
||||||
const framesDropped = stat.framesDropped;
|
const framesDropped = stat.framesDropped;
|
||||||
const framesReceived = stat.framesReceived || 1;
|
const framesReceived = stat.framesReceived;
|
||||||
StreamStats.#$fl.textContent = `${framesDropped} (${(framesDropped * 100 / framesReceived).toFixed(2)}%)`;
|
const framesDroppedPercentage = (framesDropped * 100 / ((framesDropped + framesReceived) || 1)).toFixed(2);
|
||||||
|
StreamStats.#$fl.textContent = `${framesDropped} (${framesDroppedPercentage}%)`;
|
||||||
|
|
||||||
if (StreamStats.#lastStat) {
|
if (StreamStats.#lastStat) {
|
||||||
const lastStat = StreamStats.#lastStat;
|
const lastStat = StreamStats.#lastStat;
|
||||||
@ -217,6 +417,10 @@ class StreamStats {
|
|||||||
|
|
||||||
static hideSettingsUi() {
|
static hideSettingsUi() {
|
||||||
StreamStats.#$settings.style.display = 'none';
|
StreamStats.#$settings.style.display = 'none';
|
||||||
|
|
||||||
|
if (StreamStats.#isGlancing() && !PREFS.get(Preferences.STATS_QUICK_GLANCE)) {
|
||||||
|
StreamStats.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static #toggleSettingsUi() {
|
static #toggleSettingsUi() {
|
||||||
@ -230,7 +434,7 @@ class StreamStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CE = createElement;
|
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'),
|
CE('label', {}, 'FPS'),
|
||||||
StreamStats.#$fps = CE('span', {}, 0),
|
StreamStats.#$fps = CE('span', {}, 0),
|
||||||
CE('label', {}, 'RTT'),
|
CE('label', {}, 'RTT'),
|
||||||
@ -267,7 +471,10 @@ class StreamStats {
|
|||||||
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
|
const $position = PREFS.toElement(Preferences.STATS_POSITION, refreshFunc);
|
||||||
|
|
||||||
let $close;
|
let $close;
|
||||||
const $showStartup = PREFS.toElement(Preferences.STATS_SHOW_WHEN_PLAYING, refreshFunc);
|
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 $transparent = PREFS.toElement(Preferences.STATS_TRANSPARENT, refreshFunc);
|
||||||
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
|
const $formatting = PREFS.toElement(Preferences.STATS_CONDITIONAL_FORMATTING, refreshFunc);
|
||||||
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
|
const $opacity = PREFS.toElement(Preferences.STATS_OPACITY, refreshFunc);
|
||||||
@ -279,6 +486,10 @@ class StreamStats {
|
|||||||
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_SHOW_WHEN_PLAYING}`}, 'Show stats when starting the game'),
|
||||||
$showStartup
|
$showStartup
|
||||||
),
|
),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {'for': `xcloud_setting_${Preferences.STATS_QUICK_GLANCE}`}, 'Enable quick glance'),
|
||||||
|
$quickGlance
|
||||||
|
),
|
||||||
CE('div', {},
|
CE('div', {},
|
||||||
CE('label', {}, 'Position'),
|
CE('label', {}, 'Position'),
|
||||||
$position
|
$position
|
||||||
@ -308,15 +519,16 @@ class StreamStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class UserAgent {
|
class UserAgent {
|
||||||
static get PROFILE_EDGE_WINDOWS() { return 'edge-windows'; }
|
static get PROFILE_EDGE_WINDOWS() { return 'edge-windows'; }
|
||||||
|
static get PROFILE_SAFARI_MACOS() { return 'safari-macos'; }
|
||||||
static get PROFILE_SMARTTV_TIZEN() { return 'smarttv-tizen'; }
|
static get PROFILE_SMARTTV_TIZEN() { return 'smarttv-tizen'; }
|
||||||
static get PROFILE_DEFAULT() { return 'default'; }
|
static get PROFILE_DEFAULT() { return 'default'; }
|
||||||
static get PROFILE_CUSTOM() { return 'custom'; }
|
static get PROFILE_CUSTOM() { return 'custom'; }
|
||||||
|
|
||||||
static #USER_AGENTS = {
|
static #USER_AGENTS = {
|
||||||
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188',
|
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/115.0.0.0 Safari/537.36 Edg/115.0.1901.188',
|
||||||
|
[UserAgent.PROFILE_SAFARI_MACOS]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
|
||||||
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36',
|
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) 94.0.4606.31/7.0 TV Safari/537.36',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +564,7 @@ class UserAgent {
|
|||||||
get: () => this._state,
|
get: () => this._state,
|
||||||
set: (state) => {
|
set: (state) => {
|
||||||
state.appContext.requestInfo.userAgent = userAgent;
|
state.appContext.requestInfo.userAgent = userAgent;
|
||||||
|
state.appContext.requestInfo.origin = 'https://www.xbox.com';
|
||||||
this._state = state;
|
this._state = state;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -370,6 +583,8 @@ class Preferences {
|
|||||||
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
|
static get USE_DESKTOP_CODEC() { return 'use_desktop_codec'; }
|
||||||
static get USER_AGENT_PROFILE() { return 'user_agent_profile'; }
|
static get USER_AGENT_PROFILE() { return 'user_agent_profile'; }
|
||||||
static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; }
|
static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; }
|
||||||
|
static get STREAM_HIDE_TOUCH_CONTROLLER() { return 'stream_hide_touch_controller'; }
|
||||||
|
static get STREAM_SIMPLIFY_MENU() { return 'stream_simplify_menu'; }
|
||||||
|
|
||||||
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
|
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
|
||||||
static get BLOCK_TRACKING() { return 'block_tracking'; }
|
static get BLOCK_TRACKING() { return 'block_tracking'; }
|
||||||
@ -385,6 +600,7 @@ class Preferences {
|
|||||||
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
||||||
|
|
||||||
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_POSITION() { return 'stats_position'; }
|
static get STATS_POSITION() { return 'stats_position'; }
|
||||||
static get STATS_TEXT_SIZE() { return 'stats_text_size'; }
|
static get STATS_TEXT_SIZE() { return 'stats_text_size'; }
|
||||||
static get STATS_TRANSPARENT() { return 'stats_transparent'; }
|
static get STATS_TRANSPARENT() { return 'stats_transparent'; }
|
||||||
@ -477,6 +693,14 @@ class Preferences {
|
|||||||
'label': 'Hide Dots icon while playing',
|
'label': 'Hide Dots icon while playing',
|
||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
|
[Preferences.STREAM_HIDE_TOUCH_CONTROLLER]: {
|
||||||
|
'label': 'Disable touch controller',
|
||||||
|
'default': false,
|
||||||
|
},
|
||||||
|
[Preferences.STREAM_SIMPLIFY_MENU]: {
|
||||||
|
'label': 'Simplify Stream\'s menu',
|
||||||
|
'default': false,
|
||||||
|
},
|
||||||
[Preferences.HIDE_IDLE_CURSOR]: {
|
[Preferences.HIDE_IDLE_CURSOR]: {
|
||||||
'label': 'Hide mouse cursor while playing',
|
'label': 'Hide mouse cursor while playing',
|
||||||
'default': false,
|
'default': false,
|
||||||
@ -499,6 +723,7 @@ class Preferences {
|
|||||||
'options': {
|
'options': {
|
||||||
[UserAgent.PROFILE_DEFAULT]: 'Default',
|
[UserAgent.PROFILE_DEFAULT]: 'Default',
|
||||||
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Edge on Windows',
|
[UserAgent.PROFILE_EDGE_WINDOWS]: 'Edge on Windows',
|
||||||
|
[UserAgent.PROFILE_SAFARI_MACOS]: 'Safari on macOS',
|
||||||
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Samsung Smart TV',
|
[UserAgent.PROFILE_SMARTTV_TIZEN]: 'Samsung Smart TV',
|
||||||
[UserAgent.PROFILE_CUSTOM]: 'Custom',
|
[UserAgent.PROFILE_CUSTOM]: 'Custom',
|
||||||
},
|
},
|
||||||
@ -537,6 +762,10 @@ class Preferences {
|
|||||||
'default': false,
|
'default': false,
|
||||||
'hidden': true,
|
'hidden': true,
|
||||||
},
|
},
|
||||||
|
[Preferences.STATS_QUICK_GLANCE]: {
|
||||||
|
'default': false,
|
||||||
|
'hidden': true,
|
||||||
|
},
|
||||||
[Preferences.STATS_POSITION]: {
|
[Preferences.STATS_POSITION]: {
|
||||||
'default': 'top-left',
|
'default': 'top-left',
|
||||||
'options': {
|
'options': {
|
||||||
@ -724,12 +953,13 @@ function addCss() {
|
|||||||
.better_xcloud_settings {
|
.better_xcloud_settings {
|
||||||
background-color: #151515;
|
background-color: #151515;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: "Segoe UI", Arial, Helvetica, sans-serif
|
font-family: "Segoe UI", Arial, Helvetica, sans-serif
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-settings-gone {
|
.better-xcloud-gone {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-settings-wrapper {
|
.better-xcloud-settings-wrapper {
|
||||||
@ -847,9 +1077,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
|
|
||||||
.better-xcloud-badges {
|
.better-xcloud-badges {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 155px;
|
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-badge {
|
.better-xcloud-badge {
|
||||||
@ -858,6 +1088,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
|
font-family: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
margin: 0 8px 8px 0;
|
margin: 0 8px 8px 0;
|
||||||
box-shadow: 0px 0px 6px #000;
|
box-shadow: 0px 0px 6px #000;
|
||||||
@ -911,8 +1142,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar {
|
.better-xcloud-stats-bar {
|
||||||
display: none;
|
display: block;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 0;
|
top: 0;
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
@ -924,6 +1156,11 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.better-xcloud-stats-bar[data-display=glancing]::before {
|
||||||
|
content: '👀 ';
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-bar[data-position=top-left] {
|
.better-xcloud-stats-bar[data-position=top-left] {
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
@ -999,6 +1236,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
|
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
|
||||||
box-shadow: 0 0 6px #000;
|
box-shadow: 0 0 6px #000;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better-xcloud-stats-settings *:focus {
|
.better-xcloud-stats-settings *:focus {
|
||||||
@ -1056,6 +1294,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
|||||||
.better-xcloud-quick-settings-bar {
|
.better-xcloud-quick-settings-bar {
|
||||||
display: none;
|
display: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
@ -1139,17 +1378,6 @@ div[class*=NotFocusedDialog] {
|
|||||||
#game-stream video {
|
#game-stream video {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust Stream menu icon's size */
|
|
||||||
button[class*=MenuItem-module__container] {
|
|
||||||
min-width: auto !important;
|
|
||||||
width: 100px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
button[class*=MenuItem-module__container] div[class*=MenuItem-module__label] {
|
|
||||||
margin-left: 8px !important;
|
|
||||||
margin-right: 8px !important;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Reduce animations
|
// Reduce animations
|
||||||
@ -1184,6 +1412,80 @@ div[class*=StreamHUD-module__buttonsContainer] {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide touch controller
|
||||||
|
if (PREFS.get(Preferences.STREAM_HIDE_TOUCH_CONTROLLER)) {
|
||||||
|
css += `
|
||||||
|
#MultiTouchSurface, #BabylonCanvasContainer-main {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simplify Stream's menu
|
||||||
|
css += `
|
||||||
|
div[class*=StreamMenu-module__menu] {
|
||||||
|
min-width: 100vw !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
if (PREFS.get(Preferences.STREAM_SIMPLIFY_MENU)) {
|
||||||
|
css += `
|
||||||
|
div[class*=Menu-module__scrollable] {
|
||||||
|
--bxStreamMenuItemSize: 80px;
|
||||||
|
--streamMenuItemSize: calc(var(--bxStreamMenuItemSize) + 40px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better-xcloud-badges {
|
||||||
|
top: calc(var(--streamMenuItemSize) - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body[data-media-type=tv] .better-xcloud-badges {
|
||||||
|
top: calc(var(--streamMenuItemSize) - 10px) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
button[class*=MenuItem-module__container] {
|
||||||
|
min-width: auto !important;
|
||||||
|
min-height: auto !important;
|
||||||
|
width: var(--bxStreamMenuItemSize) !important;
|
||||||
|
height: var(--bxStreamMenuItemSize) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[class*=MenuItem-module__label] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
svg[class*=MenuItem-module__icon] {
|
||||||
|
width: 36px;
|
||||||
|
height: 100% !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
css += `
|
||||||
|
body[data-media-type=tv] .better-xcloud-badges {
|
||||||
|
top: calc(var(--streamMenuItemSize) + 30px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-media-type=tv]) .better-xcloud-badges {
|
||||||
|
top: calc(var(--streamMenuItemSize) + 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-media-type=tv]) button[class*=MenuItem-module__container] {
|
||||||
|
min-width: auto !important;
|
||||||
|
width: 100px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2) {
|
||||||
|
margin-left: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not([data-media-type=tv]) div[class*=MenuItem-module__label] {
|
||||||
|
margin-left: 8px !important;
|
||||||
|
margin-right: 8px !important;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
const $style = createElement('style', {}, css);
|
const $style = createElement('style', {}, css);
|
||||||
document.documentElement.appendChild($style);
|
document.documentElement.appendChild($style);
|
||||||
}
|
}
|
||||||
@ -1454,7 +1756,7 @@ function injectSettingsButton($parent) {
|
|||||||
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
|
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
|
||||||
$button.addEventListener('click', e => {
|
$button.addEventListener('click', e => {
|
||||||
const $settings = document.querySelector('.better_xcloud_settings');
|
const $settings = document.querySelector('.better_xcloud_settings');
|
||||||
$settings.classList.toggle('better-xcloud-settings-gone');
|
$settings.classList.toggle('better-xcloud-gone');
|
||||||
$settings.scrollIntoView();
|
$settings.scrollIntoView();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1465,7 +1767,7 @@ function injectSettingsButton($parent) {
|
|||||||
$parent.appendChild($button);
|
$parent.appendChild($button);
|
||||||
|
|
||||||
const $container = CE('div', {
|
const $container = CE('div', {
|
||||||
'class': 'better_xcloud_settings better-xcloud-settings-gone',
|
'class': 'better_xcloud_settings better-xcloud-gone',
|
||||||
});
|
});
|
||||||
|
|
||||||
let $updateAvailable;
|
let $updateAvailable;
|
||||||
@ -1706,7 +2008,8 @@ function injectVideoSettingsButton() {
|
|||||||
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');
|
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');
|
||||||
const $parent = $screen.parentElement;
|
const $parent = $screen.parentElement;
|
||||||
const hideQuickBarFunc = e => {
|
const hideQuickBarFunc = e => {
|
||||||
if (e.target != $parent && e.target.id !== 'MultiTouchSurface') {
|
e.stopPropagation();
|
||||||
|
if (e.target != $parent && e.target.id !== 'MultiTouchSurface' && !e.target.querySelector('#BabylonCanvasContainer-main')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1714,7 +2017,7 @@ function injectVideoSettingsButton() {
|
|||||||
$quickBar.style.display = 'none';
|
$quickBar.style.display = 'none';
|
||||||
|
|
||||||
$parent.removeEventListener('click', hideQuickBarFunc);
|
$parent.removeEventListener('click', hideQuickBarFunc);
|
||||||
$parent.removeEventListener('touchend', hideQuickBarFunc);
|
$parent.removeEventListener('touchstart', hideQuickBarFunc);
|
||||||
|
|
||||||
if (e.target.id === 'MultiTouchSurface') {
|
if (e.target.id === 'MultiTouchSurface') {
|
||||||
e.target.removeEventListener('touchstart', hideQuickBarFunc);
|
e.target.removeEventListener('touchstart', hideQuickBarFunc);
|
||||||
@ -1727,7 +2030,7 @@ function injectVideoSettingsButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
item.addedNodes.forEach(node => {
|
item.addedNodes.forEach(async node => {
|
||||||
if (!node.className || !node.className.startsWith('StreamMenu')) {
|
if (!node.className || !node.className.startsWith('StreamMenu')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1737,8 +2040,6 @@ function injectVideoSettingsButton() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
|
|
||||||
const ICON_VIDEO_SETTINGS = '<path d="M8 2c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.186C5.602 7.158 6.706 8 8 8s2.395-.843 2.813-2h10.188a1 1 0 1 0 0-2H10.813C10.395 2.843 9.293 2 8 2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h9.186c.417 1.158 1.521 2 2.814 2s2.395-.843 2.813-2H21a1 1 0 1 0 0-2h-3.187c-.418-1.157-1.52-2-2.813-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm-7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.188c.417 1.157 1.519 2 2.813 2s2.398-.842 2.814-2H21a1 1 0 1 0 0-2H10.812c-.417-1.157-1.519-2-2.812-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
|
|
||||||
// Create Video Settings button
|
// Create Video Settings button
|
||||||
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
|
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
|
||||||
$btnVideoSettings.addEventListener('click', e => {
|
$btnVideoSettings.addEventListener('click', e => {
|
||||||
@ -1749,10 +2050,10 @@ function injectVideoSettingsButton() {
|
|||||||
$quickBar.style.display = 'flex';
|
$quickBar.style.display = 'flex';
|
||||||
|
|
||||||
$parent.addEventListener('click', hideQuickBarFunc);
|
$parent.addEventListener('click', hideQuickBarFunc);
|
||||||
$parent.addEventListener('touchend', hideQuickBarFunc);
|
$parent.addEventListener('touchstart', hideQuickBarFunc);
|
||||||
|
|
||||||
const $touchSurface = document.querySelector('#MultiTouchSurface');
|
const $touchSurface = document.querySelector('#MultiTouchSurface');
|
||||||
$touchSurface && $touchSurface.addEventListener('touchstart', hideQuickBarFunc);
|
$touchSurface && $touchSurface.style.display != 'none' && $touchSurface.addEventListener('touchstart', hideQuickBarFunc);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add button at the beginning
|
// Add button at the beginning
|
||||||
@ -1764,7 +2065,6 @@ function injectVideoSettingsButton() {
|
|||||||
$quickBar.style.display = 'none';
|
$quickBar.style.display = 'none';
|
||||||
});
|
});
|
||||||
|
|
||||||
const ICON_STREAM_STATS = '<path d="M12.005 5C9.184 5 6.749 6.416 5.009 7.903c-.87.743-1.571 1.51-2.074 2.18-.251.335-.452.644-.605.934-.434.733-.389 1.314-.004 1.98a6.98 6.98 0 0 0 .609.949 13.62 13.62 0 0 0 2.076 2.182C6.753 17.606 9.188 19 12.005 19s5.252-1.394 6.994-2.873a13.62 13.62 0 0 0 2.076-2.182 6.98 6.98 0 0 0 .609-.949c.425-.737.364-1.343-.004-1.98-.154-.29-.354-.599-.605-.934-.503-.669-1.204-1.436-2.074-2.18C17.261 6.416 14.826 5 12.005 5zm0 2c2.135 0 4.189 1.135 5.697 2.424.754.644 1.368 1.32 1.773 1.859.203.27.354.509.351.733s-.151.462-.353.732c-.404.541-1.016 1.214-1.77 1.854C16.198 15.881 14.145 17 12.005 17s-4.193-1.12-5.699-2.398a11.8 11.8 0 0 1-1.77-1.854c-.202-.27-.351-.508-.353-.732s.149-.463.351-.733c.406-.54 1.019-1.215 1.773-1.859C7.816 8.135 9.87 7 12.005 7zm.025 1.975c-1.645 0-3 1.355-3 3s1.355 3 3 3 3-1.355 3-3-1.355-3-3-3zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
|
|
||||||
// Create Stream Stats button
|
// Create Stream Stats button
|
||||||
const $btnStreamStats = cloneStreamMenuButton($orgButton, 'Stream stats', ICON_STREAM_STATS);
|
const $btnStreamStats = cloneStreamMenuButton($orgButton, 'Stream stats', ICON_STREAM_STATS);
|
||||||
$btnStreamStats.addEventListener('click', e => {
|
$btnStreamStats.addEventListener('click', e => {
|
||||||
@ -1780,9 +2080,37 @@ function injectVideoSettingsButton() {
|
|||||||
// Insert after Video Settings button
|
// Insert after Video Settings button
|
||||||
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings);
|
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings);
|
||||||
|
|
||||||
|
// Get "Quit game" button
|
||||||
|
const $btnQuit = $orgButton.parentElement.querySelector('button:last-of-type');
|
||||||
|
|
||||||
|
let isHolding = false;
|
||||||
|
let holdTimeout;
|
||||||
|
const onMouseDown = e => {
|
||||||
|
isHolding = false;
|
||||||
|
holdTimeout = setTimeout(() => {
|
||||||
|
isHolding = true;
|
||||||
|
confirm('Do you want to refresh the stream?') && window.location.reload();
|
||||||
|
}, 1000);
|
||||||
|
};
|
||||||
|
const onMouseUp = e => {
|
||||||
|
holdTimeout && clearTimeout(holdTimeout);
|
||||||
|
|
||||||
|
if (isHolding) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
isHolding = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$btnQuit.addEventListener('mousedown', onMouseDown);
|
||||||
|
$btnQuit.addEventListener('click', onMouseUp);
|
||||||
|
|
||||||
|
$btnQuit.addEventListener('touchstart', onMouseDown);
|
||||||
|
$btnQuit.addEventListener('touchend', onMouseUp);
|
||||||
|
|
||||||
// Render stream badges
|
// Render stream badges
|
||||||
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
|
const $menu = document.querySelector('div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]');
|
||||||
$menu.appendChild(StreamBadges.render());
|
$menu.appendChild(await StreamBadges.render());
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -1793,6 +2121,7 @@ function injectVideoSettingsButton() {
|
|||||||
function patchVideoApi() {
|
function patchVideoApi() {
|
||||||
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
|
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
|
||||||
const PREF_SCREENSHOT_BUTTON_POSITION = PREFS.get(Preferences.SCREENSHOT_BUTTON_POSITION);
|
const PREF_SCREENSHOT_BUTTON_POSITION = PREFS.get(Preferences.SCREENSHOT_BUTTON_POSITION);
|
||||||
|
const PREF_STATS_QUICK_GLANCE = PREFS.get(Preferences.STATS_QUICK_GLANCE);
|
||||||
|
|
||||||
// Show video player when it's ready
|
// Show video player when it's ready
|
||||||
var showFunc;
|
var showFunc;
|
||||||
@ -1804,36 +2133,76 @@ function patchVideoApi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (PREF_STATS_QUICK_GLANCE) {
|
||||||
|
StreamStats.quickGlanceSetup();
|
||||||
|
}
|
||||||
|
|
||||||
$STREAM_VIDEO = this;
|
$STREAM_VIDEO = this;
|
||||||
$SCREENSHOT_CANVAS.width = this.videoWidth;
|
$SCREENSHOT_CANVAS.width = this.videoWidth;
|
||||||
$SCREENSHOT_CANVAS.height = this.videoHeight;
|
$SCREENSHOT_CANVAS.height = this.videoHeight;
|
||||||
StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight};
|
|
||||||
|
|
||||||
STREAM_WEBRTC.getStats().then(stats => {
|
StreamBadges.resolution = {width: this.videoWidth, height: this.videoHeight};
|
||||||
stats.forEach(stat => {
|
StreamBadges.startTimestamp = +new Date;
|
||||||
if (stat.type !== 'codec') {
|
|
||||||
return;
|
// Get battery level
|
||||||
|
if (navigator.getBattery) {
|
||||||
|
try {
|
||||||
|
navigator.getBattery().then(bm => {
|
||||||
|
StreamBadges.startBatteryLevel = Math.round(bm.level * 100);
|
||||||
|
});
|
||||||
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
STREAM_WEBRTC.getStats().then(stats => {
|
||||||
|
const allVideoCodecs = {};
|
||||||
|
let videoCodecId;
|
||||||
|
|
||||||
|
const allAudioCodecs = {};
|
||||||
|
let audioCodecId;
|
||||||
|
|
||||||
|
stats.forEach(stat => {
|
||||||
|
if (stat.type == 'codec') {
|
||||||
const mimeType = stat.mimeType.split('/');
|
const mimeType = stat.mimeType.split('/');
|
||||||
if (mimeType[0] === 'video') {
|
if (mimeType[0] === 'video') {
|
||||||
|
// Store all video stats
|
||||||
|
allVideoCodecs[stat.id] = stat;
|
||||||
|
} else if (mimeType[0] === 'audio') {
|
||||||
|
// Store all audio stats
|
||||||
|
allAudioCodecs[stat.id] = stat;
|
||||||
|
}
|
||||||
|
} else if (stat.type === 'inbound-rtp' && stat.packetsReceived > 0) {
|
||||||
|
// Get the codecId of the video/audio track currently being used
|
||||||
|
if (stat.kind === 'video') {
|
||||||
|
videoCodecId = stat.codecId;
|
||||||
|
} else if (stat.kind === 'audio') {
|
||||||
|
audioCodecId = stat.codecId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get video codec from codecId
|
||||||
|
if (videoCodecId) {
|
||||||
|
const videoStat = allVideoCodecs[videoCodecId];
|
||||||
const video = {
|
const video = {
|
||||||
codec: mimeType[1],
|
codec: videoStat.mimeType.substring(6),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (video.codec === 'H264') {
|
if (video.codec === 'H264') {
|
||||||
const match = /profile-level-id=([0-9a-f]{6})/.exec(stat.sdpFmtpLine);
|
const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
|
||||||
video.profile = match ? match[1] : null;
|
video.profile = match ? match[1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
StreamBadges.video = video;
|
StreamBadges.video = video;
|
||||||
} else if (!StreamBadges.audio && mimeType[0] === 'audio') {
|
|
||||||
StreamBadges.audio = {
|
|
||||||
codec: mimeType[1],
|
|
||||||
bitrate: stat.clockRate,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// Get audio codec from codecId
|
||||||
|
if (audioCodecId) {
|
||||||
|
const audioStat = allAudioCodecs[audioCodecId];
|
||||||
|
StreamBadges.audio = {
|
||||||
|
codec: audioStat.mimeType.substring(6),
|
||||||
|
bitrate: audioStat.clockRate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
|
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
|
||||||
StreamStats.start();
|
StreamStats.start();
|
||||||
@ -2097,7 +2466,7 @@ function patchHistoryMethod(type) {
|
|||||||
function onHistoryChange() {
|
function onHistoryChange() {
|
||||||
const $settings = document.querySelector('.better_xcloud_settings');
|
const $settings = document.querySelector('.better_xcloud_settings');
|
||||||
if ($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');
|
const $quickBar = document.querySelector('.better-xcloud-quick-settings-bar');
|
||||||
@ -2107,8 +2476,7 @@ function onHistoryChange() {
|
|||||||
|
|
||||||
STREAM_WEBRTC = null;
|
STREAM_WEBRTC = null;
|
||||||
$STREAM_VIDEO = null;
|
$STREAM_VIDEO = null;
|
||||||
StreamStats.stop();
|
StreamStats.onStoppedPlaying();
|
||||||
StreamStats.hideSettingsUi();
|
|
||||||
document.querySelector('.better-xcloud-screenshot-button').style = '';
|
document.querySelector('.better-xcloud-screenshot-button').style = '';
|
||||||
|
|
||||||
MouseCursorHider.stop();
|
MouseCursorHider.stop();
|
||||||
@ -2156,3 +2524,8 @@ updateVideoPlayerCss();
|
|||||||
setupVideoSettingsBar();
|
setupVideoSettingsBar();
|
||||||
setupScreenshotButton();
|
setupScreenshotButton();
|
||||||
StreamStats.render();
|
StreamStats.render();
|
||||||
|
|
||||||
|
// Disable PWA prompt in Safari on iOS/iPadOS
|
||||||
|
Object.defineProperty(window.navigator, 'standalone', {
|
||||||
|
value: true,
|
||||||
|
});
|
||||||
|
Reference in New Issue
Block a user