Compare commits

..

17 Commits

Author SHA1 Message Date
a0d0d6e1e6 Update README.md 2023-09-10 17:51:42 +07:00
cc466ff2ac Bump version to 1.16 2023-09-10 17:16:11 +07:00
b1bfe96935 Bump version to 1.16 2023-09-10 17:15:47 +07:00
241be49d09 Update README.md 2023-09-10 17:15:23 +07:00
7c48b7e6fb Volume booster (#125)
* Move Video settings box from bottom to the right side

* Rename "Video Settings" to "Stream Settings"

* Volume booster

* Fix volume booster not working

* Typo

* Fix not working in Kiwi

* Show input range

* Update monkey patching method for AudioContext

* Refactor Preferences

* Add tick markers

* Add Audio & Video headers

* Reduce stats bar size

* Show warning when Clarity Boost mode is ON

* Increase max volume to 600

* Try to fix audio problem on iOS/iPadOS

* Pause <audio>

* Fix crashing when enabling touch controller

* Fix touch controller not working

* Fix volume booster not working on iOS/iPadOS
2023-09-10 17:04:23 +07:00
cc9a644a5e Bump version to 1.15.1 2023-09-08 17:24:05 +07:00
a77db68afb Bump version to 1.15.1 2023-09-08 17:23:46 +07:00
cd7a7c92c7 Validate settings when getting its values 2023-09-08 17:16:38 +07:00
651402a6b4 Restore stretch to full screen feature 2023-09-08 17:15:45 +07:00
6cd2648325 Update README.md 2023-09-04 10:40:14 +07:00
fa0d761d24 Bump version to 1.15 2023-09-04 10:37:31 +07:00
f01d7a3b0b Bump version to 1.15 2023-09-04 10:37:12 +07:00
b520e8173e Update README.md 2023-09-04 10:36:43 +07:00
f15f43faf7 Remove "Bitrate" & "Decode Time" stats from default items 2023-09-04 10:31:52 +07:00
e470cb20a3 Replace "Stretch Video" setting with "Video Ratio" (#121)
* Make Game Pass' app version easier to read

* Replace numberPicker with NumberStepper

* Replace "Stretch Video" setting with "Video Ratio"

* Make number stepper's buttons a little bit bigger
2023-09-04 09:11:08 +07:00
d1882046e2 Update README.md 2023-08-29 08:42:04 +07:00
fb7bd2da0d Update README.md 2023-08-28 14:31:13 +07:00
3 changed files with 404 additions and 239 deletions

View File

@ -6,10 +6,9 @@ This script makes me spend more time with xCloud, and I hope the same thing happ
If you like this project please give it a 🌟. 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) [![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)
<!--
[![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](https://github.com/redphx/better-xcloud/releases) [![Total downloads](https://img.shields.io/github/downloads/redphx/better-xcloud/total?color=%23e15f2c)](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)
## Table of Contents ## Table of Contents
- [**Features**](#features) - [**Features**](#features)
@ -27,10 +26,9 @@ If you like this project please give it a 🌟. Thank you 🙏.
<img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/0eedde97-74c7-44df-bc89-2ebf8edb6e2c"> <img width="400" alt="Settings UI" src="https://github.com/redphx/better-xcloud/assets/96280/0eedde97-74c7-44df-bc89-2ebf8edb6e2c">
<br> <br>
<img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/e30f6514-13ca-41c6-bff2-979573cff956"> <img width="600" alt="Stream HUD" src="https://github.com/redphx/better-xcloud/assets/96280/51bdb96c-79ab-402f-902a-a9e6229973b2">
<br> <br>
<img width="600" alt="Video settings" src="https://github.com/redphx/better-xcloud/assets/96280/a8614693-7f56-4a49-82ad-c1fd7e2e00a5"> <img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/18ed4598-4eca-4626-9434-5f74266b00e7">
&nbsp; &nbsp;
@ -93,14 +91,13 @@ If you like this project please give it a 🌟. Thank you 🙏.
> <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062"> > <img width="400" alt="Button styles" src="https://github.com/redphx/better-xcloud/assets/96280/2bfef2b3-6712-4924-b067-c2312f8c8062">
### Loading screen ### Loading screen
- Show game art - **Show game art**
> Replace the black background with game art if it's available. > Replace the black background with game art if it's available.
- Show the estimated wait time - **Show the estimated wait time**
> The time is estimated by the server. > The time is estimated by the server.
> It's not 100% correct: you might get in the game sooner or later. > It's not 100% correct: you might get in the game sooner or later.
> Don't be mad when the estimated time is inaccurate.
> Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info. > Check [#51](https://github.com/redphx/better-xcloud/issues/51) for more info.
- Show/hide the rocket animation - **Show/hide the rocket animation**
> Always show/Hide when queuing/Always hide. > Always show/Hide when queuing/Always hide.
> Hide this animation might save some battery life while queuing. > Hide this animation might save some battery life while queuing.
@ -123,8 +120,9 @@ If you like this project please give it a 🌟. Thank you 🙏.
- **Disable xCloud analytics** - **Disable xCloud analytics**
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future. > 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 ### In-game settings
- **Volume control**
> Increase stream's volume up to 600%
- **🔥 Improve stream's clarity** - **🔥 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). > 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". > Also known as poor man's "Clarity Boost".
@ -135,7 +133,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
> ![clarity](https://github.com/redphx/better-xcloud/assets/96280/ed63bbb0-fcbf-43e2-8e51-ac2733e697b8) > ![clarity](https://github.com/redphx/better-xcloud/assets/96280/ed63bbb0-fcbf-43e2-8e51-ac2733e697b8)
> *(click to enlarge)* > *(click to enlarge)*
- **Stretch video to full sctreen** - **Change video's ratio**
> Useful when you don't have a 16:9 screen > Useful when you don't have a 16:9 screen
- **Adjust video filters** - **Adjust video filters**
> Brightness/Contrast/Saturation. > Brightness/Contrast/Saturation.
@ -159,7 +157,7 @@ If you like this project please give it a 🌟. Thank you 🙏.
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). 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**: 2. Install **Better xCloud**:
- [Stable version](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) <!-- - [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)-->
I only distribute **Better xCloud** on GitHub, *DO NOT* download it on other websites or from unknown sources. I only distribute **Better xCloud** on GitHub, *DO NOT* download it on other websites or from unknown sources.
3. Refresh [xCloud web page](https://www.xbox.com/play/). 3. Refresh [xCloud web page](https://www.xbox.com/play/).
4. Click on the new "SERVER NAME" button next to your profile picture to adjust settings. 4. Click on the new "SERVER NAME" button next to your profile picture to adjust settings.
@ -176,18 +174,19 @@ If you still have trouble installing **Better xCloud**, you can follow one of th
## Compatibility ## Compatibility
✅ = confirmed to be working - 👍 = best choice, all features work as intended
❓ = not yet tested - ✅ = confirmed to be working, might miss some features
❌ = not supported (mostly because of lacking Userscript/extension support) - ❌ = not supported (mostly because of lacking Userscript/extension support)
= unavailable - = unavailable
= see custom notes - 🗒 = see custom notes
| | Desktop | Android/Android TV | iOS |
|-----------------------------------------|:-----------------|:-------------------|:----------------| | | Windows/Linux | macOS | Android/Android TV | iOS |
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ | |-----------------------------------------|:-----------------|:-----------------|:------------------|:-----------------|
| Firefox | | ⚠️<sup>(1)</sup> | ❌ | | Chrome/Edge/Chromium variants | 👍 | 👍 | | ❌ |
| Safari | ✅<sup>(2)</sup> | | <sup>(3)</sup> | | Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ |
| [Hermit](https://hermit.chimbori.com) | | ⚠️<sup>(4)</sup> | | | Safari | | <sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [Kiwi Browser](https://kiwibrowser.com) | | | | | [Kiwi Browser](https://kiwibrowser.com) | | | 👍 | |
| [Hermit](https://hermit.chimbori.com) | | | 🗒️<sup>(4)</sup> | |
Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**. Don't see your browser in the table? If it supports Tampermonkey/Userscript then the answer is likely **"YES"**.
@ -215,7 +214,7 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
|------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------| |------:|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------|
| PING | Ping | The number of seconds it takes for data to be sent from your device to the server and back over (the correct term is "Round Trip Time") | | PING | Ping | The number of seconds it takes for data to be sent from your device to the server and back over (the correct term is "Round Trip Time") |
| 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) | | 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) |
| 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)) | | DT | Decode Time | The average time it took to decode one frame in the last second (bugged on Android [#26](https://github.com/redphx/better-xcloud/issues/26)) |
| BR | Bitrate | The amount of data the server sent to your device in the last second | | 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 | | 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 | | FL | Frames Lost | The total number of frames dropped prior to decode or dropped because the frame missed its display deadline |

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 1.14.1 // @version 1.16
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -13,7 +13,7 @@
// ==/UserScript== // ==/UserScript==
'use strict'; 'use strict';
const SCRIPT_VERSION = '1.14.1'; const SCRIPT_VERSION = '1.16';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud'; const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
console.log(`[Better xCloud] readyState: ${document.readyState}`); console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -107,6 +107,8 @@ window.addEventListener('load', e => {
const SERVER_REGIONS = {}; const SERVER_REGIONS = {};
var STREAM_WEBRTC; var STREAM_WEBRTC;
var STREAM_AUDIO_CONTEXT;
var STREAM_AUDIO_GAIN_NODE;
var $STREAM_VIDEO; var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS; var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID; var GAME_TITLE_ID;
@ -449,8 +451,12 @@ class TouchController {
RTCPeerConnection.prototype.orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel; RTCPeerConnection.prototype.orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() { RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = this.orgCreateDataChannel.apply(this, arguments);
if (!TouchController.#enable || dataChannel.label !== 'message') {
return dataChannel;
}
// Apply touch controller's style // Apply touch controller's style
const $babylonCanvas = document.getElementById('babylon-canvas');
let filter = ''; let filter = '';
if (TouchController.#enable) { if (TouchController.#enable) {
if (PREF_STYLE_STANDARD === 'white') { if (PREF_STYLE_STANDARD === 'white') {
@ -463,16 +469,7 @@ class TouchController {
} }
if (filter) { if (filter) {
$style.textContent = ` $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;
#babylon-canvas {
filter: ${filter} !important;
}
`;
}
const dataChannel = this.orgCreateDataChannel.apply(this, arguments);
if (!TouchController.#enable) {
return dataChannel;
} }
TouchController.#dataChannel = dataChannel; TouchController.#dataChannel = dataChannel;
@ -1149,12 +1146,13 @@ class Preferences {
static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; } static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; }
static get VIDEO_CLARITY() { return 'video_clarity'; } static get VIDEO_CLARITY() { return 'video_clarity'; }
static get VIDEO_FILL_FULL_SCREEN() { return 'video_fill_full_screen'; } static get VIDEO_RATIO() { return 'video_ratio' }
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'; }
static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; } static get AUDIO_MIC_ON_PLAYING() { return 'audio_mic_on_playing'; }
static get AUDIO_VOLUME() { return 'audio_volume'; }
static get STATS_ITEMS() { return 'stats_items'; }; static get STATS_ITEMS() { return 'stats_items'; };
static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; } static get STATS_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
@ -1313,30 +1311,46 @@ class Preferences {
'min': 0, 'min': 0,
'max': 5, 'max': 5,
}, },
[Preferences.VIDEO_FILL_FULL_SCREEN]: { [Preferences.VIDEO_RATIO]: {
'default': false, 'default': '16:9',
'options': {
'16:9': '16:9',
'21:9': '21:9',
'16:10': '16:10',
'4:3': '4:3',
'fill': 'Stretch',
'cover': 'Cover',
},
}, },
[Preferences.VIDEO_SATURATION]: { [Preferences.VIDEO_SATURATION]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[Preferences.VIDEO_CONTRAST]: { [Preferences.VIDEO_CONTRAST]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[Preferences.VIDEO_BRIGHTNESS]: { [Preferences.VIDEO_BRIGHTNESS]: {
'default': 100, 'default': 100,
'min': 0, 'min': 50,
'max': 150, 'max': 150,
}, },
[Preferences.AUDIO_MIC_ON_PLAYING]: { [Preferences.AUDIO_MIC_ON_PLAYING]: {
'default': false, 'default': false,
}, },
[Preferences.AUDIO_VOLUME]: {
'default': 100,
'min': 0,
'max': 600,
},
[Preferences.STATS_ITEMS]: { [Preferences.STATS_ITEMS]: {
'default': [StreamStats.PING, StreamStats.FPS, StreamStats.BITRATE, StreamStats.DECODE_TIME, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST], 'default': [StreamStats.PING, StreamStats.FPS, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST],
'multiple_options': { 'multiple_options': {
[StreamStats.PING]: 'Ping', [StreamStats.PING]: 'Ping',
[StreamStats.FPS]: 'FPS', [StreamStats.FPS]: 'FPS',
@ -1381,17 +1395,17 @@ class Preferences {
}, },
} }
constructor() { #storage = localStorage;
this._storage = localStorage; #key = 'better_xcloud';
this._key = 'better_xcloud'; #prefs = {};
let savedPrefs = this._storage.getItem(this._key); constructor() {
let savedPrefs = this.#storage.getItem(this.#key);
if (savedPrefs == null) { if (savedPrefs == null) {
savedPrefs = '{}'; savedPrefs = '{}';
} }
savedPrefs = JSON.parse(savedPrefs); savedPrefs = JSON.parse(savedPrefs);
this._prefs = {};
for (let settingId in Preferences.SETTINGS) { for (let settingId in Preferences.SETTINGS) {
if (!settingId) { if (!settingId) {
alert('Undefined setting key'); alert('Undefined setting key');
@ -1401,14 +1415,50 @@ class Preferences {
const setting = Preferences.SETTINGS[settingId]; const setting = Preferences.SETTINGS[settingId];
if (settingId in savedPrefs) { if (settingId in savedPrefs) {
this._prefs[settingId] = savedPrefs[settingId]; this.#prefs[settingId] = savedPrefs[settingId];
} else { } else {
this._prefs[settingId] = setting.default; this.#prefs[settingId] = setting.default;
} }
} }
} }
get(key, defaultValue=null) { #validateValue(key, value) {
const config = Preferences.SETTINGS[key];
if (!config) {
return value;
}
if (typeof value === 'undefined' || value === null) {
value = config.default;
}
if ('min' in config) {
value = Math.max(config.min, value);
}
if ('max' in config) {
value = Math.min(config.max, value);
}
if ('options' in config && !(value in config.options)) {
value = config.default;
} else if ('multiple_options' in config) {
if (value.length) {
const validOptions = Object.keys(config.multiple_options);
value.forEach((item, idx) => {
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
});
}
if (!value.length) {
value = config.default;
}
}
return value;
}
get(key) {
if (typeof key === 'undefined') { if (typeof key === 'undefined') {
debugger; debugger;
return; return;
@ -1419,53 +1469,21 @@ class Preferences {
return 'default'; return 'default';
} }
const value = this._prefs[key]; let value = this.#prefs[key];
value = this.#validateValue(key, value);
if (typeof value !== 'undefined' && value !== null && value !== '') { return value;
return value;
}
if (defaultValue !== null) {
return defaultValue;
}
// Return default value
return Preferences.SETTINGS[key].default;
} }
set(key, value) { set(key, value) {
const config = Preferences.SETTINGS[key]; value = this.#validateValue(key, value);
if (config) {
if ('min' in config) {
value = Math.max(config.min, value);
}
if ('max' in config) { this.#prefs[key] = value;
value = Math.min(config.max, value); this.#updateStorage();
}
if ('options' in config && !(value in config.options)) {
value = config.default;
} else if ('multiple_options' in config) {
if (value.length) {
const validOptions = Object.keys(config.multiple_options);
value.forEach((item, idx) => {
(validOptions.indexOf(item) === -1) && value.splice(idx, 1);
});
}
if (!value.length) {
value = config.default;
}
}
}
this._prefs[key] = value;
this._update_storage();
} }
_update_storage() { #updateStorage() {
this._storage.setItem(this._key, JSON.stringify(this._prefs)); this.#storage.setItem(this.#key, JSON.stringify(this.#prefs));
} }
toElement(key, onChange) { toElement(key, onChange) {
@ -1547,6 +1565,118 @@ class Preferences {
$control.id = `xcloud_setting_${key}`; $control.id = `xcloud_setting_${key}`;
return $control; return $control;
} }
toNumberStepper(key, onChange, options={}) {
options = options || {};
options.suffix = options.suffix || '';
options.disabled = !!options.disabled;
options.hideSlider = !!options.hideSlider;
const setting = Preferences.SETTINGS[key]
let value = PREFS.get(key);
let $text, $decBtn, $incBtn, $range;
const MIN = setting.min;
const MAX= setting.max;
const STEPS = Math.max(setting.steps || 1, 1);
const CE = createElement;
const $wrapper = CE('div', {},
$decBtn = CE('button', {'data-type': 'dec'}, '-'),
$text = CE('span', {}, value + options.suffix),
$incBtn = CE('button', {'data-type': 'inc'}, '+'),
);
if (!options.disabled && !options.hideSlider) {
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value});
$range.addEventListener('input', e => {
value = parseInt(e.target.value);
$text.textContent = value + options.suffix;
PREFS.set(key, value);
onChange && onChange(e, value);
});
$wrapper.appendChild($range);
if (options.ticks) {
const markersId = `markers-${key}`;
const $markers = CE('datalist', {'id': markersId});
$range.setAttribute('list', markersId);
for (let i = MIN; i <= MAX; i += options.ticks) {
$markers.appendChild(CE('option', {'value': i}));
}
$wrapper.appendChild($markers);
}
}
if (options.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;
const onClick = e => {
if (isHolding) {
e.preventDefault();
isHolding = false;
return;
}
const btnType = e.target.getAttribute('data-type');
if (btnType === 'dec') {
value = Math.max(MIN, value - STEPS);
} else {
value = Math.min(MAX, value + STEPS);
}
$text.textContent = value + options.suffix;
$range && ($range.value = value);
PREFS.set(key, value);
isHolding = false;
onChange && onChange(e, value);
}
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;
}
} }
@ -1556,8 +1686,8 @@ const PREFS = new Preferences();
function checkForUpdate() { function checkForUpdate() {
const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION, ''); const currentVersion = PREFS.get(Preferences.CURRENT_VERSION);
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK, 0); const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK);
const now = Math.round((+new Date) / 1000); const now = Math.round((+new Date) / 1000);
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) { if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
@ -1726,7 +1856,7 @@ function addCss() {
.better-xcloud-settings-app-version { .better-xcloud-settings-app-version {
margin-top: 10px; margin-top: 10px;
text-align: center; text-align: center;
color: #484848; color: #747474;
font-size: 12px; font-size: 12px;
} }
@ -1908,7 +2038,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.better-xcloud-stats-bar span:first-of-type { .better-xcloud-stats-bar span:first-of-type {
min-width: 30px; min-width: 22px;
} }
.better-xcloud-stats-settings { .better-xcloud-stats-settings {
@ -1986,24 +2116,44 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-quick-settings-bar { .better-xcloud-quick-settings-bar {
display: none; display: none;
flex-direction: column;
user-select: none; user-select: none;
-webkit-user-select: none; -webkit-user-select: none;
position: fixed; position: fixed;
bottom: 0; right: 0;
left: 50%; top: 20px;
transform: translate(-50%, 0); bottom: 20px;
z-index: 9999; z-index: 9999;
padding: 16px; padding: 8px;
width: 600px; width: 220px;
background: #1a1b1e; background: #1a1b1e;
color: #fff; color: #fff;
border-radius: 8px 8px 0 0; border-radius: 8px 0 0 8px;
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 16px;
font-family: Bahnschrift, Arial, Helvetica, sans-serif; font-family: Bahnschrift, Arial, Helvetica, sans-serif;
text-align: center; text-align: center;
box-shadow: 0px 0px 6px #000; box-shadow: 0px 0px 6px #000;
opacity: 0.95; opacity: 0.95;
overflow: overlay;
}
.better-xcloud-quick-settings-bar:not([data-clarity-boost="true"]) .better-xcloud-clarity-boost-warning {
display: none;
}
.better-xcloud-quick-settings-bar[data-clarity-boost="true"] .better-xcloud-clarity-boost-warning {
display: block;
margin: 0px 8px;
padding: 12px;
font-size: 16px;
font-weight: normal;
background: #282828;
border-radius: 4px;
}
.better-xcloud-quick-settings-bar[data-clarity-boost="true"] > div[data-type="video"] {
display: none;
} }
.better-xcloud-quick-settings-bar *:focus { .better-xcloud-quick-settings-bar *:focus {
@ -2011,29 +2161,41 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
} }
.better-xcloud-quick-settings-bar > div { .better-xcloud-quick-settings-bar > div {
flex: 1; margin-bottom: 16px;
}
.better-xcloud-quick-settings-bar h2 {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.better-xcloud-quick-settings-bar input[type="range"] {
display: block;
margin: 12px auto;
width: 80%;
color: #959595 !important;
} }
.better-xcloud-quick-settings-bar label { .better-xcloud-quick-settings-bar label {
font-size: 16px; font-size: 16px;
font-weight: bold;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
} }
.better-xcloud-quick-settings-bar input {
width: 22px;
height: 22px;
}
.better-xcloud-quick-settings-bar button { .better-xcloud-quick-settings-bar button {
border: none; border: none;
width: 22px; width: 24px;
height: 22px; height: 24px;
margin: 0 4px; margin: 0 4px;
line-height: 22px; line-height: 24px;
background-color: #515151; background-color: #515151;
color: #fff; color: #fff;
border-radius: 4px; border-radius: 4px;
font-weight: bold;
font-size: 14px;
font-family: Consolas, "Courier New", Courier, monospace;
} }
@media (hover: hover) { @media (hover: hover) {
@ -2051,8 +2213,8 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-quick-settings-bar span { .better-xcloud-quick-settings-bar span {
display: inline-block; display: inline-block;
width: 40px; width: 40px;
font-weight: bold;
font-family: Consolas, "Courier New", Courier, monospace; font-family: Consolas, "Courier New", Courier, monospace;
font-size: 14px;
} }
.better-xcloud-stream-menu-button-on { .better-xcloud-stream-menu-button-on {
@ -2579,7 +2741,7 @@ function injectSettingsButton($parent) {
const CE = createElement; const CE = createElement;
const PREF_PREFERRED_REGION = getPreferredServerRegion(); const PREF_PREFERRED_REGION = getPreferredServerRegion();
const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION, null); const PREF_LATEST_VERSION = PREFS.get(Preferences.LATEST_VERSION);
// Setup Settings button // Setup Settings button
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION); const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
@ -2834,17 +2996,42 @@ function updateVideoPlayerCss() {
} }
let filters = getVideoPlayerFilterStyle(); let filters = getVideoPlayerFilterStyle();
let css = ''; let videoCss = '';
if (filters) { if (filters) {
css += `filter: ${filters} !important;`; videoCss += `filter: ${filters} !important;`;
} }
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) { const PREF_RATIO = PREFS.get(Preferences.VIDEO_RATIO);
css += 'object-fit: fill !important;'; if (PREF_RATIO && PREF_RATIO !== '16:9') {
if (PREF_RATIO.includes(':')) {
videoCss += `aspect-ratio: ${PREF_RATIO.replace(':', '/')}; object-fit: unset !important;`;
const tmp = PREF_RATIO.split(':');
const ratio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
const maxRatio = window.innerWidth / window.innerHeight;
if (ratio < maxRatio) {
videoCss += 'width: fit-content !important;'
} else {
videoCss += 'height: fit-content !important;'
}
} else {
videoCss += `object-fit: ${PREF_RATIO} !important;`;
}
} }
if (css) { let css = '';
css = `#game-stream video {${css}}`; if (videoCss) {
css = `
div[data-testid="media-container"] {
display: flex;
}
#game-stream video {
margin: 0 auto;
align-self: center;
${videoCss}
}
`;
} }
$elm.textContent = css; $elm.textContent = css;
@ -2939,17 +3126,14 @@ function injectStreamMenuButtons() {
return; return;
} }
// Create Video Settings button // Create Stream Settings button
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS); const $btnStreamSettings = cloneStreamMenuButton($orgButton, 'Stream settings', ICON_VIDEO_SETTINGS);
$btnVideoSettings.addEventListener('click', e => { $btnStreamSettings.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const msVideoProcessing = $STREAM_VIDEO.msVideoProcessing; const msVideoProcessing = $STREAM_VIDEO.msVideoProcessing;
if (msVideoProcessing && msVideoProcessing !== 'default') { $quickBar.setAttribute('data-clarity-boost', (msVideoProcessing && msVideoProcessing !== 'default'));
alert('This feature doesn\'t work when the Clarity Boost mode is ON');
return;
}
// Close HUD // Close HUD
$btnCloseHud.click(); $btnCloseHud.click();
@ -2965,7 +3149,7 @@ function injectStreamMenuButtons() {
}); });
// Add button at the beginning // Add button at the beginning
$orgButton.parentElement.insertBefore($btnVideoSettings, $orgButton.parentElement.firstChild); $orgButton.parentElement.insertBefore($btnStreamSettings, $orgButton.parentElement.firstChild);
// Hide Quick bar when closing HUD // Hide Quick bar when closing HUD
const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]'); const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
@ -2988,8 +3172,8 @@ function injectStreamMenuButtons() {
const btnStreamStatsOn = (!StreamStats.isHidden() && !StreamStats.isGlancing()); const btnStreamStatsOn = (!StreamStats.isHidden() && !StreamStats.isGlancing());
$btnStreamStats.classList.toggle('better-xcloud-stream-menu-button-on', btnStreamStatsOn); $btnStreamStats.classList.toggle('better-xcloud-stream-menu-button-on', btnStreamStatsOn);
// Insert after Video Settings button // Insert after Stream Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings); $orgButton.parentElement.insertBefore($btnStreamStats, $btnStreamSettings);
// Get "Quit game" button // Get "Quit game" button
const $btnQuit = $orgButton.parentElement.querySelector('button:last-of-type'); const $btnQuit = $orgButton.parentElement.querySelector('button:last-of-type');
@ -3132,116 +3316,40 @@ function patchRtcCodecs() {
} }
function numberPicker(key, suffix='', disabled=false) {
const setting = Preferences.SETTINGS[key]
let value = PREFS.get(key);
let $text, $decBtn, $incBtn;
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 + 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;
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 + suffix;
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() { function setupVideoSettingsBar() {
const CE = createElement; const CE = createElement;
const isSafari = UserAgent.isSafari(); const isSafari = UserAgent.isSafari();
const onVideoChange = e => {
updateVideoPlayerCss();
}
let $stretchInp; let $stretchInp;
const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'}, const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'},
CE('h2', {}, 'Audio'),
CE('div', {}, CE('div', {},
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Stretch Video'), CE('label', {}, 'Volume'),
$stretchInp = CE('input', {'id': 'better-xcloud-quick-setting-stretch', 'type': 'checkbox'})), PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
CE('div', {}, STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
CE('label', {}, 'Clarity'), }, {suffix: '%', ticks: 100})),
numberPicker(Preferences.VIDEO_CLARITY, '', isSafari)), // disable this feature in Safari
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); CE('h2', {}, 'Video'),
$stretchInp.addEventListener('change', e => { CE('div', {'class': 'better-xcloud-clarity-boost-warning'}, '⚠️ These settings don\'t work when the Clarity Boost mode is ON'),
PREFS.set(Preferences.VIDEO_FILL_FULL_SCREEN, e.target.checked); CE('div', {'data-type': 'video'},
updateVideoPlayerCss(); CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Ratio'),
}); PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange)),
CE('div', {'data-type': 'video'},
CE('label', {}, 'Clarity'),
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true})), // disable this feature in Safari
CE('div', {'data-type': 'video'},
CE('label', {}, 'Saturation'),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25})),
CE('div', {'data-type': 'video'},
CE('label', {}, 'Contrast'),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25})),
CE('div', {'data-type': 'video'},
CE('label', {}, 'Brightness'),
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}))
);
document.documentElement.appendChild($wrapper); document.documentElement.appendChild($wrapper);
} }
@ -3339,7 +3447,7 @@ function onHistoryChanged() {
$quickBar.style.display = 'none'; $quickBar.style.display = 'none';
} }
STREAM_WEBRTC = null; STREAM_AUDIO_GAIN_NODE = null;
$STREAM_VIDEO = null; $STREAM_VIDEO = null;
StreamStats.onStoppedPlaying(); StreamStats.onStoppedPlaying();
document.querySelector('.better-xcloud-screenshot-button').style = ''; document.querySelector('.better-xcloud-screenshot-button').style = '';
@ -3491,11 +3599,27 @@ if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
checkForUpdate(); checkForUpdate();
// Monkey patches // Monkey patches
if (UserAgent.isSafari(true)) {
window.AudioContext.prototype.orgCreateGain = window.AudioContext.prototype.createGain;
window.AudioContext.prototype.createGain = function() {
const gainNode = this.orgCreateGain.apply(this);
gainNode.gain.value = (PREFS.get(Preferences.AUDIO_VOLUME) / 100).toFixed(2);
STREAM_AUDIO_GAIN_NODE = gainNode;
return gainNode;
}
}
const OrgAudioContext = window.AudioContext;
window.AudioContext = function() {
const ctx = new OrgAudioContext();
STREAM_AUDIO_CONTEXT = ctx;
return ctx;
}
RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate; RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate;
RTCPeerConnection.prototype.addIceCandidate = function(...args) { RTCPeerConnection.prototype.addIceCandidate = function(...args) {
const candidate = args[0].candidate; const candidate = args[0].candidate;
if (candidate && candidate.startsWith('a=candidate:1 ')) { if (candidate && candidate.startsWith('a=candidate:1 ')) {
STREAM_WEBRTC = this;
StreamBadges.ipv6 = candidate.substring(20).includes(':'); StreamBadges.ipv6 = candidate.substring(20).includes(':');
} }
@ -3506,6 +3630,46 @@ if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.setup(); TouchController.setup();
} }
const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() {
const peer = new OrgRTCPeerConnection();
peer.addEventListener('track', e => {
if (e.track.kind !== 'audio') {
return;
}
const $audio = document.querySelector('#game-stream audio');
if (!$audio) {
return;
}
try {
// Prevent double sounds
$audio.muted = true;
const audioCtx = STREAM_AUDIO_CONTEXT;
const audioStream = audioCtx.createMediaStreamSource(e.streams[0]);
const gainNode = audioCtx.createGain();
audioStream.connect(gainNode);
gainNode.connect(audioCtx.destination);
gainNode.gain.value = (PREFS.get(Preferences.AUDIO_VOLUME) / 100).toFixed(2);
STREAM_AUDIO_GAIN_NODE = gainNode;
$audio.pause();
$audio.addEventListener('play', e => {
$audio.pause();
});
} catch (e) {
$audio && ($audio.muted = false);
}
});
STREAM_WEBRTC = peer;
return peer;
}
patchRtcCodecs(); patchRtcCodecs();
interceptHttpRequests(); interceptHttpRequests();
@ -3514,6 +3678,8 @@ patchVideoApi();
// Setup UI // Setup UI
addCss(); addCss();
updateVideoPlayerCss(); updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
setupVideoSettingsBar(); setupVideoSettingsBar();
setupScreenshotButton(); setupScreenshotButton();
StreamStats.render(); StreamStats.render();