Compare commits

..

21 Commits

Author SHA1 Message Date
3a82b74cda Version 1.12 (#95)
* Update README.md

* Bump version to 1.12

* Bump version to 1.12
2023-08-13 16:48:46 +07:00
dce0a44d2a Add poor man's "Clarity Boost" feature (#94)
* Add Video clarity setting

* Disable Clarity feature in Safari

* Make the Video settings bar smaller

* Stop loading the page when running into the "We are sorry..." error message

* Fix problems with named parameters
2023-08-13 16:22:13 +07:00
1d0d69850f Update issue templates 2023-08-13 10:31:30 +07:00
e719e6e1c5 Update README.md 2023-08-13 10:13:31 +07:00
9f3c6e5a6d Update README.md 2023-08-13 10:11:15 +07:00
b6e1d3debc Update README.md 2023-08-12 21:26:13 +07:00
c229cf7c47 Update README.md 2023-08-12 19:49:51 +07:00
343e243c77 Version 1.11 (#91)
* Update README.md

* Bump version to 1.11

* Bump version to 1.11
2023-08-12 19:41:14 +07:00
0722e02ab0 Minor fix 2023-08-12 17:56:23 +07:00
c520fde438 Show GamePass app's version (#85) 2023-08-12 17:35:40 +07:00
2f4396d948 Update Settings UI 2023-08-12 17:01:44 +07:00
88d46581bf Add touch's controller button styles: default/white/muted 2023-08-12 16:24:37 +07:00
0cd6106957 Increase TouchController.#dispatchMessage timeout time from 10ms to 100ms 2023-08-12 14:55:54 +07:00
43b932f5d6 Add "Enable microphone on game launch" setting (#89)
* Add "Enable microphone on game launch" setting

* Stream menu: clone the second last button instead of the first one (usually the mic button)

* Close the HUD when clicking clicking on the Video Settings button

* Use a different CSS for the Stream Stats button when it's ON
2023-08-12 14:52:27 +07:00
8315cee7ec Update README.md 2023-08-12 14:31:21 +07:00
9d6db3ed37 Fix for Safari (#88)
* Reload the page until the script is loaded first

* Trying to fix the issue with error page

* Show a message when reloading the page

* Minor refactoring in overrides.inputConfiguration

* Automatically reload the page when running into the "We are sorry..." error message
2023-08-12 10:43:51 +07:00
e85fc9aa47 Update README.md 2023-08-11 08:18:41 +07:00
2d680d63a1 Update README.md 2023-08-10 11:03:22 +07:00
aefd9e9320 Add warnings for Safari 2023-08-08 21:33:40 +07:00
ecaead1522 Update README.md 2023-08-08 08:47:01 +07:00
f71152595f Update issue templates 2023-08-08 08:14:58 +07:00
5 changed files with 405 additions and 129 deletions

View File

@ -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.4]
- Device: Phone, Laptop, Desktop, TV...
- OS: Windows, Android, iOS...
- Browser: Chrome, Safari, Kiwi...
- Browser Version:
- Better xCloud Version:
**Describe the bug**
...

View File

@ -1,7 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[Feature]"
title: "[Feature] ..."
labels: enhancement
assignees: ''

106
README.md
View File

@ -1,9 +1,9 @@
# Better xCloud
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience on web browser.
The main target of this script is mobile users, but it should work great on desktop too.
The main target of this script is mobile users, but it should work great on desktop too.
This script makes me spend more time with xCloud, and I hope the same thing happens to you.
Give this project a 🌟 if you like it. Thank you 🙏.
If you like this project please give it a 🌟. Thank you 🙏.
[![Latest version](https://img.shields.io/github/v/release/redphx/better-xcloud?label=latest)](https://github.com/redphx/better-xcloud/releases)
[![Total stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers)
@ -11,19 +11,34 @@ Give this project a 🌟 if you like it. Thank you 🙏.
[![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases)
-->
## Table of Contents
- [**Features**](#features)
- [**How to install**](#how-to-install)
- [**Compatibility**](#compatibility)
- [**Stream stats**](#stream-stats)
- [**Capture screenshot**](#capture-screenshot)
- [**FAQ**](#faq)
- [**Donation**](#donation)
- [**Acknowledgements**](#acknowledgements)
- [**Disclaimers**](#disclaimers)
## Features
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/6e74fdaf-dc84-416a-a2bc-5e117a3717e3">
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/65ee4193-c31d-46fb-b580-196614246ee6">
<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">
&nbsp;
**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**
@ -42,7 +57,7 @@ Give this project a 🌟 if you like it. Thank you 🙏.
- **Prefer IPv6 server**
> Might reduce latency.
### Stream quality
### Stream
- **Set target resolution**
> By default you only get 1080p stream when playing on desktop.
> This feature can give you 1080p stream even on mobile, without having to change User-Agent.
@ -54,18 +69,27 @@ Give this project a 🌟 if you like it. Thank you 🙏.
> Comparison video with the setting ON & OFF: https://youtu.be/-9PuBJJSgR4
- **Disable bandwidth checking**
> xCloud won't warn about slow connection speed.
- **Enable microphone on game launch**
> Automatically enable the mic when starting to play a game.
- **Hide mouse cursor on idle**
> Hide the mouse cursor after 3 seconds of not moving.
### Controller
- **🔥 Touch controller**
> Only for mobile (Android/iOS/iPadOS).
### 🔥 Touch controller
- **Availability**
> 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.
> ![touch-controller](https://github.com/redphx/better-xcloud/assets/96280/6ad6fc76-74aa-46f8-8fb0-806474f494ad)
- **Hide mouse cursor on idle**
> Hide the mouse cursor after 3 seconds of not moving.
> Double-tap anywhere at the bottom of the screen to show/hide the controller. Useful when you're viewing cutscenes.
> &nbsp;
> ![toggle-touch-controller](https://github.com/redphx/better-xcloud/assets/96280/8b9c7091-529a-45ae-8b45-73e61531ecc8)
- **Button styles**
> - Default
> - Muted
> - All white (only for standard/default controller)
> &nbsp;
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
### UI
- **Simplify Stream's menu**
@ -84,12 +108,20 @@ Give this project a 🌟 if you like it. 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).
> Doesn't work with Safari.
> &nbsp;
> ![clarity](https://github.com/redphx/better-xcloud/assets/96280/32879a1e-d56f-428c-bc86-bbacf9588506)
- **Stretch video to full sctreen**
> Useful when you don't have a 16:9 screen
- **Adjust video filters**
> 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.
@ -105,8 +137,8 @@ Give this project a 🌟 if you like it. Thank you 🙏.
<sup>(\*)</sup> By default (for compatibility reasons) xCloud only uses high quality codec profile when you use Tizen TV or Chrome/Edge/Chromium browser on Chrome/MacOS. Enable this setting will give you the best experience no matter what platform & browser you're on.
## How to use
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. For Safari, use [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887).
## How to install
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)
@ -117,7 +149,9 @@ Give this project a 🌟 if you like it. Thank you 🙏.
To update manually, just install the script again (you won't lose your settings).
## Tutorial videos
⚠️⚠️⚠️ If you're using Kiwi Browser on Android, make sure to follow the steps correctly and install the script with Tampermonkey (not installing it as an extension), or else it won't work.
### Tutorial videos
If you still have trouble installing **Better xCloud**, you can follow one of these tutorial videos:
- 🇧🇷 [Tudo isso agora tem no xCloud!! (ChipTec)](https://youtu.be/zS8Zy0mYIbU?t=40)
- 🇫🇷 [#Tuto Xbox Cloud Gaming : Ecran ultra large et adieu les bandes noires sur smartphone (Cloud Gaming France)](https://www.youtube.com/watch?v=5U05KoTdDHs)
@ -140,14 +174,17 @@ If you still have trouble installing **Better xCloud**, you can follow one of th
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
<sup>1</sup> Follow [this guide](https://support.mozilla.org/en-US/kb/find-and-install-add-ons-firefox-android) to install Tampermonkey on Firefox Android. Its Gamepad API doesn't work properly so it might not recognize your controller.
<sup>2, 3</sup> Requires [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887) (free & open source).
<sup>2, 3</sup> Requires [Userscripts app](https://apps.apple.com/us/app/userscripts/id1463298887) (free & open-source). Check [this page](https://github.com/redphx/better-xcloud/wiki/Using-with-Safari) before using.
<sup>4</sup> NOT RECOMMENDED at the moment since its Userscript implementation is not working properly (see https://github.com/redphx/better-xcloud/issues/5 for full details).
---
- **Kiwi Browser** is the best choice on Android. All features work, it means you can get 1080p stream + high quality codec profile (the best possible quality).
- **Kiwi Browser** is the best choice on Android. All features work, it means you can get 1080p stream + high-quality codec profile (the best possible quality).
- **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
![stats](https://github.com/redphx/better-xcloud/assets/96280/736548db-316d-4bb3-a0f8-467766ae810b)
<img width="500" alt="Stream stats" src="https://github.com/redphx/better-xcloud/assets/96280/0d4abb6b-49ab-4c9a-a52d-df7e396d2145">
- While playing > `...` > `Stream Stats`.
@ -156,14 +193,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`
@ -195,7 +232,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">
![screenshot](https://github.com/redphx/better-xcloud/assets/96280/b277193e-df94-4d72-b75c-3f728c984974)
## FAQ
1. **Will I get banned for using this?**
@ -205,20 +242,27 @@ I think it's very unlikely that you'll get banned for using this. Most of the fe
It's because not many browsers on Android support installing extensions (and not all extensions can be installed).
3. **Why doesn't the xCloud website implement *this* or *that* feature from Better xCloud?**
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.
Think of this project as an unofficial beta version of xCloud.
- **Better xCloud** doesn't have to worry about the compatibility much: if it doesn't work on this browser, it can just suggest you switch to another one. xCloud can't do the same.
- 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?**
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
I'm doing this for fun, so you don't have to donate anything. You're already supporting me by using this script. Save that money toward your Xbox Game Pass Ultimate subscription 😄.
## User-Agent
Moved to [wiki](https://github.com/redphx/better-xcloud/wiki/UserAgent).

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.10.2
// @version 1.12
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,15 +13,105 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.10.2';
const SCRIPT_VERSION = '1.12';
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 = {}) {
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;
}
if (hasNs) {
$elm.setAttributeNS(null, key, props[key]);
} else {
$elm.setAttribute(key, props[key]);
}
}
for (let i = 2, size = arguments.length; i < size; i++) {
const arg = arguments[i];
const argType = typeof arg;
if (argType === 'string' || argType === 'number') {
$elm.textContent = arg;
} else if (arg) {
$elm.appendChild(arg);
}
}
return $elm;
}
const ENABLE_SAFARI_WORKAROUND = true;
if (ENABLE_SAFARI_WORKAROUND && document.readyState !== 'loading') {
// Stop loading
window.stop();
// Show the reloading overlay
const $elm = createElement('div', {'class': 'better-xcloud-reload-overlay'}, 'Failed to run Better xCloud. Retrying, please wait...');
const css = `
.better-xcloud-reload-overlay {
position: fixed;
top: 0;
background: #000000cc;
z-index: 9999;
width: 100%;
line-height: 100vh;
color: #fff;
text-align: center;
font-weight: 400;
font-family: "Segoe UI", SegoeUI, "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 1.3rem;
}
`;
document.documentElement.appendChild(createElement('style', {}, css));
document.documentElement.appendChild($elm);
// Reload the page
window.location.reload(true);
// Stop processing the script
throw new Error('[Better xCloud] Executing workaround for Safari');
}
// Automatically reload the page when running into the "We are sorry..." error message
window.addEventListener('load', e => {
setTimeout(() => {
if (document.body.classList.contains('legacyBackground')) {
// Has error message -> reload page
window.stop();
window.location.reload(true);
}
}, 2000);
});
const SERVER_REGIONS = {};
var STREAM_WEBRTC;
var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID;
const HAS_TOUCH_SUPPORT = ('ontouchstart' in window || navigator.maxTouchPoints > 0);
const TOUCH_SUPPORTED_GAME_IDS = new Set();
// Credit: https://phosphoricons.com
@ -99,6 +189,9 @@ class TouchController {
}
static setup() {
const $style = document.createElement('style');
document.documentElement.appendChild($style);
const $bar = createElement('div', {'id': 'better-xcloud-touch-controller-bar'});
document.documentElement.appendChild($bar);
@ -120,8 +213,32 @@ class TouchController {
TouchController.#$bar = $bar;
const PREF_STYLE_STANDARD = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD);
const PREF_STYLE_CUSTOM = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM);
RTCPeerConnection.prototype.orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
// Apply touch controller's style
const $babylonCanvas = document.getElementById('babylon-canvas');
let filter = '';
if (TouchController.#enable) {
if (PREF_STYLE_STANDARD === 'white') {
filter = 'grayscale(1) brightness(2)';
} else if (PREF_STYLE_STANDARD === 'muted') {
filter = 'sepia(0.5)';
}
} else if (PREF_STYLE_CUSTOM === 'muted') {
filter = 'sepia(0.5)';
}
if (filter) {
$style.textContent = `
#babylon-canvas {
filter: ${filter} !important;
}
`;
}
const dataChannel = this.orgCreateDataChannel.apply(this, arguments);
if (!TouchController.#enable) {
return dataChannel;
@ -131,7 +248,7 @@ class TouchController {
// Fix sometimes the touch controller doesn't show at the beginning
dataChannel.addEventListener('open', e => {
TouchController.#show();
setTimeout(TouchController.#show, 1000);
});
dataChannel.addEventListener('message', msg => {
@ -405,7 +522,7 @@ class StreamStats {
static #quickGlanceObserver;
static start(glancing=false) {
if (!StreamStats.isHidden() || (glancing && StreamStats.#isGlancing())) {
if (!StreamStats.isHidden() || (glancing && StreamStats.isGlancing())) {
return;
}
@ -416,7 +533,7 @@ class StreamStats {
}
static stop(glancing=false) {
if (glancing && !StreamStats.#isGlancing()) {
if (glancing && !StreamStats.isGlancing()) {
return;
}
@ -429,7 +546,7 @@ class StreamStats {
}
static toggle() {
if (StreamStats.#isGlancing()) {
if (StreamStats.isGlancing()) {
StreamStats.#$container.setAttribute('data-display', 'fixed');
} else {
StreamStats.isHidden() ? StreamStats.start() : StreamStats.stop();
@ -443,7 +560,7 @@ class StreamStats {
}
static isHidden = () => StreamStats.#$container.classList.contains('better-xcloud-gone');
static #isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing';
static isGlancing = () => StreamStats.#$container.getAttribute('data-display') === 'glancing';
static quickGlanceSetup() {
if (StreamStats.#quickGlanceObserver) {
@ -551,7 +668,7 @@ class StreamStats {
static hideSettingsUi() {
StreamStats.#$settings.style.display = 'none';
if (StreamStats.#isGlancing() && !PREFS.get(Preferences.STATS_QUICK_GLANCE)) {
if (StreamStats.isGlancing() && !PREFS.get(Preferences.STATS_QUICK_GLANCE)) {
StreamStats.stop();
}
}
@ -665,8 +782,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, '');
}
@ -674,6 +795,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) {
@ -701,15 +833,18 @@ class PreloadedState {
static override() {
Object.defineProperty(window, '__PRELOADED_STATE__', {
configurable: true,
get: () => this._state,
set: (state) => {
get: () => {
// Override User-Agent
const userAgent = UserAgent.spoof();
if (userAgent) {
state.appContext.requestInfo.userAgent = userAgent;
state.appContext.requestInfo.origin = 'https://www.xbox.com';
this._state.appContext.requestInfo.userAgent = userAgent;
}
return this._state;
},
set: (state) => {
this._state = state;
// Get a list of touch-supported games
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
let titles = {};
@ -725,7 +860,6 @@ class PreloadedState {
}
}
}
this._state = state;
}
});
}
@ -745,9 +879,12 @@ class Preferences {
static get USER_AGENT_PROFILE() { return 'user_agent_profile'; }
static get USER_AGENT_CUSTOM() { return 'user_agent_custom'; }
static get STREAM_HIDE_IDLE_CURSOR() { return 'stream_hide_idle_cursor';}
static get STREAM_TOUCH_CONTROLLER() { return 'stream_touch_controller'; }
static get STREAM_SIMPLIFY_MENU() { return 'stream_simplify_menu'; }
static get STREAM_TOUCH_CONTROLLER() { return 'stream_touch_controller'; }
static get STREAM_TOUCH_CONTROLLER_STYLE_STANDARD() { return 'stream_touch_controller_style_standard'; }
static get STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM() { return 'stream_touch_controller_style_custom'; }
static get SCREENSHOT_BUTTON_POSITION() { return 'screenshot_button_position'; }
static get BLOCK_TRACKING() { return 'block_tracking'; }
static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; }
@ -756,11 +893,14 @@ class Preferences {
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
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'; }
static get VIDEO_SATURATION() { return 'video_saturation'; }
static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_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'; }
@ -855,6 +995,21 @@ class Preferences {
'off': 'Off',
},
},
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
'default': 'default',
'options': {
'default': 'Default colors',
'white': 'All white',
'muted': 'Muted colors',
},
},
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
'default': 'default',
'options': {
'default': 'Default colors',
'muted': 'Muted colors',
},
},
[Preferences.STREAM_SIMPLIFY_MENU]: {
'default': false,
},
@ -883,6 +1038,11 @@ class Preferences {
[Preferences.USER_AGENT_CUSTOM]: {
'default': '',
},
[Preferences.VIDEO_CLARITY]: {
'default': 0,
'min': 0,
'max': 3,
},
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
'default': false,
},
@ -901,6 +1061,9 @@ class Preferences {
'min': 0,
'max': 150,
},
[Preferences.AUDIO_MIC_ON_PLAYING]: {
'default': false,
},
[Preferences.STATS_SHOW_WHEN_PLAYING]: {
'default': false,
},
@ -969,6 +1132,11 @@ class Preferences {
return;
}
// Return "default" for STREAM_TOUCH_CONTROLLER pref when the browser doesn't support touch
if (!HAS_TOUCH_SUPPORT && key === Preferences.STREAM_TOUCH_CONTROLLER) {
return 'default';
}
const value = this._prefs[key];
if (typeof value !== 'undefined' && value !== null && value !== '') {
@ -1112,6 +1280,10 @@ function addCss() {
display: none !important;
}
.better-xcloud-hidden {
visibility: hidden !important;
}
.better-xcloud-settings-wrapper {
width: 450px;
margin: auto;
@ -1223,6 +1395,13 @@ function addCss() {
background-color: #00753c;
}
.better-xcloud-settings-app-version {
margin-top: 10px;
text-align: center;
color: #484848;
font-size: 12px;
}
.better-xcloud-settings-custom-user-agent {
display: block;
width: 100%;
@ -1461,17 +1640,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;
@ -1487,22 +1666,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;
@ -1527,6 +1706,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: Consolas, "Courier New", Courier, monospace;
}
.better-xcloud-stream-menu-button-on {
fill: #000 !important;
background-color: #fff !important;
color: #000 !important;
}
#better-xcloud-touch-controller-bar {
display: none;
opacity: 0;
@ -1777,9 +1962,12 @@ function interceptHttpRequests() {
const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER);
const PREF_STREAM_TARGET_RESOLUTION = PREFS.get(Preferences.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = PREFS.get(Preferences.STREAM_PREFERRED_LOCALE);
const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER);
const PREF_USE_DESKTOP_CODEC = PREFS.get(Preferences.USE_DESKTOP_CODEC);
const PREF_STREAM_TOUCH_CONTROLLER = PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER);
const PREF_AUDIO_MIC_ON_PLAYING = PREFS.get(Preferences.AUDIO_MIC_ON_PLAYING);
const PREF_OVERRIDE_CONFIGURATION = PREF_AUDIO_MIC_ON_PLAYING || PREF_STREAM_TOUCH_CONTROLLER === 'all';
const orgFetch = window.fetch;
window.fetch = async (...arg) => {
const request = arg[0];
@ -1859,21 +2047,27 @@ function interceptHttpRequests() {
return orgFetch(...arg);
}
if (PREF_STREAM_TOUCH_CONTROLLER === 'all' && url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
TouchController.disable();
// 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 (PREF_OVERRIDE_CONFIGURATION && url.endsWith('/configuration') && url.includes('/sessions/cloud/') && request.method === 'GET') {
const promise = orgFetch(...arg);
if (!TouchController.isEnabled()) {
return promise;
// Touch controller for all games
if (PREF_STREAM_TOUCH_CONTROLLER === 'all') {
TouchController.disable();
// 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 both settings are invalid -> return promise
if (!PREF_AUDIO_MIC_ON_PLAYING && !TouchController.isEnabled()) {
return promise;
}
}
// Intercept result to make xCloud show the touch controller
// Intercept configurations
return promise.then(response => {
return response.clone().text().then(text => {
if (!text.length) {
@ -1882,10 +2076,20 @@ function interceptHttpRequests() {
const obj = JSON.parse(text);
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
overrides.inputConfiguration = {
enableTouchInput: true,
maxTouchPoints: 10,
};
// Enable touch controller
if (TouchController.isEnabled()) {
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10;
}
// Enable mic
if (PREF_AUDIO_MIC_ON_PLAYING) {
overrides.audioConfiguration = overrides.audioConfiguration || {};
overrides.audioConfiguration.enableMicrophone = true;
}
obj.clientStreamingConfigOverrides = JSON.stringify(overrides);
response.json = () => Promise.resolve(obj);
@ -1950,33 +2154,6 @@ function interceptHttpRequests() {
}
// Quickly create a tree of elements without having to use innerHTML
function createElement(elmName, props = {}) {
const $elm = document.createElement(elmName);
for (let key in props) {
if (!props.hasOwnProperty(key) || $elm.hasOwnProperty(key)) {
continue;
}
let value = props[key];
$elm.setAttribute(key, value);
}
for (let i = 2, size = arguments.length; i < size; i++) {
const arg = arguments[i];
const argType = typeof arg;
if (argType == 'string' || argType == 'number') {
$elm.innerText = arg;
} else if (arg) {
$elm.appendChild(arg);
}
}
return $elm;
}
function injectSettingsButton($parent) {
if (!$parent) {
return;
@ -2037,15 +2214,18 @@ function injectSettingsButton($parent) {
[Preferences.STREAM_PREFERRED_LOCALE]: 'Preferred game\'s language',
[Preferences.PREFER_IPV6_SERVER]: 'Prefer IPv6 server',
},
'Stream quality': {
'Stream': {
[Preferences.STREAM_TARGET_RESOLUTION]: 'Target resolution',
[Preferences.USE_DESKTOP_CODEC]: 'Force high-quality codec',
[Preferences.DISABLE_BANDWIDTH_CHECKING]: 'Disable bandwidth checking',
},
'Controller': {
[Preferences.STREAM_TOUCH_CONTROLLER]: 'Touch controller',
[Preferences.AUDIO_MIC_ON_PLAYING]: 'Enable microphone on game launch',
[Preferences.STREAM_HIDE_IDLE_CURSOR]: 'Hide mouse cursor on idle',
},
'Touch controller': {
[Preferences.STREAM_TOUCH_CONTROLLER]: 'Availability',
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: 'Standard layout\'s button style',
[Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: 'Custom layout\'s button style',
},
'UI': {
[Preferences.STREAM_SIMPLIFY_MENU]: 'Simplify Stream\'s menu',
[Preferences.SKIP_SPLASH_VIDEO]: 'Skip Xbox splash video',
@ -2133,9 +2313,9 @@ function injectSettingsButton($parent) {
$control.disabled = true;
$control.checked = false;
$control.title = 'Your browser doesn\'t support this feature';
} else if (settingId === Preferences.STREAM_TOUCH_CONTROLLER) {
} else if (!HAS_TOUCH_SUPPORT) {
// Disable this setting for non-touchable devices
if (!('ontouchstart' in window) && navigator.maxTouchPoints === 0) {
if ([Preferences.STREAM_TOUCH_CONTROLLER, Preferences.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD, Preferences.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM].indexOf(settingId) > -1) {
$control.disabled = true;
$control.title = 'Your device doesn\'t have touch support';
}
@ -2166,6 +2346,13 @@ function injectSettingsButton($parent) {
});
$wrapper.appendChild($reloadBtn);
// Show Game Pass app version
try {
const appVersion = document.querySelector('meta[name=gamepass-app-version]').content;
const appDate = new Date(document.querySelector('meta[name=gamepass-app-date]').content).toISOString().substring(0, 10);
$wrapper.appendChild(CE('div', {'class': 'better-xcloud-settings-app-version'}, `GamePass app ${appVersion} (${appDate})`));
} catch (e) {}
// Add Settings UI to the web page
const $pageContent = document.getElementById('PageContent');
$pageContent.parentNode.insertBefore($container, $pageContent);
@ -2174,6 +2361,15 @@ function injectSettingsButton($parent) {
function getVideoPlayerFilterStyle() {
const filters = [];
const clarity = PREFS.get(Preferences.VIDEO_CLARITY);
if (clarity != 0) {
const level = 7 - (clarity - 1); // 5,6,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}%)`);
@ -2196,8 +2392,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();
@ -2258,7 +2468,7 @@ function cloneStreamMenuButton($orgButton, label, svg_icon) {
}
function injectVideoSettingsButton() {
function injectStreamMenuButtons() {
const $screen = document.querySelector('#PageContent section[class*=PureScreens]');
if (!$screen) {
return;
@ -2300,7 +2510,8 @@ function injectVideoSettingsButton() {
return;
}
const $orgButton = node.querySelector('div > div > button');
// Get the second last button
const $orgButton = node.querySelector('div > div > button:nth-last-child(2)');
if (!$orgButton) {
return;
}
@ -2311,6 +2522,9 @@ function injectVideoSettingsButton() {
e.preventDefault();
e.stopPropagation();
// Close HUD
$btnCloseHud.click();
// Show Quick settings bar
$quickBar.style.display = 'flex';
@ -2342,6 +2556,9 @@ function injectVideoSettingsButton() {
StreamStats.toggle();
});
const btnStreamStatsOn = (!StreamStats.isHidden() && !StreamStats.isGlancing());
$btnStreamStats.classList.toggle('better-xcloud-stream-menu-button-on', btnStreamStatsOn);
// Insert after Video Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings);
@ -2413,7 +2630,7 @@ function patchVideoApi() {
}
this.addEventListener('playing', showFunc);
injectVideoSettingsButton();
injectStreamMenuButtons();
return this.orgPlay.apply(this);
};
@ -2480,20 +2697,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;
@ -2512,7 +2740,7 @@ function numberPicker(key) {
value = (value >= MAX) ? MAX : value + 1;
}
$text.textContent = value + '%';
$text.textContent = value + suffix;
PREFS.set(key, value);
updateVideoPlayerCss();
@ -2553,21 +2781,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);
@ -2791,7 +3023,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,