mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 02:41:44 +02:00
Compare commits
22 Commits
Author | SHA1 | Date | |
---|---|---|---|
d2debc0623 | |||
0c3d8519e1 | |||
e323bd5b47 | |||
ce54aa4580 | |||
d29447173c | |||
2f3a3167e1 | |||
e17459934b | |||
ecf9b414ba | |||
f78bd31b58 | |||
da54bd5302 | |||
5479eeb66f | |||
a9eb70eaae | |||
0e2066e461 | |||
2ee2469a10 | |||
9056b5926f | |||
730799821b | |||
fed7d489f4 | |||
42b600714e | |||
5dd02f04de | |||
c6c7fc7de5 | |||
169a0a0d4f | |||
90bb75fe56 |
43
README.md
43
README.md
@ -1,23 +1,25 @@
|
|||||||
# Better xCloud
|
# Better xCloud
|
||||||
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience.
|
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience on web browser.
|
||||||
The main target of this script is Android users, but it should work great on desktop too.
|
The main target of this script is Android users, but it should work great on desktop too.
|
||||||
|
|
||||||
## Features:
|
## Features
|
||||||
|
|
||||||
<img width="474" alt="screenshot" src="https://github.com/redphx/better-xcloud/assets/96280/a0e85915-4e3f-4c1b-8885-eda1c712eeb6">
|
<img width="474" alt="image" src="https://github.com/redphx/better-xcloud/assets/96280/2793d404-3185-4c91-a500-dde362c661dd">
|
||||||
|
|
||||||
- Switch region of streaming server.
|
- Switch region of streaming server.
|
||||||
- Prefer IPv6 streaming server (might improve latency).
|
- Prefer IPv6 streaming server (might reduce latency).
|
||||||
- Force HD stream by disabling bandwidth checking -> xCloud always tries to use the best possible quality.
|
- Force HD stream by disabling bandwidth checking -> xCloud always tries to use the best possible quality.
|
||||||
- Skip Xbox splash video.
|
- Skip Xbox splash video (save 3 seconds).
|
||||||
- Make the top-left dots icon invisible while playing. You can still click on it, but it doesn't block the screen anymore.
|
- Make the top-left dots icon invisible while playing. You can still click on it, but it doesn't block the screen anymore.
|
||||||
|
- Stretch video to full sctreen. Useful when you don't have a 16:9 screen.
|
||||||
- Adjust video filters (brightness/contrast/saturation).
|
- Adjust video filters (brightness/contrast/saturation).
|
||||||
|
- You can change video settings while playing.
|
||||||
- Hide footer and other UI elements.
|
- Hide footer and other UI elements.
|
||||||
- Reduce UI animations (the smooth scrolling cannot be disabled).
|
- Reduce UI animations (the smooth scrolling cannot be disabled).
|
||||||
- Disable social features (friends, chat...).
|
- Disable social features (friends, chat...).
|
||||||
- Disable xCloud analytics. The analytics contains statistics of your streaming session, so I'd recommend to enable analytics to help Xbox improve xCloud's experence in the future.
|
- Disable xCloud analytics. The analytics contains statistics of your streaming session, so I'd recommend to allow analytics to help Xbox improve xCloud's experence in the future.
|
||||||
|
|
||||||
## How to use:
|
## How to use
|
||||||
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. It's also available for Firefox on Android.
|
1. Install [Tampermonkey extension](https://www.tampermonkey.net/) on suppported browsers. It's also available for Firefox on Android.
|
||||||
2. Install **Better xCloud**:
|
2. Install **Better xCloud**:
|
||||||
- [Directly on Github](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
|
- [Directly on Github](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
|
||||||
@ -41,10 +43,20 @@ Other options (only do one of these):
|
|||||||
- Add ` 36102dd3-6953-45f6-8b48-031fb95e0e0d` to become a Logitech G Cloud device.
|
- 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.
|
- Add ` 0ed22b6f-b61d-41eb-810a-a1ed586a550b` to become a Razer Edge device.
|
||||||
|
|
||||||
## Tested on:
|
## Compatibility
|
||||||
- Chrome on macOS.
|
✅ = confirmed to be working
|
||||||
- Firefox for Android with Tampermonkey add-on.
|
❓ = not yet tested
|
||||||
- *(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.
|
❌ = not supported (mostly because of lacking Userscript/extension support)
|
||||||
|
⚠️ = see custom notes
|
||||||
|
| | Desktop | Android | iOS |
|
||||||
|
|----------------------------------------|----------|------------------|-----|
|
||||||
|
| Chrome | ✅ | ❌ | ❌ |
|
||||||
|
| Firefox | ✅ | ✅ | ❌ |
|
||||||
|
| Edge | ❓ | ❌ | ❌ |
|
||||||
|
| Safari | ❓ | ❌ | ❓ |
|
||||||
|
| [Hermit](https://hermit.chimbori.com) | ❌ | ⚠️<sup>(1)</sup> | ❌ |
|
||||||
|
|
||||||
|
<sup>1</sup> NOT RECOMMENDED at the moment since its Userscript implementation is not working properly. Non-network related features (skip splash video, video settings...) still work. It's still my favorite app to play xCloud on because it's lightweight, supports both custom User-Agent and Userscript (premium features, only $1.99 for Userscript feature or $7.99 if you want both) without having to install anything else. I built **Better xCloud** just so I could use it with Hermit.
|
||||||
|
|
||||||
## FAQ
|
## FAQ
|
||||||
1. **Why is it an Userscript and not extension?**
|
1. **Why is it an Userscript and not extension?**
|
||||||
@ -54,11 +66,14 @@ It's because not many browsers on Android support installing extensions (and not
|
|||||||
That means Tampermonkey is not working properly. Please make sure you're using the latest version or switch to a well-known browser.
|
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?**
|
3. **Can I use this with the Xbox Android app?**
|
||||||
No you can't. You'll have to modidy the app.
|
No you can't. You'll have to modify the app.
|
||||||
|
|
||||||
|
4. **Will you able to enable "Clarity Boost" feature on non-Edge browsers?**
|
||||||
|
No. "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
**Better xCloud** is inspired by these projects:
|
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector) for the idea of IPv6 feature
|
||||||
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector)
|
- Icons by [Adam Design](https://www.iconfinder.com/iconsets/user-interface-outline-27)
|
||||||
|
|
||||||
## Disclaimers
|
## Disclaimers
|
||||||
- Use as your own risk.
|
- Use as your own risk.
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Better xCloud
|
// @name Better xCloud
|
||||||
// @namespace https://github.com/redphx
|
// @namespace https://github.com/redphx
|
||||||
// @version 1.0
|
// @version 1.2
|
||||||
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
// @description Improve Xbox Cloud Gaming (xCloud) experience
|
||||||
// @author redphx
|
// @author redphx
|
||||||
// @license MIT
|
// @license MIT
|
||||||
// @match https://www.xbox.com/*/play*
|
// @match https://www.xbox.com/*/play*
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @grant none
|
// @grant none
|
||||||
// @updateURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js
|
// @updateURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
||||||
// @downloadURL https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js
|
// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const SCRIPT_VERSION = '1.0';
|
const SCRIPT_VERSION = '1.2';
|
||||||
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
|
||||||
|
|
||||||
const SERVER_REGIONS = {};
|
const SERVER_REGIONS = {};
|
||||||
@ -30,6 +30,7 @@ class Preferences {
|
|||||||
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
static get HIDE_DOTS_ICON() { return 'hide_dots_icon'; }
|
||||||
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
static get REDUCE_ANIMATIONS() { return 'reduce_animations'; }
|
||||||
|
|
||||||
|
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; }
|
||||||
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
static get VIDEO_BRIGHTNESS() { return 'video_brightness'; }
|
||||||
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
static get VIDEO_CONTRAST() { return 'video_contrast'; }
|
||||||
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
static get VIDEO_SATURATION() { return 'video_saturation'; }
|
||||||
@ -83,12 +84,18 @@ class Preferences {
|
|||||||
'default': false,
|
'default': false,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
'id': Preferences.VIDEO_FILL_FULL_SCREEN,
|
||||||
|
'label': 'Stretch video to full screen',
|
||||||
|
'default': false,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
'id': Preferences.VIDEO_SATURATION,
|
'id': Preferences.VIDEO_SATURATION,
|
||||||
'label': 'Video saturation (%)',
|
'label': 'Video saturation (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 200,
|
'max': 150,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -96,7 +103,7 @@ class Preferences {
|
|||||||
'label': 'Video contrast (%)',
|
'label': 'Video contrast (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 200,
|
'max': 150,
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -104,7 +111,7 @@ class Preferences {
|
|||||||
'label': 'Video brightness (%)',
|
'label': 'Video brightness (%)',
|
||||||
'default': 100,
|
'default': 100,
|
||||||
'min': 0,
|
'min': 0,
|
||||||
'max': 200,
|
'max': 150,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -243,14 +250,24 @@ function addCss() {
|
|||||||
background-color: #06743f;
|
background-color: #06743f;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_settings_color_bars {
|
.better_xcloud_settings_preview_screen {
|
||||||
display: none;
|
display: none;
|
||||||
width: 100%;
|
aspect-ratio: 20/9;
|
||||||
aspect-ratio: 16/6;
|
background: #1e1e1e;
|
||||||
margin-top: 10px;
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
max-height: 180px;
|
||||||
|
margin: 10px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.better_xcloud_settings_color_bars div {
|
.better_xcloud_settings_preview_video {
|
||||||
|
display: flex;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
height: 100%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_settings_preview_video div {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,6 +284,9 @@ div[class*=NotFocusedDialog] {
|
|||||||
height: 0px !important;
|
height: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#game-stream video {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Reduce animations
|
// Reduce animations
|
||||||
@ -290,6 +310,14 @@ div[class*=Grip-module__container] {
|
|||||||
button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] {
|
button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button[class*=GripHandle-module__container][aria-expanded=false] {
|
||||||
|
background-color: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
div[class*=StreamHUD-module__buttonsContainer] {
|
||||||
|
padding: 0px !important;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,6 +536,31 @@ function createElement(elmName, props = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function generateVideoPreviewBox() {
|
||||||
|
const $screen = createElement('div', {'class': 'better_xcloud_settings_preview_screen'});
|
||||||
|
const $video = createElement('div', {'class': 'better_xcloud_settings_preview_video'});
|
||||||
|
|
||||||
|
const COLOR_BARS = [
|
||||||
|
'white',
|
||||||
|
'yellow',
|
||||||
|
'cyan',
|
||||||
|
'green',
|
||||||
|
'magenta',
|
||||||
|
'red',
|
||||||
|
'blue',
|
||||||
|
'black',
|
||||||
|
];
|
||||||
|
|
||||||
|
COLOR_BARS.forEach(color => {
|
||||||
|
$video.appendChild(createElement('div', {
|
||||||
|
style: `background-color: ${color}`,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
$screen.appendChild($video);
|
||||||
|
return $screen;
|
||||||
|
}
|
||||||
|
|
||||||
function injectSettingsButton($parent) {
|
function injectSettingsButton($parent) {
|
||||||
if (!$parent) {
|
if (!$parent) {
|
||||||
return;
|
return;
|
||||||
@ -587,13 +640,7 @@ function injectSettingsButton($parent) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PREFS.set(e.target.getAttribute('data-key'), parseInt(e.target.value));
|
PREFS.set(e.target.getAttribute('data-key'), parseInt(e.target.value));
|
||||||
|
updateVideoPlayerPreview();
|
||||||
const filters = getVideoPlayerFilterStyle();
|
|
||||||
const $elm = document.querySelector('.better_xcloud_settings_color_bars');
|
|
||||||
$elm.style.display = 'flex';
|
|
||||||
$elm.style.filter = filters;
|
|
||||||
|
|
||||||
updateVideoPlayerCss();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -602,8 +649,13 @@ function injectSettingsButton($parent) {
|
|||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
'data-key': setting.id,
|
'data-key': setting.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
$control.addEventListener('change', e => {
|
$control.addEventListener('change', e => {
|
||||||
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
|
PREFS.set(e.target.getAttribute('data-key'), e.target.checked);
|
||||||
|
|
||||||
|
if (setting.id == Preferences.VIDEO_FILL_FULL_SCREEN) {
|
||||||
|
updateVideoPlayerPreview();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
setting.value = PREFS.get(setting.id);
|
setting.value = PREFS.get(setting.id);
|
||||||
@ -618,25 +670,8 @@ function injectSettingsButton($parent) {
|
|||||||
$wrapper.appendChild($elm);
|
$wrapper.appendChild($elm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const COLOR_BARS = [
|
const $videoPreview = generateVideoPreviewBox();
|
||||||
'white',
|
$wrapper.appendChild($videoPreview);
|
||||||
'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');
|
const $reloadBtn = CE('button', {'class': 'setting_button'}, 'Reload page to reflect changes');
|
||||||
$reloadBtn.addEventListener('click', e => window.location.reload());
|
$reloadBtn.addEventListener('click', e => window.location.reload());
|
||||||
@ -678,21 +713,45 @@ function updateVideoPlayerCss() {
|
|||||||
let filters = getVideoPlayerFilterStyle();
|
let filters = getVideoPlayerFilterStyle();
|
||||||
let css = '';
|
let css = '';
|
||||||
if (filters) {
|
if (filters) {
|
||||||
css = `#game-stream video {filter: ${filters}}`;
|
css += `filter: ${filters} !important;`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) {
|
||||||
|
css += 'object-fit: fill !important;';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css) {
|
||||||
|
css = `#game-stream video {${css}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
$elm.textContent = css;
|
$elm.textContent = css;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function updateVideoPlayerPreview() {
|
||||||
|
const $screen = document.querySelector('.better_xcloud_settings_preview_screen');
|
||||||
|
$screen.style.display = 'block';
|
||||||
|
|
||||||
|
const filters = getVideoPlayerFilterStyle();
|
||||||
|
const $video = document.querySelector('.better_xcloud_settings_preview_video');
|
||||||
|
$video.style.filter = filters;
|
||||||
|
|
||||||
|
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) {
|
||||||
|
$video.style.height = 'auto';
|
||||||
|
} else {
|
||||||
|
$video.style.height = '100%';
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVideoPlayerCss();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function checkHeader() {
|
function checkHeader() {
|
||||||
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
|
const $button = document.querySelector('#PageContent header .better_xcloud_settings_button');
|
||||||
|
|
||||||
if (!$button) {
|
if (!$button) {
|
||||||
const $rightHeader = document.querySelector('#PageContent header div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
const $rightHeader = document.querySelector('#PageContent header div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||||
injectSettingsButton($rightHeader);
|
injectSettingsButton($rightHeader);
|
||||||
|
|
||||||
updateVideoPlayerCss();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -711,68 +770,332 @@ function watchHeader() {
|
|||||||
|
|
||||||
timeout = setTimeout(checkHeader, 2000);
|
timeout = setTimeout(checkHeader, 2000);
|
||||||
});
|
});
|
||||||
observer.observe($header, { subtree: true, childList: true});
|
observer.observe($header, {subtree: true, childList: true});
|
||||||
|
|
||||||
checkHeader();
|
checkHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function injectVideoSettingsButton() {
|
||||||
|
const $screen = document.querySelector('#PageContent section[class*=PureScreens]');
|
||||||
|
if (!$screen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($screen.xObserving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$screen.xObserving = true;
|
||||||
|
|
||||||
|
const $quickBar = document.querySelector('.better_xcloud_quick_settings_bar');
|
||||||
|
const $parent = $screen.parentElement;
|
||||||
|
const hideQuickBarFunc = e => {
|
||||||
|
if (e.target != $parent && e.target.id !== 'MultiTouchSurface') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide Quick settings bar
|
||||||
|
$quickBar.style.display = 'none';
|
||||||
|
|
||||||
|
$parent.removeEventListener('click', hideQuickBarFunc);
|
||||||
|
$parent.removeEventListener('touchend', hideQuickBarFunc);
|
||||||
|
|
||||||
|
if (e.target.id === 'MultiTouchSurface') {
|
||||||
|
e.target.removeEventListener('touchstart', hideQuickBarFunc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(mutationList => {
|
||||||
|
mutationList.forEach(item => {
|
||||||
|
if (item.type !== 'childList') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.addedNodes.forEach(node => {
|
||||||
|
if (!node.className || !node.className.startsWith('StreamMenu')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = 'better-xcloud-video-settings-btn';
|
||||||
|
let $wrapper = document.getElementById('#' + id);
|
||||||
|
if ($wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $orgButton = node.querySelector('div > div > button');
|
||||||
|
if (!$orgButton) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone other button
|
||||||
|
const $button = $orgButton.cloneNode(true);
|
||||||
|
$button.setAttribute('aria-label', 'Video settings');
|
||||||
|
$button.querySelector('div[class*=label]').textContent = 'Video settings';
|
||||||
|
|
||||||
|
// Credit: https://www.iconfinder.com/iconsets/user-interface-outline-27
|
||||||
|
const SVG_ICON = '<path d="M8 2c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.186C5.602 7.158 6.706 8 8 8s2.395-.843 2.813-2h10.188a1 1 0 1 0 0-2H10.813C10.395 2.843 9.293 2 8 2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h9.186c.417 1.158 1.521 2 2.814 2s2.395-.843 2.813-2H21a1 1 0 1 0 0-2h-3.187c-.418-1.157-1.52-2-2.813-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1zm-7 5c-1.293 0-2.395.843-2.812 2H3a1 1 0 1 0 0 2h2.188c.417 1.157 1.519 2 2.813 2s2.398-.842 2.814-2H21a1 1 0 1 0 0-2H10.812c-.417-1.157-1.519-2-2.812-2zm0 2c.564 0 1 .436 1 1s-.436 1-1 1-1-.436-1-1 .436-1 1-1z"/>';
|
||||||
|
const $svg = $button.querySelector('svg');
|
||||||
|
$svg.innerHTML = SVG_ICON;
|
||||||
|
$svg.setAttribute('viewBox', '0 0 24 24');
|
||||||
|
|
||||||
|
$button.addEventListener('click', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Show Quick settings bar
|
||||||
|
$quickBar.style.display = 'flex';
|
||||||
|
|
||||||
|
// Close HUD
|
||||||
|
document.querySelector('button[class*=StreamMenu-module__backButton]').click();
|
||||||
|
|
||||||
|
$parent.addEventListener('click', hideQuickBarFunc);
|
||||||
|
$parent.addEventListener('touchend', hideQuickBarFunc);
|
||||||
|
|
||||||
|
const $touchSurface = document.querySelector('#MultiTouchSurface');
|
||||||
|
if ($touchSurface) {
|
||||||
|
$touchSurface.addEventListener('touchstart', hideQuickBarFunc);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$orgButton.parentElement.insertBefore($button, $orgButton.parentElement.firstChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
observer.observe($screen, {subtree: true, childList: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function patchVideoApi() {
|
function patchVideoApi() {
|
||||||
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
|
const PREF_SKIP_SPLASH_VIDEO = PREFS.get(Preferences.SKIP_SPLASH_VIDEO);
|
||||||
// Do nothing if the "Skip splash video" setting is off
|
|
||||||
if (!PREF_SKIP_SPLASH_VIDEO) {
|
// Show video player when it's ready
|
||||||
return;
|
var showFunc;
|
||||||
|
showFunc = function() {
|
||||||
|
this.style.visibility = 'visible';
|
||||||
|
this.removeEventListener('playing', showFunc);
|
||||||
}
|
}
|
||||||
|
|
||||||
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
|
||||||
HTMLMediaElement.prototype.play = function() {
|
HTMLMediaElement.prototype.play = function() {
|
||||||
if (!this.className.startsWith('XboxSplashVideo')) {
|
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
|
||||||
return this.orgPlay.apply(this);
|
this.volume = 0;
|
||||||
|
this.style.display = 'none';
|
||||||
|
this.dispatchEvent(new Event('ended'));
|
||||||
|
|
||||||
|
return {
|
||||||
|
catch: () => {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.volume = 0;
|
this.addEventListener('playing', showFunc);
|
||||||
this.style.display = 'none';
|
injectVideoSettingsButton();
|
||||||
this.dispatchEvent(new Event('ended'));
|
|
||||||
|
|
||||||
return {
|
return this.orgPlay.apply(this);
|
||||||
catch: () => {},
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function numberPicker(key) {
|
||||||
|
let value = PREFS.get(key);
|
||||||
|
let $text, $decBtn, $incBtn;
|
||||||
|
|
||||||
|
const MIN = 0;
|
||||||
|
const MAX= 150;
|
||||||
|
|
||||||
|
const CE = createElement;
|
||||||
|
const $wrapper = CE('div', {},
|
||||||
|
$decBtn = CE('button', {'data-type': 'dec'}, '-'),
|
||||||
|
$text = CE('span', {}, value),
|
||||||
|
$incBtn = CE('button', {'data-type': 'inc'}, '+'),
|
||||||
|
);
|
||||||
|
|
||||||
|
let interval;
|
||||||
|
let isHolding = false;
|
||||||
|
|
||||||
|
const onClick = e => {
|
||||||
|
if (isHolding) {
|
||||||
|
e.preventDefault();
|
||||||
|
isHolding = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnType = e.target.getAttribute('data-type');
|
||||||
|
if (btnType === 'dec') {
|
||||||
|
value = (value <= MIN) ? MIN : value - 1;
|
||||||
|
} else {
|
||||||
|
value = (value >= MAX) ? MAX : value + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$text.textContent = value;
|
||||||
|
PREFS.set(key, value);
|
||||||
|
updateVideoPlayerCss();
|
||||||
|
|
||||||
|
isHolding = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const onMouseDown = e => {
|
||||||
|
isHolding = true;
|
||||||
|
|
||||||
|
const args = arguments;
|
||||||
|
interval = setInterval(() => {
|
||||||
|
const event = new Event('click');
|
||||||
|
event.arguments = args;
|
||||||
|
|
||||||
|
e.target.dispatchEvent(event);
|
||||||
|
}, 200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMouseUp = e => {
|
||||||
|
clearInterval(interval);
|
||||||
|
isHolding = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
$decBtn.addEventListener('click', onClick);
|
||||||
|
$decBtn.addEventListener('mousedown', onMouseDown);
|
||||||
|
$decBtn.addEventListener('mouseup', onMouseUp);
|
||||||
|
$decBtn.addEventListener('touchstart', onMouseDown);
|
||||||
|
$decBtn.addEventListener('touchend', onMouseUp);
|
||||||
|
|
||||||
|
$incBtn.addEventListener('click', onClick);
|
||||||
|
$incBtn.addEventListener('mousedown', onMouseDown);
|
||||||
|
$incBtn.addEventListener('mouseup', onMouseUp);
|
||||||
|
$incBtn.addEventListener('touchstart', onMouseDown);
|
||||||
|
$incBtn.addEventListener('touchend', onMouseUp);
|
||||||
|
|
||||||
|
return $wrapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupVideoSettingsBar() {
|
||||||
|
const CE = createElement;
|
||||||
|
|
||||||
|
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', {}, 'Saturation'),
|
||||||
|
numberPicker(Preferences.VIDEO_SATURATION)),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {}, 'Contrast'),
|
||||||
|
numberPicker(Preferences.VIDEO_CONTRAST)),
|
||||||
|
CE('div', {},
|
||||||
|
CE('label', {}, 'Brightness'),
|
||||||
|
numberPicker(Preferences.VIDEO_BRIGHTNESS))
|
||||||
|
);
|
||||||
|
|
||||||
|
$stretchInp.checked = PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN);
|
||||||
|
$stretchInp.addEventListener('change', e => {
|
||||||
|
PREFS.set(Preferences.VIDEO_FILL_FULL_SCREEN, e.target.checked);
|
||||||
|
updateVideoPlayerCss();
|
||||||
|
});
|
||||||
|
|
||||||
|
const $style = CE('style', {}, `
|
||||||
|
.better_xcloud_quick_settings_bar {
|
||||||
|
display: none;
|
||||||
|
user-select: none;
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, 0);
|
||||||
|
z-index: 9999;
|
||||||
|
padding: 20px;
|
||||||
|
width: 620px;
|
||||||
|
background: #1a1b1e;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 16px;
|
||||||
|
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar *:focus {
|
||||||
|
outline: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar > div {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar label {
|
||||||
|
font-size: 24px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar input {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar button {
|
||||||
|
border: none;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
margin: 0 8px;
|
||||||
|
line-height: 24px;
|
||||||
|
background-color: #fff;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
.better_xcloud_quick_settings_bar button:hover {
|
||||||
|
background-color: #414141;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar button:active {
|
||||||
|
background-color: #414141;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.better_xcloud_quick_settings_bar span {
|
||||||
|
display: inline-block;
|
||||||
|
width: 26px;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
document.documentElement.appendChild($wrapper);
|
||||||
|
document.documentElement.appendChild($style);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function patchHistoryMethod(type) {
|
function patchHistoryMethod(type) {
|
||||||
var orig = window.history[type];
|
var orig = window.history[type];
|
||||||
return function(...args) {
|
return function(...args) {
|
||||||
const rv = orig.apply(this, arguments);
|
|
||||||
|
|
||||||
const event = new Event('xcloud_popstate');
|
const event = new Event('xcloud_popstate');
|
||||||
event.arguments = args;
|
event.arguments = args;
|
||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
|
|
||||||
return rv;
|
return orig.apply(this, arguments);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
function hideSettingsOnPageChange() {
|
function hideUiOnPageChange() {
|
||||||
const $settings = document.querySelector('.better_xcloud_settings');
|
const $settings = document.querySelector('.better_xcloud_settings');
|
||||||
if ($settings) {
|
if ($settings) {
|
||||||
$settings.classList.add('better_xcloud_settings_gone');
|
$settings.classList.add('better_xcloud_settings_gone');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const $quickBar = document.querySelector('.better_xcloud_quick_settings_bar');
|
||||||
|
if ($quickBar) {
|
||||||
|
$quickBar.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Hide Settings UI when navigate to another page
|
// Hide Settings UI when navigate to another page
|
||||||
window.addEventListener('xcloud_popstate', hideSettingsOnPageChange);
|
window.addEventListener('xcloud_popstate', hideUiOnPageChange);
|
||||||
window.addEventListener('popstate', hideSettingsOnPageChange);
|
window.addEventListener('popstate', hideUiOnPageChange);
|
||||||
// Make pushState/replaceState methods dispatch "xcloud_popstate" event
|
// Make pushState/replaceState methods dispatch "xcloud_popstate" event
|
||||||
window.history.pushState = patchHistoryMethod('pushState');
|
window.history.pushState = patchHistoryMethod('pushState');
|
||||||
window.history.replaceState = patchHistoryMethod('replaceState');
|
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
|
// Clear data of window.navigator.userAgentData, force Xcloud to detect browser based on User-Agent header
|
||||||
Object.defineProperty(window.navigator, 'userAgentData', {});
|
Object.defineProperty(window.navigator, 'userAgentData', {});
|
||||||
|
|
||||||
@ -787,6 +1110,11 @@ interceptHttpRequests();
|
|||||||
|
|
||||||
patchVideoApi();
|
patchVideoApi();
|
||||||
|
|
||||||
|
// Setup UI
|
||||||
|
addCss();
|
||||||
|
updateVideoPlayerCss();
|
||||||
|
setupVideoSettingsBar();
|
||||||
|
|
||||||
// Workaround for Hermit browser
|
// Workaround for Hermit browser
|
||||||
var onLoadTriggered = false;
|
var onLoadTriggered = false;
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
|
Reference in New Issue
Block a user