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 🙏.
[![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 stars](https://img.shields.io/github/stars/redphx/better-xcloud?color=%23cca400)](https://github.com/redphx/better-xcloud/stargazers)
## Table of Contents
- [**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">
<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>
<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;
@ -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">
### Loading screen
- Show game art
- **Show game art**
> 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.
> It's not 100% correct: you might get in the game sooner or later.
> Don't be mad when the estimated time is inaccurate.
> It's not 100% correct: you might get in the game sooner or later.
> 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.
> 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**
> 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**
> 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".
@ -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)
> *(click to enlarge)*
- **Stretch video to full sctreen**
- **Change video's ratio**
> Useful when you don't have a 16:9 screen
- **Adjust video filters**
> 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).
2. Install **Better xCloud**:
- [Stable version](https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js)
- [Dev version](https://github.com/redphx/better-xcloud/raw/main/better-xcloud.user.js)
<!-- - [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.
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.
@ -176,18 +174,19 @@ If you still have trouble installing **Better xCloud**, you can follow one of th
## Compatibility
✅ = confirmed to be working
❓ = not yet tested
❌ = not supported (mostly because of lacking Userscript/extension support)
= unavailable
= see custom notes
| | Desktop | Android/Android TV | iOS |
|-----------------------------------------|:-----------------|:-------------------|:----------------|
| Chrome/Edge/Chromium variants | ✅ | ❌ | ❌ |
| Firefox | | ⚠️<sup>(1)</sup> | ❌ |
| Safari | ✅<sup>(2)</sup> | | <sup>(3)</sup> |
| [Hermit](https://hermit.chimbori.com) | | ⚠️<sup>(4)</sup> | |
| [Kiwi Browser](https://kiwibrowser.com) | | | |
- 👍 = best choice, all features work as intended
- ✅ = confirmed to be working, might miss some features
- ❌ = not supported (mostly because of lacking Userscript/extension support)
- = unavailable
- 🗒 = see custom notes
| | Windows/Linux | macOS | Android/Android TV | iOS |
|-----------------------------------------|:-----------------|:-----------------|:------------------|:-----------------|
| Chrome/Edge/Chromium variants | 👍 | 👍 | | ❌ |
| Firefox | ✅ | ✅ | 🗒️<sup>(1)</sup> | ❌ |
| Safari | | <sup>(2)</sup> | | ✅<sup>(3)</sup> |
| [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"**.
@ -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") |
| 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 |
| 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 |

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 1.14.1
// @version 1.16
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,7 +13,7 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '1.14.1';
const SCRIPT_VERSION = '1.16';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -107,6 +107,8 @@ window.addEventListener('load', e => {
const SERVER_REGIONS = {};
var STREAM_WEBRTC;
var STREAM_AUDIO_CONTEXT;
var STREAM_AUDIO_GAIN_NODE;
var $STREAM_VIDEO;
var $SCREENSHOT_CANVAS;
var GAME_TITLE_ID;
@ -449,8 +451,12 @@ class TouchController {
RTCPeerConnection.prototype.orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = this.orgCreateDataChannel.apply(this, arguments);
if (!TouchController.#enable || dataChannel.label !== 'message') {
return dataChannel;
}
// Apply touch controller's style
const $babylonCanvas = document.getElementById('babylon-canvas');
let filter = '';
if (TouchController.#enable) {
if (PREF_STYLE_STANDARD === 'white') {
@ -463,16 +469,7 @@ class TouchController {
}
if (filter) {
$style.textContent = `
#babylon-canvas {
filter: ${filter} !important;
}
`;
}
const dataChannel = this.orgCreateDataChannel.apply(this, arguments);
if (!TouchController.#enable) {
return dataChannel;
$style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;
}
TouchController.#dataChannel = dataChannel;
@ -1149,12 +1146,13 @@ class Preferences {
static get UI_LOADING_SCREEN_ROCKET() { return 'ui_loading_screen_rocket'; }
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_CONTRAST() { return 'video_contrast'; }
static get VIDEO_SATURATION() { return 'video_saturation'; }
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_SHOW_WHEN_PLAYING() { return 'stats_show_when_playing'; }
@ -1313,30 +1311,46 @@ class Preferences {
'min': 0,
'max': 5,
},
[Preferences.VIDEO_FILL_FULL_SCREEN]: {
'default': false,
[Preferences.VIDEO_RATIO]: {
'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]: {
'default': 100,
'min': 0,
'min': 50,
'max': 150,
},
[Preferences.VIDEO_CONTRAST]: {
'default': 100,
'min': 0,
'min': 50,
'max': 150,
},
[Preferences.VIDEO_BRIGHTNESS]: {
'default': 100,
'min': 0,
'min': 50,
'max': 150,
},
[Preferences.AUDIO_MIC_ON_PLAYING]: {
'default': false,
},
[Preferences.AUDIO_VOLUME]: {
'default': 100,
'min': 0,
'max': 600,
},
[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': {
[StreamStats.PING]: 'Ping',
[StreamStats.FPS]: 'FPS',
@ -1381,17 +1395,17 @@ class Preferences {
},
}
constructor() {
this._storage = localStorage;
this._key = 'better_xcloud';
#storage = localStorage;
#key = 'better_xcloud';
#prefs = {};
let savedPrefs = this._storage.getItem(this._key);
constructor() {
let savedPrefs = this.#storage.getItem(this.#key);
if (savedPrefs == null) {
savedPrefs = '{}';
}
savedPrefs = JSON.parse(savedPrefs);
this._prefs = {};
for (let settingId in Preferences.SETTINGS) {
if (!settingId) {
alert('Undefined setting key');
@ -1401,14 +1415,50 @@ class Preferences {
const setting = Preferences.SETTINGS[settingId];
if (settingId in savedPrefs) {
this._prefs[settingId] = savedPrefs[settingId];
this.#prefs[settingId] = savedPrefs[settingId];
} 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') {
debugger;
return;
@ -1419,53 +1469,21 @@ class Preferences {
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;
}
if (defaultValue !== null) {
return defaultValue;
}
// Return default value
return Preferences.SETTINGS[key].default;
return value;
}
set(key, value) {
const config = Preferences.SETTINGS[key];
if (config) {
if ('min' in config) {
value = Math.max(config.min, value);
}
value = this.#validateValue(key, 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;
}
}
}
this._prefs[key] = value;
this._update_storage();
this.#prefs[key] = value;
this.#updateStorage();
}
_update_storage() {
this._storage.setItem(this._key, JSON.stringify(this._prefs));
#updateStorage() {
this.#storage.setItem(this.#key, JSON.stringify(this.#prefs));
}
toElement(key, onChange) {
@ -1547,6 +1565,118 @@ class Preferences {
$control.id = `xcloud_setting_${key}`;
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() {
const CHECK_INTERVAL_SECONDS = 4 * 3600; // check every 4 hours
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION, '');
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK, 0);
const currentVersion = PREFS.get(Preferences.CURRENT_VERSION);
const lastCheck = PREFS.get(Preferences.LAST_UPDATE_CHECK);
const now = Math.round((+new Date) / 1000);
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS) {
@ -1726,7 +1856,7 @@ function addCss() {
.better-xcloud-settings-app-version {
margin-top: 10px;
text-align: center;
color: #484848;
color: #747474;
font-size: 12px;
}
@ -1908,7 +2038,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.better-xcloud-stats-bar span:first-of-type {
min-width: 30px;
min-width: 22px;
}
.better-xcloud-stats-settings {
@ -1986,24 +2116,44 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-quick-settings-bar {
display: none;
flex-direction: column;
user-select: none;
-webkit-user-select: none;
position: fixed;
bottom: 0;
left: 50%;
transform: translate(-50%, 0);
right: 0;
top: 20px;
bottom: 20px;
z-index: 9999;
padding: 16px;
width: 600px;
padding: 8px;
width: 220px;
background: #1a1b1e;
color: #fff;
border-radius: 8px 8px 0 0;
border-radius: 8px 0 0 8px;
font-weight: 400;
font-size: 14px;
font-size: 16px;
font-family: Bahnschrift, Arial, Helvetica, sans-serif;
text-align: center;
box-shadow: 0px 0px 6px #000;
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 {
@ -2011,29 +2161,41 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
}
.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 {
font-size: 16px;
font-weight: bold;
display: block;
margin-bottom: 8px;
}
.better-xcloud-quick-settings-bar input {
width: 22px;
height: 22px;
}
.better-xcloud-quick-settings-bar button {
border: none;
width: 22px;
height: 22px;
width: 24px;
height: 24px;
margin: 0 4px;
line-height: 22px;
line-height: 24px;
background-color: #515151;
color: #fff;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
font-family: Consolas, "Courier New", Courier, monospace;
}
@media (hover: hover) {
@ -2051,8 +2213,8 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
.better-xcloud-quick-settings-bar span {
display: inline-block;
width: 40px;
font-weight: bold;
font-family: Consolas, "Courier New", Courier, monospace;
font-size: 14px;
}
.better-xcloud-stream-menu-button-on {
@ -2579,7 +2741,7 @@ function injectSettingsButton($parent) {
const CE = createElement;
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
const $button = CE('button', {'class': 'better-xcloud-settings-button'}, PREF_PREFERRED_REGION);
@ -2834,17 +2996,42 @@ function updateVideoPlayerCss() {
}
let filters = getVideoPlayerFilterStyle();
let css = '';
let videoCss = '';
if (filters) {
css += `filter: ${filters} !important;`;
videoCss += `filter: ${filters} !important;`;
}
if (PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN)) {
css += 'object-fit: fill !important;';
const PREF_RATIO = PREFS.get(Preferences.VIDEO_RATIO);
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) {
css = `#game-stream video {${css}}`;
let css = '';
if (videoCss) {
css = `
div[data-testid="media-container"] {
display: flex;
}
#game-stream video {
margin: 0 auto;
align-self: center;
${videoCss}
}
`;
}
$elm.textContent = css;
@ -2939,17 +3126,14 @@ function injectStreamMenuButtons() {
return;
}
// Create Video Settings button
const $btnVideoSettings = cloneStreamMenuButton($orgButton, 'Video settings', ICON_VIDEO_SETTINGS);
$btnVideoSettings.addEventListener('click', e => {
// Create Stream Settings button
const $btnStreamSettings = cloneStreamMenuButton($orgButton, 'Stream settings', ICON_VIDEO_SETTINGS);
$btnStreamSettings.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
const msVideoProcessing = $STREAM_VIDEO.msVideoProcessing;
if (msVideoProcessing && msVideoProcessing !== 'default') {
alert('This feature doesn\'t work when the Clarity Boost mode is ON');
return;
}
$quickBar.setAttribute('data-clarity-boost', (msVideoProcessing && msVideoProcessing !== 'default'));
// Close HUD
$btnCloseHud.click();
@ -2965,7 +3149,7 @@ function injectStreamMenuButtons() {
});
// 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
const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
@ -2988,8 +3172,8 @@ function injectStreamMenuButtons() {
const btnStreamStatsOn = (!StreamStats.isHidden() && !StreamStats.isGlancing());
$btnStreamStats.classList.toggle('better-xcloud-stream-menu-button-on', btnStreamStatsOn);
// Insert after Video Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnVideoSettings);
// Insert after Stream Settings button
$orgButton.parentElement.insertBefore($btnStreamStats, $btnStreamSettings);
// Get "Quit game" button
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() {
const CE = createElement;
const isSafari = UserAgent.isSafari();
const onVideoChange = e => {
updateVideoPlayerCss();
}
let $stretchInp;
const $wrapper = CE('div', {'class': 'better-xcloud-quick-settings-bar'},
CE('h2', {}, 'Audio'),
CE('div', {},
CE('label', {'for': 'better-xcloud-quick-setting-stretch'}, 'Stretch Video'),
$stretchInp = CE('input', {'id': 'better-xcloud-quick-setting-stretch', 'type': 'checkbox'})),
CE('div', {},
CE('label', {}, 'Clarity'),
numberPicker(Preferences.VIDEO_CLARITY, '', isSafari)), // disable this feature in Safari
CE('div', {},
CE('label', {}, 'Saturation'),
numberPicker(Preferences.VIDEO_SATURATION, '%')),
CE('div', {},
CE('label', {}, 'Contrast'),
numberPicker(Preferences.VIDEO_CONTRAST, '%')),
CE('div', {},
CE('label', {}, 'Brightness'),
numberPicker(Preferences.VIDEO_BRIGHTNESS, '%'))
);
CE('label', {}, 'Volume'),
PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
}, {suffix: '%', ticks: 100})),
$stretchInp.checked = PREFS.get(Preferences.VIDEO_FILL_FULL_SCREEN);
$stretchInp.addEventListener('change', e => {
PREFS.set(Preferences.VIDEO_FILL_FULL_SCREEN, e.target.checked);
updateVideoPlayerCss();
});
CE('h2', {}, 'Video'),
CE('div', {'class': 'better-xcloud-clarity-boost-warning'}, '⚠️ These settings don\'t work when the Clarity Boost mode is ON'),
CE('div', {'data-type': 'video'},
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);
}
@ -3339,7 +3447,7 @@ function onHistoryChanged() {
$quickBar.style.display = 'none';
}
STREAM_WEBRTC = null;
STREAM_AUDIO_GAIN_NODE = null;
$STREAM_VIDEO = null;
StreamStats.onStoppedPlaying();
document.querySelector('.better-xcloud-screenshot-button').style = '';
@ -3491,11 +3599,27 @@ if (PREFS.get(Preferences.DISABLE_BANDWIDTH_CHECKING)) {
checkForUpdate();
// 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.addIceCandidate = function(...args) {
const candidate = args[0].candidate;
if (candidate && candidate.startsWith('a=candidate:1 ')) {
STREAM_WEBRTC = this;
StreamBadges.ipv6 = candidate.substring(20).includes(':');
}
@ -3506,6 +3630,46 @@ if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
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();
interceptHttpRequests();
@ -3514,6 +3678,8 @@ patchVideoApi();
// Setup UI
addCss();
updateVideoPlayerCss();
window.addEventListener('resize', updateVideoPlayerCss);
setupVideoSettingsBar();
setupScreenshotButton();
StreamStats.render();