Compare commits

...

13 Commits
v2.0.3 ... v2.1

Author SHA1 Message Date
47ef7da37b Bump version to 2.1 2023-12-12 17:55:41 +07:00
63896469e2 Update README.md 2023-12-12 17:52:19 +07:00
4ab265e370 Controller & device vibration (#184)
* Test vibration

* Modify vibration patterns

* Try another vibration patterns

* Update vibration patterns

* Add setting to toggle device vibration

* Disable device vibration based on setting

* Bug fixes

* Test PWM

* Rename GamepadVibration to VibrationManager

* Add setting to toggle controller vibration

* Add vibration intensity setting

* Move Controller settings to the top

* Fix device vibration intensity

* No longer parse "delayMs" and "repeat"

* Fix device vibration intensity

* Disable vibration features on unsupported browsers

* Add "step" property to Vibration intensity slider

* Disable PWA prompt & Stream Gate dialog

* Update translations
2023-12-12 17:37:04 +07:00
584509a53d Improve IPv6 server detection 2023-12-09 11:09:08 +07:00
f3b9ebdb22 Bump version to 2.0.5 2023-12-08 07:43:30 +07:00
abd1aae57a Add "enableConsoleLogging" patch 2023-12-08 07:35:45 +07:00
ccdb944b99 Fix the Settings button not showing for some users 2023-12-08 07:23:44 +07:00
b4149e718b Bump version to 2.0.4 2023-12-06 20:06:41 +07:00
7c22685e95 Update README.md 2023-12-05 14:50:32 +07:00
ad98eb60e1 Only call eval() once per patch group 2023-12-05 06:38:35 +07:00
049e65429a Fix not applying patches correctly 2023-12-03 16:36:02 +07:00
a5b77ae8c0 Improve Patcher class 2023-12-03 10:38:10 +07:00
49550eed0a Add "enableXcloudLogger" patch 2023-12-02 17:31:51 +07:00
3 changed files with 443 additions and 56 deletions

View File

@ -1,5 +1,5 @@
# Better xCloud
Improve [Xbox Cloud Gaming (xCloud)](https://www.xbox.com/play/) experience on web browser.
Improve Xbox Cloud Gaming (xCloud) experience on [xbox.com/play](https://www.xbox.com/play).
The main target of this script is mobile users, but it should work great on desktop too.
Supported platforms:
@ -73,7 +73,8 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
<br>
<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="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/18ed4598-4eca-4626-9434-5f74266b00e7">
<img width="600" alt="Stream settings" src="https://github.com/redphx/better-xcloud/assets/96280/f7df312c-e6bc-49a1-8239-24c280ed7fa6">
&nbsp;
@ -179,6 +180,9 @@ Don't see your browser in the table? If it supports Tampermonkey/Userscript then
> The analytics contains statistics of your streaming session, so I'd recommend allowing analytics to help Xbox improve xCloud's experience in the future.
### In-game settings
- **🔥 Controller & device vibrations**
> Control vibration settings
> Adjust vibration intensity
- **Volume control**
> Increase stream's volume up to 600%
> Can be disabled in the Main Settings

View File

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

View File

@ -1,7 +1,7 @@
// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 2.0.3
// @version 2.1
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
@ -13,10 +13,11 @@
// ==/UserScript==
'use strict';
const SCRIPT_VERSION = '2.0.3';
const SCRIPT_VERSION = '2.1';
const SCRIPT_HOME = 'https://github.com/redphx/better-xcloud';
const ENABLE_MKB = false;
const ENABLE_XCLOUD_LOGGER = false;
console.log(`[Better xCloud] readyState: ${document.readyState}`);
@ -53,7 +54,7 @@ function createElement(elmName, props = {}) {
const argType = typeof arg;
if (argType === 'string' || argType === 'number') {
$elm.textContent = arg;
$elm.appendChild(document.createTextNode(arg));
} else if (arg) {
$elm.appendChild(arg);
}
@ -485,6 +486,13 @@ const Translations = {
"uk-UA": "Частота опитувань контролера",
"vi-VN": "Tần suất cập nhật của bộ điều khiển",
},
"controller-vibration": {
"de-DE": "Vibration des Controllers",
"en-US": "Controller vibration",
"ja-JP": "コントローラーの振動",
"tr-TR": "Oyun kumandası titreşimi",
"vi-VN": "Rung bộ điều khiển",
},
"custom": {
"de-DE": "Benutzerdefiniert",
"en-US": "Custom",
@ -533,6 +541,22 @@ const Translations = {
"vi-VN": "Thiết bị này không hỗ trợ cảm ứng",
"zh-CN": "您的设备不支持触摸",
},
"device-vibration": {
"de-DE": "Vibration des Geräts",
"en-US": "Device vibration",
"ja-JP": "デバイスの振動",
"tr-TR": "Cihaz titreşimi",
"uk-UA": "Вібрація пристрою",
"vi-VN": "Rung thiết bị",
},
"device-vibration-not-using-gamepad": {
"de-DE": "Aktiviert, wenn kein Gamepad verwendet wird",
"en-US": "On when not using gamepad",
"ja-JP": "ゲームパッド未使用時にオン",
"tr-TR": "Oyun kumandası bağlanmadan titreşim",
"uk-UA": "Увімкнена, коли не використовується геймпад",
"vi-VN": "Bật khi không dùng tay cầm",
},
"disable": {
"de-DE": "Deaktiviert",
"en-US": "Disable",
@ -646,6 +670,7 @@ const Translations = {
"de-DE": "Maus- und Tastaturunterstützung aktivieren",
"en-US": "Enable Mouse & Keyboard support",
"es-ES": "Habilitar soporte para ratón y teclado",
"it-IT": "Abilitare il supporto di mouse e tastiera",
"ja-JP": "マウス&キーボードのサポートを有効化",
"pl-PL": "Włącz obsługę myszy i klawiatury",
"pt-BR": "Habilitar suporte ao Mouse & Teclado",
@ -674,6 +699,7 @@ const Translations = {
"de-DE": "\"Remote Play\" Funktion aktivieren",
"en-US": "Enable the \"Remote Play\" feature",
"es-ES": "Activar la función \"Reproducción remota\"",
"it-IT": "Abilitare la funzione \"Riproduzione remota\"",
"ja-JP": "リモートプレイ機能を有効化",
"pl-PL": "Włącz funkcję \"Gra zdalna\"",
"pt-BR": "Ativar o recurso \"Reprodução Remota\"",
@ -702,6 +728,7 @@ const Translations = {
"de-DE": "Schnell",
"en-US": "Fast",
"es-ES": "Rápido",
"it-IT": "Veloce",
"ja-JP": "高速",
"pl-PL": "Szybko",
"pt-BR": "Rápido",
@ -791,6 +818,7 @@ const Translations = {
"de-DE": "Layout",
"en-US": "Layout",
"es-ES": "Diseño",
"it-IT": "Layout",
"ja-JP": "レイアウト",
"pl-PL": "Układ",
"pt-BR": "Layout",
@ -819,6 +847,7 @@ const Translations = {
"de-DE": "Max. Bitrate",
"en-US": "Max bitrate",
"es-ES": "Tasa de bits máxima",
"it-IT": "Bitrate massimo",
"ja-JP": "最大ビットレート",
"pl-PL": "Maksymalny bitrate",
"pt-BR": "Taxa máxima dos bits",
@ -831,6 +860,7 @@ const Translations = {
"de-DE": "Funktioniert evtl. nicht fehlerfrei!",
"en-US": "May not work properly!",
"es-ES": "¡Puede que no funcione correctamente!",
"it-IT": "Potrebbe non funzionare correttamente!",
"ja-JP": "正常に動作しない場合があります!",
"pl-PL": "Może nie działać poprawnie!",
"pt-BR": "Pode não funcionar corretamente!",
@ -889,6 +919,7 @@ const Translations = {
"de-DE": "Maus & Tastatur",
"en-US": "Mouse & Keyboard",
"es-ES": "Ratón y teclado",
"it-IT": "Mouse e tastiera",
"ja-JP": "マウス&キーボード",
"pl-PL": "Mysz i klawiatura",
"pt-BR": "Mouse e Teclado",
@ -975,6 +1006,7 @@ const Translations = {
"de-DE": "Unterstützt nur einige Spiele",
"en-US": "Only supports some games",
"es-ES": "Sólo soporta algunos juegos",
"it-IT": "Supporta solo alcuni giochi",
"ja-JP": "一部のゲームのみサポート",
"pl-PL": "Wspiera tylko niektóre gry",
"pt-BR": "Suporta apenas alguns jogos",
@ -1397,6 +1429,7 @@ const Translations = {
"de-DE": "Langsam",
"en-US": "Slow",
"es-ES": "Lento",
"it-IT": "Lento",
"ja-JP": "低速",
"pl-PL": "Wolno",
"pt-BR": "Lento",
@ -1425,6 +1458,7 @@ const Translations = {
"de-DE": "Smart TV",
"en-US": "Smart TV",
"es-ES": "Smart TV",
"it-IT": "Smart TV",
"ja-JP": "スマートTV",
"pl-PL": "Smart TV",
"pt-BR": "Smart TV",
@ -1621,6 +1655,15 @@ const Translations = {
"vi-VN": "Kéo giãn",
"zh-CN": "拉伸",
},
"swap-buttons": {
"de-DE": "Tasten tauschen",
"en-US": "Swap buttons",
"ja-JP": "ボタン入れ替え",
"pt-BR": "Trocar botões",
"tr-TR": "Düğme düzenini ters çevir",
"uk-UA": "Поміняти кнопки місцями",
"vi-VN": "Hoán đổi nút",
},
"target-resolution": {
"de-DE": "Festgelegte Auflösung",
"en-US": "Target resolution",
@ -1863,6 +1906,7 @@ const Translations = {
"de-DE": "Unbegrenzt",
"en-US": "Unlimited",
"es-ES": "Ilimitado",
"it-IT": "Illimitato",
"ja-JP": "無制限",
"pl-PL": "Bez ograniczeń",
"pt-BR": "Ilimitado",
@ -1913,6 +1957,13 @@ const Translations = {
"vi-VN": "User-Agent",
"zh-CN": "浏览器UA伪装",
},
"vibration-intensity": {
"de-DE": "Vibrationsstärke",
"en-US": "Vibration intensity",
"ja-JP": "振動の強さ",
"tr-TR": "Titreşim gücü",
"vi-VN": "Cường độ rung",
},
"video": {
"de-DE": "Video",
"en-US": "Video",
@ -2909,6 +2960,147 @@ class GamepadHandler {
}
}
class VibrationManager {
static #playDeviceVibration(data) {
// console.log(+new Date, data);
const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY;
if (intensity === 0 || intensity === 100) {
// Stop vibration
window.navigator.vibrate(intensity ? data.durationMs : 0);
return;
}
const pulseDuration = 200;
const onDuration = Math.floor(pulseDuration * intensity / 100);
const offDuration = pulseDuration - onDuration;
const repeats = Math.ceil(data.durationMs / pulseDuration);
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
// console.log(pulses);
window.navigator.vibrate(pulses);
}
static supportControllerVibration() {
return Gamepad.prototype.hasOwnProperty('vibrationActuator');
}
static supportDeviceVibration() {
return !!window.navigator.vibrate;
}
static updateGlobalVars() {
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? PREFS.get(Preferences.CONTROLLER_ENABLE_VIBRATION) : false;
window.BX_VIBRATION_INTENSITY = PREFS.get(Preferences.CONTROLLER_VIBRATION_INTENSITY) / 100;
if (!VibrationManager.supportDeviceVibration()) {
window.BX_ENABLE_DEVICE_VIBRATION = false;
return;
}
// Stop vibration
window.navigator.vibrate(0);
const value = PREFS.get(Preferences.CONTROLLER_DEVICE_VIBRATION);
let enabled;
if (value === 'on') {
enabled = true;
} else if (value === 'auto') {
enabled = true;
const gamepads = window.navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
enabled = false;
break;
}
}
} else {
enabled = false;
}
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
}
static initialSetup() {
window.addEventListener('gamepadconnected', VibrationManager.updateGlobalVars);
window.addEventListener('gamepaddisconnected', VibrationManager.updateGlobalVars);
VibrationManager.updateGlobalVars();
const orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = orgCreateDataChannel.apply(this, arguments);
if (dataChannel.label !== 'input') {
return dataChannel;
}
const VIBRATION_DATA_MAP = {
'gamepadIndex': 8,
'leftMotorPercent': 8,
'rightMotorPercent': 8,
'leftTriggerMotorPercent': 8,
'rightTriggerMotorPercent': 8,
'durationMs': 16,
// 'delayMs': 16,
// 'repeat': 8,
};
dataChannel.addEventListener('message', e => {
if (!window.BX_ENABLE_DEVICE_VIBRATION) {
return;
}
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
return;
}
const dataView = new DataView(e.data);
let offset = 0;
let messageType;
if (dataView.byteLength === 13) { // version >= 8
messageType = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
messageType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
if (!(messageType & 128)) { // Vibration
return;
}
const vibrationType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
if (vibrationType !== 0) { // FourMotorRumble
return;
}
const data = {};
for (const key in VIBRATION_DATA_MAP) {
if (VIBRATION_DATA_MAP[key] === 16) {
data[key] = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
data[key] = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
}
VibrationManager.#playDeviceVibration(data);
});
return dataChannel;
};
}
}
class MouseCursorHider {
static #timeout;
static #cursorVisible = true;
@ -3032,7 +3224,7 @@ class StreamBadges {
let totalIn = 0;
let totalOut = 0;
stats.forEach(stat => {
if (stat.type === 'candidate-pair' && stat.state == 'succeeded') {
if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
totalIn += stat.bytesReceived;
totalOut += stat.bytesSent;
}
@ -3552,6 +3744,9 @@ class Preferences {
static get STREAM_DISABLE_FEEDBACK_DIALOG() { return 'stream_disable_feedback_dialog'; }
static get CONTROLLER_ENABLE_SHORTCUTS() { return 'controller_enable_shortcuts'; }
static get CONTROLLER_ENABLE_VIBRATION() { return 'controller_enable_vibration'; }
static get CONTROLLER_DEVICE_VIBRATION() { return 'controller_device_vibration'; }
static get CONTROLLER_VIBRATION_INTENSITY() { return 'controller_vibration_intensity'; }
static get MKB_ENABLED() { return 'mkb_enabled'; }
static get MKB_ABSOLUTE_MOUSE() { return 'mkb_absolute_mouse'; }
@ -3785,6 +3980,26 @@ class Preferences {
'default': false,
},
[Preferences.CONTROLLER_ENABLE_VIBRATION]: {
'default': true,
},
[Preferences.CONTROLLER_DEVICE_VIBRATION]: {
'default': 'off',
'options': {
'on': __('on'),
'auto': __('device-vibration-not-using-gamepad'),
'off': __('off'),
},
},
[Preferences.CONTROLLER_VIBRATION_INTENSITY]: {
'default': 100,
'min': 0,
'max': 100,
'steps': 10,
},
[Preferences.MKB_ENABLED]: {
'default': false,
},
@ -4163,7 +4378,7 @@ class Preferences {
);
if (!options.disabled && !options.hideSlider) {
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value});
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value, 'step': STEPS});
$range.addEventListener('input', e => {
value = parseInt(e.target.value);
@ -4304,30 +4519,21 @@ class Patcher {
return funcStr.replace(funcStr.substring(index - 9, index + 15), 'https://www.xbox.com/play');
},
// Disable trackEvent() function
disableTrackEvent: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.trackEvent=';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, 'this.trackEvent=e=>{},this.uwuwu=');
},
remotePlayKeepAlive: PREFS.get(Preferences.REMOTE_PLAY_ENABLED) && function(funcStr) {
if (!funcStr.includes('onServerDisconnectMessage(e){')) {
return false;
}
funcStr = funcStr.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage (e) {
funcStr = funcStr.replace('onServerDisconnectMessage(e){', `onServerDisconnectMessage(e) {
const msg = JSON.parse(e);
if (msg.reason === 'WarningForBeingIdle') {
try {
this.sendKeepAlive();
return;
} catch (ex) {}
} catch (ex) { console.log(ex); }
}
`);
return funcStr;
},
@ -4341,6 +4547,16 @@ class Patcher {
return funcStr.replace(text, `connectMode:window.BX_REMOTE_PLAY_CONFIG?"xhome-connect":"cloud-connect",remotePlayServerId:(window.BX_REMOTE_PLAY_CONFIG&&window.BX_REMOTE_PLAY_CONFIG.serverId)||''`);
},
// Disable trackEvent() function
disableTrackEvent: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.trackEvent=';
if (!funcStr.includes(text)) {
return false;
}
return funcStr.replace(text, 'this.trackEvent=e=>{},this.uwuwu=');
},
// Block WebRTC stats collector
blockWebRtcStatsCollector: PREFS.get(Preferences.BLOCK_TRACKING) && function(funcStr) {
const text = 'this.intervalMs=0,';
@ -4351,6 +4567,68 @@ class Patcher {
return funcStr.replace(text, 'false,' + text);
},
enableXcloudLogger: ENABLE_XCLOUD_LOGGER && function(funcStr) {
const text = '}log(e,t,n){';
if (!funcStr.includes(text)) {
return false;
}
funcStr = funcStr.replaceAll(text, text + 'console.log(arguments);');
return funcStr;
},
enableConsoleLogging: ENABLE_XCLOUD_LOGGER && function(funcStr) {
const text = 'static isConsoleLoggingAllowed(){';
if (!funcStr.includes(text)) {
return false;
}
funcStr = funcStr.replaceAll(text, text + 'return true;');
return funcStr;
},
// Control controller vibration
playVibration: function(funcStr) {
const text = '}playVibration(e){';
if (!funcStr.includes(text)) {
return false;
}
const newCode = `
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
return void(0);
}
if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
}
`;
VibrationManager.updateGlobalVars();
funcStr = funcStr.replaceAll(text, text + newCode);
return funcStr;
},
// Override website's settings
overrideSettings: function(funcStr) {
const index = funcStr.indexOf(',EnableStreamGate:');
if (index === -1) {
return false;
}
// Find the next "},"
const endIndex = funcStr.indexOf('},', index);
const newCode = `
EnableStreamGate: false,
PwaPrompt: false,
`;
funcStr = funcStr.substring(0, endIndex) + ',' + newCode + funcStr.substring(endIndex);
return funcStr;
},
// Enable Mouse and Keyboard support
enableMouseAndKeyboard: PREFS.get(Preferences.MKB_ENABLED) && function(funcStr) {
if (!funcStr.includes('EnableMouseAndKeyboard:')) {
@ -4366,6 +4644,32 @@ class Patcher {
},
};
static #PATCH_ORDERS = [
[
'disableAiTrack',
'disableTelemetry',
],
['tvLayout'],
['enableXcloudLogger'],
[
// 'enableMouseAndKeyboard',
'overrideSettings',
'remotePlayDirectConnectUrl',
'disableTrackEvent',
'enableConsoleLogging',
'remotePlayKeepAlive',
'blockWebRtcStatsCollector',
],
// Only when playing
['remotePlayConnectMode'],
['playVibration'],
['enableConsoleLogging'],
];
static #patchFunctionBind() {
Function.prototype.nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
@ -4400,39 +4704,82 @@ class Patcher {
};
}
static length() { return Object.keys(Patcher.#PATCHES).length };
static length() { return Patcher.#PATCH_ORDERS.length; };
static patch(item) {
let patchName;
let appliedPatches;
for (let id in item[1]) {
if (Patcher.length() <= 0) {
if (Patcher.#PATCH_ORDERS.length <= 0) {
return;
}
appliedPatches = [];
const func = item[1][id];
const funcStr = func.toString();
let funcStr = func.toString();
// Only check the first patch
if (!patchName) {
patchName = Object.keys(Patcher.#PATCHES)[0];
}
for (let groupIndex = 0; groupIndex < Patcher.#PATCH_ORDERS.length; groupIndex++) {
const group = Patcher.#PATCH_ORDERS[groupIndex];
let modified = false;
const patchedFuncStr = Patcher.#PATCHES[patchName].call(null, funcStr);
if (patchedFuncStr) {
console.log(`[Better xCloud] Applied "${patchName}" patch`);
for (let patchIndex = 0; patchIndex < group.length; patchIndex++) {
const patchName = group[patchIndex];
if (appliedPatches.indexOf(patchName) > -1) {
continue;
}
item[1][id] = eval(patchedFuncStr);
delete Patcher.#PATCHES[patchName];
patchName = null;
const patchedFuncStr = Patcher.#PATCHES[patchName].call(null, funcStr);
if (!patchedFuncStr) {
// Only stop if the first patch is failed
if (patchIndex === 0) {
break;
} else {
continue;
}
}
modified = true;
funcStr = patchedFuncStr;
console.log(`[Better xCloud] Applied "${patchName}" patch`);
appliedPatches.push(patchName);
// Remove patch from group
group.splice(patchIndex, 1);
patchIndex--;
}
// Apply patched functions
if (modified) {
item[1][id] = eval(funcStr);
}
// Remove empty group
if (!group.length) {
Patcher.#PATCH_ORDERS.splice(groupIndex, 1);
groupIndex--;
}
}
}
}
static initialize() {
// Remove disabled patches
for (const patchName in Patcher.#PATCHES) {
if (!Patcher.#PATCHES[patchName]) {
delete Patcher.#PATCHES[patchName];
for (let groupIndex = Patcher.#PATCH_ORDERS.length - 1; groupIndex >= 0; groupIndex--) {
const group = Patcher.#PATCH_ORDERS[groupIndex];
for (let patchIndex = group.length - 1; patchIndex >= 0; patchIndex--) {
const patchName = group[patchIndex];
if (!Patcher.#PATCHES[patchName]) {
// Remove disabled patch
group.splice(patchIndex, 1);
}
}
// Remove empty group
if (!group.length) {
Patcher.#PATCH_ORDERS.splice(groupIndex, 1);
}
}
@ -5002,7 +5349,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
bottom: 20px;
z-index: var(--bx-stream-settings-z-index);
padding: 8px;
width: 220px;
width: 320px;
background: #1a1b1e;
color: #fff;
border-radius: 8px 0 0 8px;
@ -5075,6 +5422,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: var(--bx-monospaced-font);
}
.bx-quick-settings-bar-note {
font-size: 12px;
font-weight: lighter;
font-style: italic;
}
.bx-toast {
position: fixed;
left: 50%;
@ -5689,7 +6042,7 @@ function interceptHttpRequests() {
}
// Start rendering UI
if (!document.getElementById('gamepass-root')) {
if (document.querySelector('div[class^=UnsupportedMarketPage]')) {
setTimeout(watchHeader, 2000);
} else {
watchHeader();
@ -5807,6 +6160,7 @@ function interceptHttpRequests() {
// Enable touch controller
if (TouchController.isEnabled()) {
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.enableVibration = true;
overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10;
}
@ -6504,30 +6858,54 @@ function setupVideoSettingsBar() {
let $stretchInp;
const $wrapper = CE('div', {'class': 'bx-quick-settings-bar'},
CE('h2', {}, __('controller')),
CE('div', {},
CE('label', {}, __('controller-vibration')),
VibrationManager.supportControllerVibration() && PREFS.toElement(Preferences.CONTROLLER_ENABLE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportControllerVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
CE('div', {},
CE('label', {}, __('device-vibration')),
VibrationManager.supportDeviceVibration() && PREFS.toElement(Preferences.CONTROLLER_DEVICE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportDeviceVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) &&
CE('div', {},
CE('label', {}, __('vibration-intensity')),
PREFS.toNumberStepper(Preferences.CONTROLLER_VIBRATION_INTENSITY, VibrationManager.updateGlobalVars, {suffix: '%', ticks: 50}),
),
CE('h2', {}, __('audio')),
CE('div', {},
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, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)})),
}, {suffix: '%', ticks: 100, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)}),
),
CE('h2', {}, __('video')),
CE('div', {'class': 'bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'class': 'bx-quick-settings-bar-note bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'data-type': 'video'},
CE('label', {'for': 'bx-quick-setting-stretch'}, __('ratio')),
PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange)),
CE('label', {}, __('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
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})),
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})),
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}))
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}),
),
);
document.documentElement.appendChild($wrapper);
@ -6704,6 +7082,9 @@ function onStreamStarted($video) {
const allAudioCodecs = {};
let audioCodecId;
const allCandidates = {};
let candidateId;
stats.forEach(stat => {
if (stat.type == 'codec') {
const mimeType = stat.mimeType.split('/');
@ -6721,6 +7102,10 @@ function onStreamStarted($video) {
} else if (stat.kind === 'audio') {
audioCodecId = stat.codecId;
}
} else if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
candidateId = stat.remoteCandidateId;
} else if (stat.type === 'remote-candidate') {
allCandidates[stat.id] = stat.address;
}
});
@ -6748,6 +7133,12 @@ function onStreamStarted($video) {
}
}
// Get server type
if (candidateId) {
console.log(candidateId, allCandidates);
StreamBadges.ipv6 = allCandidates[candidateId].includes(':');
}
if (PREFS.get(Preferences.STATS_SHOW_WHEN_PLAYING)) {
StreamStats.start();
}
@ -6845,20 +7236,12 @@ if (PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)) {
}
}
RTCPeerConnection.prototype.orgAddIceCandidate = RTCPeerConnection.prototype.addIceCandidate;
RTCPeerConnection.prototype.addIceCandidate = function(...args) {
const candidate = args[0].candidate;
if (candidate && candidate.startsWith('a=candidate:1 ')) {
StreamBadges.ipv6 = candidate.substring(20).includes(':');
}
return this.orgAddIceCandidate.apply(this, args);
}
if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.setup();
}
VibrationManager.initialSetup();
const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() {
const peer = new OrgRTCPeerConnection();