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
This commit is contained in:
redphx 2023-09-10 17:04:23 +07:00 committed by GitHub
parent cc9a644a5e
commit 7c48b7e6fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -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;
@ -1155,6 +1152,7 @@ class Preferences {
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'; }
@ -1340,9 +1338,16 @@ class Preferences {
'min': 50, '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.PACKETS_LOST, StreamStats.FRAMES_LOST], 'default': [StreamStats.PING, StreamStats.FPS, StreamStats.PACKETS_LOST, StreamStats.FRAMES_LOST],
@ -1390,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');
@ -1410,9 +1415,9 @@ 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;
} }
} }
} }
@ -1464,7 +1469,7 @@ class Preferences {
return 'default'; return 'default';
} }
let value = this._prefs[key]; let value = this.#prefs[key];
value = this.#validateValue(key, value); value = this.#validateValue(key, value);
return value; return value;
@ -1473,12 +1478,12 @@ class Preferences {
set(key, value) { set(key, value) {
value = this.#validateValue(key, value); value = this.#validateValue(key, value);
this._prefs[key] = value; this.#prefs[key] = value;
this._update_storage(); this.#updateStorage();
} }
_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) {
@ -1561,11 +1566,16 @@ class Preferences {
return $control; return $control;
} }
toNumberStepper(key, onChange, suffix='', disabled=false) { toNumberStepper(key, onChange, options={}) {
options = options || {};
options.suffix = options.suffix || '';
options.disabled = !!options.disabled;
options.hideSlider = !!options.hideSlider;
const setting = Preferences.SETTINGS[key] const setting = Preferences.SETTINGS[key]
let value = PREFS.get(key); let value = PREFS.get(key);
let $text, $decBtn, $incBtn; let $text, $decBtn, $incBtn, $range;
const MIN = setting.min; const MIN = setting.min;
const MAX= setting.max; const MAX= setting.max;
@ -1574,11 +1584,34 @@ class Preferences {
const CE = createElement; const CE = createElement;
const $wrapper = CE('div', {}, const $wrapper = CE('div', {},
$decBtn = CE('button', {'data-type': 'dec'}, '-'), $decBtn = CE('button', {'data-type': 'dec'}, '-'),
$text = CE('span', {}, value + suffix), $text = CE('span', {}, value + options.suffix),
$incBtn = CE('button', {'data-type': 'inc'}, '+'), $incBtn = CE('button', {'data-type': 'inc'}, '+'),
); );
if (disabled) { 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.disabled = true;
$incBtn.classList.add('better-xcloud-hidden'); $incBtn.classList.add('better-xcloud-hidden');
@ -1605,12 +1638,12 @@ class Preferences {
value = Math.min(MAX, value + STEPS); value = Math.min(MAX, value + STEPS);
} }
$text.textContent = value + suffix; $text.textContent = value + options.suffix;
$range && ($range.value = value);
PREFS.set(key, value); PREFS.set(key, value);
isHolding = false; isHolding = false;
onChange && onChange(e, value);
onChange && onChange();
} }
const onMouseDown = e => { const onMouseDown = e => {
@ -2005,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 {
@ -2083,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 {
@ -2108,11 +2161,24 @@ 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: 18px; font-size: 16px;
font-weight: bold; font-weight: bold;
display: block; display: block;
margin-bottom: 8px; margin-bottom: 8px;
@ -2127,6 +2193,9 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
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) {
@ -2144,9 +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: 16px; font-size: 14px;
} }
.better-xcloud-stream-menu-button-on { .better-xcloud-stream-menu-button-on {
@ -3058,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();
@ -3084,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]');
@ -3107,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');
@ -3254,27 +3319,36 @@ function patchRtcCodecs() {
function setupVideoSettingsBar() { function setupVideoSettingsBar() {
const CE = createElement; const CE = createElement;
const isSafari = UserAgent.isSafari(); const isSafari = UserAgent.isSafari();
const onChange = e => { const onVideoChange = e => {
updateVideoPlayerCss(); 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'}, 'Video Ratio'), CE('label', {}, 'Volume'),
PREFS.toElement(Preferences.VIDEO_RATIO, onChange, ':9')), PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
CE('div', {}, STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
}, {suffix: '%', ticks: 100})),
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'), CE('label', {}, 'Clarity'),
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onChange, '', isSafari)), // disable this feature in Safari PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true})), // disable this feature in Safari
CE('div', {}, CE('div', {'data-type': 'video'},
CE('label', {}, 'Saturation'), CE('label', {}, 'Saturation'),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onChange, '%')), PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25})),
CE('div', {}, CE('div', {'data-type': 'video'},
CE('label', {}, 'Contrast'), CE('label', {}, 'Contrast'),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onChange, '%')), PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25})),
CE('div', {}, CE('div', {'data-type': 'video'},
CE('label', {}, 'Brightness'), CE('label', {}, 'Brightness'),
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onChange, '%')) PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}))
); );
document.documentElement.appendChild($wrapper); document.documentElement.appendChild($wrapper);
@ -3373,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 = '';
@ -3525,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(':');
} }
@ -3540,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();