@ -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 ] ;
cons t funcStr = func . toString ( ) ;
le t 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 ( patchedF uncStr) ;
delete Patcher . # PATCHES [ patchName ] ;
patchName = null ;
const patchedFuncStr = Patcher . # PATCHES [ patchName ] . call ( null , f uncStr ) ;
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: 2 20px;
width: 3 20px;
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 ( ) ;