mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-06 07:37:19 +02:00
Initial commit
This commit is contained in:
commit
4489ea2a41
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 redphx
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
64
README.md
Normal file
64
README.md
Normal file
@ -0,0 +1,64 @@
|
||||
# Better xCloud
|
||||
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience.
|
||||
The main target of this script is Android users, but it should work great on desktop too.
|
||||
|
||||
## Features:
|
||||
|
||||
- Switch region of streaming server.
|
||||
- Prefer IPv6 streaming server (might improve latency).
|
||||
- Force HD stream by disabling bandwidth checking -> xCloud always tries to use the best possible quality.
|
||||
- Skip Xbox splash video.
|
||||
- Make the top-left dots icon invisible while playing. You can still click on it, but it doesn't block the screen anymore.
|
||||
- Adjust video filters (brightness/contrast/saturation).
|
||||
- Hide footer and other UI elements.
|
||||
- Reduce UI animations (the smooth scrolling cannot be disabled).
|
||||
- Disable analytics.
|
||||
- Disable social features (friends, chat...).
|
||||
|
||||
## How to use:
|
||||
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. It's also available for Firefox on Android.
|
||||
2. Install **Better xCloud**:
|
||||
- [Directly on Github](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)
|
||||
4. Refresh [xCloud web page](https://www.xbox.com/play/).
|
||||
5. Click on the new "SERVER NAME" button next to your profile picture to adjust settings.
|
||||
6. Optional but recommended: change your browser's User-Agent. Check the [User-Agent section](#user-agent) below for more info.
|
||||
7. Don't forget to reload the page after changing settings.
|
||||
|
||||
## User-Agent
|
||||
Optional, as changing User-Agent won't guarantee a better streaming experience, but it's worth a try. You might need to install an external extension to do that.
|
||||
|
||||
It's recommended to change User-Agent to:
|
||||
```
|
||||
Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67
|
||||
```
|
||||
This will trick xCloud into thinking you're using Edge browser on desktop.
|
||||
|
||||
Other options (only do one of these):
|
||||
- Add ` smarttv` to switch to Smart TV layout.
|
||||
- Add ` Xbox;` to become an Xbox console.
|
||||
- Add ` 36102dd3-6953-45f6-8b48-031fb95e0e0d` to become a Logitech G Cloud device.
|
||||
- Add ` 0ed22b6f-b61d-41eb-810a-a1ed586a550b` to become a Razer Edge device.
|
||||
|
||||
## Tested on:
|
||||
- Chrome on macOS.
|
||||
- Firefox for Android with Tampermonkey add-on.
|
||||
- *(NOT RECOMMENDED at the moment since its Userscript implementation is not working properly)* [Hermit Browser](https://hermit.chimbori.com) on Android. It supports custom User-Agent and has built-in Userscript support (premium features, only $7.99) so you don't have to install anything else. I built **Better xCloud** just so I could use it with Hermit.
|
||||
|
||||
## FAQ
|
||||
1. **Why is it an Userscript and not extension?**
|
||||
It's because not many browsers on Android support installing extensions (and not all extensions can be installed).
|
||||
|
||||
2. **I see "???" button instead of server's name**
|
||||
That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser.
|
||||
|
||||
3. **Can I use this with the Xbox Android app?**
|
||||
No you can't. You'll have to modidy the app.
|
||||
|
||||
## Acknowledgements
|
||||
**Better xCloud** is inspired by these projects:
|
||||
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector)
|
||||
|
||||
## Disclaimers
|
||||
- Use as your own risk.
|
||||
- This project is not affiliated with Xbox in any way. All Xbox logos/icons/trademarks are copyright of their respective owners.
|
||||
|
798
better-xcloud.user.js
Normal file
798
better-xcloud.user.js
Normal file
@ -0,0 +1,798 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 1.0
|
||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||
// @author redphx
|
||||
// @license MIT
|
||||
// @match https://www.xbox.com/*/play*
|
||||
// @run-at document-start
|
||||
// @grant none
|
||||
// @updateURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js
|
||||
// @downloadURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js
|
||||
// ==/UserScript==
|
||||
'use strict';
|
||||
|
||||
const SCRIPT_VERSION = '1.0';
|
||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||
|
||||
const SERVER_REGIONS = {};
|
||||
|
||||
|
||||
class Preferences {
|
||||
static get SERVER_REGION() { return 'server_region'; }
|
||||
static get PREFER_IPV6_SERVER() { return 'prefer_ipv6_server'; }
|
||||
|
||||
static get BLOCK_TRACKING() { return 'block_tracking'; }
|
||||
static get BLOCK_SOCIAL_FEATURES() { return 'block_social_features'; }
|
||||
static get DISABLE_BANDWIDTH_CHECKING() { return 'disable_bandwidth_checking'; }
|
||||
static get SKIP_SPLASH_VIDEO() { return 'skip_splash_video'; }
|
||||
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
||||
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
||||
|
||||
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
||||
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
||||
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
||||
|
||||
static SETTINGS = [
|
||||
{
|
||||
'id': Preferences.SERVER_REGION,
|
||||
'label': 'Region of streaming server',
|
||||
'default': 'default',
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.PREFER_IPV6_SERVER,
|
||||
'label': 'Prefer IPv6 streaming server',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.DISABLE_BANDWIDTH_CHECKING,
|
||||
'label': 'Force HD stream',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.SKIP_SPLASH_VIDEO,
|
||||
'label': 'Skip Xbox splash video',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.HIDE_DOTS_ICON,
|
||||
'label': 'Hide Dots icon while playing',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.REDUCE_ANIMATIONS,
|
||||
'label': 'Reduce UI animations',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.BLOCK_SOCIAL_FEATURES,
|
||||
'label': 'Disable social features (Friends, Chat...)',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.BLOCK_TRACKING,
|
||||
'label': 'Disable analytics',
|
||||
'default': false,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.VIDEO_SATURATION,
|
||||
'label': 'Video saturation (%)',
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'max': 200,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.VIDEO_CONTRAST,
|
||||
'label': 'Video contrast (%)',
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'max': 200,
|
||||
},
|
||||
|
||||
{
|
||||
'id': Preferences.VIDEO_BRIGHTNESS,
|
||||
'label': 'Video brightness (%)',
|
||||
'default': 100,
|
||||
'min': 0,
|
||||
'max': 200,
|
||||
},
|
||||
]
|
||||
|
||||
constructor() {
|
||||
this._storage = localStorage;
|
||||
this._key = 'better_xcloud';
|
||||
|
||||
let savedPrefs = this._storage.getItem(this._key);
|
||||
if (savedPrefs == null) {
|
||||
savedPrefs = '{}';
|
||||
}
|
||||
savedPrefs = JSON.parse(savedPrefs);
|
||||
|
||||
this._prefs = {};
|
||||
for (let setting of Preferences.SETTINGS) {
|
||||
if (setting.id in savedPrefs) {
|
||||
this._prefs[setting.id] = savedPrefs[setting.id];
|
||||
} else {
|
||||
this._prefs[setting.id] = setting.default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get(key, defaultValue=null) {
|
||||
const value = this._prefs[key];
|
||||
|
||||
if (typeof value !== 'undefined' && value !== null && value !== '') {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (defaultValue !== null) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
// Get default value
|
||||
for (let setting of Preferences.SETTINGS) {
|
||||
if (setting.id == key) {
|
||||
return setting.default;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
this._prefs[key] = value;
|
||||
this._update_storage();
|
||||
}
|
||||
|
||||
_update_storage() {
|
||||
this._storage.setItem(this._key, JSON.stringify(this._prefs));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const PREFS = new Preferences();
|
||||
|
||||
|
||||
function addCss() {
|
||||
let css = `
|
||||
.better_xcloud_settings_button {
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
line-height: 30px;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_button:hover, .better_xlcoud_settings_button:focus {
|
||||
background-color: #515863;
|
||||
}
|
||||
|
||||
.better_xcloud_settings {
|
||||
background-color: #151515;
|
||||
user-select: none;
|
||||
color: #fff;
|
||||
font-family: "Segoe UI", Arial, Helvetica, sans-serif
|
||||
}
|
||||
|
||||
.better_xcloud_settings_gone {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper {
|
||||
width: 400px;
|
||||
margin: auto;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper *:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper a {
|
||||
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||
font-size: 20px;
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper .setting_row {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper .setting_row label {
|
||||
flex: 1;
|
||||
align-self: center;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper .setting_row input {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper .setting_button {
|
||||
padding: 8px 32px;
|
||||
margin: 10px auto 0;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
display: block;
|
||||
background-color: #044e2a;
|
||||
text-align: center;
|
||||
color: white;
|
||||
text-transform: uppercase;
|
||||
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_wrapper .setting_button:hover {
|
||||
background-color: #06743f;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_color_bars {
|
||||
display: none;
|
||||
width: 100%;
|
||||
aspect-ratio: 16/6;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.better_xcloud_settings_color_bars div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* Hide UI elements */
|
||||
#headerArea, #uhfSkipToMain, .uhf-footer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div[class*=NotFocusedDialog] {
|
||||
position: absolute !important;
|
||||
top: -9999px !important;
|
||||
left: -9999px !important;
|
||||
width: 0px !important;
|
||||
height: 0px !important;
|
||||
}
|
||||
|
||||
`;
|
||||
|
||||
// Reduce animations
|
||||
if (PREFS.get(Preferences.REDUCE_ANIMATIONS)) {
|
||||
css += 'div[class*=GameImageItem-module], div[class*=ScrollArrows-module] { transition: none !important; }';
|
||||
}
|
||||
|
||||
// Hide the top-left dots icon while playing
|
||||
if (PREFS.get(Preferences.HIDE_DOTS_ICON)) {
|
||||
css += `
|
||||
div[class*=Grip-module__container] {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container] {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
const $style = createElement('style', {}, css);
|
||||
document.documentElement.appendChild($style);
|
||||
}
|
||||
|
||||
|
||||
function getPreferredServerRegion() {
|
||||
let preferredRegion = PREFS.get(Preferences.SERVER_REGION);
|
||||
if (preferredRegion in SERVER_REGIONS) {
|
||||
return preferredRegion;
|
||||
}
|
||||
|
||||
for (let regionName in SERVER_REGIONS) {
|
||||
const region = SERVER_REGIONS[regionName];
|
||||
if (region.isDefault) {
|
||||
return regionName;
|
||||
}
|
||||
}
|
||||
|
||||
return '???';
|
||||
}
|
||||
|
||||
|
||||
function updateIceCandidates(candidates) {
|
||||
const pattern = new RegExp(/a=candidate:(?<order>\d+) (?<num>\d+) UDP (?<priority>\d+) (?<ip>[^\s]+) (?<the_rest>.*)/);
|
||||
|
||||
const lst = [];
|
||||
for (let item of candidates) {
|
||||
if (item.candidate == 'a=end-of-candidates') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const groups = pattern.exec(item.candidate).groups;
|
||||
lst.push(groups);
|
||||
}
|
||||
|
||||
lst.sort((a, b) => (a.ip.includes(':') || a.ip > b.ip) ? -1 : 1);
|
||||
|
||||
const newCandidates = [];
|
||||
let order = 1;
|
||||
let priority = 100;
|
||||
lst.forEach(item => {
|
||||
item.order = order;
|
||||
item.priority = priority;
|
||||
|
||||
newCandidates.push({
|
||||
'candidate': `a=candidate:${item.order} 1 UDP ${item.priority} ${item.ip} ${item.the_rest}`,
|
||||
'messageType': 'iceCandidate',
|
||||
'sdpMLineIndex': '0',
|
||||
'sdpMid': '0',
|
||||
});
|
||||
|
||||
++order;
|
||||
--priority;
|
||||
});
|
||||
|
||||
newCandidates.push({
|
||||
'candidate': 'a=end-of-candidates',
|
||||
'messageType': 'iceCandidate',
|
||||
'sdpMLineIndex': '0',
|
||||
'sdpMid': '0',
|
||||
});
|
||||
|
||||
return newCandidates;
|
||||
}
|
||||
|
||||
|
||||
function interceptHttpRequests() {
|
||||
var BLOCKED_URLS = [];
|
||||
if (PREFS.get(Preferences.BLOCK_TRACKING)) {
|
||||
BLOCKED_URLS = BLOCKED_URLS.concat([
|
||||
'https://arc.msn.com',
|
||||
'https://browser.events.data.microsoft.com',
|
||||
'https://dc.services.visualstudio.com',
|
||||
// 'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
|
||||
]);
|
||||
}
|
||||
|
||||
if (PREFS.get(Preferences.BLOCK_SOCIAL_FEATURES)) {
|
||||
// Disable WebSocket
|
||||
WebSocket = {
|
||||
CLOSING: 2,
|
||||
};
|
||||
|
||||
BLOCKED_URLS = BLOCKED_URLS.concat([
|
||||
'https://peoplehub.xboxlive.com/users/me',
|
||||
'https://accounts.xboxlive.com/family/memberXuid',
|
||||
'https://notificationinbox.xboxlive.com',
|
||||
]);
|
||||
}
|
||||
|
||||
const xhrPrototype = XMLHttpRequest.prototype;
|
||||
xhrPrototype.orgOpen = xhrPrototype.open;
|
||||
xhrPrototype.orgSend = xhrPrototype.send;
|
||||
|
||||
xhrPrototype.open = function(method, url) {
|
||||
// Save URL to use it later in send()
|
||||
this._url = url;
|
||||
return this.orgOpen.apply(this, arguments);
|
||||
};
|
||||
|
||||
xhrPrototype.send = function(...arg) {
|
||||
for (let blocked of BLOCKED_URLS) {
|
||||
if (this._url.startsWith(blocked)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return this.orgSend.apply(this, arguments);
|
||||
};
|
||||
|
||||
const PREF_PREFER_IPV6_SERVER = PREFS.get(Preferences.PREFER_IPV6_SERVER);
|
||||
|
||||
const orgFetch = window.fetch;
|
||||
window.fetch = async (...arg) => {
|
||||
const request = arg[0];
|
||||
const url = (typeof request === 'string') ? request : request.url;
|
||||
|
||||
// Server list
|
||||
if (url.endsWith('/v2/login/user')) {
|
||||
const promise = orgFetch(...arg);
|
||||
|
||||
return promise.then(response => {
|
||||
return response.clone().json().then(obj => {
|
||||
// Get server list
|
||||
if (!Object.keys(SERVER_REGIONS).length) {
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
SERVER_REGIONS[region.name] = Object.assign({}, region);
|
||||
}
|
||||
|
||||
// Start rendering UI
|
||||
if (!document.getElementById('gamepass-root')) {
|
||||
setTimeout(watchHeader, 2000);
|
||||
} else {
|
||||
watchHeader();
|
||||
}
|
||||
}
|
||||
|
||||
const preferredRegion = getPreferredServerRegion();
|
||||
if (preferredRegion in SERVER_REGIONS) {
|
||||
const tmp = Object.assign({}, SERVER_REGIONS[preferredRegion]);
|
||||
tmp.isDefault = true;
|
||||
|
||||
obj.offeringSettings.regions = [tmp];
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ICE server candidates
|
||||
if (PREF_PREFER_IPV6_SERVER && url.endsWith('/ice') && url.includes('/sessions/cloud/')) {
|
||||
const promise = orgFetch(...arg);
|
||||
|
||||
return promise.then(response => {
|
||||
return response.clone().text().then(text => {
|
||||
if (!text.length) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = JSON.parse(text);
|
||||
let exchangeResponse = JSON.parse(obj.exchangeResponse);
|
||||
exchangeResponse = updateIceCandidates(exchangeResponse)
|
||||
obj.exchangeResponse = JSON.stringify(exchangeResponse);
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
for (let blocked of BLOCKED_URLS) {
|
||||
if (!url.startsWith(blocked)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
return new Response('{"acc":1,"webResult":{}}', {
|
||||
status: 200,
|
||||
statusText: '200 OK',
|
||||
});
|
||||
}
|
||||
|
||||
return orgFetch(...arg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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 {
|
||||
$elm.appendChild(arg);
|
||||
}
|
||||
}
|
||||
|
||||
return $elm;
|
||||
}
|
||||
|
||||
|
||||
function injectSettingsButton($parent) {
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CE = createElement;
|
||||
const preferredRegion = getPreferredServerRegion();
|
||||
|
||||
const $button = CE('button', {'class': 'better_xcloud_settings_button'}, preferredRegion);
|
||||
$button.addEventListener('click', e => {
|
||||
const $settings = document.querySelector('.better_xcloud_settings');
|
||||
$settings.classList.toggle('better_xcloud_settings_gone');
|
||||
$settings.scrollIntoView();
|
||||
});
|
||||
$parent.appendChild($button);
|
||||
|
||||
const $container = CE('div', {
|
||||
'class': 'better_xcloud_settings better_xcloud_settings_gone',
|
||||
});
|
||||
|
||||
const $wrapper = CE('div', {
|
||||
'class': 'better_xcloud_settings_wrapper',
|
||||
});
|
||||
$container.appendChild($wrapper);
|
||||
|
||||
const $title = CE('a', {
|
||||
href: SCRIPT_HOME,
|
||||
target: '_blank',
|
||||
}, 'Better xCloud ' + SCRIPT_VERSION);
|
||||
$wrapper.appendChild($title);
|
||||
|
||||
for (let setting of Preferences.SETTINGS) {
|
||||
let $control;
|
||||
|
||||
if (setting.id === Preferences.SERVER_REGION) {
|
||||
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
|
||||
$control.addEventListener('change', e => {
|
||||
PREFS.set(Preferences.SERVER_REGION, e.target.value);
|
||||
});
|
||||
|
||||
for (let regionName in SERVER_REGIONS) {
|
||||
const region = SERVER_REGIONS[regionName];
|
||||
let value = regionName;
|
||||
|
||||
let label = regionName;
|
||||
if (region.isDefault) {
|
||||
label += ' (Default)';
|
||||
value = 'default';
|
||||
}
|
||||
|
||||
const $option = CE('option', {value: value}, label);
|
||||
$option.selected = regionName === preferredRegion;
|
||||
|
||||
$control.appendChild($option);
|
||||
}
|
||||
} else if (typeof setting.default === 'number') {
|
||||
$control = CE('input', {
|
||||
id: 'xcloud_setting_' + setting.id,
|
||||
type: 'number',
|
||||
size: 5,
|
||||
'data-key': setting.id,
|
||||
});
|
||||
|
||||
if ('min' in setting) {
|
||||
$control.setAttribute('min', setting.min);
|
||||
}
|
||||
|
||||
if ('max' in setting) {
|
||||
$control.setAttribute('max', setting.max);
|
||||
}
|
||||
|
||||
$control.value = PREFS.get(setting.id);
|
||||
if (setting.id.startsWith('video_')) {
|
||||
$control.addEventListener('change', e => {
|
||||
if (!e.target.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
PREFS.set(e.target.getAttribute('data-key'), parseInt(e.target.value));
|
||||
|
||||
const filters = getVideoPlayerFilterStyle();
|
||||
const $elm = document.querySelector('.better_xcloud_settings_color_bars');
|
||||
$elm.style.display = 'flex';
|
||||
$elm.style.filter = filters;
|
||||
|
||||
updateVideoPlayerCss();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
$control = CE('input', {
|
||||
id: 'xcloud_setting_' + setting.id,
|
||||
type: 'checkbox',
|
||||
'data-key': setting.id,
|
||||
});
|
||||
$control.addEventListener('change', e => {
|
||||
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
|
||||
});
|
||||
|
||||
setting.value = PREFS.get(setting.id);
|
||||
$control.checked = setting.value;
|
||||
}
|
||||
|
||||
const $elm = CE('div', {'class': 'setting_row'},
|
||||
CE('label', {'for': 'xcloud_setting_' + setting.id}, setting.label),
|
||||
$control
|
||||
);
|
||||
|
||||
$wrapper.appendChild($elm);
|
||||
}
|
||||
|
||||
const COLOR_BARS = [
|
||||
'white',
|
||||
'yellow',
|
||||
'cyan',
|
||||
'green',
|
||||
'magenta',
|
||||
'red',
|
||||
'blue',
|
||||
'black',
|
||||
];
|
||||
|
||||
const $colorBars = CE('div', {'class': 'better_xcloud_settings_color_bars'});
|
||||
COLOR_BARS.forEach(color => {
|
||||
$colorBars.appendChild(CE('div', {
|
||||
style: `background-color: ${color}`,
|
||||
}));
|
||||
});
|
||||
|
||||
$wrapper.appendChild($colorBars);
|
||||
|
||||
const $reloadBtn = CE('button', {'class': 'setting_button'}, 'Reload page to reflect changes');
|
||||
$reloadBtn.addEventListener('click', e => window.location.reload());
|
||||
$wrapper.appendChild($reloadBtn);
|
||||
|
||||
const $pageContent = document.getElementById('PageContent');
|
||||
$pageContent.parentNode.insertBefore($container, $pageContent);
|
||||
}
|
||||
|
||||
function getVideoPlayerFilterStyle() {
|
||||
const filters = [];
|
||||
|
||||
const saturation = PREFS.get(Preferences.VIDEO_SATURATION);
|
||||
if (saturation != 100) {
|
||||
filters.push(`saturate(${saturation}%)`);
|
||||
}
|
||||
|
||||
const contrast = PREFS.get(Preferences.VIDEO_CONTRAST);
|
||||
if (contrast != 100) {
|
||||
filters.push(`contrast(${contrast}%)`);
|
||||
}
|
||||
|
||||
const brightness = PREFS.get(Preferences.VIDEO_BRIGHTNESS);
|
||||
if (brightness != 100) {
|
||||
filters.push(`brightness(${brightness}%)`);
|
||||
}
|
||||
|
||||
return filters.join(' ');
|
||||
}
|
||||
|
||||
|
||||
function updateVideoPlayerCss() {
|
||||
let $elm = document.getElementById('better-xcloud-video-css');
|
||||
if (!$elm) {
|
||||
$elm = createElement('style', {id: 'better-xcloud-video-css'});
|
||||
document.documentElement.appendChild($elm);
|
||||
}
|
||||
|
||||
let filters = getVideoPlayerFilterStyle();
|
||||
let css = '';
|
||||
if (filters) {
|
||||
css = `#game-stream video {filter: ${filters}}`;
|
||||
}
|
||||
|
||||
$elm.textContent = css;
|
||||
}
|
||||
|
||||
|
||||
function checkHeader() {
|
||||
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
|
||||
|
||||
if (!$button) {
|
||||
const $rightHeader = document.querySelector('#PageContent header div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||
injectSettingsButton($rightHeader);
|
||||
|
||||
updateVideoPlayerCss();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function watchHeader() {
|
||||
const $header = document.querySelector('#PageContent header');
|
||||
if (!$header) {
|
||||
return;
|
||||
}
|
||||
|
||||
let timeout;
|
||||
const observer = new MutationObserver(mutationList => {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
|
||||
timeout = setTimeout(checkHeader, 2000);
|
||||
});
|
||||
observer.observe($header, { subtree: true, childList: true});
|
||||
|
||||
checkHeader();
|
||||
}
|
||||
|
||||
|
||||
function patchVideoApi() {
|
||||
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
|
||||
// Do nothing if the "Skip splash video" setting is off
|
||||
if (!PREF_SKIP_SPLASH_VIDEO) {
|
||||
return;
|
||||
}
|
||||
|
||||
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
if (!this.className.startsWith('XboxSplashVideo')) {
|
||||
return this.orgPlay.apply(this);
|
||||
}
|
||||
|
||||
this.volume = 0;
|
||||
this.style.display = 'none';
|
||||
this.dispatchEvent(new Event('ended'));
|
||||
|
||||
return {
|
||||
catch: () => {},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function patchHistoryMethod(type) {
|
||||
var orig = window.history[type];
|
||||
return function(...args) {
|
||||
const rv = orig.apply(this, arguments);
|
||||
|
||||
const event = new Event('xcloud_popstate');
|
||||
event.arguments = args;
|
||||
window.dispatchEvent(event);
|
||||
|
||||
return rv;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
function hideSettingsOnPageChange() {
|
||||
const $settings = document.querySelector('.better_xcloud_settings');
|
||||
if ($settings) {
|
||||
$settings.classList.add('better_xcloud_settings_gone');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Hide Settings UI when navigate to another page
|
||||
window.addEventListener('xcloud_popstate', hideSettingsOnPageChange);
|
||||
window.addEventListener('popstate', hideSettingsOnPageChange);
|
||||
// Make pushState/replaceState methods dispatch "xcloud_popstate" event
|
||||
window.history.pushState = patchHistoryMethod('pushState');
|
||||
window.history.replaceState = patchHistoryMethod('replaceState');
|
||||
|
||||
// Add additional CSS
|
||||
addCss();
|
||||
|
||||
// Clear data of window.navigator.userAgentData, force Xcloud to detect browser based on User-Agent header
|
||||
Object.defineProperty(window.navigator, 'userAgentData', {});
|
||||
|
||||
// Disable bandwidth checking
|
||||
if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
|
||||
Object.defineProperty(window.navigator, 'connection', {
|
||||
get: () => undefined,
|
||||
});
|
||||
}
|
||||
|
||||
interceptHttpRequests();
|
||||
|
||||
patchVideoApi();
|
||||
|
||||
// Workaround for Hermit browser
|
||||
var onLoadTriggered = false;
|
||||
window.onload = () => {
|
||||
onLoadTriggered = true;
|
||||
};
|
||||
|
||||
if (document.readyState === 'complete' && !onLoadTriggered) {
|
||||
watchHeader();
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user