mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-07-03 21:01:43 +02:00
Compare commits
17 Commits
Author | SHA1 | Date | |
---|---|---|---|
eec41c58b6 | |||
38cc78e0da | |||
3cf029818e | |||
5104cf33b4 | |||
771111d1f8 | |||
79e0661977 | |||
2dc3097737 | |||
0833afc0a2 | |||
ca8b3cfbd8 | |||
bd852c788d | |||
3a82b74cda | |||
dce0a44d2a | |||
1d0d69850f | |||
e719e6e1c5 | |||
9f3c6e5a6d | |||
b6e1d3debc | |||
c229cf7c47 |
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
12
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,18 +1,18 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug]"
|
||||
title: "[Bug] ..."
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Platform**
|
||||
- Device: [e.g. Phone, Laptop, Desktop, TV]
|
||||
- OS: [e.g. Windows, Android, iOS]
|
||||
- Browser: [e.g. Chrome, Kiwi]
|
||||
- Browser Version: [e.g. 100]
|
||||
- Better xCloud Version: [e.g. 1.10]
|
||||
- Device: Phone, Laptop, Desktop, TV...
|
||||
- OS: Windows, Android, iOS...
|
||||
- Browser: Chrome, Safari, Kiwi...
|
||||
- Browser Version:
|
||||
- Better xCloud Version:
|
||||
|
||||
**Describe the bug**
|
||||
...
|
||||
|
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: "[Feature]"
|
||||
title: "[Feature] ..."
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
|
78
README.md
78
README.md
@ -25,17 +25,19 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
|
||||
## Features
|
||||
|
||||
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/65ee4193-c31d-46fb-b580-196614246ee6">
|
||||
|
||||
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/0eedde97-74c7-44df-bc89-2ebf8edb6e2c">
|
||||
<br>
|
||||
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956">
|
||||
<br>
|
||||
<img width="600" alt="Video settings" src="https://github.com/redphx/better-xcloud/assets/96280/c45877f9-379c-4ba4-977c-021d3d8835e4">
|
||||
<img width="600" alt="Video settings" src="https://github.com/redphx/better-xcloud/assets/96280/a8614693-7f56-4a49-82ad-c1fd7e2e00a5">
|
||||
|
||||
|
||||
|
||||
|
||||
**Demo video:** [https://youtu.be/oDr5Eddp55E ](https://youtu.be/AYb-EUcz72U)
|
||||
|
||||
- **🔥 Improve visual quality of the stream**
|
||||
> Similar to (but not as good as) the "Clarity Boost" of xCloud on Edge browser. [Demo video](https://youtu.be/ZhW2choAHUs).
|
||||
- **🔥 Show stream stats**
|
||||
> Check [Stream stats section](#stream-stats) for more info.
|
||||
- **🔥 Capture screenshot**
|
||||
@ -48,7 +50,9 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
|
||||
### Server
|
||||
- **Set the 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.
|
||||
> It's not using VPN.
|
||||
> ["Can I get banned for using this?"](#faq)
|
||||
- **Preferred game's language**
|
||||
> If the game doesn't support this language, it will use the same language as xCloud's website.
|
||||
- **Prefer IPv6 server**
|
||||
@ -74,18 +78,34 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
|
||||
### 🔥 Touch controller
|
||||
- **Availability**
|
||||
> Only for devices with touch support (Android/iOS/iPadOS/...).
|
||||
> Only for devices with touch support (Android/iOS/iPadOS/...). Using "Desktop mode" in mobile browsers also disables this feature.
|
||||
> - **Default**: nothing change.
|
||||
> - **Off**: 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, etc.
|
||||
> - **All games**: enable touch controller support for all games. Games with custom layout won't be affected.
|
||||
> Double-tap anywhere at the bottom of the screen to show/hide the controller. Useful when you're viewing cutscenes.
|
||||
> 
|
||||
> Double-tap anywhere at the bottom of the screen to show/hide the controller. Useful when you're viewing cutscenes.
|
||||
>
|
||||
> 
|
||||
- **Button styles**
|
||||
> - Default
|
||||
> - Muted
|
||||
> - All white (only for standard/default controller)
|
||||
> - All white (only for standard/default controller)
|
||||
>
|
||||
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
|
||||
|
||||
### Loading screen
|
||||
- Show game art
|
||||
> Replace the black background with game art if it's available.
|
||||
- Show the estimated wait time
|
||||
> The time is estimated by the server.
|
||||
> It's not 100% correct: you might get in the game sooner or later.
|
||||
> Don't be mad when the estimated time is inaccurate.
|
||||
> Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info.
|
||||
- Show/hide the rocket animation
|
||||
> Always show/Hide when queuing/Always hide.
|
||||
> Hide this animation might save some battery life while queuing.
|
||||
|
||||
<img height="300" alt="Loading screen" src="https://github.com/redphx/better-xcloud/assets/96280/46074b14-1abb-466d-a859-d46ad4dac2fd">
|
||||
|
||||
|
||||
### UI
|
||||
- **Simplify Stream's menu**
|
||||
@ -104,12 +124,22 @@ If you like this project please give it a 🌟. Thank you 🙏.
|
||||
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
|
||||
|
||||
### Stream's video features
|
||||
⚠️ These features don't work when xCloud's "Clarity Boost" feature is ON ([#64](https://github.com/redphx/better-xcloud/issues/64)).
|
||||
|
||||
- **🔥 Improve stream's clarity**
|
||||
> Similar to (but not as good as) the "Clarity Boost" of xCloud on Edge browser. [Demo video](https://youtu.be/ZhW2choAHUs).
|
||||
> Also known as poor man's "Clarity Boost".
|
||||
> Affects the stream's performance, uses more battery, and may causes frames to drop (especially on lower-end devices).
|
||||
> Works with Chrome/Chromium browsers.
|
||||
> Doesn't work with Safari.
|
||||
>
|
||||
> 
|
||||
> *(click to enlarge)*
|
||||
|
||||
- **Stretch video to full sctreen**
|
||||
> Useful when you don't have a 16:9 screen
|
||||
- **Adjust video filters**
|
||||
> Brightness/Contrast/Saturation.
|
||||
> Brightness/Contrast/Saturation.
|
||||
> ⚠️ These features don't work when xCloud's "Clarity Boost" feature is ON ([#64](https://github.com/redphx/better-xcloud/issues/64)).
|
||||
- **Display stream's statuses**
|
||||
> Region/Server/Codecs/Resolution...
|
||||
> Current playtime of the session.
|
||||
@ -126,7 +156,7 @@ If you like this project please give it a 🌟. 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 install
|
||||
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887) (not working properly, see [#81](https://github.com/redphx/better-xcloud/issues/81)).
|
||||
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use the [Userscripts extension](https://apps.apple.com/us/app/userscripts/id1463298887) (check [this page](https://github.com/redphx/better-xcloud/wiki/Using-with-Safari) before using).
|
||||
2. Install **Better xCloud**:
|
||||
- [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
|
||||
- [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)
|
||||
@ -170,6 +200,9 @@ 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.
|
||||
|
||||
## Stream stats
|
||||
|
||||

|
||||
|
||||
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/0d4abb6b-49ab-4c9a-a52d-df7e396d2145">
|
||||
|
||||
- While playing > `...` > `Stream Stats`.
|
||||
@ -178,14 +211,14 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|
||||
- **Quick glance** feature: only show the stats bar when the System menu 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.
|
||||
|
||||
| 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) |
|
||||
| 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) |
|
||||
| RTT | Round Trip Time | The number of seconds it takes for data to be sent from your device to the server and back over (similar to ping, lower is better) |
|
||||
| DT | Decode Time | The average time it took to decode one frame in the last second (bugged in Kiwi Browser [#26](https://github.com/redphx/better-xcloud/issues/26)) |
|
||||
| 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 |
|
||||
| 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`
|
||||
@ -217,7 +250,7 @@ Colors:
|
||||
5. Screenshot will be saved by the browser.
|
||||
6. You can double-tap that corner to capture screenshot.
|
||||
|
||||
<img width="600" alt="Screenshot button" src="https://github.com/redphx/better-xcloud/assets/96280/a911b141-5dc0-450a-aeac-30d9cf202b44">
|
||||

|
||||
|
||||
## FAQ
|
||||
1. **Will I get banned for using this?**
|
||||
@ -236,12 +269,13 @@ Think of this project as an unofficial beta version of xCloud.
|
||||
No, you can't. You'll have to modify the app.
|
||||
|
||||
5. **Will it be able to enable the "Clarity Boost" feature on non-Edge browsers?**
|
||||
No. The "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
|
||||
~~No. The "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.~~
|
||||
Fake news! This feature has been implemented in **Better xCloud** since version 1.12, but the original "Clarity Boost" still perform better.
|
||||
|
||||
6. **Will it be able to request a lower FPS or increase the maximum bitrate (15Mbps) of the stream?**
|
||||
7. **Will it be able to request a lower FPS or increase the maximum bitrate (15Mbps) of the stream?**
|
||||
Sorry, no. The server decides all these settings.
|
||||
|
||||
7. **What's the meaning behind the name "Better xCloud"?**
|
||||
8. **What's the meaning behind the name "Better xCloud"?**
|
||||
It's a reference to an Userscript called "better360" that I created many years ago. I regret not choosing the name "xCloud Enhancement Suite", or XES for short.
|
||||
|
||||
## Donation
|
||||
|
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.11
|
||||
// @version 1.13.1
|
||||
// ==/UserScript==
|
||||
|
@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.11
|
||||
// @version 1.13.1
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
@ -13,18 +13,37 @@
|
||||
// ==/UserScript==
|
||||
'use strict';
|
||||
|
||||
const SCRIPT_VERSION = '1.13.1';
|
||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||
|
||||
console.log(`[Better xCloud] readyState: ${document.readyState}`);
|
||||
|
||||
|
||||
// Quickly create a tree of elements without having to use innerHTML
|
||||
function createElement(elmName, props = {}) {
|
||||
const $elm = document.createElement(elmName);
|
||||
let $elm;
|
||||
const hasNs = 'xmlns' in props;
|
||||
|
||||
if (hasNs) {
|
||||
$elm = document.createElementNS(props.xmlns, elmName);
|
||||
} else {
|
||||
$elm = document.createElement(elmName);
|
||||
}
|
||||
|
||||
for (let key in props) {
|
||||
if (key === 'xmlns') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!props.hasOwnProperty(key) || $elm.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$elm.setAttribute(key, props[key]);
|
||||
if (hasNs) {
|
||||
$elm.setAttributeNS(null, key, props[key]);
|
||||
} else {
|
||||
$elm.setAttribute(key, props[key]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 2, size = arguments.length; i < size; i++) {
|
||||
@ -79,23 +98,21 @@ window.addEventListener('load', e => {
|
||||
setTimeout(() => {
|
||||
if (document.body.classList.contains('legacyBackground')) {
|
||||
// Has error message -> reload page
|
||||
window.stop();
|
||||
window.location.reload(true);
|
||||
}
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
|
||||
const SCRIPT_VERSION = '1.11';
|
||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||
|
||||
const SERVER_REGIONS = {};
|
||||
var STREAM_WEBRTC;
|
||||
var $STREAM_VIDEO;
|
||||
var $SCREENSHOT_CANVAS;
|
||||
var GAME_TITLE_ID;
|
||||
var APP_CONTEXT;
|
||||
|
||||
const HAS_TOUCH_SUPPORT = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
|
||||
const TOUCH_SUPPORTED_GAME_IDS = new Set();
|
||||
|
||||
// Credit: https://phosphoricons.com
|
||||
const ICON_VIDEO_SETTINGS = '<path d="M16 9.144A6.89 6.89 0 0 0 9.144 16 6.89 6.89 0 0 0 16 22.856 6.89 6.89 0 0 0 22.856 16 6.9 6.9 0 0 0 16 9.144zm0 11.427c-2.507 0-4.571-2.064-4.571-4.571s2.064-4.571 4.571-4.571 4.571 2.064 4.571 4.571-2.064 4.571-4.571 4.571zm15.704-7.541c-.065-.326-.267-.607-.556-.771l-4.26-2.428-.017-4.802c-.001-.335-.15-.652-.405-.868-1.546-1.307-3.325-2.309-5.245-2.953-.306-.103-.641-.073-.923.085L16 3.694l-4.302-2.407c-.282-.158-.618-.189-.924-.086a16.02 16.02 0 0 0-5.239 2.964 1.14 1.14 0 0 0-.403.867L5.109 9.84.848 12.268a1.14 1.14 0 0 0-.555.771 15.22 15.22 0 0 0 0 5.936c.064.326.267.607.555.771l4.261 2.428.017 4.802c.001.335.149.652.403.868 1.546 1.307 3.326 2.309 5.245 2.953.306.103.641.073.923-.085L16 28.306l4.302 2.407a1.13 1.13 0 0 0 .558.143 1.18 1.18 0 0 0 .367-.059c1.917-.648 3.695-1.652 5.239-2.962.255-.216.402-.532.405-.866l.021-4.807 4.261-2.428a1.14 1.14 0 0 0 .555-.771 15.21 15.21 0 0 0-.003-5.931zm-2.143 4.987l-4.082 2.321a1.15 1.15 0 0 0-.429.429l-.258.438a1.13 1.13 0 0 0-.174.601l-.022 4.606a13.71 13.71 0 0 1-3.623 2.043l-4.117-2.295a1.15 1.15 0 0 0-.559-.143h-.546c-.205-.005-.407.045-.586.143l-4.119 2.3a13.74 13.74 0 0 1-3.634-2.033l-.016-4.599a1.14 1.14 0 0 0-.174-.603l-.257-.437c-.102-.182-.249-.333-.429-.437l-4.085-2.328a12.92 12.92 0 0 1 0-4.036l4.074-2.325a1.15 1.15 0 0 0 .429-.429l.258-.438a1.14 1.14 0 0 0 .175-.601l.021-4.606a13.7 13.7 0 0 1 3.625-2.043l4.11 2.295a1.14 1.14 0 0 0 .585.143h.52c.205.005.407-.045.586-.143l4.119-2.3a13.74 13.74 0 0 1 3.634 2.033l.016 4.599a1.14 1.14 0 0 0 .174.603l.257.437c.102.182.249.333.429.438l4.085 2.327a12.88 12.88 0 0 1 .007 4.041h.007z" fill-rule="nonzero"/>';
|
||||
@ -103,6 +120,237 @@ const ICON_STREAM_STATS = '<path d="M27.295 9.31C24.303 6.313 20.234 4.631 16 4.
|
||||
const ICON_SCREENSHOT_B64 = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDMyIDMyIiBmaWxsPSIjZmZmIj48cGF0aCBkPSJNMjguMzA4IDUuMDM4aC00LjI2NWwtMi4wOTctMy4xNDVhMS4yMyAxLjIzIDAgMCAwLTEuMDIzLS41NDhoLTkuODQ2YTEuMjMgMS4yMyAwIDAgMC0xLjAyMy41NDhMNy45NTYgNS4wMzhIMy42OTJBMy43MSAzLjcxIDAgMCAwIDAgOC43MzF2MTcuMjMxYTMuNzEgMy43MSAwIDAgMCAzLjY5MiAzLjY5MmgyNC42MTVBMy43MSAzLjcxIDAgMCAwIDMyIDI1Ljk2MlY4LjczMWEzLjcxIDMuNzEgMCAwIDAtMy42OTItMy42OTJ6bS02Ljc2OSAxMS42OTJjMCAzLjAzOS0yLjUgNS41MzgtNS41MzggNS41MzhzLTUuNTM4LTIuNS01LjUzOC01LjUzOCAyLjUtNS41MzggNS41MzgtNS41MzggNS41MzggMi41IDUuNTM4IDUuNTM4eiIvPjwvc3ZnPgo=';
|
||||
|
||||
|
||||
class TitlesInfo {
|
||||
static #INFO = {};
|
||||
|
||||
static get(titleId) {
|
||||
return TitlesInfo.#INFO[titleId];
|
||||
}
|
||||
|
||||
static update(titleId, info) {
|
||||
TitlesInfo.#INFO[titleId] = TitlesInfo.#INFO[titleId] || {};
|
||||
Object.assign(TitlesInfo.#INFO[titleId], info);
|
||||
}
|
||||
|
||||
static saveFromTitleInfo(titleInfo) {
|
||||
const details = titleInfo.details;
|
||||
TitlesInfo.#INFO[details.productId] = {
|
||||
titleId: titleInfo.titleId,
|
||||
// Has more than one input type -> must have touch support
|
||||
hasTouchSupport: (details.supportedInputTypes.length > 1),
|
||||
};
|
||||
}
|
||||
|
||||
static saveFromCatalogInfo(catalogInfo) {
|
||||
const titleId = catalogInfo.StoreId;
|
||||
|
||||
TitlesInfo.update(titleId, {
|
||||
imageHero: catalogInfo.Image_Hero ? catalogInfo.Image_Hero.URL : '',
|
||||
});
|
||||
}
|
||||
|
||||
static hasTouchSupport(titleId) {
|
||||
const gameInfo = TitlesInfo.#INFO[titleId] || {};
|
||||
return !!gameInfo.hasTouchSupport;
|
||||
}
|
||||
|
||||
static requestCatalogInfo(titleId, callback) {
|
||||
const url = `https://catalog.gamepass.com/v3/products?market=${APP_CONTEXT.marketInfo.market}&language=${APP_CONTEXT.marketInfo.locale}&hydration=RemoteHighSapphire0`;
|
||||
const appVersion = document.querySelector('meta[name=gamepass-app-version]').content;
|
||||
|
||||
fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Ms-Cv': APP_CONTEXT.telemetryInfo.initialCv,
|
||||
'Calling-App-Name': 'Xbox Cloud Gaming Web',
|
||||
'Calling-App-Version': appVersion,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
Products: [titleId],
|
||||
}),
|
||||
}).then(resp => {
|
||||
callback && callback(TitlesInfo.get(titleId));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class LoadingScreen {
|
||||
static #$bgStyle;
|
||||
static #$waitTimeBox;
|
||||
|
||||
static #waitTimeInterval;
|
||||
static #orgWebTitle;
|
||||
|
||||
static #secondsToString(seconds) {
|
||||
const m = Math.floor(seconds / 60);
|
||||
const s = Math.floor(seconds % 60);
|
||||
|
||||
const mDisplay = m > 0 ? `${m}m`: '';
|
||||
const sDisplay = `${s}s`.padStart(s >=0 ? 3 : 4, '0');
|
||||
return mDisplay + sDisplay;
|
||||
}
|
||||
|
||||
static setup() {
|
||||
// Get titleId from location
|
||||
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!LoadingScreen.#$bgStyle) {
|
||||
const $bgStyle = createElement('style');
|
||||
document.documentElement.appendChild($bgStyle);
|
||||
LoadingScreen.#$bgStyle = $bgStyle;
|
||||
}
|
||||
|
||||
const titleId = match[1];
|
||||
const titleInfo = TitlesInfo.get(titleId);
|
||||
if (titleInfo && titleInfo.imageHero) {
|
||||
LoadingScreen.#setBackground(titleInfo.imageHero);
|
||||
} else {
|
||||
TitlesInfo.requestCatalogInfo(titleId, info => {
|
||||
info && info.imageHero && LoadingScreen.#setBackground(info.imageHero);
|
||||
});
|
||||
}
|
||||
|
||||
if (PREFS.get(Preferences.UI_LOADING_SCREEN_ROCKET) === 'hide') {
|
||||
LoadingScreen.#hideRocket();
|
||||
}
|
||||
}
|
||||
|
||||
static #hideRocket() {
|
||||
let $bgStyle = LoadingScreen.#$bgStyle;
|
||||
|
||||
const css = `
|
||||
#game-stream div[class*=RocketAnimation-module__container] > svg {
|
||||
display: none;
|
||||
}
|
||||
`;
|
||||
$bgStyle.textContent += css;
|
||||
}
|
||||
|
||||
static #setBackground(imageUrl) {
|
||||
// Setup style tag
|
||||
let $bgStyle = LoadingScreen.#$bgStyle;
|
||||
|
||||
// Limit max width to reduce image size
|
||||
imageUrl = imageUrl + '?w=1920';
|
||||
|
||||
const css = `
|
||||
#game-stream {
|
||||
background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;
|
||||
background-color: transparent !important;
|
||||
background-position: center center !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: cover !important;
|
||||
}
|
||||
|
||||
#game-stream rect[width="800"] {
|
||||
transition: opacity 0.3s ease-in-out !important;
|
||||
}
|
||||
`;
|
||||
$bgStyle.textContent += css;
|
||||
|
||||
const bg = new Image();
|
||||
bg.onload = e => {
|
||||
$bgStyle.textContent += `
|
||||
#game-stream rect[width="800"] {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
`;
|
||||
};
|
||||
bg.src = imageUrl;
|
||||
}
|
||||
|
||||
static setupWaitTime(waitTime) {
|
||||
const CE = createElement;
|
||||
|
||||
// Hide rocket when queing
|
||||
if (PREFS.get(Preferences.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') {
|
||||
LoadingScreen.#hideRocket();
|
||||
}
|
||||
|
||||
let secondsLeft = waitTime;
|
||||
let $countDown;
|
||||
let $estimated;
|
||||
|
||||
LoadingScreen.#orgWebTitle = document.title;
|
||||
|
||||
const endDate = new Date();
|
||||
const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
|
||||
endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
|
||||
|
||||
let endDateStr = endDate.toISOString().slice(0, 19);
|
||||
endDateStr = endDateStr.substring(0, 10) + ' ' + endDateStr.substring(11, 19);
|
||||
endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`;
|
||||
|
||||
let estimatedWaitTime = LoadingScreen.#secondsToString(waitTime);
|
||||
|
||||
let $waitTimeBox = LoadingScreen.#$waitTimeBox;
|
||||
if (!$waitTimeBox) {
|
||||
$waitTimeBox = CE('div', {'class': 'better-xcloud-wait-time-box'},
|
||||
CE('label', {}, 'Estimated finish time'),
|
||||
$estimated = CE('span', {'class': 'better-xcloud-wait-time-estimated'}),
|
||||
CE('label', {}, 'Countdown'),
|
||||
$countDown = CE('span', {'class': 'better-xcloud-wait-time-countdown'}),
|
||||
);
|
||||
|
||||
document.documentElement.appendChild($waitTimeBox);
|
||||
LoadingScreen.#$waitTimeBox = $waitTimeBox;
|
||||
} else {
|
||||
$waitTimeBox.classList.remove('better-xcloud-gone');
|
||||
$estimated = $waitTimeBox.querySelector('.better-xcloud-wait-time-estimated');
|
||||
$countDown = $waitTimeBox.querySelector('.better-xcloud-wait-time-countdown');
|
||||
}
|
||||
|
||||
$estimated.textContent = endDateStr;
|
||||
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft);
|
||||
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`;
|
||||
|
||||
LoadingScreen.#waitTimeInterval = setInterval(() => {
|
||||
secondsLeft--;
|
||||
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft);
|
||||
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`;
|
||||
|
||||
if (secondsLeft <= 0) {
|
||||
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval);
|
||||
LoadingScreen.#waitTimeInterval = null;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
static hide() {
|
||||
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle);
|
||||
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone');
|
||||
|
||||
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
|
||||
$rocketBg && $rocketBg.addEventListener('transitionend', e => {
|
||||
LoadingScreen.#$bgStyle.textContent += `
|
||||
#game-stream {
|
||||
background: #000 !important;
|
||||
}
|
||||
`;
|
||||
});
|
||||
|
||||
LoadingScreen.#$bgStyle.textContent += `
|
||||
#game-stream rect[width="800"] {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
static reset() {
|
||||
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('better-xcloud-gone');
|
||||
LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = '');
|
||||
|
||||
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval);
|
||||
LoadingScreen.#waitTimeInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class TouchController {
|
||||
static get #EVENT_SHOW_CONTROLLER() {
|
||||
return new MessageEvent('message', {
|
||||
@ -765,8 +1013,12 @@ class UserAgent {
|
||||
[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',
|
||||
}
|
||||
|
||||
static getDefault() {
|
||||
return window.navigator.orgUserAgent || window.navigator.userAgent;
|
||||
}
|
||||
|
||||
static get(profile) {
|
||||
const defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent;
|
||||
const defaultUserAgent = UserAgent.getDefault();
|
||||
if (profile === UserAgent.PROFILE_CUSTOM) {
|
||||
return PREFS.get(Preferences.USER_AGENT_CUSTOM, '');
|
||||
}
|
||||
@ -774,6 +1026,17 @@ class UserAgent {
|
||||
return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
|
||||
}
|
||||
|
||||
static isSafari(mobile=false) {
|
||||
const userAgent = (UserAgent.getDefault() || '').toLowerCase();
|
||||
let result = userAgent.includes('safari') && !userAgent.includes('chrom');
|
||||
|
||||
if (result && mobile) {
|
||||
result = userAgent.includes('mobile');
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
static spoof() {
|
||||
const profile = PREFS.get(Preferences.USER_AGENT_PROFILE);
|
||||
if (profile === UserAgent.PROFILE_DEFAULT) {
|
||||
@ -812,6 +1075,7 @@ class PreloadedState {
|
||||
},
|
||||
set: (state) => {
|
||||
this._state = state;
|
||||
APP_CONTEXT = structuredClone(state.appContext);
|
||||
|
||||
// Get a list of touch-supported games
|
||||
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
|
||||
@ -821,11 +1085,7 @@ class PreloadedState {
|
||||
} catch (e) {}
|
||||
|
||||
for (let id in titles) {
|
||||
const details = titles[id].data.details;
|
||||
// Has move than one input type -> must have touch support
|
||||
if (details.supportedInputTypes.length > 1) {
|
||||
TOUCH_SUPPORTED_GAME_IDS.add(details.productId);
|
||||
}
|
||||
TitlesInfo.saveFromTitleInfo(titles[id].data);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -861,6 +1121,11 @@ class Preferences {
|
||||
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
||||
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
||||
|
||||
static get UI_LOADING_SCREEN_GAME_ART() { return 'ui_loading_screen_game_art'; }
|
||||
static get UI_LOADING_SCREEN_WAIT_TIME() { return 'ui_loading_screen_wait_time'; }
|
||||
static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; }
|
||||
|
||||
static get VIDEO_CLARITY() { return 'video_clarity'; }
|
||||
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; }
|
||||
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
||||
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
||||
@ -986,6 +1251,20 @@ class Preferences {
|
||||
[Preferences.REDUCE_ANIMATIONS]: {
|
||||
'default': false,
|
||||
},
|
||||
[Preferences.UI_LOADING_SCREEN_GAME_ART]: {
|
||||
'default': true,
|
||||
},
|
||||
[Preferences.UI_LOADING_SCREEN_WAIT_TIME]: {
|
||||
'default': false,
|
||||
},
|
||||
[Preferences.UI_LOADING_SCREEN_ROCKET]: {
|
||||
'default': 'show',
|
||||
'options': {
|
||||
'show': 'Always show',
|
||||
'hide-queue': 'Hide when queuing',
|
||||
'hide': 'Always hide',
|
||||
},
|
||||
},
|
||||
[Preferences.BLOCK_SOCIAL_FEATURES]: {
|
||||
'default': false,
|
||||
},
|
||||
@ -1005,6 +1284,11 @@ class Preferences {
|
||||
[Preferences.USER_AGENT_CUSTOM]: {
|
||||
'default': '',
|
||||
},
|
||||
[Preferences.VIDEO_CLARITY]: {
|
||||
'default': 0,
|
||||
'min': 0,
|
||||
'max': 5,
|
||||
},
|
||||
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
|
||||
'default': false,
|
||||
},
|
||||
@ -1242,6 +1526,10 @@ function addCss() {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.better-xcloud-hidden {
|
||||
visibility: hidden !important;
|
||||
}
|
||||
|
||||
.better-xcloud-settings-wrapper {
|
||||
width: 450px;
|
||||
margin: auto;
|
||||
@ -1478,7 +1766,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
|
||||
.better-xcloud-stats-bar[data-transparent=true] {
|
||||
background: none;
|
||||
filter: drop-shadow(1px 0 0 #000) drop-shadow(-1px 0 0 #000) drop-shadow(0 1px 0 #000) drop-shadow(0 -1px 0 #000);
|
||||
filter: drop-shadow(1px 0 0 #000000f0) drop-shadow(-1px 0 0 #000000f0) drop-shadow(0 1px 0 #000000f0) drop-shadow(0 -1px 0 #000000f0);
|
||||
}
|
||||
|
||||
.better-xcloud-stats-bar label {
|
||||
@ -1598,17 +1886,17 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 9999;
|
||||
padding: 20px;
|
||||
width: 620px;
|
||||
padding: 16px;
|
||||
width: 600px;
|
||||
background: #1a1b1e;
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
border-radius: 8px 8px 0 0;
|
||||
font-weight: 400;
|
||||
font-size: 16px;
|
||||
font-size: 14px;
|
||||
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||
text-align: center;
|
||||
box-shadow: 0px 0px 6px #000;
|
||||
@ -1624,22 +1912,22 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar label {
|
||||
font-size: 20px;
|
||||
font-size: 16px;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar input {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
|
||||
.better-xcloud-quick-settings-bar button {
|
||||
border: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 8px;
|
||||
line-height: 24px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin: 0 4px;
|
||||
line-height: 22px;
|
||||
background-color: #515151;
|
||||
color: #fff;
|
||||
border-radius: 4px;
|
||||
@ -1685,6 +1973,37 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.better-xcloud-wait-time-box {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background-color: #000000cc;
|
||||
color: #fff;
|
||||
z-index: 9999;
|
||||
padding: 12px;
|
||||
border-radius: 0 0 0 8px;
|
||||
}
|
||||
|
||||
.better-xcloud-wait-time-box label {
|
||||
display: block;
|
||||
text-transform: uppercase;
|
||||
text-align: right;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.better-xcloud-wait-time-estimated, .better-xcloud-wait-time-countdown {
|
||||
display: block;
|
||||
font-family: Consolas, "Courier New", Courier, monospace;
|
||||
text-align: right;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.better-xcloud-wait-time-estimated {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* Hide UI elements */
|
||||
#headerArea, #uhfSkipToMain, .uhf-footer {
|
||||
display: none;
|
||||
@ -1698,7 +2017,7 @@ div[class*=NotFocusedDialog] {
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
#game-stream video {
|
||||
#game-stream video:not([src]) {
|
||||
visibility: hidden;
|
||||
}
|
||||
`;
|
||||
@ -1921,6 +2240,8 @@ function interceptHttpRequests() {
|
||||
const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE);
|
||||
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
|
||||
const PREF_UI_LOADING_SCREEN_GAME_ART = PREFS.get(Preferences.UI_LOADING_SCREEN_GAME_ART);
|
||||
const PREF_UI_LOADING_SCREEN_WAIT_TIME = PREFS.get(Preferences.UI_LOADING_SCREEN_WAIT_TIME);
|
||||
|
||||
const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER);
|
||||
const PREF_AUDIO_MIC_ON_PLAYING = PREFS.get(Preferences.AUDIO_MIC_ON_PLAYING);
|
||||
@ -1967,6 +2288,9 @@ function interceptHttpRequests() {
|
||||
|
||||
// Get region
|
||||
if (url.endsWith('/sessions/cloud/play')) {
|
||||
// Setup loading screen
|
||||
PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.setup();
|
||||
|
||||
// Start hiding cursor
|
||||
if (PREFS.get(Preferences.STREAM_HIDE_IDLE_CURSOR)) {
|
||||
MouseCursorHider.start();
|
||||
@ -2005,8 +2329,28 @@ function interceptHttpRequests() {
|
||||
return orgFetch(...arg);
|
||||
}
|
||||
|
||||
if (PREF_OVERRIDE_CONFIGURATION && url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
||||
// Get wait time
|
||||
if (PREF_UI_LOADING_SCREEN_WAIT_TIME && url.includes('xboxlive.com') && url.includes('/waittime/')) {
|
||||
const promise = orgFetch(...arg);
|
||||
return promise.then(response => {
|
||||
return response.clone().json().then(json => {
|
||||
if (json.estimatedAllocationTimeInSeconds > 0) {
|
||||
// Setup wait time overlay
|
||||
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
|
||||
PREF_UI_LOADING_SCREEN_GAME_ART && LoadingScreen.hide();
|
||||
|
||||
const promise = orgFetch(...arg);
|
||||
if (!PREF_OVERRIDE_CONFIGURATION) {
|
||||
return promise;
|
||||
}
|
||||
|
||||
// Touch controller for all games
|
||||
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
|
||||
@ -2015,8 +2359,9 @@ function interceptHttpRequests() {
|
||||
// Get game ID from window.location
|
||||
const match = window.location.pathname.match(/\/launch\/[^\/]+\/([\w\d]+)/);
|
||||
// Check touch support
|
||||
if (match && !TOUCH_SUPPORTED_GAME_IDS.has(match[1])) {
|
||||
TouchController.enable();
|
||||
if (match) {
|
||||
const titleId = match[1];
|
||||
!TitlesInfo.hasTouchSupport(titleId) && TouchController.enable();
|
||||
}
|
||||
|
||||
// If both settings are invalid -> return promise
|
||||
@ -2058,14 +2403,26 @@ function interceptHttpRequests() {
|
||||
});
|
||||
}
|
||||
|
||||
// catalog.gamepass
|
||||
if (url.startsWith('https://catalog.gamepass.com') && url.includes('/products')) {
|
||||
const promise = orgFetch(...arg);
|
||||
return promise.then(response => {
|
||||
return response.clone().json().then(json => {
|
||||
for (let productId in json.Products) {
|
||||
TitlesInfo.saveFromCatalogInfo(json.Products[productId]);
|
||||
}
|
||||
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && (url.endsWith('/titles') || url.endsWith('/mru'))) {
|
||||
const promise = orgFetch(...arg);
|
||||
return promise.then(response => {
|
||||
return response.clone().json().then(json => {
|
||||
for (let game of json.results) {
|
||||
if (game.details.supportedInputTypes.length > 1) {
|
||||
TOUCH_SUPPORTED_GAME_IDS.add(game.details.productId);
|
||||
}
|
||||
TitlesInfo.saveFromTitleInfo(game);
|
||||
}
|
||||
|
||||
return response;
|
||||
@ -2184,6 +2541,11 @@ function injectSettingsButton($parent) {
|
||||
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: 'Standard layout\'s button style',
|
||||
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: 'Custom layout\'s button style',
|
||||
},
|
||||
'Loading screen': {
|
||||
[Preferences.UI_LOADING_SCREEN_GAME_ART]: 'Show game art',
|
||||
[Preferences.UI_LOADING_SCREEN_WAIT_TIME]: 'Show the estimated wait time',
|
||||
[Preferences.UI_LOADING_SCREEN_ROCKET]: 'Rocket animation',
|
||||
},
|
||||
'UI': {
|
||||
[Preferences.STREAM_SIMPLIFY_MENU]: 'Simplify Stream\'s menu',
|
||||
[Preferences.SKIP_SPLASH_VIDEO]: 'Skip Xbox splash video',
|
||||
@ -2319,6 +2681,15 @@ function injectSettingsButton($parent) {
|
||||
function getVideoPlayerFilterStyle() {
|
||||
const filters = [];
|
||||
|
||||
const clarity = PREFS.get(Preferences.VIDEO_CLARITY);
|
||||
if (clarity != 0) {
|
||||
const level = (7 - (clarity - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
|
||||
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
|
||||
document.getElementById('better-xcloud-filter-clarity-matrix').setAttributeNS(null, 'kernelMatrix', matrix);
|
||||
|
||||
filters.push(`url(#better-xcloud-filter-clarity)`);
|
||||
}
|
||||
|
||||
const saturation = PREFS.get(Preferences.VIDEO_SATURATION);
|
||||
if (saturation != 100) {
|
||||
filters.push(`saturate(${saturation}%)`);
|
||||
@ -2341,8 +2712,22 @@ function getVideoPlayerFilterStyle() {
|
||||
function updateVideoPlayerCss() {
|
||||
let $elm = document.getElementById('better-xcloud-video-css');
|
||||
if (!$elm) {
|
||||
$elm = createElement('style', {id: 'better-xcloud-video-css'});
|
||||
const CE = createElement;
|
||||
|
||||
$elm = CE('style', {id: 'better-xcloud-video-css'});
|
||||
document.documentElement.appendChild($elm);
|
||||
|
||||
// Setup SVG filters
|
||||
const $svg = CE('svg', {
|
||||
'id': 'better-xcloud-video-filters',
|
||||
'xmlns': 'http://www.w3.org/2000/svg',
|
||||
'class': 'better-xcloud-gone',
|
||||
}, CE('defs', {'xmlns': 'http://www.w3.org/2000/svg'},
|
||||
CE('filter', {'id': 'better-xcloud-filter-clarity', 'xmlns': 'http://www.w3.org/2000/svg'},
|
||||
CE('feConvolveMatrix', {'id': 'better-xcloud-filter-clarity-matrix', 'order': '3', 'xmlns': 'http://www.w3.org/2000/svg'}))
|
||||
)
|
||||
);
|
||||
document.documentElement.appendChild($svg);
|
||||
}
|
||||
|
||||
let filters = getVideoPlayerFilterStyle();
|
||||
@ -2554,14 +2939,20 @@ function patchVideoApi() {
|
||||
|
||||
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
|
||||
this.volume = 0;
|
||||
this.style.display = 'none';
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
LoadingScreen.reset();
|
||||
|
||||
return {
|
||||
catch: () => {},
|
||||
};
|
||||
if (this.className && this.className.startsWith('XboxSplashVideo')) {
|
||||
if (PREF_SKIP_SPLASH_VIDEO) {
|
||||
this.volume = 0;
|
||||
this.style.display = 'none';
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
|
||||
return {
|
||||
catch: () => {},
|
||||
};
|
||||
}
|
||||
|
||||
return this.orgPlay.apply(this);
|
||||
}
|
||||
|
||||
this.addEventListener('playing', showFunc);
|
||||
@ -2632,20 +3023,31 @@ function patchRtcCodecs() {
|
||||
}
|
||||
|
||||
|
||||
function numberPicker(key) {
|
||||
function numberPicker(key, suffix='', disabled=false) {
|
||||
const setting = Preferences.SETTINGS[key]
|
||||
let value = PREFS.get(key);
|
||||
|
||||
let $text, $decBtn, $incBtn;
|
||||
|
||||
const MIN = 0;
|
||||
const MAX= 150;
|
||||
const MIN = setting.min;
|
||||
const MAX= setting.max;
|
||||
|
||||
const CE = createElement;
|
||||
const $wrapper = CE('div', {},
|
||||
$decBtn = CE('button', {'data-type': 'dec'}, '-'),
|
||||
$text = CE('span', {}, value + '%'),
|
||||
$text = CE('span', {}, value + suffix),
|
||||
$incBtn = CE('button', {'data-type': 'inc'}, '+'),
|
||||
);
|
||||
|
||||
if (disabled) {
|
||||
$incBtn.disabled = true;
|
||||
$incBtn.classList.add('better-xcloud-hidden');
|
||||
|
||||
$decBtn.disabled = true;
|
||||
$decBtn.classList.add('better-xcloud-hidden');
|
||||
return $wrapper;
|
||||
}
|
||||
|
||||
let interval;
|
||||
let isHolding = false;
|
||||
|
||||
@ -2664,7 +3066,7 @@ function numberPicker(key) {
|
||||
value = (value >= MAX) ? MAX : value + 1;
|
||||
}
|
||||
|
||||
$text.textContent = value + '%';
|
||||
$text.textContent = value + suffix;
|
||||
PREFS.set(key, value);
|
||||
updateVideoPlayerCss();
|
||||
|
||||
@ -2705,21 +3107,25 @@ function numberPicker(key) {
|
||||
|
||||
function setupVideoSettingsBar() {
|
||||
const CE = createElement;
|
||||
const isSafari = UserAgent.isSafari();
|
||||
|
||||
let $stretchInp;
|
||||
const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'},
|
||||
CE('div', {},
|
||||
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Stretch Video'),
|
||||
$stretchInp = CE('input', {'id': 'better-xcloud-quick-setting-stretch', 'type': 'checkbox'})),
|
||||
CE('div', {},
|
||||
CE('label', {}, 'Clarity'),
|
||||
numberPicker(Preferences.VIDEO_CLARITY, '', isSafari)), // disable this feature in Safari
|
||||
CE('div', {},
|
||||
CE('label', {}, 'Saturation'),
|
||||
numberPicker(Preferences.VIDEO_SATURATION)),
|
||||
numberPicker(Preferences.VIDEO_SATURATION, '%')),
|
||||
CE('div', {},
|
||||
CE('label', {}, 'Contrast'),
|
||||
numberPicker(Preferences.VIDEO_CONTRAST)),
|
||||
numberPicker(Preferences.VIDEO_CONTRAST, '%')),
|
||||
CE('div', {},
|
||||
CE('label', {}, 'Brightness'),
|
||||
numberPicker(Preferences.VIDEO_BRIGHTNESS))
|
||||
numberPicker(Preferences.VIDEO_BRIGHTNESS, '%'))
|
||||
);
|
||||
|
||||
$stretchInp.checked = PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN);
|
||||
@ -2831,6 +3237,8 @@ function onHistoryChanged() {
|
||||
|
||||
MouseCursorHider.stop();
|
||||
TouchController.reset();
|
||||
|
||||
LoadingScreen.reset();
|
||||
}
|
||||
|
||||
|
||||
@ -2943,7 +3351,7 @@ function disablePwa() {
|
||||
}
|
||||
|
||||
// Check if it's Safari on mobile
|
||||
if (userAgent.includes('mobile') && userAgent.includes('safari') && !userAgent.includes('chrom')) {
|
||||
if (UserAgent.isSafari(true)) {
|
||||
// Disable the PWA prompt
|
||||
Object.defineProperty(window.navigator, 'standalone', {
|
||||
value: true,
|
||||
|
Reference in New Issue
Block a user