Compare commits

..

23 Commits
v1.1 ... 1.2.1

Author SHA1 Message Date
9971fe6c0c Update README.md 2023-07-15 20:55:34 +07:00
cc11bcea41 Bump version to 1.2.1 2023-07-15 20:54:15 +07:00
048d085e91 Merge branch 'main' of https://github.com/redphx/better-xcloud 2023-07-15 20:53:37 +07:00
ed32044480 Update Video settings UI 2023-07-15 20:53:28 +07:00
32c087966b Update README.md 2023-07-15 20:52:51 +07:00
e4ad010e0a Remove video settings in the main Settings UI 2023-07-15 20:29:50 +07:00
d2debc0623 Bump version to 1.2 2023-07-15 17:23:44 +07:00
0c3d8519e1 Update README.md 2023-07-15 17:23:11 +07:00
e323bd5b47 Fix Quick bar not hiding after quitting game 2023-07-15 17:07:08 +07:00
ce54aa4580 Update CSS of Quick video settings bar 2023-07-15 16:59:28 +07:00
d29447173c Fix Quick video settings feature not working with touch screen 2023-07-15 16:56:01 +07:00
2f3a3167e1 Add Quick video settings bar while playing 2023-07-15 16:32:20 +07:00
e17459934b Update README.md 2023-07-15 12:38:01 +07:00
ecf9b414ba Bump version to 1.1.3 2023-07-15 12:27:30 +07:00
f78bd31b58 Fix video not showing then the "Skip splash video" setting is off 2023-07-15 12:27:03 +07:00
da54bd5302 Update README.md 2023-07-15 09:56:59 +07:00
5479eeb66f Bump version to 1.1.2 2023-07-15 09:46:08 +07:00
a9eb70eaae Hide video player until it's ready 2023-07-15 09:40:13 +07:00
0e2066e461 Add Compatibility section 2023-07-15 07:45:54 +07:00
2ee2469a10 Hide the ugly black line in Grip Handle 2023-07-14 18:05:32 +07:00
9056b5926f Bump version to 1.1.1 2023-07-14 17:34:55 +07:00
730799821b Fix not applying video settings in some cases 2023-07-14 17:34:00 +07:00
fed7d489f4 Hide background of GripHandle 2023-07-14 17:31:08 +07:00
2 changed files with 342 additions and 121 deletions

View File

@ -2,14 +2,16 @@
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.
## Features:
## Features
<img width="500" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/db9b4f88-6958-4ec8-90cb-3cf37da5ab26">
<img width="500" alt="Video Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/130aa870-6938-4604-9e23-45e217b800cc">
<img width="474" alt="image" src="https://github.com/redphx/better-xcloud/assets/96280/2793d404-3185-4c91-a500-dde362c661dd">
- 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.
- 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.
- Stretch video to full sctreen. Useful when you don't have a 16:9 screen.
- Adjust video filters (brightness/contrast/saturation).
@ -18,10 +20,11 @@ The main target of this script is Android users, but it should work great on des
- Disable social features (friends, chat...).
- 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.
2. Install **Better xCloud**:
- [Directly on Github](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
- [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
- [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)
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.
@ -42,10 +45,19 @@ Other options (only do one of these):
- 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.
## Compatibility
✅ = confirmed to be working
❓ = not yet tested
❌ = not supported (mostly because of lacking Userscript/extension support)
⚠️ = see custom notes
| | Desktop | Android | iOS |
|----------------------------------------|----------|------------------|-----|
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ |
| Firefox | ✅ | ✅ | ❌ |
| 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
1. **Why is it an Userscript and not extension?**
@ -61,8 +73,8 @@ No you can't. You'll have to modify the app.
No. "Clarity Boost" feature uses an exclusive API (`Video.msVideoProcessing`) that's only available on Edge browser for desktop at the moment.
## Acknowledgements
**Better xCloud** is inspired by these projects:
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector)
- [n-thumann/xbox-cloud-server-selector](https://github.com/n-thumann/xbox-cloud-server-selector) for the idea of IPv6 feature
- Icons by [Adam Design](https://www.iconfinder.com/iconsets/user-interface-outline-27)
## Disclaimers
- Use as your own risk.

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.1
// @version 1.2.1
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,7 +13,7 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.1';
const SCRIPT_VERSION = '1.2.1';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const SERVER_REGIONS = {};
@ -88,6 +88,7 @@ class Preferences {
'id': Preferences.VIDEO_FILL_FULL_SCREEN,
'label': 'Stretch video to full screen',
'default': false,
'hidden': true,
},
{
@ -95,7 +96,8 @@ class Preferences {
'label': 'Video saturation (%)',
'default': 100,
'min': 0,
'max': 200,
'max': 150,
'hidden': true,
},
{
@ -103,7 +105,8 @@ class Preferences {
'label': 'Video contrast (%)',
'default': 100,
'min': 0,
'max': 200,
'max': 150,
'hidden': true,
},
{
@ -111,7 +114,8 @@ class Preferences {
'label': 'Video brightness (%)',
'default': 100,
'min': 0,
'max': 200,
'max': 150,
'hidden': true,
},
]
@ -246,31 +250,6 @@ function addCss() {
line-height: 24px;
}
.better_xcloud_settings_wrapper .setting_button:hover {
background-color: #06743f;
}
.better_xcloud_settings_preview_screen {
display: none;
aspect-ratio: 20/9;
background: #1e1e1e;
border-radius: 8px;
overflow: hidden;
max-height: 180px;
margin: 10px auto;
}
.better_xcloud_settings_preview_video {
display: flex;
aspect-ratio: 16/9;
height: 100%;
margin: auto;
}
.better_xcloud_settings_preview_video div {
flex: 1;
}
/* Hide UI elements */
#headerArea, #uhfSkipToMain, .uhf-footer {
display: none;
@ -284,6 +263,9 @@ div[class*=NotFocusedDialog] {
height: 0px !important;
}
#game-stream video {
visibility: hidden;
}
`;
// Reduce animations
@ -307,6 +289,14 @@ div[class*=Grip-module__container] {
button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] {
visibility: visible;
}
button[class*=GripHandle-module__container][aria-expanded=false] {
background-color: transparent !important;
}
div[class*=StreamHUD-module__buttonsContainer] {
padding: 0px !important;
}
`;
}
@ -525,31 +515,6 @@ 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) {
if (!$parent) {
return;
@ -582,8 +547,11 @@ function injectSettingsButton($parent) {
$wrapper.appendChild($title);
for (let setting of Preferences.SETTINGS) {
let $control;
if (setting.hidden) {
continue;
}
let $control;
if (setting.id === Preferences.SERVER_REGION) {
$control = CE('select', {id: 'xcloud_setting_' + setting.id});
$control.addEventListener('change', e => {
@ -605,33 +573,6 @@ function injectSettingsButton($parent) {
$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));
updateVideoPlayerPreview();
});
}
} else {
$control = CE('input', {
id: 'xcloud_setting_' + setting.id,
@ -659,9 +600,6 @@ function injectSettingsButton($parent) {
$wrapper.appendChild($elm);
}
const $videoPreview = generateVideoPreviewBox();
$wrapper.appendChild($videoPreview);
const $reloadBtn = CE('button', {'class': 'setting_button'}, 'Reload page to reflect changes');
$reloadBtn.addEventListener('click', e => window.location.reload());
$wrapper.appendChild($reloadBtn);
@ -741,8 +679,6 @@ function checkHeader() {
if (!$button) {
const $rightHeader = document.querySelector('#PageContent header div[class*=EdgewaterHeader-module__rightSectionSpacing]');
injectSettingsButton($rightHeader);
updateVideoPlayerCss();
}
}
@ -761,68 +697,336 @@ function watchHeader() {
timeout = setTimeout(checkHeader, 2000);
});
observer.observe($header, { subtree: true, childList: true});
observer.observe($header, {subtree: true, childList: true});
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() {
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;
// Show video player when it's ready
var showFunc;
showFunc = function() {
this.style.visibility = 'visible';
this.removeEventListener('playing', showFunc);
}
HTMLMediaElement.prototype.orgPlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
if (!this.className.startsWith('XboxSplashVideo')) {
return this.orgPlay.apply(this);
if (PREF_SKIP_SPLASH_VIDEO && this.className.startsWith('XboxSplashVideo')) {
this.volume = 0;
this.style.display = 'none';
this.dispatchEvent(new Event('ended'));
return {
catch: () => {},
};
}
this.volume = 0;
this.style.display = 'none';
this.dispatchEvent(new Event('ended'));
this.addEventListener('playing', showFunc);
injectVideoSettingsButton();
return {
catch: () => {},
};
return this.orgPlay.apply(this);
};
}
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: 20px;
display: block;
margin-bottom: 8px;
}
.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: #515151;
color: #fff;
border-radius: 4px;
}
@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: 40px;
font-weight: bold;
font-family: Consolas, "Courier New", Courier, monospace;
}
`);
document.documentElement.appendChild($wrapper);
document.documentElement.appendChild($style);
}
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;
return orig.apply(this, arguments);
};
};
function hideSettingsOnPageChange() {
function hideUiOnPageChange() {
const $settings = document.querySelector('.better_xcloud_settings');
if ($settings) {
$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
window.addEventListener('xcloud_popstate', hideSettingsOnPageChange);
window.addEventListener('popstate', hideSettingsOnPageChange);
window.addEventListener('xcloud_popstate', hideUiOnPageChange);
window.addEventListener('popstate', hideUiOnPageChange);
// 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', {});
@ -837,6 +1041,11 @@ interceptHttpRequests();
patchVideoApi();
// Setup UI
addCss();
updateVideoPlayerCss();
setupVideoSettingsBar();
// Workaround for Hermit browser
var onLoadTriggered = false;
window.onload = () => {