// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 5.0.1-beta
// @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx
// @license MIT
// @match https://www.xbox.com/*/play*
// @match https://www.xbox.com/*/auth/msa?*loggedIn*
// @run-at document-start
// @grant none
// @updateURL https://raw.githubusercontent.com/redphx/better-xcloud/typescript/dist/better-xcloud.meta.js
// @downloadURL https://github.com/redphx/better-xcloud/releases/latest/download/better-xcloud.user.js
// ==/UserScript==
'use strict';
// src/enums/user-agent.ts
var UserAgentProfile;
(function(UserAgentProfile2) {
UserAgentProfile2["WINDOWS_EDGE"] = "windows-edge";
UserAgentProfile2["MACOS_SAFARI"] = "macos-safari";
UserAgentProfile2["SMARTTV_GENERIC"] = "smarttv-generic";
UserAgentProfile2["SMARTTV_TIZEN"] = "smarttv-tizen";
UserAgentProfile2["VR_OCULUS"] = "vr-oculus";
UserAgentProfile2["ANDROID_KIWI_V123"] = "android-kiwi-v123";
UserAgentProfile2["DEFAULT"] = "default";
UserAgentProfile2["CUSTOM"] = "custom";
})(UserAgentProfile || (UserAgentProfile = {}));
// src/utils/user-agent.ts
var CHROMIUM_VERSION = "123.0.0.0";
if (!!window.chrome || window.navigator.userAgent.includes("Chrome")) {
const match = window.navigator.userAgent.match(/\s(?:Chrome|Edg)\/([\d\.]+)/);
if (match)
CHROMIUM_VERSION = match[1];
}
class UserAgent {
static STORAGE_KEY = "better_xcloud_user_agent";
static #config;
static #isMobile = null;
static #isSafari = null;
static #isSafariMobile = null;
static #USER_AGENTS = {
[UserAgentProfile.WINDOWS_EDGE]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
[UserAgentProfile.MACOS_SAFARI]: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1",
[UserAgentProfile.SMARTTV_GENERIC]: window.navigator.userAgent + " SmartTV",
[UserAgentProfile.SMARTTV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36`,
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + " OculusBrowser VR",
[UserAgentProfile.ANDROID_KIWI_V123]: "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.6312.118 Mobile Safari/537.36"
};
static init() {
if (UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}"), !UserAgent.#config.profile)
UserAgent.#config.profile = UserAgentProfile.DEFAULT;
if (!UserAgent.#config.custom)
UserAgent.#config.custom = "";
UserAgent.spoof();
}
static updateStorage(profile, custom) {
const clonedConfig = structuredClone(UserAgent.#config);
if (clonedConfig.profile = profile, typeof custom !== "undefined")
clonedConfig.custom = custom;
window.localStorage.setItem(UserAgent.STORAGE_KEY, JSON.stringify(clonedConfig));
}
static getDefault() {
return window.navigator.orgUserAgent || window.navigator.userAgent;
}
static get(profile) {
const defaultUserAgent = window.navigator.userAgent;
switch (profile) {
case UserAgentProfile.DEFAULT:
return defaultUserAgent;
case UserAgentProfile.CUSTOM:
return UserAgent.#config.custom || defaultUserAgent;
default:
return UserAgent.#USER_AGENTS[profile] || defaultUserAgent;
}
}
static isSafari() {
if (this.#isSafari !== null)
return this.#isSafari;
const userAgent = UserAgent.getDefault().toLowerCase();
let result = userAgent.includes("safari") && !userAgent.includes("chrom");
return this.#isSafari = result, result;
}
static isSafariMobile() {
if (this.#isSafariMobile !== null)
return this.#isSafariMobile;
const userAgent = UserAgent.getDefault().toLowerCase(), result = this.isSafari() && userAgent.includes("mobile");
return this.#isSafariMobile = result, result;
}
static isMobile() {
if (this.#isMobile !== null)
return this.#isMobile;
const userAgent = UserAgent.getDefault().toLowerCase(), result = /iphone|ipad|android/.test(userAgent);
return this.#isMobile = result, result;
}
static spoof() {
const profile = UserAgent.#config.profile;
if (profile === UserAgentProfile.DEFAULT)
return;
const newUserAgent = UserAgent.get(profile);
window.navigator.orgUserAgentData = window.navigator.userAgentData, Object.defineProperty(window.navigator, "userAgentData", {}), window.navigator.orgUserAgent = window.navigator.userAgent, Object.defineProperty(window.navigator, "userAgent", {
value: newUserAgent
});
}
}
// src/utils/global.ts
var SCRIPT_VERSION = "5.0.1-beta", AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, STATES = {
isPlaying: !1,
appContext: {},
serverRegions: {},
userAgentHasTouchSupport,
browserHasTouchSupport,
currentStream: {},
remotePlay: {},
pointerServerPort: 9269
};
// src/utils/bx-event.ts
var BxEvent;
(function(BxEvent2) {
BxEvent2["JUMP_BACK_IN_READY"] = "bx-jump-back-in-ready";
BxEvent2["POPSTATE"] = "bx-popstate";
BxEvent2["TITLE_INFO_READY"] = "bx-title-info-ready";
BxEvent2["STREAM_LOADING"] = "bx-stream-loading";
BxEvent2["STREAM_STARTING"] = "bx-stream-starting";
BxEvent2["STREAM_STARTED"] = "bx-stream-started";
BxEvent2["STREAM_PLAYING"] = "bx-stream-playing";
BxEvent2["STREAM_STOPPED"] = "bx-stream-stopped";
BxEvent2["STREAM_ERROR_PAGE"] = "bx-stream-error-page";
BxEvent2["STREAM_WEBRTC_CONNECTED"] = "bx-stream-webrtc-connected";
BxEvent2["STREAM_WEBRTC_DISCONNECTED"] = "bx-stream-webrtc-disconnected";
BxEvent2["STREAM_SESSION_READY"] = "bx-stream-session-ready";
BxEvent2["CUSTOM_TOUCH_LAYOUTS_LOADED"] = "bx-custom-touch-layouts-loaded";
BxEvent2["TOUCH_LAYOUT_MANAGER_READY"] = "bx-touch-layout-manager-ready";
BxEvent2["REMOTE_PLAY_READY"] = "bx-remote-play-ready";
BxEvent2["REMOTE_PLAY_FAILED"] = "bx-remote-play-failed";
BxEvent2["XCLOUD_SERVERS_READY"] = "bx-servers-ready";
BxEvent2["DATA_CHANNEL_CREATED"] = "bx-data-channel-created";
BxEvent2["GAME_BAR_ACTION_ACTIVATED"] = "bx-game-bar-action-activated";
BxEvent2["MICROPHONE_STATE_CHANGED"] = "bx-microphone-state-changed";
BxEvent2["CAPTURE_SCREENSHOT"] = "bx-capture-screenshot";
BxEvent2["GAINNODE_VOLUME_CHANGED"] = "bx-gainnode-volume-changed";
BxEvent2["POINTER_LOCK_REQUESTED"] = "bx-pointer-lock-requested";
BxEvent2["POINTER_LOCK_EXITED"] = "bx-pointer-lock-exited";
BxEvent2["XCLOUD_DIALOG_SHOWN"] = "bx-xcloud-dialog-shown";
BxEvent2["XCLOUD_DIALOG_DISMISSED"] = "bx-xcloud-dialog-dismissed";
BxEvent2["XCLOUD_GUIDE_MENU_SHOWN"] = "bx-xcloud-guide-menu-shown";
BxEvent2["XCLOUD_POLLING_MODE_CHANGED"] = "bx-xcloud-polling-mode-changed";
})(BxEvent || (BxEvent = {}));
var XcloudEvent;
(function(XcloudEvent2) {
XcloudEvent2["MICROPHONE_STATE_CHANGED"] = "microphoneStateChanged";
})(XcloudEvent || (XcloudEvent = {}));
(function(BxEvent) {
function dispatch(target, eventName, data) {
if (!eventName) {
alert("BxEvent.dispatch(): eventName is null");
return;
}
const event = new Event(eventName);
if (data)
for (let key in data)
event[key] = data[key];
target.dispatchEvent(event), AppInterface && AppInterface.onEvent(eventName);
}
BxEvent.dispatch = dispatch;
})(BxEvent || (BxEvent = {}));
window.BxEvent = BxEvent;
// src/utils/bx-flags.ts
/* ADDITIONAL CODE */
var DEFAULT_FLAGS = {
CheckForUpdate: !0,
PreloadRemotePlay: !0,
PreloadUi: !1,
EnableXcloudLogging: !1,
SafariWorkaround: !0,
UseDevTouchLayout: !1,
ForceNativeMkbTitles: [],
FeatureGates: null
}, BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
try {
delete window.BX_FLAGS;
} catch (e) {
}
var NATIVE_FETCH = window.fetch;
// src/enums/stream-player.ts
var StreamPlayerType;
(function(StreamPlayerType2) {
StreamPlayerType2["VIDEO"] = "default";
StreamPlayerType2["WEBGL2"] = "webgl2";
})(StreamPlayerType || (StreamPlayerType = {}));
var StreamVideoProcessing;
(function(StreamVideoProcessing2) {
StreamVideoProcessing2["USM"] = "usm";
StreamVideoProcessing2["CAS"] = "cas";
})(StreamVideoProcessing || (StreamVideoProcessing = {}));
// src/utils/html.ts
var createElement = function(elmName, props = {}, ..._) {
let $elm;
const hasNs = "xmlns" in props;
if (hasNs)
$elm = document.createElementNS(props.xmlns, elmName), delete props.xmlns;
else
$elm = document.createElement(elmName);
for (let key in props) {
if ($elm.hasOwnProperty(key))
continue;
if (hasNs)
$elm.setAttributeNS(null, key, props[key]);
else
$elm.setAttribute(key, props[key]);
}
for (let i = 2, size = arguments.length;i < size; i++) {
const arg = arguments[i], argType = typeof arg;
if (argType === "string" || argType === "number")
$elm.appendChild(document.createTextNode(arg));
else if (arg)
$elm.appendChild(arg);
}
return $elm;
};
function escapeHtml(html) {
const text = document.createTextNode(html), $span = document.createElement("span");
return $span.appendChild(text), $span.innerHTML;
}
var CE = createElement, svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement, createSvgIcon = (icon) => {
return svgParser(icon.toString());
}, ButtonStyle = {};
ButtonStyle[ButtonStyle.PRIMARY = 1] = "bx-primary";
ButtonStyle[ButtonStyle.DANGER = 2] = "bx-danger";
ButtonStyle[ButtonStyle.GHOST = 4] = "bx-ghost";
ButtonStyle[ButtonStyle.FOCUSABLE = 8] = "bx-focusable";
ButtonStyle[ButtonStyle.FULL_WIDTH = 16] = "bx-full-width";
ButtonStyle[ButtonStyle.FULL_HEIGHT = 32] = "bx-full-height";
ButtonStyle[ButtonStyle.TALL = 64] = "bx-tall";
var ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonStyle).length / 2).map((i) => parseInt(i)), createButton = (options) => {
let $btn;
if (options.url)
$btn = CE("a", { class: "bx-button" }), $btn.href = options.url, $btn.target = "_blank";
else
$btn = CE("button", { class: "bx-button", type: "button" });
const style = options.style || 0;
return style && ButtonStyleIndices.forEach((index) => {
style & index && $btn.classList.add(ButtonStyle[index]);
}), options.classes && $btn.classList.add(...options.classes), options.icon && $btn.appendChild(createSvgIcon(options.icon)), options.label && $btn.appendChild(CE("span", {}, options.label)), options.title && $btn.setAttribute("title", options.title), options.disabled && ($btn.disabled = !0), options.onClick && $btn.addEventListener("click", options.onClick), $btn;
}, CTN = document.createTextNode.bind(document);
window.BX_CE = createElement;
// src/utils/bx-logger.ts
var TextColor;
(function(TextColor2) {
TextColor2["INFO"] = "#008746";
TextColor2["WARNING"] = "#c1a404";
TextColor2["ERROR"] = "#c10404";
})(TextColor || (TextColor = {}));
class BxLogger {
static #PREFIX = "[BxC]";
static info(tag, ...args) {
BxLogger.#log(TextColor.INFO, tag, ...args);
}
static warning(tag, ...args) {
BxLogger.#log(TextColor.WARNING, tag, ...args);
}
static error(tag, ...args) {
BxLogger.#log(TextColor.ERROR, tag, ...args);
}
static #log(color, tag, ...args) {
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, "//", ...args);
}
}
window.BxLogger = BxLogger;
// src/utils/translation.ts
var SUPPORTED_LANGUAGES = {
"en-US": "English (United States)",
"ca-CA": "Català",
"de-DE": "Deutsch",
"en-ID": "Bahasa Indonesia",
"es-ES": "español (España)",
"fr-FR": "français",
"it-IT": "italiano",
"ja-JP": "日本語",
"ko-KR": "한국어",
"pl-PL": "polski",
"pt-BR": "português (Brasil)",
"ru-RU": "русский",
"th-TH": "ภาษาไทย",
"tr-TR": "Türkçe",
"uk-UA": "українська",
"vi-VN": "Tiếng Việt",
"zh-CN": "中文(简体)"
}, Texts = {
activate: "Activate",
activated: "Activated",
active: "Active",
advanced: "Advanced",
"amd-fidelity-cas": "AMD FidelityFX CAS",
"android-app-settings": "Android app settings",
apply: "Apply",
"aspect-ratio": "Aspect ratio",
"aspect-ratio-note": "Don't use with native touch games",
audio: "Audio",
auto: "Auto",
"back-to-home": "Back to home",
"back-to-home-confirm": "Do you want to go back to the home page (without disconnecting)?",
"badge-audio": "Audio",
"badge-battery": "Battery",
"badge-in": "In",
"badge-out": "Out",
"badge-playtime": "Playtime",
"badge-server": "Server",
"badge-video": "Video",
"bitrate-audio-maximum": "Maximum audio bitrate",
"bitrate-video-maximum": "Maximum video bitrate",
"bottom-left": "Bottom-left",
"bottom-right": "Bottom-right",
brightness: "Brightness",
"browser-unsupported-feature": "Your browser doesn't support this feature",
"can-stream-xbox-360-games": "Can stream Xbox 360 games",
cancel: "Cancel",
"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
"clarity-boost": "Clarity boost",
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
clear: "Clear",
close: "Close",
"close-app": "Close app",
"combine-audio-video-streams": "Combine audio & video streams",
"combine-audio-video-streams-summary": "May fix the laggy audio problem",
"conditional-formatting": "Conditional formatting text color",
"confirm-delete-preset": "Do you want to delete this preset?",
"confirm-reload-stream": "Do you want to refresh the stream?",
connected: "Connected",
"console-connect": "Connect",
contrast: "Contrast",
controller: "Controller",
"controller-shortcuts": "Controller shortcuts",
"controller-shortcuts-connect-note": "Connect a controller to use this feature",
"controller-shortcuts-xbox-note": "Button to open the Guide menu",
"controller-vibration": "Controller vibration",
copy: "Copy",
custom: "Custom",
"deadzone-counterweight": "Deadzone counterweight",
decrease: "Decrease",
default: "Default",
delete: "Delete",
device: "Device",
"device-unsupported-touch": "Your device doesn't have touch support",
"device-vibration": "Device vibration",
"device-vibration-not-using-gamepad": "On when not using gamepad",
disable: "Disable",
"disable-home-context-menu": "Disable context menu in Home page",
"disable-post-stream-feedback-dialog": "Disable post-stream feedback dialog",
"disable-social-features": "Disable social features",
"disable-xcloud-analytics": "Disable xCloud analytics",
disabled: "Disabled",
disconnected: "Disconnected",
edit: "Edit",
"enable-controller-shortcuts": "Enable controller shortcuts",
"enable-local-co-op-support": "Enable local co-op support",
"enable-local-co-op-support-note": "Only works if the game doesn't require a different profile",
"enable-mic-on-startup": "Enable microphone on game launch",
"enable-mkb": "Emulate controller with Mouse & Keyboard",
"enable-quick-glance-mode": "Enable \"Quick Glance\" mode",
"enable-remote-play-feature": "Enable the \"Remote Play\" feature",
"enable-volume-control": "Enable volume control feature",
enabled: "Enabled",
experimental: "Experimental",
export: "Export",
fast: "Fast",
"fortnite-allow-stw-mode": "Allows playing STW mode on mobile",
"fortnite-force-console-version": "Fortnite: force console version",
"game-bar": "Game Bar",
"getting-consoles-list": "Getting the list of consoles...",
help: "Help",
hide: "Hide",
"hide-idle-cursor": "Hide mouse cursor on idle",
"hide-scrollbar": "Hide web page's scrollbar",
"hide-system-menu-icon": "Hide System menu's icon",
"hide-touch-controller": "Hide touch controller",
"horizontal-scroll-sensitivity": "Horizontal scroll sensitivity",
"horizontal-sensitivity": "Horizontal sensitivity",
ignore: "Ignore",
import: "Import",
increase: "Increase",
"install-android": "Install Better xCloud app for Android",
"keyboard-shortcuts": "Keyboard shortcuts",
language: "Language",
large: "Large",
layout: "Layout",
"left-stick": "Left stick",
"loading-screen": "Loading screen",
"local-co-op": "Local co-op",
"map-mouse-to": "Map mouse to",
"may-not-work-properly": "May not work properly!",
menu: "Menu",
microphone: "Microphone",
"mkb-adjust-ingame-settings": "You may also need to adjust the in-game sensitivity & deadzone settings",
"mkb-click-to-activate": "Click to activate",
"mkb-disclaimer": "Using this feature when playing online could be viewed as cheating",
"mouse-and-keyboard": "Mouse & Keyboard",
"mouse-wheel": "Mouse wheel",
muted: "Muted",
name: "Name",
"native-mkb": "Native Mouse & Keyboard",
new: "New",
"no-consoles-found": "No consoles found",
normal: "Normal",
off: "Off",
on: "On",
"only-supports-some-games": "Only supports some games",
opacity: "Opacity",
other: "Other",
playing: "Playing",
position: "Position",
"powered-off": "Powered off",
"powered-on": "Powered on",
"prefer-ipv6-server": "Prefer IPv6 server",
"preferred-game-language": "Preferred game's language",
preset: "Preset",
"press-esc-to-cancel": "Press Esc to cancel",
"press-key-to-toggle-mkb": [
(e) => `Press ${e.key} to toggle this feature`,
,
(e) => `${e.key}: Funktion an-/ausschalten`,
,
(e) => `Pulsa ${e.key} para alternar esta función`,
(e) => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
(e) => `Premi ${e.key} per attivare questa funzionalità`,
(e) => `${e.key} でこの機能を切替`,
(e) => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
(e) => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
,
(e) => `Нажмите ${e.key} для переключения этой функции`,
,
(e) => `Etkinleştirmek için ${e.key} tuşuna basın`,
(e) => `Натисніть ${e.key} щоб перемкнути цю функцію`,
(e) => `Nhấn ${e.key} để bật/tắt tính năng này`,
,
],
"press-to-bind": "Press a key or do a mouse click to bind...",
"prompt-preset-name": "Preset's name:",
"reduce-animations": "Reduce UI animations",
region: "Region",
"reload-stream": "Reload stream",
"remote-play": "Remote Play",
rename: "Rename",
renderer: "Renderer",
"right-click-to-unbind": "Right-click on a key to unbind it",
"right-stick": "Right stick",
"rocket-always-hide": "Always hide",
"rocket-always-show": "Always show",
"rocket-animation": "Rocket animation",
"rocket-hide-queue": "Hide when queuing",
"safari-failed-message": "Failed to run Better xCloud. Retrying, please wait...",
saturation: "Saturation",
save: "Save",
screen: "Screen",
"screenshot-apply-filters": "Applies video filters to screenshots",
"separate-touch-controller": "Separate Touch controller & Controller #1",
"separate-touch-controller-note": "Touch controller is Player 1, Controller #1 is Player 2",
server: "Server",
settings: "Settings",
"settings-reload": "Reload page to reflect changes",
"settings-reloading": "Reloading...",
sharpness: "Sharpness",
"shortcut-keys": "Shortcut keys",
show: "Show",
"show-game-art": "Show game art",
"show-hide": "Show/hide",
"show-stats-on-startup": "Show stats when starting the game",
"show-touch-controller": "Show touch controller",
"show-wait-time": "Show the estimated wait time",
"simplify-stream-menu": "Simplify Stream's menu",
"skip-splash-video": "Skip Xbox splash video",
slow: "Slow",
small: "Small",
"smart-tv": "Smart TV",
sound: "Sound",
standby: "Standby",
"stat-bitrate": "Bitrate",
"stat-decode-time": "Decode time",
"stat-fps": "FPS",
"stat-frames-lost": "Frames lost",
"stat-packets-lost": "Packets lost",
"stat-ping": "Ping",
stats: "Stats",
"stick-decay-minimum": "Stick decay minimum",
"stick-decay-strength": "Stick decay strength",
stream: "Stream",
"stream-settings": "Stream settings",
"stream-stats": "Stream stats",
stretch: "Stretch",
"support-better-xcloud": "Support Better xCloud",
"swap-buttons": "Swap buttons",
"take-screenshot": "Take screenshot",
"target-resolution": "Target resolution",
"tc-all-games": "All games",
"tc-all-white": "All white",
"tc-auto-off": "Off when controller found",
"tc-availability": "Availability",
"tc-custom-layout-style": "Custom layout's button style",
"tc-default-opacity": "Default opacity",
"tc-muted-colors": "Muted colors",
"tc-standard-layout-style": "Standard layout's button style",
"text-size": "Text size",
toggle: "Toggle",
"top-center": "Top-center",
"top-left": "Top-left",
"top-right": "Top-right",
"touch-control-layout": "Touch control layout",
"touch-control-layout-by": [
(e) => `Touch control layout by ${e.name}`,
(e) => `Format del control tàctil per ${e.name}`,
(e) => `Touch-Steuerungslayout von ${e.name}`,
(e) => `Tata letak Sentuhan layar oleh ${e.name}`,
(e) => `Disposición del control táctil por ${e.nombre}`,
(e) => `Disposition du contrôleur tactile par ${e.name}`,
(e) => `Configurazione dei comandi su schermo creata da ${e.name}`,
(e) => `タッチ操作レイアウト作成者: ${e.name}`,
(e) => `${e.name} 제작, 터치 컨트롤 레이아웃`,
(e) => `Układ sterowania dotykowego stworzony przez ${e.name}`,
(e) => `Disposição de controle por toque feito por ${e.name}`,
(e) => `Сенсорная раскладка по ${e.name}`,
,
(e) => `${e.name} kişisinin dokunmatik kontrolcü tuş şeması`,
(e) => `Розташування сенсорного керування від ${e.name}`,
(e) => `Bố cục điều khiển cảm ứng tạo bởi ${e.name}`,
(e) => `由 ${e.name} 提供的虚拟按键样式`
],
"touch-controller": "Touch controller",
"transparent-background": "Transparent background",
ui: "UI",
"unexpected-behavior": "May cause unexpected behavior",
unknown: "Unknown",
unlimited: "Unlimited",
unmuted: "Unmuted",
"unsharp-masking": "Unsharp masking",
"use-mouse-absolute-position": "Use mouse's absolute position",
"user-agent-profile": "User-Agent profile",
"vertical-scroll-sensitivity": "Vertical scroll sensitivity",
"vertical-sensitivity": "Vertical sensitivity",
"vibration-intensity": "Vibration intensity",
"vibration-status": "Vibration",
video: "Video",
"virtual-controller": "Virtual controller",
"visual-quality": "Visual quality",
"visual-quality-high": "High",
"visual-quality-low": "Low",
"visual-quality-normal": "Normal",
volume: "Volume",
"wait-time-countdown": "Countdown",
"wait-time-estimated": "Estimated finish time",
webgl2: "WebGL2"
};
class Translations {
static #EN_US = "en-US";
static #KEY_LOCALE = "better_xcloud_locale";
static #KEY_TRANSLATIONS = "better_xcloud_translations";
static #enUsIndex = -1;
static #selectedLocaleIndex = -1;
static #selectedLocale = "en-US";
static #supportedLocales = Object.keys(SUPPORTED_LANGUAGES);
static #foreignTranslations = {};
static async init() {
Translations.#enUsIndex = Translations.#supportedLocales.indexOf(Translations.#EN_US), Translations.refreshCurrentLocale(), await Translations.#loadTranslations();
}
static refreshCurrentLocale() {
const supportedLocales = Translations.#supportedLocales;
let locale = localStorage.getItem(Translations.#KEY_LOCALE);
if (!locale) {
if (locale = window.navigator.language || Translations.#EN_US, supportedLocales.indexOf(locale) === -1)
locale = Translations.#EN_US;
localStorage.setItem(Translations.#KEY_LOCALE, locale);
}
Translations.#selectedLocale = locale, Translations.#selectedLocaleIndex = supportedLocales.indexOf(locale);
}
static get(key, values) {
let text = null;
if (Translations.#foreignTranslations && Translations.#selectedLocale !== Translations.#EN_US)
text = Translations.#foreignTranslations[key];
if (!text)
text = Texts[key] || alert(`Missing translation key: ${key}`);
let translation;
if (Array.isArray(text))
return translation = text[Translations.#selectedLocaleIndex] || text[Translations.#enUsIndex], translation(values);
return translation = text, translation;
}
static async#loadTranslations() {
if (Translations.#selectedLocale === Translations.#EN_US)
return;
try {
Translations.#foreignTranslations = JSON.parse(window.localStorage.getItem(Translations.#KEY_TRANSLATIONS));
} catch (e) {
}
if (!Translations.#foreignTranslations)
await this.downloadTranslations(Translations.#selectedLocale);
}
static async updateTranslations(async = !1) {
if (Translations.#selectedLocale === Translations.#EN_US)
return;
if (async)
Translations.downloadTranslationsAsync(Translations.#selectedLocale);
else
await Translations.downloadTranslations(Translations.#selectedLocale);
}
static async downloadTranslations(locale) {
try {
const translations = await (await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`)).json();
return window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations, !0;
} catch (e) {
debugger;
}
return !1;
}
static downloadTranslationsAsync(locale) {
NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`).then((resp) => resp.json()).then((translations) => {
window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations)), Translations.#foreignTranslations = translations;
});
}
}
var t = Translations.get;
Translations.init();
// src/utils/settings.ts
var SettingElementType;
(function(SettingElementType2) {
SettingElementType2["OPTIONS"] = "options";
SettingElementType2["MULTIPLE_OPTIONS"] = "multiple-options";
SettingElementType2["NUMBER"] = "number";
SettingElementType2["NUMBER_STEPPER"] = "number-stepper";
SettingElementType2["CHECKBOX"] = "checkbox";
})(SettingElementType || (SettingElementType = {}));
class SettingElement {
static #renderOptions(key, setting, currentValue, onChange) {
const $control = CE("select", {
title: setting.label,
tabindex: 0
});
for (let value in setting.options) {
const label = setting.options[value], $option = CE("option", { value }, label);
$control.appendChild($option);
}
return $control.value = currentValue, onChange && $control.addEventListener("change", (e) => {
const target = e.target, value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
onChange(e, value);
}), $control.setValue = (value) => {
$control.value = value;
}, $control;
}
static #renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
const $control = CE("select", {
title: setting.label,
multiple: !0,
tabindex: 0
});
if (params && params.size)
$control.setAttribute("size", params.size.toString());
for (let value in setting.multipleOptions) {
const label = setting.multipleOptions[value], $option = CE("option", { value }, label);
$option.selected = currentValue.indexOf(value) > -1, $option.addEventListener("mousedown", function(e) {
e.preventDefault();
const target = e.target;
target.selected = !target.selected;
const $parent = target.parentElement;
$parent.focus(), $parent.dispatchEvent(new Event("change"));
}), $control.appendChild($option);
}
return $control.addEventListener("mousedown", function(e) {
const self = this, orgScrollTop = self.scrollTop;
window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
}), $control.addEventListener("mousemove", (e) => e.preventDefault()), onChange && $control.addEventListener("change", (e) => {
const target = e.target, values = Array.from(target.selectedOptions).map((i) => i.value);
onChange(e, values);
}), $control;
}
static #renderNumber(key, setting, currentValue, onChange) {
const $control = CE("input", { tabindex: 0, type: "number", min: setting.min, max: setting.max });
return $control.value = currentValue, onChange && $control.addEventListener("change", (e) => {
const target = e.target, value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value)));
target.value = value.toString(), onChange(e, value);
}), $control;
}
static #renderCheckbox(key, setting, currentValue, onChange) {
const $control = CE("input", { type: "checkbox", tabindex: 0 });
return $control.checked = currentValue, onChange && $control.addEventListener("change", (e) => {
onChange(e, e.target.checked);
}), $control;
}
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
options = options || {}, options.suffix = options.suffix || "", options.disabled = !!options.disabled, options.hideSlider = !!options.hideSlider;
let $text, $btnDec, $btnInc, $range, controlValue = value;
const { min: MIN, max: MAX } = setting, STEPS = Math.max(setting.steps || 1, 1), renderTextValue = (value2) => {
value2 = parseInt(value2);
let textContent = null;
if (options.customTextValue)
textContent = options.customTextValue(value2);
if (textContent === null)
textContent = value2.toString() + options.suffix;
return textContent;
}, updateButtonsVisibility = () => {
$btnDec.classList.toggle("bx-hidden", controlValue === MIN), $btnInc.classList.toggle("bx-hidden", controlValue === MAX);
}, $wrapper = CE("div", { class: "bx-number-stepper", id: `bx_setting_${key}` }, $btnDec = CE("button", {
"data-type": "dec",
type: "button",
tabindex: -1
}, "-"), $text = CE("span", {}, renderTextValue(value)), $btnInc = CE("button", {
"data-type": "inc",
type: "button",
tabindex: -1
}, "+"));
if (!options.disabled && !options.hideSlider) {
if ($range = CE("input", {
id: `bx_setting_${key}`,
type: "range",
min: MIN,
max: MAX,
value,
step: STEPS,
tabindex: 0
}), $range.addEventListener("input", (e) => {
value = parseInt(e.target.value), controlValue = value, updateButtonsVisibility(), $text.textContent = renderTextValue(value), !e.ignoreOnChange && onChange && onChange(e, value);
}), $wrapper.appendChild($range), options.ticks || options.exactTicks) {
const markersId = `markers-${key}`, $markers = CE("datalist", { id: markersId });
if ($range.setAttribute("list", markersId), options.exactTicks) {
let start = Math.max(Math.floor(MIN / options.exactTicks), 1) * options.exactTicks;
if (start === MIN)
start += options.exactTicks;
for (let i = start;i < MAX; i += options.exactTicks)
$markers.appendChild(CE("option", { value: i }));
} else
for (let i = MIN + options.ticks;i < MAX; i += options.ticks)
$markers.appendChild(CE("option", { value: i }));
$wrapper.appendChild($markers);
}
}
if (options.disabled)
return $btnInc.disabled = !0, $btnInc.classList.add("bx-hidden"), $btnDec.disabled = !0, $btnDec.classList.add("bx-hidden"), $wrapper;
updateButtonsVisibility();
let interval, isHolding = !1;
const onClick = (e) => {
if (isHolding) {
e.preventDefault(), isHolding = !1;
return;
}
const $btn = e.target;
let value2 = parseInt(controlValue);
if ($btn.dataset.type === "dec")
value2 = Math.max(MIN, value2 - STEPS);
else
value2 = Math.min(MAX, value2 + STEPS);
controlValue = value2, updateButtonsVisibility(), $text.textContent = renderTextValue(value2), $range && ($range.value = value2.toString()), isHolding = !1, onChange && onChange(e, value2);
}, onMouseDown = (e) => {
e.preventDefault(), isHolding = !0;
const args = arguments;
interval && clearInterval(interval), interval = window.setInterval(() => {
const event = new Event("click");
event.arguments = args, e.target?.dispatchEvent(event);
}, 200);
}, onMouseUp = (e) => {
e.preventDefault(), interval && clearInterval(interval), isHolding = !1;
}, onContextMenu = (e) => e.preventDefault();
return $wrapper.setValue = (value2) => {
controlValue = parseInt(value2), $text.textContent = renderTextValue(value2), $range && ($range.value = value2);
}, $btnDec.addEventListener("click", onClick), $btnDec.addEventListener("pointerdown", onMouseDown), $btnDec.addEventListener("pointerup", onMouseUp), $btnDec.addEventListener("contextmenu", onContextMenu), $btnInc.addEventListener("click", onClick), $btnInc.addEventListener("pointerdown", onMouseDown), $btnInc.addEventListener("pointerup", onMouseUp), $btnInc.addEventListener("contextmenu", onContextMenu), $wrapper;
}
static #METHOD_MAP = {
[SettingElementType.OPTIONS]: SettingElement.#renderOptions,
[SettingElementType.MULTIPLE_OPTIONS]: SettingElement.#renderMultipleOptions,
[SettingElementType.NUMBER]: SettingElement.#renderNumber,
[SettingElementType.NUMBER_STEPPER]: SettingElement.#renderNumberStepper,
[SettingElementType.CHECKBOX]: SettingElement.#renderCheckbox
};
static render(type, key, setting, currentValue, onChange, options) {
const method = SettingElement.#METHOD_MAP[type], $control = method(...Array.from(arguments).slice(1));
if (type !== SettingElementType.NUMBER_STEPPER)
$control.id = `bx_setting_${key}`;
if (type === SettingElementType.OPTIONS || type === SettingElementType.MULTIPLE_OPTIONS)
$control.name = $control.id;
return $control;
}
}
// src/modules/stream/stream-stats.ts
var StreamStat;
(function(StreamStat2) {
StreamStat2["PING"] = "ping";
StreamStat2["FPS"] = "fps";
StreamStat2["BITRATE"] = "btr";
StreamStat2["DECODE_TIME"] = "dt";
StreamStat2["PACKETS_LOST"] = "pl";
StreamStat2["FRAMES_LOST"] = "fl";
})(StreamStat || (StreamStat = {}));
class StreamStats {
static instance;
static getInstance() {
if (!StreamStats.instance)
StreamStats.instance = new StreamStats;
return StreamStats.instance;
}
#timeoutId;
#updateInterval = 1000;
#$container;
#$fps;
#$ping;
#$dt;
#$pl;
#$fl;
#$br;
#lastVideoStat;
#quickGlanceObserver;
start(glancing = !1) {
if (!this.isHidden() || glancing && this.isGlancing())
return;
if (this.#$container)
this.#$container.classList.remove("bx-gone"), this.#$container.dataset.display = glancing ? "glancing" : "fixed";
this.#timeoutId = window.setTimeout(this.#update.bind(this), this.#updateInterval);
}
stop(glancing = !1) {
if (glancing && !this.isGlancing())
return;
if (this.#timeoutId && clearTimeout(this.#timeoutId), this.#timeoutId = null, this.#lastVideoStat = null, this.#$container)
this.#$container.removeAttribute("data-display"), this.#$container.classList.add("bx-gone");
}
toggle() {
if (this.isGlancing())
this.#$container && (this.#$container.dataset.display = "fixed");
else
this.isHidden() ? this.start() : this.stop();
}
onStoppedPlaying() {
this.stop(), this.quickGlanceStop(), this.hideSettingsUi();
}
isHidden = () => this.#$container && this.#$container.classList.contains("bx-gone");
isGlancing = () => this.#$container && this.#$container.dataset.display === "glancing";
quickGlanceSetup() {
if (this.#quickGlanceObserver)
return;
const $uiContainer = document.querySelector("div[data-testid=ui-container]");
this.#quickGlanceObserver = new MutationObserver((mutationList, observer) => {
for (let record of mutationList)
if (record.attributeName && record.attributeName === "aria-expanded")
if (record.target.ariaExpanded === "true")
this.isHidden() && this.start(!0);
else
this.stop(!0);
}), this.#quickGlanceObserver.observe($uiContainer, {
attributes: !0,
attributeFilter: ["aria-expanded"],
subtree: !0
});
}
quickGlanceStop() {
this.#quickGlanceObserver && this.#quickGlanceObserver.disconnect(), this.#quickGlanceObserver = null;
}
async#update() {
if (this.isHidden() || !STATES.currentStream.peerConnection) {
this.onStoppedPlaying();
return;
}
this.#timeoutId = null;
const startTime = performance.now(), PREF_STATS_CONDITIONAL_FORMATTING = getPref(PrefKey.STATS_CONDITIONAL_FORMATTING), stats = await STATES.currentStream.peerConnection.getStats();
let grade = "";
stats.forEach((stat) => {
if (stat.type === "inbound-rtp" && stat.kind === "video") {
this.#$fps.textContent = stat.framesPerSecond || 0;
const { packetsLost, packetsReceived } = stat, packetsLostPercentage = (packetsLost * 100 / (packetsLost + packetsReceived || 1)).toFixed(2);
this.#$pl.textContent = packetsLostPercentage === "0.00" ? packetsLost : `${packetsLost} (${packetsLostPercentage}%)`;
const { framesDropped, framesReceived } = stat, framesDroppedPercentage = (framesDropped * 100 / (framesDropped + framesReceived || 1)).toFixed(2);
if (this.#$fl.textContent = framesDroppedPercentage === "0.00" ? framesDropped : `${framesDropped} (${framesDroppedPercentage}%)`, !this.#lastVideoStat) {
this.#lastVideoStat = stat;
return;
}
const lastStat = this.#lastVideoStat, timeDiff = stat.timestamp - lastStat.timestamp, bitrate = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
this.#$br.textContent = `${bitrate.toFixed(2)} Mbps`;
const totalDecodeTimeDiff = stat.totalDecodeTime - lastStat.totalDecodeTime, framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded, currentDecodeTime = totalDecodeTimeDiff / framesDecodedDiff * 1000;
if (isNaN(currentDecodeTime))
this.#$dt.textContent = "??ms";
else
this.#$dt.textContent = `${currentDecodeTime.toFixed(2)}ms`;
if (PREF_STATS_CONDITIONAL_FORMATTING)
grade = currentDecodeTime > 12 ? "bad" : currentDecodeTime > 9 ? "ok" : currentDecodeTime > 6 ? "good" : "", this.#$dt.dataset.grade = grade;
this.#lastVideoStat = stat;
} else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") {
const roundTripTime = stat.currentRoundTripTime ? stat.currentRoundTripTime * 1000 : -1;
if (this.#$ping.textContent = roundTripTime === -1 ? "???" : roundTripTime.toString(), PREF_STATS_CONDITIONAL_FORMATTING)
grade = roundTripTime > 100 ? "bad" : roundTripTime > 75 ? "ok" : roundTripTime > 40 ? "good" : "", this.#$ping.dataset.grade = grade;
}
});
const lapsedTime = performance.now() - startTime;
this.#timeoutId = window.setTimeout(this.#update.bind(this), this.#updateInterval - lapsedTime);
}
refreshStyles() {
const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS), PREF_POSITION = getPref(PrefKey.STATS_POSITION), PREF_TRANSPARENT = getPref(PrefKey.STATS_TRANSPARENT), PREF_OPACITY = getPref(PrefKey.STATS_OPACITY), PREF_TEXT_SIZE = getPref(PrefKey.STATS_TEXT_SIZE), $container = this.#$container;
$container.dataset.stats = "[" + PREF_ITEMS.join("][") + "]", $container.dataset.position = PREF_POSITION, $container.dataset.transparent = PREF_TRANSPARENT, $container.style.opacity = PREF_OPACITY + "%", $container.style.fontSize = PREF_TEXT_SIZE;
}
hideSettingsUi() {
if (this.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE))
this.stop();
}
#render() {
if (this.#$container)
return;
const stats = {
[StreamStat.PING]: [t("stat-ping"), this.#$ping = CE("span", {}, "0")],
[StreamStat.FPS]: [t("stat-fps"), this.#$fps = CE("span", {}, "0")],
[StreamStat.BITRATE]: [t("stat-bitrate"), this.#$br = CE("span", {}, "0 Mbps")],
[StreamStat.DECODE_TIME]: [t("stat-decode-time"), this.#$dt = CE("span", {}, "0ms")],
[StreamStat.PACKETS_LOST]: [t("stat-packets-lost"), this.#$pl = CE("span", {}, "0")],
[StreamStat.FRAMES_LOST]: [t("stat-frames-lost"), this.#$fl = CE("span", {}, "0")]
}, $barFragment = document.createDocumentFragment();
let statKey;
for (statKey in stats) {
const $div = CE("div", {
class: `bx-stat-${statKey}`,
title: stats[statKey][0]
}, CE("label", {}, statKey.toUpperCase()), stats[statKey][1]);
$barFragment.appendChild($div);
}
this.#$container = CE("div", { class: "bx-stats-bar bx-gone" }, $barFragment), this.refreshStyles(), document.documentElement.appendChild(this.#$container);
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_LOADING, (e) => {
StreamStats.getInstance().#render();
}), window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE), PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING), streamStats = StreamStats.getInstance();
if (PREF_STATS_SHOW_WHEN_PLAYING)
streamStats.start();
else if (PREF_STATS_QUICK_GLANCE)
streamStats.quickGlanceSetup(), !PREF_STATS_SHOW_WHEN_PLAYING && streamStats.start(!0);
});
}
static refreshStyles() {
StreamStats.getInstance().refreshStyles();
}
}
// src/utils/preferences.ts
var PrefKey;
(function(PrefKey2) {
PrefKey2["LAST_UPDATE_CHECK"] = "version_last_check";
PrefKey2["LATEST_VERSION"] = "version_latest";
PrefKey2["CURRENT_VERSION"] = "version_current";
PrefKey2["BETTER_XCLOUD_LOCALE"] = "bx_locale";
PrefKey2["SERVER_REGION"] = "server_region";
PrefKey2["PREFER_IPV6_SERVER"] = "prefer_ipv6_server";
PrefKey2["STREAM_TARGET_RESOLUTION"] = "stream_target_resolution";
PrefKey2["STREAM_PREFERRED_LOCALE"] = "stream_preferred_locale";
PrefKey2["STREAM_CODEC_PROFILE"] = "stream_codec_profile";
PrefKey2["USER_AGENT_PROFILE"] = "user_agent_profile";
PrefKey2["STREAM_SIMPLIFY_MENU"] = "stream_simplify_menu";
PrefKey2["STREAM_COMBINE_SOURCES"] = "stream_combine_sources";
PrefKey2["STREAM_TOUCH_CONTROLLER"] = "stream_touch_controller";
PrefKey2["STREAM_TOUCH_CONTROLLER_AUTO_OFF"] = "stream_touch_controller_auto_off";
PrefKey2["STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY"] = "stream_touch_controller_default_opacity";
PrefKey2["STREAM_TOUCH_CONTROLLER_STYLE_STANDARD"] = "stream_touch_controller_style_standard";
PrefKey2["STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM"] = "stream_touch_controller_style_custom";
PrefKey2["STREAM_DISABLE_FEEDBACK_DIALOG"] = "stream_disable_feedback_dialog";
PrefKey2["BITRATE_VIDEO_MAX"] = "bitrate_video_max";
PrefKey2["GAME_BAR_POSITION"] = "game_bar_position";
PrefKey2["LOCAL_CO_OP_ENABLED"] = "local_co_op_enabled";
PrefKey2["CONTROLLER_ENABLE_SHORTCUTS"] = "controller_enable_shortcuts";
PrefKey2["CONTROLLER_ENABLE_VIBRATION"] = "controller_enable_vibration";
PrefKey2["CONTROLLER_DEVICE_VIBRATION"] = "controller_device_vibration";
PrefKey2["CONTROLLER_VIBRATION_INTENSITY"] = "controller_vibration_intensity";
PrefKey2["NATIVE_MKB_ENABLED"] = "native_mkb_enabled";
PrefKey2["NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY"] = "native_mkb_scroll_x_sensitivity";
PrefKey2["NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY"] = "native_mkb_scroll_y_sensitivity";
PrefKey2["MKB_ENABLED"] = "mkb_enabled";
PrefKey2["MKB_HIDE_IDLE_CURSOR"] = "mkb_hide_idle_cursor";
PrefKey2["MKB_ABSOLUTE_MOUSE"] = "mkb_absolute_mouse";
PrefKey2["MKB_DEFAULT_PRESET_ID"] = "mkb_default_preset_id";
PrefKey2["SCREENSHOT_APPLY_FILTERS"] = "screenshot_apply_filters";
PrefKey2["BLOCK_TRACKING"] = "block_tracking";
PrefKey2["BLOCK_SOCIAL_FEATURES"] = "block_social_features";
PrefKey2["SKIP_SPLASH_VIDEO"] = "skip_splash_video";
PrefKey2["HIDE_DOTS_ICON"] = "hide_dots_icon";
PrefKey2["REDUCE_ANIMATIONS"] = "reduce_animations";
PrefKey2["UI_LOADING_SCREEN_GAME_ART"] = "ui_loading_screen_game_art";
PrefKey2["UI_LOADING_SCREEN_WAIT_TIME"] = "ui_loading_screen_wait_time";
PrefKey2["UI_LOADING_SCREEN_ROCKET"] = "ui_loading_screen_rocket";
PrefKey2["UI_LAYOUT"] = "ui_layout";
PrefKey2["UI_SCROLLBAR_HIDE"] = "ui_scrollbar_hide";
PrefKey2["UI_HOME_CONTEXT_MENU_DISABLED"] = "ui_home_context_menu_disabled";
PrefKey2["VIDEO_PLAYER_TYPE"] = "video_player_type";
PrefKey2["VIDEO_PROCESSING"] = "video_processing";
PrefKey2["VIDEO_SHARPNESS"] = "video_sharpness";
PrefKey2["VIDEO_RATIO"] = "video_ratio";
PrefKey2["VIDEO_BRIGHTNESS"] = "video_brightness";
PrefKey2["VIDEO_CONTRAST"] = "video_contrast";
PrefKey2["VIDEO_SATURATION"] = "video_saturation";
PrefKey2["AUDIO_MIC_ON_PLAYING"] = "audio_mic_on_playing";
PrefKey2["AUDIO_ENABLE_VOLUME_CONTROL"] = "audio_enable_volume_control";
PrefKey2["AUDIO_VOLUME"] = "audio_volume";
PrefKey2["STATS_ITEMS"] = "stats_items";
PrefKey2["STATS_SHOW_WHEN_PLAYING"] = "stats_show_when_playing";
PrefKey2["STATS_QUICK_GLANCE"] = "stats_quick_glance";
PrefKey2["STATS_POSITION"] = "stats_position";
PrefKey2["STATS_TEXT_SIZE"] = "stats_text_size";
PrefKey2["STATS_TRANSPARENT"] = "stats_transparent";
PrefKey2["STATS_OPACITY"] = "stats_opacity";
PrefKey2["STATS_CONDITIONAL_FORMATTING"] = "stats_conditional_formatting";
PrefKey2["REMOTE_PLAY_ENABLED"] = "xhome_enabled";
PrefKey2["REMOTE_PLAY_RESOLUTION"] = "xhome_resolution";
PrefKey2["GAME_FORTNITE_FORCE_CONSOLE"] = "game_fortnite_force_console";
})(PrefKey || (PrefKey = {}));
class Preferences {
static SETTINGS = {
[PrefKey.LAST_UPDATE_CHECK]: {
default: 0
},
[PrefKey.LATEST_VERSION]: {
default: ""
},
[PrefKey.CURRENT_VERSION]: {
default: ""
},
[PrefKey.BETTER_XCLOUD_LOCALE]: {
label: t("language"),
default: localStorage.getItem("better_xcloud_locale") || "en-US",
options: SUPPORTED_LANGUAGES
},
[PrefKey.SERVER_REGION]: {
label: t("region"),
default: "default"
},
[PrefKey.STREAM_PREFERRED_LOCALE]: {
label: t("preferred-game-language"),
default: "default",
options: {
default: t("default"),
"ar-SA": "العربية",
"cs-CZ": "čeština",
"da-DK": "dansk",
"de-DE": "Deutsch",
"el-GR": "Ελληνικά",
"en-GB": "English (United Kingdom)",
"en-US": "English (United States)",
"es-ES": "español (España)",
"es-MX": "español (Latinoamérica)",
"fi-FI": "suomi",
"fr-FR": "français",
"he-IL": "עברית",
"hu-HU": "magyar",
"it-IT": "italiano",
"ja-JP": "日本語",
"ko-KR": "한국어",
"nb-NO": "norsk bokmål",
"nl-NL": "Nederlands",
"pl-PL": "polski",
"pt-BR": "português (Brasil)",
"pt-PT": "português (Portugal)",
"ru-RU": "русский",
"sk-SK": "slovenčina",
"sv-SE": "svenska",
"tr-TR": "Türkçe",
"zh-CN": "中文(简体)",
"zh-TW": "中文 (繁體)"
}
},
[PrefKey.STREAM_TARGET_RESOLUTION]: {
label: t("target-resolution"),
default: "auto",
options: {
auto: t("default"),
"1080p": "1080p",
"720p": "720p"
}
},
[PrefKey.STREAM_CODEC_PROFILE]: {
label: t("visual-quality"),
default: "default",
options: (() => {
const options = {
default: t("default")
};
if (!("getCapabilities" in RTCRtpReceiver) || typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype))
return options;
let hasLowCodec = !1, hasNormalCodec = !1, hasHighCodec = !1;
const codecs = RTCRtpReceiver.getCapabilities("video").codecs;
for (let codec of codecs) {
if (codec.mimeType.toLowerCase() !== "video/h264" || !codec.sdpFmtpLine)
continue;
const fmtp = codec.sdpFmtpLine.toLowerCase();
if (!hasHighCodec && fmtp.includes("profile-level-id=4d"))
hasHighCodec = !0;
else if (!hasNormalCodec && fmtp.includes("profile-level-id=42e"))
hasNormalCodec = !0;
else if (!hasLowCodec && fmtp.includes("profile-level-id=420"))
hasLowCodec = !0;
}
if (hasHighCodec)
if (!hasLowCodec && !hasNormalCodec)
options.default = `${t("visual-quality-high")} (${t("default")})`;
else
options.high = t("visual-quality-high");
if (hasNormalCodec)
if (!hasLowCodec && !hasHighCodec)
options.default = `${t("visual-quality-normal")} (${t("default")})`;
else
options.normal = t("visual-quality-normal");
if (hasLowCodec)
if (!hasNormalCodec && !hasHighCodec)
options.default = `${t("visual-quality-low")} (${t("default")})`;
else
options.low = t("visual-quality-low");
return options;
})(),
ready: (setting) => {
const options = setting.options;
if (Object.keys(options).length <= 1)
setting.unsupported = !0, setting.note = "⚠️ " + t("browser-unsupported-feature");
}
},
[PrefKey.PREFER_IPV6_SERVER]: {
label: t("prefer-ipv6-server"),
default: !1
},
[PrefKey.SCREENSHOT_APPLY_FILTERS]: {
label: t("screenshot-apply-filters"),
default: !1
},
[PrefKey.SKIP_SPLASH_VIDEO]: {
label: t("skip-splash-video"),
default: !1
},
[PrefKey.HIDE_DOTS_ICON]: {
label: t("hide-system-menu-icon"),
default: !1
},
[PrefKey.STREAM_COMBINE_SOURCES]: {
label: t("combine-audio-video-streams"),
default: !1,
experimental: !0,
note: t("combine-audio-video-streams-summary")
},
[PrefKey.STREAM_TOUCH_CONTROLLER]: {
label: t("tc-availability"),
default: "all",
options: {
default: t("default"),
all: t("tc-all-games"),
off: t("off")
},
unsupported: !STATES.userAgentHasTouchSupport,
ready: (setting) => {
if (setting.unsupported)
setting.default = "default";
}
},
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
label: t("tc-auto-off"),
default: !1,
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY]: {
type: SettingElementType.NUMBER_STEPPER,
label: t("tc-default-opacity"),
default: 100,
min: 10,
max: 100,
steps: 10,
params: {
suffix: "%",
ticks: 10,
hideSlider: !0
},
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD]: {
label: t("tc-standard-layout-style"),
default: "default",
options: {
default: t("default"),
white: t("tc-all-white"),
muted: t("tc-muted-colors")
},
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM]: {
label: t("tc-custom-layout-style"),
default: "default",
options: {
default: t("default"),
muted: t("tc-muted-colors")
},
unsupported: !STATES.userAgentHasTouchSupport
},
[PrefKey.STREAM_SIMPLIFY_MENU]: {
label: t("simplify-stream-menu"),
default: !1
},
[PrefKey.MKB_HIDE_IDLE_CURSOR]: {
label: t("hide-idle-cursor"),
default: !1
},
[PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG]: {
label: t("disable-post-stream-feedback-dialog"),
default: !1
},
[PrefKey.BITRATE_VIDEO_MAX]: {
type: SettingElementType.NUMBER_STEPPER,
label: t("bitrate-video-maximum"),
note: "⚠️ " + t("unexpected-behavior"),
default: 0,
min: 0,
max: 14336000,
steps: 102400,
params: {
exactTicks: 5120000,
customTextValue: (value) => {
if (value = parseInt(value), value === 0)
return t("unlimited");
else
return (value / 1024000).toFixed(1) + " Mb/s";
}
},
migrate: function(savedPrefs, value) {
try {
if (value = parseInt(value), value !== 0 && value < 100)
value *= 1024000;
this.set(PrefKey.BITRATE_VIDEO_MAX, value, !0), savedPrefs[PrefKey.BITRATE_VIDEO_MAX] = value;
} catch (e) {
}
}
},
[PrefKey.GAME_BAR_POSITION]: {
label: t("position"),
default: "bottom-left",
options: {
"bottom-left": t("bottom-left"),
"bottom-right": t("bottom-right"),
off: t("off")
}
},
[PrefKey.LOCAL_CO_OP_ENABLED]: {
label: t("enable-local-co-op-support"),
default: !1,
note: CE("a", {
href: "https://github.com/redphx/better-xcloud/discussions/275",
target: "_blank"
}, t("enable-local-co-op-support-note"))
},
[PrefKey.CONTROLLER_ENABLE_SHORTCUTS]: {
default: !1
},
[PrefKey.CONTROLLER_ENABLE_VIBRATION]: {
label: t("controller-vibration"),
default: !0
},
[PrefKey.CONTROLLER_DEVICE_VIBRATION]: {
label: t("device-vibration"),
default: "off",
options: {
on: t("on"),
auto: t("device-vibration-not-using-gamepad"),
off: t("off")
}
},
[PrefKey.CONTROLLER_VIBRATION_INTENSITY]: {
label: t("vibration-intensity"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 0,
max: 100,
steps: 10,
params: {
suffix: "%",
ticks: 10
}
},
[PrefKey.MKB_ENABLED]: {
label: t("enable-mkb"),
default: !1,
unsupported: (() => {
const userAgent2 = (window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase();
return !AppInterface && userAgent2.match(/(android|iphone|ipad)/) ? t("browser-unsupported-feature") : !1;
})(),
ready: (setting) => {
let note, url;
if (setting.unsupported)
note = t("browser-unsupported-feature"), url = "https://github.com/redphx/better-xcloud/issues/206#issuecomment-1920475657";
else
note = t("mkb-disclaimer"), url = "https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer";
setting.note = CE("a", {
href: url,
target: "_blank"
}, "⚠️ " + note);
}
},
[PrefKey.NATIVE_MKB_ENABLED]: {
label: t("native-mkb"),
default: "default",
options: {
default: t("default"),
on: t("on"),
off: t("off")
},
ready: (setting) => {
if (AppInterface)
;
else if (UserAgent.isMobile())
setting.unsupported = !0, setting.default = "off", delete setting.options.default, delete setting.options.on;
else
delete setting.options.on;
}
},
[PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY]: {
label: t("horizontal-scroll-sensitivity"),
type: SettingElementType.NUMBER_STEPPER,
default: 0,
min: 0,
max: 1e4,
steps: 10,
params: {
exactTicks: 2000,
customTextValue: (value) => {
if (!value)
return t("default");
return (value / 100).toFixed(1) + "x";
}
}
},
[PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY]: {
label: t("vertical-scroll-sensitivity"),
type: SettingElementType.NUMBER_STEPPER,
default: 0,
min: 0,
max: 1e4,
steps: 10,
params: {
exactTicks: 2000,
customTextValue: (value) => {
if (!value)
return t("default");
return (value / 100).toFixed(1) + "x";
}
}
},
[PrefKey.MKB_DEFAULT_PRESET_ID]: {
default: 0
},
[PrefKey.MKB_ABSOLUTE_MOUSE]: {
default: !1
},
[PrefKey.REDUCE_ANIMATIONS]: {
label: t("reduce-animations"),
default: !1
},
[PrefKey.UI_LOADING_SCREEN_GAME_ART]: {
label: t("show-game-art"),
default: !0
},
[PrefKey.UI_LOADING_SCREEN_WAIT_TIME]: {
label: t("show-wait-time"),
default: !0
},
[PrefKey.UI_LOADING_SCREEN_ROCKET]: {
label: t("rocket-animation"),
default: "show",
options: {
show: t("rocket-always-show"),
"hide-queue": t("rocket-hide-queue"),
hide: t("rocket-always-hide")
}
},
[PrefKey.UI_LAYOUT]: {
label: t("layout"),
default: "default",
options: {
default: t("default"),
normal: t("normal"),
tv: t("smart-tv")
}
},
[PrefKey.UI_SCROLLBAR_HIDE]: {
label: t("hide-scrollbar"),
default: !1
},
[PrefKey.UI_HOME_CONTEXT_MENU_DISABLED]: {
label: t("disable-home-context-menu"),
default: STATES.browserHasTouchSupport
},
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
label: t("disable-social-features"),
default: !1
},
[PrefKey.BLOCK_TRACKING]: {
label: t("disable-xcloud-analytics"),
default: !1
},
[PrefKey.USER_AGENT_PROFILE]: {
label: t("user-agent-profile"),
note: "⚠️ " + t("unexpected-behavior"),
default: "default",
options: {
[UserAgentProfile.DEFAULT]: t("default"),
[UserAgentProfile.WINDOWS_EDGE]: "Edge + Windows",
[UserAgentProfile.MACOS_SAFARI]: "Safari + macOS",
[UserAgentProfile.SMARTTV_GENERIC]: "Smart TV",
[UserAgentProfile.SMARTTV_TIZEN]: "Samsung Smart TV",
[UserAgentProfile.VR_OCULUS]: "Meta Quest VR",
[UserAgentProfile.ANDROID_KIWI_V123]: "Kiwi Browser v123",
[UserAgentProfile.CUSTOM]: t("custom")
}
},
[PrefKey.VIDEO_PLAYER_TYPE]: {
label: t("renderer"),
default: "default",
options: {
[StreamPlayerType.VIDEO]: t("default"),
[StreamPlayerType.WEBGL2]: t("webgl2")
}
},
[PrefKey.VIDEO_PROCESSING]: {
label: t("clarity-boost"),
default: StreamVideoProcessing.USM,
options: {
[StreamVideoProcessing.USM]: t("unsharp-masking"),
[StreamVideoProcessing.CAS]: t("amd-fidelity-cas")
}
},
[PrefKey.VIDEO_SHARPNESS]: {
label: t("sharpness"),
type: SettingElementType.NUMBER_STEPPER,
default: 0,
min: 0,
max: 10,
params: {
hideSlider: !0,
customTextValue: (value) => {
return value = parseInt(value), value === 0 ? t("off") : value.toString();
}
}
},
[PrefKey.VIDEO_RATIO]: {
label: t("aspect-ratio"),
note: t("aspect-ratio-note"),
default: "16:9",
options: {
"16:9": "16:9",
"18:9": "18:9",
"21:9": "21:9",
"16:10": "16:10",
"4:3": "4:3",
fill: t("stretch")
}
},
[PrefKey.VIDEO_SATURATION]: {
label: t("saturation"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 50,
max: 150,
params: {
suffix: "%",
ticks: 25
}
},
[PrefKey.VIDEO_CONTRAST]: {
label: t("contrast"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 50,
max: 150,
params: {
suffix: "%",
ticks: 25
}
},
[PrefKey.VIDEO_BRIGHTNESS]: {
label: t("brightness"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 50,
max: 150,
params: {
suffix: "%",
ticks: 25
}
},
[PrefKey.AUDIO_MIC_ON_PLAYING]: {
label: t("enable-mic-on-startup"),
default: !1
},
[PrefKey.AUDIO_ENABLE_VOLUME_CONTROL]: {
label: t("enable-volume-control"),
default: !1
},
[PrefKey.AUDIO_VOLUME]: {
label: t("volume"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 0,
max: 600,
params: {
suffix: "%",
ticks: 100
}
},
[PrefKey.STATS_ITEMS]: {
label: t("stats"),
default: [StreamStat.PING, StreamStat.FPS, StreamStat.BITRATE, StreamStat.DECODE_TIME, StreamStat.PACKETS_LOST, StreamStat.FRAMES_LOST],
multipleOptions: {
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t("stat-ping")}`,
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t("stat-fps")}`,
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t("stat-bitrate")}`,
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t("stat-decode-time")}`,
[StreamStat.PACKETS_LOST]: `${StreamStat.PACKETS_LOST.toUpperCase()}: ${t("stat-packets-lost")}`,
[StreamStat.FRAMES_LOST]: `${StreamStat.FRAMES_LOST.toUpperCase()}: ${t("stat-frames-lost")}`
},
params: {
size: 6
}
},
[PrefKey.STATS_SHOW_WHEN_PLAYING]: {
label: t("show-stats-on-startup"),
default: !1
},
[PrefKey.STATS_QUICK_GLANCE]: {
label: "👀 " + t("enable-quick-glance-mode"),
default: !0
},
[PrefKey.STATS_POSITION]: {
label: t("position"),
default: "top-right",
options: {
"top-left": t("top-left"),
"top-center": t("top-center"),
"top-right": t("top-right")
}
},
[PrefKey.STATS_TEXT_SIZE]: {
label: t("text-size"),
default: "0.9rem",
options: {
"0.9rem": t("small"),
"1.0rem": t("normal"),
"1.1rem": t("large")
}
},
[PrefKey.STATS_TRANSPARENT]: {
label: t("transparent-background"),
default: !1
},
[PrefKey.STATS_OPACITY]: {
label: t("opacity"),
type: SettingElementType.NUMBER_STEPPER,
default: 80,
min: 50,
max: 100,
params: {
suffix: "%",
ticks: 10
}
},
[PrefKey.STATS_CONDITIONAL_FORMATTING]: {
label: t("conditional-formatting"),
default: !1
},
[PrefKey.REMOTE_PLAY_ENABLED]: {
label: t("enable-remote-play-feature"),
default: !1
},
[PrefKey.REMOTE_PLAY_RESOLUTION]: {
default: "1080p",
options: {
"1080p": "1080p",
"720p": "720p"
}
},
[PrefKey.GAME_FORTNITE_FORCE_CONSOLE]: {
label: "🎮 " + t("fortnite-force-console-version"),
default: !1,
note: t("fortnite-allow-stw-mode")
}
};
#storage = localStorage;
#key = "better_xcloud";
#prefs = {};
constructor() {
let savedPrefsStr = this.#storage.getItem(this.#key);
if (savedPrefsStr == null)
savedPrefsStr = "{}";
const savedPrefs = JSON.parse(savedPrefsStr);
for (let settingId in Preferences.SETTINGS) {
const setting = Preferences.SETTINGS[settingId];
if (setting.migrate && settingId in savedPrefs)
setting.migrate.call(this, savedPrefs, savedPrefs[settingId]);
setting.ready && setting.ready.call(this, setting);
}
for (let settingId in Preferences.SETTINGS) {
const setting = Preferences.SETTINGS[settingId];
if (!setting) {
alert(`Undefined setting key: ${settingId}`), console.log("Undefined setting key");
continue;
}
if (setting.migrate)
continue;
if (settingId in savedPrefs)
this.#prefs[settingId] = this.#validateValue(settingId, savedPrefs[settingId]);
else
this.#prefs[settingId] = setting.default;
}
}
#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 ("multipleOptions" in config) {
if (value.length) {
const validOptions = Object.keys(config.multipleOptions);
value.forEach((item2, idx) => {
validOptions.indexOf(item2) === -1 && value.splice(idx, 1);
});
}
if (!value.length)
value = config.default;
}
return value;
}
get(key) {
if (typeof key === "undefined") {
debugger;
return;
}
if (Preferences.SETTINGS[key].unsupported)
return Preferences.SETTINGS[key].default;
if (!(key in this.#prefs))
this.#prefs[key] = this.#validateValue(key, null);
return this.#prefs[key];
}
set(key, value, skipSave) {
return value = this.#validateValue(key, value), this.#prefs[key] = value, !skipSave && this.#updateStorage(), value;
}
#updateStorage() {
this.#storage.setItem(this.#key, JSON.stringify(this.#prefs));
}
toElement(key, onChange, overrideParams = {}) {
const setting = Preferences.SETTINGS[key];
let currentValue = this.get(key), type;
if ("type" in setting)
type = setting.type;
else if ("options" in setting)
type = SettingElementType.OPTIONS;
else if ("multipleOptions" in setting)
type = SettingElementType.MULTIPLE_OPTIONS;
else if (typeof setting.default === "number")
type = SettingElementType.NUMBER;
else
type = SettingElementType.CHECKBOX;
const params = Object.assign(overrideParams, setting.params || {});
if (params.disabled)
currentValue = Preferences.SETTINGS[key].default;
return SettingElement.render(type, key, setting, currentValue, (e, value) => {
this.set(key, value), onChange && onChange(e, value);
}, params);
}
toNumberStepper(key, onChange, options = {}) {
return SettingElement.render(SettingElementType.NUMBER_STEPPER, key, Preferences.SETTINGS[key], this.get(key), (e, value) => {
this.set(key, value), onChange && onChange(e, value);
}, options);
}
}
var prefs = new Preferences, getPref = prefs.get.bind(prefs), setPref = prefs.set.bind(prefs), toPrefElement = prefs.toElement.bind(prefs);
// src/utils/screenshot.ts
class Screenshot {
static #$canvas;
static #canvasContext;
static setup() {
if (Screenshot.#$canvas)
return;
Screenshot.#$canvas = CE("canvas", { class: "bx-gone" }), Screenshot.#canvasContext = Screenshot.#$canvas.getContext("2d", {
alpha: !1,
willReadFrequently: !1
});
}
static updateCanvasSize(width, height) {
const $canvas = Screenshot.#$canvas;
if ($canvas)
$canvas.width = width, $canvas.height = height;
}
static updateCanvasFilters(filters) {
Screenshot.#canvasContext.filter = filters;
}
static #onAnimationEnd(e) {
e.target.classList.remove("bx-taking-screenshot");
}
static takeScreenshot(callback) {
const currentStream = STATES.currentStream, streamPlayer = currentStream.streamPlayer, $canvas = Screenshot.#$canvas;
if (!streamPlayer || !$canvas)
return;
let $player;
if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS))
$player = streamPlayer.getPlayerElement();
else
$player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
if (!$player || !$player.isConnected)
return;
$player.parentElement.addEventListener("animationend", this.#onAnimationEnd), $player.parentElement.classList.add("bx-taking-screenshot");
const canvasContext = Screenshot.#canvasContext;
if ($player instanceof HTMLCanvasElement)
streamPlayer.getWebGL2Player().drawFrame();
if (canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height), AppInterface) {
const data = $canvas.toDataURL("image/png").split(";base64,")[1];
AppInterface.saveScreenshot(currentStream.titleId, data), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
return;
}
$canvas && $canvas.toBlob((blob) => {
const now = +new Date, $anchor = CE("a", {
download: `${currentStream.titleId}-${now}.png`,
href: URL.createObjectURL(blob)
});
$anchor.click(), URL.revokeObjectURL($anchor.href), canvasContext.clearRect(0, 0, $canvas.width, $canvas.height), callback && callback();
}, "image/png");
}
}
// src/enums/prompt-font.ts
var PrompFont;
(function(PrompFont2) {
PrompFont2["A"] = "⇓";
PrompFont2["B"] = "⇒";
PrompFont2["X"] = "⇐";
PrompFont2["Y"] = "⇑";
PrompFont2["LB"] = "↘";
PrompFont2["RB"] = "↙";
PrompFont2["LT"] = "↖";
PrompFont2["RT"] = "↗";
PrompFont2["SELECT"] = "⇺";
PrompFont2["START"] = "⇻";
PrompFont2["HOME"] = "";
PrompFont2["UP"] = "≻";
PrompFont2["DOWN"] = "≽";
PrompFont2["LEFT"] = "≺";
PrompFont2["RIGHT"] = "≼";
PrompFont2["L3"] = "↺";
PrompFont2["LS_UP"] = "↾";
PrompFont2["LS_DOWN"] = "⇂";
PrompFont2["LS_LEFT"] = "↼";
PrompFont2["LS_RIGHT"] = "⇀";
PrompFont2["R3"] = "↻";
PrompFont2["RS_UP"] = "↿";
PrompFont2["RS_DOWN"] = "⇃";
PrompFont2["RS_LEFT"] = "↽";
PrompFont2["RS_RIGHT"] = "⇁";
})(PrompFont || (PrompFont = {}));
// src/enums/mkb.ts
var GamepadKey;
(function(GamepadKey2) {
GamepadKey2[GamepadKey2["A"] = 0] = "A";
GamepadKey2[GamepadKey2["B"] = 1] = "B";
GamepadKey2[GamepadKey2["X"] = 2] = "X";
GamepadKey2[GamepadKey2["Y"] = 3] = "Y";
GamepadKey2[GamepadKey2["LB"] = 4] = "LB";
GamepadKey2[GamepadKey2["RB"] = 5] = "RB";
GamepadKey2[GamepadKey2["LT"] = 6] = "LT";
GamepadKey2[GamepadKey2["RT"] = 7] = "RT";
GamepadKey2[GamepadKey2["SELECT"] = 8] = "SELECT";
GamepadKey2[GamepadKey2["START"] = 9] = "START";
GamepadKey2[GamepadKey2["L3"] = 10] = "L3";
GamepadKey2[GamepadKey2["R3"] = 11] = "R3";
GamepadKey2[GamepadKey2["UP"] = 12] = "UP";
GamepadKey2[GamepadKey2["DOWN"] = 13] = "DOWN";
GamepadKey2[GamepadKey2["LEFT"] = 14] = "LEFT";
GamepadKey2[GamepadKey2["RIGHT"] = 15] = "RIGHT";
GamepadKey2[GamepadKey2["HOME"] = 16] = "HOME";
GamepadKey2[GamepadKey2["SHARE"] = 17] = "SHARE";
GamepadKey2[GamepadKey2["LS_UP"] = 100] = "LS_UP";
GamepadKey2[GamepadKey2["LS_DOWN"] = 101] = "LS_DOWN";
GamepadKey2[GamepadKey2["LS_LEFT"] = 102] = "LS_LEFT";
GamepadKey2[GamepadKey2["LS_RIGHT"] = 103] = "LS_RIGHT";
GamepadKey2[GamepadKey2["RS_UP"] = 200] = "RS_UP";
GamepadKey2[GamepadKey2["RS_DOWN"] = 201] = "RS_DOWN";
GamepadKey2[GamepadKey2["RS_LEFT"] = 202] = "RS_LEFT";
GamepadKey2[GamepadKey2["RS_RIGHT"] = 203] = "RS_RIGHT";
})(GamepadKey || (GamepadKey = {}));
var GamepadKeyName = {
[GamepadKey.A]: ["A", PrompFont.A],
[GamepadKey.B]: ["B", PrompFont.B],
[GamepadKey.X]: ["X", PrompFont.X],
[GamepadKey.Y]: ["Y", PrompFont.Y],
[GamepadKey.LB]: ["LB", PrompFont.LB],
[GamepadKey.RB]: ["RB", PrompFont.RB],
[GamepadKey.LT]: ["LT", PrompFont.LT],
[GamepadKey.RT]: ["RT", PrompFont.RT],
[GamepadKey.SELECT]: ["Select", PrompFont.SELECT],
[GamepadKey.START]: ["Start", PrompFont.START],
[GamepadKey.HOME]: ["Home", PrompFont.HOME],
[GamepadKey.UP]: ["D-Pad Up", PrompFont.UP],
[GamepadKey.DOWN]: ["D-Pad Down", PrompFont.DOWN],
[GamepadKey.LEFT]: ["D-Pad Left", PrompFont.LEFT],
[GamepadKey.RIGHT]: ["D-Pad Right", PrompFont.RIGHT],
[GamepadKey.L3]: ["L3", PrompFont.L3],
[GamepadKey.LS_UP]: ["Left Stick Up", PrompFont.LS_UP],
[GamepadKey.LS_DOWN]: ["Left Stick Down", PrompFont.LS_DOWN],
[GamepadKey.LS_LEFT]: ["Left Stick Left", PrompFont.LS_LEFT],
[GamepadKey.LS_RIGHT]: ["Left Stick Right", PrompFont.LS_RIGHT],
[GamepadKey.R3]: ["R3", PrompFont.R3],
[GamepadKey.RS_UP]: ["Right Stick Up", PrompFont.RS_UP],
[GamepadKey.RS_DOWN]: ["Right Stick Down", PrompFont.RS_DOWN],
[GamepadKey.RS_LEFT]: ["Right Stick Left", PrompFont.RS_LEFT],
[GamepadKey.RS_RIGHT]: ["Right Stick Right", PrompFont.RS_RIGHT]
}, GamepadStick;
(function(GamepadStick2) {
GamepadStick2[GamepadStick2["LEFT"] = 0] = "LEFT";
GamepadStick2[GamepadStick2["RIGHT"] = 1] = "RIGHT";
})(GamepadStick || (GamepadStick = {}));
var MouseButtonCode;
(function(MouseButtonCode2) {
MouseButtonCode2["LEFT_CLICK"] = "Mouse0";
MouseButtonCode2["RIGHT_CLICK"] = "Mouse2";
MouseButtonCode2["MIDDLE_CLICK"] = "Mouse1";
})(MouseButtonCode || (MouseButtonCode = {}));
var MouseMapTo;
(function(MouseMapTo2) {
MouseMapTo2[MouseMapTo2["OFF"] = 0] = "OFF";
MouseMapTo2[MouseMapTo2["LS"] = 1] = "LS";
MouseMapTo2[MouseMapTo2["RS"] = 2] = "RS";
})(MouseMapTo || (MouseMapTo = {}));
var WheelCode;
(function(WheelCode2) {
WheelCode2["SCROLL_UP"] = "ScrollUp";
WheelCode2["SCROLL_DOWN"] = "ScrollDown";
WheelCode2["SCROLL_LEFT"] = "ScrollLeft";
WheelCode2["SCROLL_RIGHT"] = "ScrollRight";
})(WheelCode || (WheelCode = {}));
var MkbPresetKey;
(function(MkbPresetKey2) {
MkbPresetKey2["MOUSE_MAP_TO"] = "map_to";
MkbPresetKey2["MOUSE_SENSITIVITY_X"] = "sensitivity_x";
MkbPresetKey2["MOUSE_SENSITIVITY_Y"] = "sensitivity_y";
MkbPresetKey2["MOUSE_DEADZONE_COUNTERWEIGHT"] = "deadzone_counterweight";
})(MkbPresetKey || (MkbPresetKey = {}));
// src/modules/mkb/mkb-preset.ts
class MkbPreset {
static MOUSE_SETTINGS = {
[MkbPresetKey.MOUSE_MAP_TO]: {
label: t("map-mouse-to"),
type: SettingElementType.OPTIONS,
default: MouseMapTo[MouseMapTo.RS],
options: {
[MouseMapTo[MouseMapTo.RS]]: t("right-stick"),
[MouseMapTo[MouseMapTo.LS]]: t("left-stick"),
[MouseMapTo[MouseMapTo.OFF]]: t("off")
}
},
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: {
label: t("horizontal-sensitivity"),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 300,
params: {
suffix: "%",
exactTicks: 50
}
},
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
label: t("vertical-sensitivity"),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 300,
params: {
suffix: "%",
exactTicks: 50
}
},
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
label: t("deadzone-counterweight"),
type: SettingElementType.NUMBER_STEPPER,
default: 20,
min: 1,
max: 50,
params: {
suffix: "%",
exactTicks: 10
}
}
};
static DEFAULT_PRESET = {
mapping: {
[GamepadKey.UP]: ["ArrowUp"],
[GamepadKey.DOWN]: ["ArrowDown"],
[GamepadKey.LEFT]: ["ArrowLeft"],
[GamepadKey.RIGHT]: ["ArrowRight"],
[GamepadKey.LS_UP]: ["KeyW"],
[GamepadKey.LS_DOWN]: ["KeyS"],
[GamepadKey.LS_LEFT]: ["KeyA"],
[GamepadKey.LS_RIGHT]: ["KeyD"],
[GamepadKey.RS_UP]: ["KeyI"],
[GamepadKey.RS_DOWN]: ["KeyK"],
[GamepadKey.RS_LEFT]: ["KeyJ"],
[GamepadKey.RS_RIGHT]: ["KeyL"],
[GamepadKey.A]: ["Space", "KeyE"],
[GamepadKey.X]: ["KeyR"],
[GamepadKey.B]: ["ControlLeft", "Backspace"],
[GamepadKey.Y]: ["KeyV"],
[GamepadKey.START]: ["Enter"],
[GamepadKey.SELECT]: ["Tab"],
[GamepadKey.LB]: ["KeyC", "KeyG"],
[GamepadKey.RB]: ["KeyQ"],
[GamepadKey.HOME]: ["Backquote"],
[GamepadKey.RT]: [MouseButtonCode.LEFT_CLICK],
[GamepadKey.LT]: [MouseButtonCode.RIGHT_CLICK],
[GamepadKey.L3]: ["ShiftLeft"],
[GamepadKey.R3]: ["KeyF"]
},
mouse: {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20
}
};
static convert(preset) {
const obj = {
mapping: {},
mouse: Object.assign({}, preset.mouse)
};
for (let buttonIndex in preset.mapping)
for (let keyName of preset.mapping[parseInt(buttonIndex)])
obj.mapping[keyName] = parseInt(buttonIndex);
const mouse = obj.mouse;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= EmulatedMkbHandler.DEFAULT_PANNING_SENSITIVITY, mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= EmulatedMkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]];
if (typeof mouseMapTo !== "undefined")
mouse[MkbPresetKey.MOUSE_MAP_TO] = mouseMapTo;
else
mouse[MkbPresetKey.MOUSE_MAP_TO] = MkbPreset.MOUSE_SETTINGS[MkbPresetKey.MOUSE_MAP_TO].default;
return console.log(obj), obj;
}
}
// src/utils/toast.ts
class Toast {
static #$wrapper;
static #$msg;
static #$status;
static #stack = [];
static #isShowing = !1;
static #timeout;
static #DURATION = 3000;
static show(msg, status, options = {}) {
options = options || {};
const args = Array.from(arguments);
if (options.instant)
Toast.#stack = [args], Toast.#showNext();
else
Toast.#stack.push(args), !Toast.#isShowing && Toast.#showNext();
}
static #showNext() {
if (!Toast.#stack.length) {
Toast.#isShowing = !1;
return;
}
Toast.#isShowing = !0, Toast.#timeout && clearTimeout(Toast.#timeout), Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION);
const [msg, status, options] = Toast.#stack.shift();
if (options && options.html)
Toast.#$msg.innerHTML = msg;
else
Toast.#$msg.textContent = msg;
if (status)
Toast.#$status.classList.remove("bx-gone"), Toast.#$status.textContent = status;
else
Toast.#$status.classList.add("bx-gone");
const classList = Toast.#$wrapper.classList;
classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-show");
}
static #hide() {
Toast.#timeout = null;
const classList = Toast.#$wrapper.classList;
classList.remove("bx-show"), classList.add("bx-hide");
}
static setup() {
Toast.#$wrapper = CE("div", { class: "bx-toast bx-offscreen" }, Toast.#$msg = CE("span", { class: "bx-toast-msg" }), Toast.#$status = CE("span", { class: "bx-toast-status" })), Toast.#$wrapper.addEventListener("transitionend", (e) => {
const classList = Toast.#$wrapper.classList;
if (classList.contains("bx-hide"))
classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen"), Toast.#showNext();
}), document.documentElement.appendChild(Toast.#$wrapper);
}
}
// src/utils/local-db.ts
class LocalDb {
static #instance;
static get INSTANCE() {
if (!LocalDb.#instance)
LocalDb.#instance = new LocalDb;
return LocalDb.#instance;
}
static DB_NAME = "BetterXcloud";
static DB_VERSION = 1;
static TABLE_PRESETS = "mkb_presets";
#DB;
#open() {
return new Promise((resolve, reject) => {
if (this.#DB) {
resolve();
return;
}
const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
request.onupgradeneeded = (e) => {
const db = e.target.result;
switch (e.oldVersion) {
case 0: {
db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: !0 }).createIndex("name_idx", "name");
break;
}
}
}, request.onerror = (e) => {
console.log(e), alert(e.target.error.message), reject && reject();
}, request.onsuccess = (e) => {
this.#DB = e.target.result, resolve();
};
});
}
#table(name, type) {
const table = this.#DB.transaction(name, type || "readonly").objectStore(name);
return new Promise((resolve) => resolve(table));
}
#call(method) {
const table = arguments[1];
return new Promise((resolve) => {
const request = method.call(table, ...Array.from(arguments).slice(2));
request.onsuccess = (e) => {
resolve([table, e.target.result]);
};
});
}
#count(table) {
return this.#call(table.count, ...arguments);
}
#add(table, data) {
return this.#call(table.add, ...arguments);
}
#put(table, data) {
return this.#call(table.put, ...arguments);
}
#delete(table, data) {
return this.#call(table.delete, ...arguments);
}
#get(table, id2) {
return this.#call(table.get, ...arguments);
}
#getAll(table) {
return this.#call(table.getAll, ...arguments);
}
newPreset(name, data) {
return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#add(table, { name, data })).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
}
updatePreset(preset) {
return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#put(table, preset)).then(([table, id2]) => new Promise((resolve) => resolve(id2)));
}
deletePreset(id2) {
return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#delete(table, id2)).then(([table, id3]) => new Promise((resolve) => resolve(id3)));
}
getPreset(id2) {
return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#get(table, id2)).then(([table, preset]) => new Promise((resolve) => resolve(preset)));
}
getPresets() {
return this.#open().then(() => this.#table(LocalDb.TABLE_PRESETS, "readwrite")).then((table) => this.#count(table)).then(([table, count]) => {
if (count > 0)
return new Promise((resolve) => {
this.#getAll(table).then(([table2, items]) => {
const presets = {};
items.forEach((item2) => presets[item2.id] = item2), resolve(presets);
});
});
const preset = {
name: t("default"),
data: MkbPreset.DEFAULT_PRESET
};
return new Promise((resolve) => {
this.#add(table, preset).then(([table2, id2]) => {
preset.id = id2, setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id2), resolve({ [id2]: preset });
});
});
});
}
}
// src/modules/mkb/key-helper.ts
class KeyHelper {
static #NON_PRINTABLE_KEYS = {
Backquote: "`",
[MouseButtonCode.LEFT_CLICK]: "Left Click",
[MouseButtonCode.RIGHT_CLICK]: "Right Click",
[MouseButtonCode.MIDDLE_CLICK]: "Middle Click",
[WheelCode.SCROLL_UP]: "Scroll Up",
[WheelCode.SCROLL_DOWN]: "Scroll Down",
[WheelCode.SCROLL_LEFT]: "Scroll Left",
[WheelCode.SCROLL_RIGHT]: "Scroll Right"
};
static getKeyFromEvent(e) {
let code, name;
if (e instanceof KeyboardEvent)
code = e.code || e.key;
else if (e instanceof WheelEvent) {
if (e.deltaY < 0)
code = WheelCode.SCROLL_UP;
else if (e.deltaY > 0)
code = WheelCode.SCROLL_DOWN;
else if (e.deltaX < 0)
code = WheelCode.SCROLL_LEFT;
else if (e.deltaX > 0)
code = WheelCode.SCROLL_RIGHT;
} else if (e instanceof MouseEvent)
code = "Mouse" + e.button;
if (code)
name = KeyHelper.codeToKeyName(code);
return code ? { code, name } : null;
}
static codeToKeyName(code) {
return KeyHelper.#NON_PRINTABLE_KEYS[code] || code.startsWith("Key") && code.substring(3) || code.startsWith("Digit") && code.substring(5) || code.startsWith("Numpad") && "Numpad " + code.substring(6) || code.startsWith("Arrow") && "Arrow " + code.substring(5) || code.endsWith("Lock") && code.replace("Lock", " Lock") || code.endsWith("Left") && "Left " + code.replace("Left", "") || code.endsWith("Right") && "Right " + code.replace("Right", "") || code;
}
}
// src/assets/svg/command.svg
var command_default = "\n";
// src/assets/svg/controller.svg
var controller_default = "\n";
// src/assets/svg/copy.svg
var copy_default = "\n";
// src/assets/svg/cursor-text.svg
var cursor_text_default = "\n";
// src/assets/svg/display.svg
var display_default = "\n";
// src/assets/svg/home.svg
var home_default = "\n";
// src/assets/svg/native-mkb.svg
var native_mkb_default = "\n";
// src/assets/svg/new.svg
var new_default = "\n";
// src/assets/svg/question.svg
var question_default = "\n";
// src/assets/svg/refresh.svg
var refresh_default = "\n";
// src/assets/svg/remote-play.svg
var remote_play_default = "\n";
// src/assets/svg/stream-settings.svg
var stream_settings_default = "\n";
// src/assets/svg/stream-stats.svg
var stream_stats_default = "\n";
// src/assets/svg/trash.svg
var trash_default = "\n";
// src/assets/svg/touch-control-enable.svg
var touch_control_enable_default = "\n";
// src/assets/svg/touch-control-disable.svg
var touch_control_disable_default = "\n";
// src/assets/svg/virtual-controller.svg
var virtual_controller_default = "\n";
// src/assets/svg/caret-left.svg
var caret_left_default = "\n";
// src/assets/svg/caret-right.svg
var caret_right_default = "\n";
// src/assets/svg/camera.svg
var camera_default = "\n";
// src/assets/svg/microphone.svg
var microphone_default = "\n";
// src/assets/svg/microphone-slash.svg
var microphone_slash_default = "\n";
// src/assets/svg/battery-full.svg
var battery_full_default = "\n";
// src/assets/svg/clock.svg
var clock_default = "\n";
// src/assets/svg/cloud.svg
var cloud_default = "\n";
// src/assets/svg/download.svg
var download_default = "\n";
// src/assets/svg/speaker-high.svg
var speaker_high_default = "\n";
// src/assets/svg/upload.svg
var upload_default = "\n";
// src/utils/bx-icon.ts
var BxIcon = {
STREAM_SETTINGS: stream_settings_default,
STREAM_STATS: stream_stats_default,
COMMAND: command_default,
CONTROLLER: controller_default,
DISPLAY: display_default,
HOME: home_default,
NATIVE_MKB: native_mkb_default,
NEW: new_default,
COPY: copy_default,
TRASH: trash_default,
CURSOR_TEXT: cursor_text_default,
QUESTION: question_default,
REFRESH: refresh_default,
VIRTUAL_CONTROLLER: virtual_controller_default,
REMOTE_PLAY: remote_play_default,
CARET_LEFT: caret_left_default,
CARET_RIGHT: caret_right_default,
SCREENSHOT: camera_default,
TOUCH_CONTROL_ENABLE: touch_control_enable_default,
TOUCH_CONTROL_DISABLE: touch_control_disable_default,
MICROPHONE: microphone_default,
MICROPHONE_MUTED: microphone_slash_default,
BATTERY: battery_full_default,
PLAYTIME: clock_default,
SERVER: cloud_default,
DOWNLOAD: download_default,
UPLOAD: upload_default,
AUDIO: speaker_high_default
};
// src/modules/stream/stream-badges.ts
var StreamBadge;
(function(StreamBadge2) {
StreamBadge2["PLAYTIME"] = "playtime";
StreamBadge2["BATTERY"] = "battery";
StreamBadge2["DOWNLOAD"] = "in";
StreamBadge2["UPLOAD"] = "out";
StreamBadge2["SERVER"] = "server";
StreamBadge2["VIDEO"] = "video";
StreamBadge2["AUDIO"] = "audio";
})(StreamBadge || (StreamBadge = {}));
var StreamBadgeIcon = {
[StreamBadge.PLAYTIME]: BxIcon.PLAYTIME,
[StreamBadge.VIDEO]: BxIcon.DISPLAY,
[StreamBadge.BATTERY]: BxIcon.BATTERY,
[StreamBadge.DOWNLOAD]: BxIcon.DOWNLOAD,
[StreamBadge.UPLOAD]: BxIcon.UPLOAD,
[StreamBadge.SERVER]: BxIcon.SERVER,
[StreamBadge.AUDIO]: BxIcon.AUDIO
};
class StreamBadges {
static instance;
static getInstance() {
if (!StreamBadges.instance)
StreamBadges.instance = new StreamBadges;
return StreamBadges.instance;
}
#ipv6 = !1;
#resolution = null;
#video = null;
#audio = null;
#region = "";
startBatteryLevel = 100;
startTimestamp = 0;
#$container;
#cachedDoms = {};
#interval;
#REFRESH_INTERVAL = 3000;
setRegion(region) {
this.#region = region;
}
#renderBadge(name, value, color) {
let $badge;
if (this.#cachedDoms[name])
return $badge = this.#cachedDoms[name], $badge.lastElementChild.textContent = value, $badge;
if ($badge = CE("div", { class: "bx-badge", title: t(`badge-${name}`) }, CE("span", { class: "bx-badge-name" }, createSvgIcon(StreamBadgeIcon[name])), CE("span", { class: "bx-badge-value", style: `background-color: ${color}` }, value)), name === StreamBadge.BATTERY)
$badge.classList.add("bx-badge-battery");
return this.#cachedDoms[name] = $badge, $badge;
}
async#updateBadges(forceUpdate = !1) {
if (!this.#$container || !forceUpdate && !this.#$container.isConnected) {
this.#stop();
return;
}
let now = +new Date;
const diffSeconds = Math.ceil((now - this.startTimestamp) / 1000), playtime = this.#secondsToHm(diffSeconds);
let batteryLevel = "100%", batteryLevelInt = 100, isCharging = !1;
if ("getBattery" in navigator)
try {
const bm = await navigator.getBattery();
if (isCharging = bm.charging, batteryLevelInt = Math.round(bm.level * 100), batteryLevel = `${batteryLevelInt}%`, batteryLevelInt != this.startBatteryLevel) {
const diffLevel = Math.round(batteryLevelInt - this.startBatteryLevel), sign = diffLevel > 0 ? "+" : "";
batteryLevel += ` (${sign}${diffLevel}%)`;
}
} catch (e) {
}
const stats = await STATES.currentStream.peerConnection?.getStats();
let totalIn = 0, totalOut = 0;
stats.forEach((stat) => {
if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded")
totalIn += stat.bytesReceived, totalOut += stat.bytesSent;
});
const badges = {
[StreamBadge.DOWNLOAD]: totalIn ? this.#humanFileSize(totalIn) : null,
[StreamBadge.UPLOAD]: totalOut ? this.#humanFileSize(totalOut) : null,
[StreamBadge.PLAYTIME]: playtime,
[StreamBadge.BATTERY]: batteryLevel
};
let name;
for (name in badges) {
const value = badges[name];
if (value === null)
continue;
const $elm = this.#cachedDoms[name];
if ($elm && ($elm.lastElementChild.textContent = value), name === StreamBadge.BATTERY)
if (this.startBatteryLevel === 100 && batteryLevelInt === 100)
$elm.classList.add("bx-gone");
else
$elm.dataset.charging = isCharging.toString(), $elm.classList.remove("bx-gone");
}
}
async#start() {
await this.#updateBadges(!0), this.#stop(), this.#interval = window.setInterval(this.#updateBadges.bind(this), this.#REFRESH_INTERVAL);
}
#stop() {
this.#interval && clearInterval(this.#interval), this.#interval = null;
}
#secondsToHm(seconds) {
const h = Math.floor(seconds / 3600), m = Math.floor(seconds % 3600 / 60) + 1, hDisplay = h > 0 ? `${h}h` : "", mDisplay = m > 0 ? `${m}m` : "";
return hDisplay + mDisplay;
}
#humanFileSize(size) {
const units = ["B", "KB", "MB", "GB", "TB"];
let i = size == 0 ? 0 : Math.floor(Math.log(size) / Math.log(1024));
return (size / Math.pow(1024, i)).toFixed(2) + " " + units[i];
}
async render() {
if (this.#$container)
return this.#start(), this.#$container;
await this.#getServerStats();
let video = "";
if (this.#resolution)
video = `${this.#resolution.height}p`;
if (this.#video) {
if (video && (video += "/"), video += this.#video.codec, this.#video.profile) {
const profile = this.#video.profile;
let quality = profile;
if (profile.startsWith("4d"))
quality = t("visual-quality-high");
else if (profile.startsWith("42e"))
quality = t("visual-quality-normal");
else if (profile.startsWith("420"))
quality = t("visual-quality-low");
video += ` (${quality})`;
}
}
let audio;
if (this.#audio) {
audio = this.#audio.codec;
const bitrate = this.#audio.bitrate / 1000;
audio += ` (${bitrate} kHz)`;
}
let batteryLevel = "";
if ("getBattery" in navigator)
batteryLevel = "100%";
let server = this.#region;
server += "@" + (this.#ipv6 ? "IPv6" : "IPv4");
const BADGES = [
[StreamBadge.PLAYTIME, "1m", "#ff004d"],
[StreamBadge.BATTERY, batteryLevel, "#00b543"],
[StreamBadge.DOWNLOAD, this.#humanFileSize(0), "#29adff"],
[StreamBadge.UPLOAD, this.#humanFileSize(0), "#ff77a8"],
[StreamBadge.SERVER, server, "#ff6c24"],
video ? [StreamBadge.VIDEO, video, "#742f29"] : null,
audio ? [StreamBadge.AUDIO, audio, "#5f574f"] : null
], $container = CE("div", { class: "bx-badges" });
return BADGES.forEach((item2) => {
if (!item2)
return;
const $badge = this.#renderBadge(...item2);
$container.appendChild($badge);
}), this.#$container = $container, await this.#start(), $container;
}
async#getServerStats() {
const stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {};
let videoCodecId;
const allAudioCodecs = {};
let audioCodecId;
const allCandidates = {};
let candidateId;
if (stats.forEach((stat) => {
if (stat.type === "codec") {
const mimeType = stat.mimeType.split("/")[0];
if (mimeType === "video")
allVideoCodecs[stat.id] = stat;
else if (mimeType === "audio")
allAudioCodecs[stat.id] = stat;
} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {
if (stat.kind === "video")
videoCodecId = stat.codecId;
else if (stat.kind === "audio")
audioCodecId = stat.codecId;
} else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded")
candidateId = stat.remoteCandidateId;
else if (stat.type === "remote-candidate")
allCandidates[stat.id] = stat.address;
}), videoCodecId) {
const videoStat = allVideoCodecs[videoCodecId], video = {
codec: videoStat.mimeType.substring(6)
};
if (video.codec === "H264") {
const match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);
video.profile = match ? match[1] : null;
}
this.#video = video;
}
if (audioCodecId) {
const audioStat = allAudioCodecs[audioCodecId];
this.#audio = {
codec: audioStat.mimeType.substring(6),
bitrate: audioStat.clockRate
};
}
if (candidateId)
BxLogger.info("candidate", candidateId, allCandidates), this.#ipv6 = allCandidates[candidateId].includes(":");
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
const $video = e.$video, streamBadges = StreamBadges.getInstance();
streamBadges.#resolution = {
width: $video.videoWidth,
height: $video.videoHeight
}, streamBadges.startTimestamp = +new Date;
try {
"getBattery" in navigator && navigator.getBattery().then((bm) => {
streamBadges.startBatteryLevel = Math.round(bm.level * 100);
});
} catch (e2) {
}
});
}
}
// src/modules/stream/stream-ui.ts
var cloneStreamHudButton = function($orgButton, label, svgIcon) {
const $container = $orgButton.cloneNode(!0);
let timeout;
const onTransitionStart = (e) => {
if (e.propertyName !== "opacity")
return;
timeout && clearTimeout(timeout), $container.style.pointerEvents = "none";
}, onTransitionEnd = (e) => {
if (e.propertyName !== "opacity")
return;
if (document.getElementById("StreamHud")?.style.left === "0px")
timeout && clearTimeout(timeout), timeout = window.setTimeout(() => {
$container.style.pointerEvents = "auto";
}, 100);
};
if (STATES.browserHasTouchSupport)
$container.addEventListener("transitionstart", onTransitionStart), $container.addEventListener("transitionend", onTransitionEnd);
const $button = $container.querySelector("button");
$button.setAttribute("title", label);
const $orgSvg = $button.querySelector("svg"), $svg = createSvgIcon(svgIcon);
return $svg.style.fill = "none", $svg.setAttribute("class", $orgSvg.getAttribute("class") || ""), $svg.ariaHidden = "true", $orgSvg.replaceWith($svg), $container;
}, cloneCloseButton = function($$btnOrg, icon, className, onChange) {
const $btn = $$btnOrg.cloneNode(!0), $svg = createSvgIcon(icon);
return $svg.setAttribute("class", $btn.firstElementChild.getAttribute("class") || ""), $svg.style.fill = "none", $btn.classList.add(className), $btn.removeChild($btn.firstElementChild), $btn.appendChild($svg), $btn.addEventListener("click", onChange), $btn;
};
function injectStreamMenuButtons() {
const $screen = document.querySelector("#PageContent section[class*=PureScreens]");
if (!$screen)
return;
if ($screen.xObserving)
return;
$screen.xObserving = !0;
const $settingsDialog = document.querySelector(".bx-stream-settings-dialog"), $parent = $screen.parentElement, hideSettingsFunc = (e) => {
if (e) {
const $target = e.target;
if (e.stopPropagation(), $target != $parent && $target.id !== "MultiTouchSurface" && !$target.querySelector("#BabylonCanvasContainer-main"))
return;
if ($target.id === "MultiTouchSurface")
$target.removeEventListener("touchstart", hideSettingsFunc);
}
$settingsDialog.classList.add("bx-gone"), $parent?.removeEventListener("click", hideSettingsFunc);
};
let $btnStreamSettings, $btnStreamStats;
const streamStats = StreamStats.getInstance();
new MutationObserver((mutationList) => {
mutationList.forEach((item2) => {
if (item2.type !== "childList")
return;
item2.addedNodes.forEach(async ($node) => {
if (!$node || $node.nodeType !== Node.ELEMENT_NODE)
return;
let $elm = $node;
if ($elm instanceof SVGSVGElement)
return;
if ($elm.className?.includes("PureErrorPage")) {
BxEvent.dispatch(window, BxEvent.STREAM_ERROR_PAGE);
return;
}
if ($elm.className?.startsWith("StreamMenu-module__container")) {
const $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
if (!$btnCloseHud)
return;
$btnCloseHud.addEventListener("click", (e) => {
$settingsDialog.classList.add("bx-gone");
});
const $btnRefresh = cloneCloseButton($btnCloseHud, BxIcon.REFRESH, "bx-stream-refresh-button", () => {
confirm(t("confirm-reload-stream")) && window.location.reload();
}), $btnHome = cloneCloseButton($btnCloseHud, BxIcon.HOME, "bx-stream-home-button", () => {
confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
});
$btnCloseHud.insertAdjacentElement("afterend", $btnRefresh), $btnRefresh.insertAdjacentElement("afterend", $btnHome), document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]")?.appendChild(await StreamBadges.getInstance().render()), hideSettingsFunc();
return;
}
if ($elm.className?.startsWith("Overlay-module_") || $elm.className?.startsWith("InProgressScreen"))
$elm = $elm.querySelector("#StreamHud");
if (!$elm || ($elm.id || "") !== "StreamHud")
return;
const $gripHandle = $elm.querySelector("button[class^=GripHandle]"), hideGripHandle = () => {
if (!$gripHandle)
return;
$gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click(), $gripHandle.dispatchEvent(new PointerEvent("pointerdown")), $gripHandle.click();
}, $orgButton = $elm.querySelector("div[class^=HUDButton]");
if (!$orgButton)
return;
if (!$btnStreamSettings)
$btnStreamSettings = cloneStreamHudButton($orgButton, t("stream-settings"), BxIcon.STREAM_SETTINGS), $btnStreamSettings.addEventListener("click", (e) => {
hideGripHandle(), e.preventDefault(), $settingsDialog.classList.remove("bx-gone"), $parent?.addEventListener("click", hideSettingsFunc);
const $touchSurface = document.getElementById("MultiTouchSurface");
$touchSurface && $touchSurface.style.display != "none" && $touchSurface.addEventListener("touchstart", hideSettingsFunc);
});
if (!$btnStreamStats)
$btnStreamStats = cloneStreamHudButton($orgButton, t("stream-stats"), BxIcon.STREAM_STATS), $btnStreamStats.addEventListener("click", (e) => {
hideGripHandle(), e.preventDefault(), streamStats.toggle();
const btnStreamStatsOn2 = !streamStats.isHidden() && !streamStats.isGlancing();
$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn2);
});
const btnStreamStatsOn = !streamStats.isHidden() && !streamStats.isGlancing();
if ($btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn), $orgButton) {
const $btnParent = $orgButton.parentElement;
$btnParent.insertBefore($btnStreamStats, $btnParent.lastElementChild), $btnParent.insertBefore($btnStreamSettings, $btnStreamStats);
const $dotsButton = $btnParent.lastElementChild;
$dotsButton.parentElement.insertBefore($dotsButton, $dotsButton.parentElement.firstElementChild);
}
});
});
}).observe($screen, { subtree: !0, childList: !0 });
}
function showStreamSettings(tabId) {
const $wrapper = document.querySelector(".bx-stream-settings-dialog");
if (!$wrapper)
return;
if (tabId) {
const $tab = $wrapper.querySelector(`.bx-stream-settings-tabs svg[data-group=${tabId}]`);
$tab && $tab.dispatchEvent(new Event("click"));
}
$wrapper.classList.remove("bx-gone");
const $screen = document.querySelector("#PageContent section[class*=PureScreens]");
if ($screen && $screen.parentElement) {
const $parent = $screen.parentElement;
if (!$parent || $parent.bxClick)
return;
$parent.bxClick = !0;
const onClick = (e) => {
$wrapper.classList.add("bx-gone"), $parent.bxClick = !1, $parent.removeEventListener("click", onClick);
};
$parent.addEventListener("click", onClick);
}
}
// src/modules/mkb/pointer-client.ts
var LOG_TAG = "PointerClient", PointerAction;
(function(PointerAction2) {
PointerAction2[PointerAction2["MOVE"] = 1] = "MOVE";
PointerAction2[PointerAction2["BUTTON_PRESS"] = 2] = "BUTTON_PRESS";
PointerAction2[PointerAction2["BUTTON_RELEASE"] = 3] = "BUTTON_RELEASE";
PointerAction2[PointerAction2["SCROLL"] = 4] = "SCROLL";
PointerAction2[PointerAction2["POINTER_CAPTURE_CHANGED"] = 5] = "POINTER_CAPTURE_CHANGED";
})(PointerAction || (PointerAction = {}));
class PointerClient {
static instance;
static getInstance() {
if (!PointerClient.instance)
PointerClient.instance = new PointerClient;
return PointerClient.instance;
}
#socket;
#mkbHandler;
start(port, mkbHandler) {
if (!port)
throw new Error("PointerServer port is 0");
this.#mkbHandler = mkbHandler, this.#socket = new WebSocket(`ws://localhost:${port}`), this.#socket.binaryType = "arraybuffer", this.#socket.addEventListener("open", (event) => {
BxLogger.info(LOG_TAG, "connected");
}), this.#socket.addEventListener("error", (event) => {
BxLogger.error(LOG_TAG, event), Toast.show("Cannot setup mouse: " + event);
}), this.#socket.addEventListener("close", (event) => {
this.#socket = null;
}), this.#socket.addEventListener("message", (event) => {
const dataView = new DataView(event.data);
let messageType = dataView.getInt8(0), offset = Int8Array.BYTES_PER_ELEMENT;
switch (messageType) {
case PointerAction.MOVE:
this.onMove(dataView, offset);
break;
case PointerAction.BUTTON_PRESS:
case PointerAction.BUTTON_RELEASE:
this.onPress(messageType, dataView, offset);
break;
case PointerAction.SCROLL:
this.onScroll(dataView, offset);
break;
case PointerAction.POINTER_CAPTURE_CHANGED:
this.onPointerCaptureChanged(dataView, offset);
}
});
}
onMove(dataView, offset) {
const x = dataView.getInt16(offset);
offset += Int16Array.BYTES_PER_ELEMENT;
const y = dataView.getInt16(offset);
this.#mkbHandler?.handleMouseMove({
movementX: x,
movementY: y
});
}
onPress(messageType, dataView, offset) {
const button = dataView.getUint8(offset);
this.#mkbHandler?.handleMouseClick({
pointerButton: button,
pressed: messageType === PointerAction.BUTTON_PRESS
});
}
onScroll(dataView, offset) {
const vScroll = dataView.getInt16(offset);
offset += Int16Array.BYTES_PER_ELEMENT;
const hScroll = dataView.getInt16(offset);
this.#mkbHandler?.handleMouseWheel({
vertical: vScroll,
horizontal: hScroll
});
}
onPointerCaptureChanged(dataView, offset) {
dataView.getInt8(offset) !== 1 && this.#mkbHandler?.stop();
}
stop() {
try {
this.#socket?.close();
} catch (e) {
}
this.#socket = null;
}
}
// src/modules/mkb/base-mkb-handler.ts
class MouseDataProvider {
mkbHandler;
constructor(handler) {
this.mkbHandler = handler;
}
}
class MkbHandler {
}
// src/modules/mkb/native-mkb-handler.ts
class NativeMkbHandler extends MkbHandler {
static instance;
#pointerClient;
#enabled = !1;
#mouseButtonsPressed = 0;
#mouseWheelX = 0;
#mouseWheelY = 0;
#mouseVerticalMultiply = 0;
#mouseHorizontalMultiply = 0;
#inputSink;
#$message;
static getInstance() {
if (!NativeMkbHandler.instance)
NativeMkbHandler.instance = new NativeMkbHandler;
return NativeMkbHandler.instance;
}
#onKeyboardEvent(e) {
if (e.type === "keyup" && e.code === "F8") {
e.preventDefault(), this.toggle();
return;
}
}
#onPointerLockRequested(e) {
AppInterface.requestPointerCapture(), this.start();
}
#onPointerLockExited(e) {
AppInterface.releasePointerCapture(), this.stop();
}
#onPollingModeChanged = (e) => {
if (!this.#$message)
return;
if (e.mode === "None")
this.#$message.classList.remove("bx-offscreen");
else
this.#$message.classList.add("bx-offscreen");
};
#onDialogShown = () => {
document.pointerLockElement && document.exitPointerLock();
};
#initMessage() {
if (!this.#$message)
this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg" }, CE("div", {}, CE("p", {}, t("native-mkb")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "native" }, createButton({
style: ButtonStyle.PRIMARY | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
label: t("activate"),
onClick: ((e) => {
e.preventDefault(), e.stopPropagation(), this.toggle(!0);
}).bind(this)
}), createButton({
style: ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH,
label: t("ignore"),
onClick: (e) => {
e.preventDefault(), e.stopPropagation(), this.#$message?.classList.add("bx-gone");
}
})));
if (!this.#$message.isConnected)
document.documentElement.appendChild(this.#$message);
}
handleEvent(event) {
switch (event.type) {
case "keyup":
this.#onKeyboardEvent(event);
break;
case BxEvent.XCLOUD_DIALOG_SHOWN:
this.#onDialogShown();
break;
case BxEvent.POINTER_LOCK_REQUESTED:
this.#onPointerLockRequested(event);
break;
case BxEvent.POINTER_LOCK_EXITED:
this.#onPointerLockExited(event);
break;
case BxEvent.XCLOUD_POLLING_MODE_CHANGED:
this.#onPollingModeChanged(event);
break;
}
}
init() {
this.#pointerClient = PointerClient.getInstance(), this.#inputSink = window.BX_EXPOSED.inputSink, this.#updateInputConfigurationAsync(!1);
try {
this.#pointerClient.start(STATES.pointerServerPort, this);
} catch (e) {
Toast.show("Cannot enable Mouse & Keyboard feature");
}
if (this.#mouseVerticalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY), this.#mouseHorizontalMultiply = getPref(PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY), window.addEventListener("keyup", this), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#initMessage(), AppInterface)
Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("native-mkb"), { html: !0 }), this.#$message?.classList.add("bx-gone");
else
this.#$message?.classList.remove("bx-gone");
}
toggle(force) {
let setEnable;
if (typeof force !== "undefined")
setEnable = force;
else
setEnable = !this.#enabled;
if (setEnable)
document.documentElement.requestPointerLock();
else
document.exitPointerLock();
}
#updateInputConfigurationAsync(enabled) {
window.BX_EXPOSED.streamSession.updateInputConfigurationAsync({
enableKeyboardInput: enabled,
enableMouseInput: enabled,
enableAbsoluteMouse: !1,
enableTouchInput: !1
});
}
start() {
this.#resetMouseInput(), this.#enabled = !0, this.#updateInputConfigurationAsync(!0), window.BX_EXPOSED.stopTakRendering = !0, this.#$message?.classList.add("bx-gone"), Toast.show(t("native-mkb"), t("enabled"), { instant: !0 });
}
stop() {
this.#resetMouseInput(), this.#enabled = !1, this.#updateInputConfigurationAsync(!1), this.#$message?.classList.remove("bx-gone");
}
destroy() {
this.#pointerClient?.stop(), window.removeEventListener("keyup", this), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this), window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this), this.#$message?.classList.add("bx-gone");
}
handleMouseMove(data) {
this.#sendMouseInput({
X: data.movementX,
Y: data.movementY,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY
});
}
handleMouseClick(data) {
const { pointerButton, pressed } = data;
if (pressed)
this.#mouseButtonsPressed |= pointerButton;
else
this.#mouseButtonsPressed ^= pointerButton;
this.#mouseButtonsPressed = Math.max(0, this.#mouseButtonsPressed), this.#sendMouseInput({
X: 0,
Y: 0,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY
});
}
handleMouseWheel(data) {
const { vertical, horizontal } = data;
if (this.#mouseWheelX = horizontal, this.#mouseHorizontalMultiply && this.#mouseHorizontalMultiply !== 1)
this.#mouseWheelX *= this.#mouseHorizontalMultiply;
if (this.#mouseWheelY = vertical, this.#mouseVerticalMultiply && this.#mouseVerticalMultiply !== 1)
this.#mouseWheelY *= this.#mouseVerticalMultiply;
return this.#sendMouseInput({
X: 0,
Y: 0,
Buttons: this.#mouseButtonsPressed,
WheelX: this.#mouseWheelX,
WheelY: this.#mouseWheelY
}), !0;
}
setVerticalScrollMultiplier(vertical) {
this.#mouseVerticalMultiply = vertical;
}
setHorizontalScrollMultiplier(horizontal) {
this.#mouseHorizontalMultiply = horizontal;
}
waitForMouseData(enabled) {
}
isEnabled() {
return this.#enabled;
}
#sendMouseInput(data) {
data.Type = 0, this.#inputSink?.onMouseInput(data);
}
#resetMouseInput() {
this.#mouseButtonsPressed = 0, this.#mouseWheelX = 0, this.#mouseWheelY = 0, this.#sendMouseInput({
X: 0,
Y: 0,
Buttons: 0,
WheelX: 0,
WheelY: 0
});
}
}
// src/modules/mkb/mkb-handler.ts
var LOG_TAG2 = "MkbHandler", PointerToMouseButton = {
1: 0,
2: 2,
4: 1
};
class WebSocketMouseDataProvider extends MouseDataProvider {
#pointerClient;
#connected = !1;
init() {
this.#pointerClient = PointerClient.getInstance(), this.#connected = !1;
try {
this.#pointerClient.start(STATES.pointerServerPort, this.mkbHandler), this.#connected = !0;
} catch (e) {
Toast.show("Cannot enable Mouse & Keyboard feature");
}
}
start() {
this.#connected && AppInterface.requestPointerCapture();
}
stop() {
this.#connected && AppInterface.releasePointerCapture();
}
destroy() {
this.#connected && this.#pointerClient?.stop();
}
}
class PointerLockMouseDataProvider extends MouseDataProvider {
init() {
}
start() {
window.addEventListener("mousemove", this.#onMouseMoveEvent), window.addEventListener("mousedown", this.#onMouseEvent), window.addEventListener("mouseup", this.#onMouseEvent), window.addEventListener("wheel", this.#onWheelEvent, { passive: !1 }), window.addEventListener("contextmenu", this.#disableContextMenu);
}
stop() {
document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("mousemove", this.#onMouseMoveEvent), window.removeEventListener("mousedown", this.#onMouseEvent), window.removeEventListener("mouseup", this.#onMouseEvent), window.removeEventListener("wheel", this.#onWheelEvent), window.removeEventListener("contextmenu", this.#disableContextMenu);
}
destroy() {
}
#onMouseMoveEvent = (e) => {
this.mkbHandler.handleMouseMove({
movementX: e.movementX,
movementY: e.movementY
});
};
#onMouseEvent = (e) => {
e.preventDefault();
const isMouseDown = e.type === "mousedown", data = {
mouseButton: e.button,
pressed: isMouseDown
};
this.mkbHandler.handleMouseClick(data);
};
#onWheelEvent = (e) => {
if (!KeyHelper.getKeyFromEvent(e))
return;
const data = {
vertical: e.deltaY,
horizontal: e.deltaX
};
if (this.mkbHandler.handleMouseWheel(data))
e.preventDefault();
};
#disableContextMenu = (e) => e.preventDefault();
}
class EmulatedMkbHandler extends MkbHandler {
static #instance;
static getInstance() {
if (!EmulatedMkbHandler.#instance)
EmulatedMkbHandler.#instance = new EmulatedMkbHandler;
return EmulatedMkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static DEFAULT_PANNING_SENSITIVITY = 0.001;
static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
static MAXIMUM_STICK_RANGE = 1.1;
static VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller";
#VIRTUAL_GAMEPAD = {
id: EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID,
index: 3,
connected: !1,
hapticActuators: null,
mapping: "standard",
axes: [0, 0, 0, 0],
buttons: new Array(17).fill(null).map(() => ({ pressed: !1, value: 0 })),
timestamp: performance.now(),
vibrationActuator: null
};
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
#enabled = !1;
#mouseDataProvider;
#isPolling = !1;
#prevWheelCode = null;
#wheelStoppedTimeout;
#detectMouseStoppedTimeout;
#$message;
#escKeyDownTime = -1;
#STICK_MAP;
#LEFT_STICK_X = [];
#LEFT_STICK_Y = [];
#RIGHT_STICK_X = [];
#RIGHT_STICK_Y = [];
constructor() {
super();
this.#STICK_MAP = {
[GamepadKey.LS_LEFT]: [this.#LEFT_STICK_X, 0, -1],
[GamepadKey.LS_RIGHT]: [this.#LEFT_STICK_X, 0, 1],
[GamepadKey.LS_UP]: [this.#LEFT_STICK_Y, 1, -1],
[GamepadKey.LS_DOWN]: [this.#LEFT_STICK_Y, 1, 1],
[GamepadKey.RS_LEFT]: [this.#RIGHT_STICK_X, 2, -1],
[GamepadKey.RS_RIGHT]: [this.#RIGHT_STICK_X, 2, 1],
[GamepadKey.RS_UP]: [this.#RIGHT_STICK_Y, 3, -1],
[GamepadKey.RS_DOWN]: [this.#RIGHT_STICK_Y, 3, 1]
};
}
isEnabled = () => this.#enabled;
#patchedGetGamepads = () => {
const gamepads = this.#nativeGetGamepads() || [];
return gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD, gamepads;
};
#getVirtualGamepad = () => this.#VIRTUAL_GAMEPAD;
#updateStick(stick, x, y) {
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.axes[stick * 2] = x, virtualGamepad.axes[stick * 2 + 1] = y, virtualGamepad.timestamp = performance.now();
}
#vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
#resetGamepad = () => {
const gamepad = this.#getVirtualGamepad();
gamepad.axes = [0, 0, 0, 0];
for (let button of gamepad.buttons)
button.pressed = !1, button.value = 0;
gamepad.timestamp = performance.now();
};
#pressButton = (buttonIndex, pressed) => {
const virtualGamepad = this.#getVirtualGamepad();
if (buttonIndex >= 100) {
let [valueArr, axisIndex] = this.#STICK_MAP[buttonIndex];
valueArr = valueArr, axisIndex = axisIndex;
for (let i = valueArr.length - 1;i >= 0; i--)
if (valueArr[i] === buttonIndex)
valueArr.splice(i, 1);
pressed && valueArr.push(buttonIndex);
let value;
if (valueArr.length)
value = this.#STICK_MAP[valueArr[valueArr.length - 1]][2];
else
value = 0;
virtualGamepad.axes[axisIndex] = value;
} else
virtualGamepad.buttons[buttonIndex].pressed = pressed, virtualGamepad.buttons[buttonIndex].value = pressed ? 1 : 0;
virtualGamepad.timestamp = performance.now();
};
#onKeyboardEvent = (e) => {
const isKeyDown = e.type === "keydown";
if (e.code === "F8") {
if (!isKeyDown)
e.preventDefault(), this.toggle();
return;
}
if (e.code === "Escape") {
if (e.preventDefault(), this.#enabled && isKeyDown) {
if (this.#escKeyDownTime === -1)
this.#escKeyDownTime = performance.now();
else if (performance.now() - this.#escKeyDownTime >= 1000)
this.stop();
} else
this.#escKeyDownTime = -1;
return;
}
if (!this.#isPolling)
return;
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key];
if (typeof buttonIndex === "undefined")
return;
if (e.repeat)
return;
e.preventDefault(), this.#pressButton(buttonIndex, isKeyDown);
};
#onMouseStopped = () => {
this.#detectMouseStoppedTimeout = null;
const analog = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO] === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, 0, 0);
};
handleMouseClick = (data) => {
let mouseButton;
if (typeof data.mouseButton !== "undefined")
mouseButton = data.mouseButton;
else if (typeof data.pointerButton !== "undefined")
mouseButton = PointerToMouseButton[data.pointerButton];
const keyCode = "Mouse" + mouseButton, key = {
code: keyCode,
name: KeyHelper.codeToKeyName(keyCode)
};
if (!key.name)
return;
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === "undefined")
return;
this.#pressButton(buttonIndex, data.pressed);
};
handleMouseMove = (data) => {
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF)
return;
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout), this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50);
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X], y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y], length = this.#vectorLength(x, y);
if (length !== 0 && length < deadzoneCounterweight)
x *= deadzoneCounterweight / length, y *= deadzoneCounterweight / length;
else if (length > EmulatedMkbHandler.MAXIMUM_STICK_RANGE)
x *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length, y *= EmulatedMkbHandler.MAXIMUM_STICK_RANGE / length;
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, x, y);
};
handleMouseWheel = (data) => {
let code = "";
if (data.vertical < 0)
code = WheelCode.SCROLL_UP;
else if (data.vertical > 0)
code = WheelCode.SCROLL_DOWN;
else if (data.horizontal < 0)
code = WheelCode.SCROLL_LEFT;
else if (data.horizontal > 0)
code = WheelCode.SCROLL_RIGHT;
if (!code)
return !1;
const key = {
code,
name: KeyHelper.codeToKeyName(code)
}, buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === "undefined")
return !1;
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code)
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout), this.#pressButton(buttonIndex, !0);
return this.#wheelStoppedTimeout = window.setTimeout(() => {
this.#prevWheelCode = null, this.#pressButton(buttonIndex, !1);
}, 20), !0;
};
toggle = (force) => {
if (typeof force !== "undefined")
this.#enabled = force;
else
this.#enabled = !this.#enabled;
if (this.#enabled)
document.body.requestPointerLock();
else
document.pointerLockElement && document.exitPointerLock();
};
#getCurrentPreset = () => {
return new Promise((resolve) => {
const presetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
LocalDb.INSTANCE.getPreset(presetId).then((preset) => {
resolve(preset);
});
});
};
refreshPresetData = () => {
this.#getCurrentPreset().then((preset) => {
this.#CURRENT_PRESET_DATA = MkbPreset.convert(preset ? preset.data : MkbPreset.DEFAULT_PRESET), this.#resetGamepad();
});
};
waitForMouseData = (wait) => {
this.#$message && this.#$message.classList.toggle("bx-gone", !wait);
};
#onPollingModeChanged = (e) => {
if (!this.#$message)
return;
if (e.mode === "None")
this.#$message.classList.remove("bx-offscreen");
else
this.#$message.classList.add("bx-offscreen");
};
#onDialogShown = () => {
document.pointerLockElement && document.exitPointerLock();
};
#initMessage = () => {
if (!this.#$message)
this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, CE("div", {}, CE("p", {}, t("virtual-controller")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))), CE("div", { "data-type": "virtual" }, createButton({
style: ButtonStyle.PRIMARY | ButtonStyle.TALL | ButtonStyle.FULL_WIDTH,
label: t("activate"),
onClick: ((e) => {
e.preventDefault(), e.stopPropagation(), this.toggle(!0);
}).bind(this)
}), CE("div", {}, createButton({
label: t("ignore"),
style: ButtonStyle.GHOST,
onClick: (e) => {
e.preventDefault(), e.stopPropagation(), this.toggle(!1), this.waitForMouseData(!1);
}
}), createButton({
label: t("edit"),
onClick: (e) => {
e.preventDefault(), e.stopPropagation(), showStreamSettings("mkb");
}
}))));
if (!this.#$message.isConnected)
document.documentElement.appendChild(this.#$message);
};
#onPointerLockChange = () => {
if (document.pointerLockElement)
this.start();
else
this.stop();
};
#onPointerLockError = (e) => {
console.log(e), this.stop();
};
#onPointerLockRequested = () => {
this.start();
};
#onPointerLockExited = () => {
this.#mouseDataProvider?.stop();
};
handleEvent(event) {
switch (event.type) {
case BxEvent.POINTER_LOCK_REQUESTED:
this.#onPointerLockRequested();
break;
case BxEvent.POINTER_LOCK_EXITED:
this.#onPointerLockExited();
break;
}
}
init = () => {
if (this.refreshPresetData(), this.#enabled = !1, AppInterface)
this.#mouseDataProvider = new WebSocketMouseDataProvider(this);
else
this.#mouseDataProvider = new PointerLockMouseDataProvider(this);
if (this.#mouseDataProvider.init(), window.addEventListener("keydown", this.#onKeyboardEvent), window.addEventListener("keyup", this.#onKeyboardEvent), window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.addEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), AppInterface)
window.addEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.addEventListener(BxEvent.POINTER_LOCK_EXITED, this);
else
document.addEventListener("pointerlockchange", this.#onPointerLockChange), document.addEventListener("pointerlockerror", this.#onPointerLockError);
if (this.#initMessage(), this.#$message?.classList.add("bx-gone"), AppInterface)
Toast.show(t("press-key-to-toggle-mkb", { key: "F8" }), t("virtual-controller"), { html: !0 }), this.waitForMouseData(!1);
else
this.waitForMouseData(!0);
};
destroy = () => {
if (this.#isPolling = !1, this.#enabled = !1, this.stop(), this.waitForMouseData(!1), document.pointerLockElement && document.exitPointerLock(), window.removeEventListener("keydown", this.#onKeyboardEvent), window.removeEventListener("keyup", this.#onKeyboardEvent), AppInterface)
window.removeEventListener(BxEvent.POINTER_LOCK_REQUESTED, this), window.removeEventListener(BxEvent.POINTER_LOCK_EXITED, this);
else
document.removeEventListener("pointerlockchange", this.#onPointerLockChange), document.removeEventListener("pointerlockerror", this.#onPointerLockError);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged), window.removeEventListener(BxEvent.XCLOUD_DIALOG_SHOWN, this.#onDialogShown), this.#mouseDataProvider?.destroy(), window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
};
start = () => {
if (!this.#enabled)
this.#enabled = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
this.#isPolling = !0, this.#escKeyDownTime = -1, this.#resetGamepad(), window.navigator.getGamepads = this.#patchedGetGamepads, this.waitForMouseData(!1), this.#mouseDataProvider?.start();
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = !0, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepadconnected", {
gamepad: virtualGamepad
}), window.BX_EXPOSED.stopTakRendering = !0, Toast.show(t("virtual-controller"), t("enabled"), { instant: !0 });
};
stop = () => {
this.#enabled = !1, this.#isPolling = !1, this.#escKeyDownTime = -1;
const virtualGamepad = this.#getVirtualGamepad();
if (virtualGamepad.connected)
this.#resetGamepad(), virtualGamepad.connected = !1, virtualGamepad.timestamp = performance.now(), BxEvent.dispatch(window, "gamepaddisconnected", {
gamepad: virtualGamepad
}), window.navigator.getGamepads = this.#nativeGetGamepads;
this.waitForMouseData(!0), this.#mouseDataProvider?.stop();
};
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, () => {
if (STATES.currentStream.titleInfo?.details.hasMkbSupport) {
if (AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === "on")
AppInterface && NativeMkbHandler.getInstance().init();
} else if (getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile()))
BxLogger.info(LOG_TAG2, "Emulate MKB"), EmulatedMkbHandler.getInstance().init();
});
}
}
// src/modules/shortcuts/shortcut-microphone.ts
var MicrophoneState;
(function(MicrophoneState2) {
MicrophoneState2["REQUESTED"] = "Requested";
MicrophoneState2["ENABLED"] = "Enabled";
MicrophoneState2["MUTED"] = "Muted";
MicrophoneState2["NOT_ALLOWED"] = "NotAllowed";
MicrophoneState2["NOT_FOUND"] = "NotFound";
})(MicrophoneState || (MicrophoneState = {}));
class MicrophoneShortcut {
static toggle(showToast = !0) {
if (!window.BX_EXPOSED.streamSession)
return !1;
const enableMic = window.BX_EXPOSED.streamSession._microphoneState === MicrophoneState.ENABLED ? !1 : !0;
try {
return window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic), showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: !0 }), enableMic;
} catch (e) {
console.log(e);
}
return !1;
}
}
// src/modules/shortcuts/shortcut-stream-ui.ts
class StreamUiShortcut {
static showHideStreamMenu() {
window.BX_EXPOSED.showStreamMenu && window.BX_EXPOSED.showStreamMenu();
}
}
// src/utils/utils.ts
function checkForUpdate() {
if (SCRIPT_VERSION.includes("beta"))
return;
const CHECK_INTERVAL_SECONDS = 7200, currentVersion = getPref(PrefKey.CURRENT_VERSION), lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK), now = Math.round(+new Date / 1000);
if (currentVersion === SCRIPT_VERSION && now - lastCheck < CHECK_INTERVAL_SECONDS)
return;
setPref(PrefKey.LAST_UPDATE_CHECK, now), fetch("https://api.github.com/repos/redphx/better-xcloud/releases/latest").then((response) => response.json()).then((json) => {
setPref(PrefKey.LATEST_VERSION, json.tag_name.substring(1)), setPref(PrefKey.CURRENT_VERSION, SCRIPT_VERSION);
}), Translations.updateTranslations(currentVersion === SCRIPT_VERSION);
}
function disablePwa() {
if (!(window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase())
return;
if (!!AppInterface || UserAgent.isSafariMobile())
Object.defineProperty(window.navigator, "standalone", {
value: !0
});
}
function hashCode(str2) {
let hash = 0;
for (let i = 0, len = str2.length;i < len; i++) {
const chr = str2.charCodeAt(i);
hash = (hash << 5) - hash + chr, hash |= 0;
}
return hash;
}
function renderString(str2, obj) {
return str2.replace(/\$\{.+?\}/g, (match) => {
const key = match.substring(2, match.length - 1);
if (key in obj)
return obj[key];
return match;
});
}
function ceilToNearest(value, interval) {
return Math.ceil(value / interval) * interval;
}
function floorToNearest(value, interval) {
return Math.floor(value / interval) * interval;
}
// src/modules/shortcuts/shortcut-sound.ts
class SoundShortcut {
static adjustGainNodeVolume(amount) {
if (!getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL))
return 0;
const currentValue = getPref(PrefKey.AUDIO_VOLUME);
let nearestValue;
if (amount > 0)
nearestValue = ceilToNearest(currentValue, amount);
else
nearestValue = floorToNearest(currentValue, -1 * amount);
let newValue;
if (currentValue !== nearestValue)
newValue = nearestValue;
else
newValue = currentValue + amount;
return newValue = setPref(PrefKey.AUDIO_VOLUME, newValue), SoundShortcut.setGainNodeVolume(newValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: !0 }), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, {
volume: newValue
}), newValue;
}
static setGainNodeVolume(value) {
STATES.currentStream.audioGainNode && (STATES.currentStream.audioGainNode.gain.value = value / 100);
}
static muteUnmute() {
if (getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && STATES.currentStream.audioGainNode) {
const gainValue = STATES.currentStream.audioGainNode.gain.value, settingValue = getPref(PrefKey.AUDIO_VOLUME);
let targetValue;
if (settingValue === 0)
targetValue = 100, setPref(PrefKey.AUDIO_VOLUME, targetValue), BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, {
volume: targetValue
});
else if (gainValue === 0)
targetValue = settingValue;
else
targetValue = 0;
let status;
if (targetValue === 0)
status = t("muted");
else
status = targetValue + "%";
SoundShortcut.setGainNodeVolume(targetValue), Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 });
return;
}
let $media;
if ($media = document.querySelector("div[data-testid=media-container] audio"), !$media)
$media = document.querySelector("div[data-testid=media-container] video");
if ($media) {
$media.muted = !$media.muted;
const status = $media.muted ? t("muted") : t("unmuted");
Toast.show(`${t("stream")} ❯ ${t("volume")}`, status, { instant: !0 });
}
}
}
// src/modules/controller-shortcut.ts
var ShortcutAction;
(function(ShortcutAction2) {
ShortcutAction2["STREAM_SCREENSHOT_CAPTURE"] = "stream-screenshot-capture";
ShortcutAction2["STREAM_MENU_SHOW"] = "stream-menu-show";
ShortcutAction2["STREAM_STATS_TOGGLE"] = "stream-stats-toggle";
ShortcutAction2["STREAM_SOUND_TOGGLE"] = "stream-sound-toggle";
ShortcutAction2["STREAM_MICROPHONE_TOGGLE"] = "stream-microphone-toggle";
ShortcutAction2["STREAM_VOLUME_INC"] = "stream-volume-inc";
ShortcutAction2["STREAM_VOLUME_DEC"] = "stream-volume-dec";
ShortcutAction2["DEVICE_SOUND_TOGGLE"] = "device-sound-toggle";
ShortcutAction2["DEVICE_VOLUME_INC"] = "device-volume-inc";
ShortcutAction2["DEVICE_VOLUME_DEC"] = "device-volume-dec";
ShortcutAction2["DEVICE_BRIGHTNESS_INC"] = "device-brightness-inc";
ShortcutAction2["DEVICE_BRIGHTNESS_DEC"] = "device-brightness-dec";
})(ShortcutAction || (ShortcutAction = {}));
class ControllerShortcut {
static #STORAGE_KEY = "better_xcloud_controller_shortcuts";
static #buttonsCache = {};
static #buttonsStatus = {};
static #$selectProfile;
static #$selectActions = {};
static #$container;
static #ACTIONS = {};
static reset(index) {
ControllerShortcut.#buttonsCache[index] = [], ControllerShortcut.#buttonsStatus[index] = [];
}
static handle(gamepad) {
const gamepadIndex = gamepad.index, actions = ControllerShortcut.#ACTIONS[gamepad.id];
if (!actions)
return !1;
ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0), ControllerShortcut.#buttonsStatus[gamepadIndex] = [];
const pressed = [];
let otherButtonPressed = !1;
return gamepad.buttons.forEach((button, index) => {
if (button.pressed && index !== GamepadKey.HOME) {
if (otherButtonPressed = !0, pressed[index] = !0, actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index])
setTimeout(() => ControllerShortcut.#runAction(actions[index]), 0);
}
}), ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed, otherButtonPressed;
}
static #runAction(action) {
switch (action) {
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
Screenshot.takeScreenshot();
break;
case ShortcutAction.STREAM_STATS_TOGGLE:
StreamStats.getInstance().toggle();
break;
case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
MicrophoneShortcut.toggle();
break;
case ShortcutAction.STREAM_MENU_SHOW:
StreamUiShortcut.showHideStreamMenu();
break;
case ShortcutAction.STREAM_SOUND_TOGGLE:
SoundShortcut.muteUnmute();
break;
case ShortcutAction.STREAM_VOLUME_INC:
SoundShortcut.adjustGainNodeVolume(10);
break;
case ShortcutAction.STREAM_VOLUME_DEC:
SoundShortcut.adjustGainNodeVolume(-10);
break;
case ShortcutAction.DEVICE_BRIGHTNESS_INC:
case ShortcutAction.DEVICE_BRIGHTNESS_DEC:
case ShortcutAction.DEVICE_SOUND_TOGGLE:
case ShortcutAction.DEVICE_VOLUME_INC:
case ShortcutAction.DEVICE_VOLUME_DEC:
AppInterface && AppInterface.runShortcut && AppInterface.runShortcut(action);
break;
}
}
static #updateAction(profile, button, action) {
if (!(profile in ControllerShortcut.#ACTIONS))
ControllerShortcut.#ACTIONS[profile] = [];
if (!action)
action = null;
ControllerShortcut.#ACTIONS[profile][button] = action;
for (let key in ControllerShortcut.#ACTIONS) {
let empty = !0;
for (let value of ControllerShortcut.#ACTIONS[key])
if (value) {
empty = !1;
break;
}
if (empty)
delete ControllerShortcut.#ACTIONS[key];
}
window.localStorage.setItem(ControllerShortcut.#STORAGE_KEY, JSON.stringify(ControllerShortcut.#ACTIONS)), console.log(ControllerShortcut.#ACTIONS);
}
static #updateProfileList(e) {
const $select = ControllerShortcut.#$selectProfile, $container = ControllerShortcut.#$container, $fragment = document.createDocumentFragment();
while ($select.firstElementChild)
$select.firstElementChild.remove();
const gamepads = navigator.getGamepads();
let hasGamepad = !1;
for (let gamepad of gamepads) {
if (!gamepad || !gamepad.connected)
continue;
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID)
continue;
hasGamepad = !0;
const $option = CE("option", { value: gamepad.id }, gamepad.id);
$fragment.appendChild($option);
}
if (hasGamepad)
$select.appendChild($fragment), $select.selectedIndex = 0, $select.dispatchEvent(new Event("change"));
$container.dataset.hasGamepad = hasGamepad.toString();
}
static #switchProfile(profile) {
let actions = ControllerShortcut.#ACTIONS[profile];
if (!actions)
actions = [];
let button;
for (button in ControllerShortcut.#$selectActions) {
const $select = ControllerShortcut.#$selectActions[button];
$select.value = actions[button] || "", BxEvent.dispatch($select, "change", {
ignoreOnChange: !0
});
}
}
static renderSettings() {
ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || "{}");
const buttons = new Map;
buttons.set(GamepadKey.Y, PrompFont.Y), buttons.set(GamepadKey.A, PrompFont.A), buttons.set(GamepadKey.B, PrompFont.B), buttons.set(GamepadKey.X, PrompFont.X), buttons.set(GamepadKey.UP, PrompFont.UP), buttons.set(GamepadKey.DOWN, PrompFont.DOWN), buttons.set(GamepadKey.LEFT, PrompFont.LEFT), buttons.set(GamepadKey.RIGHT, PrompFont.RIGHT), buttons.set(GamepadKey.SELECT, PrompFont.SELECT), buttons.set(GamepadKey.START, PrompFont.START), buttons.set(GamepadKey.LB, PrompFont.LB), buttons.set(GamepadKey.RB, PrompFont.RB), buttons.set(GamepadKey.LT, PrompFont.LT), buttons.set(GamepadKey.RT, PrompFont.RT), buttons.set(GamepadKey.L3, PrompFont.L3), buttons.set(GamepadKey.R3, PrompFont.R3);
const actions = {
[t("device")]: AppInterface && {
[ShortcutAction.DEVICE_SOUND_TOGGLE]: [t("sound"), t("toggle")],
[ShortcutAction.DEVICE_VOLUME_INC]: [t("volume"), t("increase")],
[ShortcutAction.DEVICE_VOLUME_DEC]: [t("volume"), t("decrease")],
[ShortcutAction.DEVICE_BRIGHTNESS_INC]: [t("brightness"), t("increase")],
[ShortcutAction.DEVICE_BRIGHTNESS_DEC]: [t("brightness"), t("decrease")]
},
[t("stream")]: {
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: t("take-screenshot"),
[ShortcutAction.STREAM_SOUND_TOGGLE]: [t("sound"), t("toggle")],
[ShortcutAction.STREAM_VOLUME_INC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t("volume"), t("increase")],
[ShortcutAction.STREAM_VOLUME_DEC]: getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && [t("volume"), t("decrease")],
[ShortcutAction.STREAM_MENU_SHOW]: [t("menu"), t("show")],
[ShortcutAction.STREAM_STATS_TOGGLE]: [t("stats"), t("show-hide")],
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t("microphone"), t("toggle")]
}
}, $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---"));
for (let groupLabel in actions) {
const items = actions[groupLabel];
if (!items)
continue;
const $optGroup = CE("optgroup", { label: groupLabel });
for (let action in items) {
let label = items[action];
if (!label)
continue;
if (Array.isArray(label))
label = label.join(" ❯ ");
const $option = CE("option", { value: action }, label);
$optGroup.appendChild($option);
}
$baseSelect.appendChild($optGroup);
}
let $remap, $selectProfile;
const $container = CE("div", { "data-has-gamepad": "false" }, CE("div", {}, CE("p", { class: "bx-shortcut-note" }, t("controller-shortcuts-connect-note"))), $remap = CE("div", {}, $selectProfile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" }), CE("p", { class: "bx-shortcut-note" }, CE("span", { class: "bx-prompt" }, PrompFont.HOME), ": " + t("controller-shortcuts-xbox-note"))));
$selectProfile.addEventListener("change", (e) => {
ControllerShortcut.#switchProfile($selectProfile.value);
});
const onActionChanged = (e) => {
const $target = e.target, profile = $selectProfile.value, button = $target.dataset.button, action = $target.value, $fakeSelect = $target.previousElementSibling;
let fakeText = "---";
if (action) {
const $selectedOption = $target.options[$target.selectedIndex];
fakeText = $selectedOption.parentElement.label + " ❯ " + $selectedOption.text;
}
$fakeSelect.firstElementChild.text = fakeText, !e.ignoreOnChange && ControllerShortcut.#updateAction(profile, button, action);
};
for (let [button, prompt2] of buttons) {
const $row = CE("div", { class: "bx-shortcut-row" }), $label = CE("label", { class: "bx-prompt" }, `${PrompFont.HOME} + ${prompt2}`), $div = CE("div", { class: "bx-shortcut-actions" }), $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---"));
$div.appendChild($fakeSelect);
const $select = $baseSelect.cloneNode(!0);
$select.dataset.button = button.toString(), $select.addEventListener("change", onActionChanged), ControllerShortcut.#$selectActions[button] = $select, $div.appendChild($select), $row.appendChild($label), $row.appendChild($div), $remap.appendChild($row);
}
return $container.appendChild($remap), ControllerShortcut.#$selectProfile = $selectProfile, ControllerShortcut.#$container = $container, window.addEventListener("gamepadconnected", ControllerShortcut.#updateProfileList), window.addEventListener("gamepaddisconnected", ControllerShortcut.#updateProfileList), ControllerShortcut.#updateProfileList(), $container;
}
}
// src/utils/bx-exposed.ts
var InputType;
(function(InputType2) {
InputType2["CONTROLLER"] = "Controller";
InputType2["MKB"] = "MKB";
InputType2["CUSTOM_TOUCH_OVERLAY"] = "CustomTouchOverlay";
InputType2["GENERIC_TOUCH"] = "GenericTouch";
InputType2["NATIVE_TOUCH"] = "NativeTouch";
InputType2["BATIVE_SENSOR"] = "NativeSensor";
})(InputType || (InputType = {}));
var BxExposed = {
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyTitleInfo: (titleInfo) => {
titleInfo = structuredClone(titleInfo);
let supportedInputTypes = titleInfo.details.supportedInputTypes;
if (BX_FLAGS.ForceNativeMkbTitles?.includes(titleInfo.details.productId))
supportedInputTypes.push(InputType.MKB);
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "off")
supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.MKB);
if (titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB), STATES.userAgentHasTouchSupport) {
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
if (touchControllerAvailability !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
const gamepads = window.navigator.getGamepads();
let gamepadFound = !1;
for (let gamepad of gamepads)
if (gamepad && gamepad.connected) {
gamepadFound = !0;
break;
}
gamepadFound && (touchControllerAvailability = "off");
}
if (touchControllerAvailability === "off")
supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.CUSTOM_TOUCH_OVERLAY && i !== InputType.GENERIC_TOUCH), titleInfo.details.supportedTabs = [];
if (titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH), titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) || supportedInputTypes.includes(InputType.GENERIC_TOUCH), !titleInfo.details.hasTouchSupport && touchControllerAvailability === "all")
titleInfo.details.hasFakeTouchSupport = !0, supportedInputTypes.push(InputType.GENERIC_TOUCH);
}
return titleInfo.details.supportedInputTypes = supportedInputTypes, STATES.currentStream.titleInfo = titleInfo, BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY), titleInfo;
},
setupGainNode: ($media, audioStream) => {
if ($media instanceof HTMLAudioElement)
$media.muted = !0, $media.addEventListener("playing", (e) => {
$media.muted = !0, $media.pause();
});
else
$media.muted = !0, $media.addEventListener("playing", (e) => {
$media.muted = !0;
});
try {
const audioCtx = STATES.currentStream.audioContext, source = audioCtx.createMediaStreamSource(audioStream), gainNode = audioCtx.createGain();
source.connect(gainNode).connect(audioCtx.destination);
} catch (e) {
BxLogger.error("setupGainNode", e), STATES.currentStream.audioGainNode = null;
}
},
handleControllerShortcut: ControllerShortcut.handle,
resetControllerShortcut: ControllerShortcut.reset
};
// src/utils/region.ts
function getPreferredServerRegion(shortName = !1) {
let preferredRegion = getPref(PrefKey.SERVER_REGION);
if (preferredRegion in STATES.serverRegions)
if (shortName && STATES.serverRegions[preferredRegion].shortName)
return STATES.serverRegions[preferredRegion].shortName;
else
return preferredRegion;
for (let regionName in STATES.serverRegions) {
const region = STATES.serverRegions[regionName];
if (!region.isDefault)
continue;
if (shortName && region.shortName)
return region.shortName;
else
return regionName;
}
return "???";
}
// src/modules/loading-screen.ts
class LoadingScreen {
static #$bgStyle;
static #$waitTimeBox;
static #waitTimeInterval = null;
static #orgWebTitle;
static #secondsToString(seconds) {
const m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");
return mDisplay + sDisplay;
}
static setup() {
const titleInfo = STATES.currentStream.titleInfo;
if (!titleInfo)
return;
if (!LoadingScreen.#$bgStyle) {
const $bgStyle = CE("style");
document.documentElement.appendChild($bgStyle), LoadingScreen.#$bgStyle = $bgStyle;
}
if (LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl), getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide")
LoadingScreen.#hideRocket();
}
static #hideRocket() {
let $bgStyle = LoadingScreen.#$bgStyle;
const css = `
#game-stream div[class*=RocketAnimation-module__container] > svg {
display: none;
}
#game-stream video[class*=RocketAnimationVideo-module__video] {
display: none;
}
`;
$bgStyle.textContent += css;
}
static #setBackground(imageUrl) {
let $bgStyle = LoadingScreen.#$bgStyle;
imageUrl = imageUrl + "?w=1920";
const css = `
#game-stream {
background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;
background-color: transparent !important;
background-position: center center !important;
background-repeat: no-repeat !important;
background-size: cover !important;
}
#game-stream rect[width="800"] {
transition: opacity 0.3s ease-in-out !important;
}
`;
$bgStyle.textContent += css;
const bg = new Image;
bg.onload = (e) => {
$bgStyle.textContent += `
#game-stream rect[width="800"] {
opacity: 0 !important;
}
`;
}, bg.src = imageUrl;
}
static setupWaitTime(waitTime) {
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === "hide-queue")
LoadingScreen.#hideRocket();
let secondsLeft = waitTime, $countDown, $estimated;
LoadingScreen.#orgWebTitle = document.title;
const endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);
let endDateStr = endDate.toISOString().slice(0, 19);
endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`;
let $waitTimeBox = LoadingScreen.#$waitTimeBox;
if (!$waitTimeBox)
$waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", {}, t("server")), CE("span", {}, getPreferredServerRegion()), CE("label", {}, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", {}, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.#$waitTimeBox = $waitTimeBox;
else
$waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");
$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`, LoadingScreen.#waitTimeInterval = window.setInterval(() => {
if (secondsLeft--, $countDown.textContent = LoadingScreen.#secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`, secondsLeft <= 0)
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval), LoadingScreen.#waitTimeInterval = null;
}, 1000);
}
static hide() {
if (LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle), LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"), getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) {
const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
$rocketBg && $rocketBg.addEventListener("transitionend", (e) => {
LoadingScreen.#$bgStyle.textContent += `
#game-stream {
background: #000 !important;
}
`;
}), LoadingScreen.#$bgStyle.textContent += `
#game-stream rect[width="800"] {
opacity: 1 !important;
}
`;
}
setTimeout(LoadingScreen.reset, 2000);
}
static reset() {
LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ""), LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone"), LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval), LoadingScreen.#waitTimeInterval = null;
}
}
// src/modules/dialog.ts
class Dialog {
$dialog;
$title;
$content;
$overlay;
onClose;
constructor(options) {
const {
title,
className,
content,
hideCloseButton,
onClose,
helpUrl
} = options, $overlay = document.querySelector(".bx-dialog-overlay");
if (!$overlay)
this.$overlay = CE("div", { class: "bx-dialog-overlay bx-gone" }), this.$overlay.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$overlay);
else
this.$overlay = $overlay;
let $close;
this.onClose = onClose, this.$dialog = CE("div", { class: `bx-dialog ${className || ""} bx-gone` }, this.$title = CE("h2", {}, CE("b", {}, title), helpUrl && createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST,
title: t("help"),
url: helpUrl
})), this.$content = CE("div", { class: "bx-dialog-content" }, content), !hideCloseButton && ($close = CE("button", { type: "button" }, t("close")))), $close && $close.addEventListener("click", (e) => {
this.hide(e);
}), !title && this.$title.classList.add("bx-gone"), !content && this.$content.classList.add("bx-gone"), this.$dialog.addEventListener("contextmenu", (e) => e.preventDefault()), document.documentElement.appendChild(this.$dialog);
}
show(newOptions) {
if (document.activeElement && document.activeElement.blur(), newOptions && newOptions.title)
this.$title.querySelector("b").textContent = newOptions.title, this.$title.classList.remove("bx-gone");
this.$dialog.classList.remove("bx-gone"), this.$overlay.classList.remove("bx-gone"), document.body.classList.add("bx-no-scroll");
}
hide(e) {
this.$dialog.classList.add("bx-gone"), this.$overlay.classList.add("bx-gone"), document.body.classList.remove("bx-no-scroll"), this.onClose && this.onClose(e);
}
toggle() {
this.$dialog.classList.toggle("bx-gone"), this.$overlay.classList.toggle("bx-gone");
}
}
// src/modules/mkb/mkb-remapper.ts
class MkbRemapper {
#BUTTON_ORDERS = [
GamepadKey.UP,
GamepadKey.DOWN,
GamepadKey.LEFT,
GamepadKey.RIGHT,
GamepadKey.A,
GamepadKey.B,
GamepadKey.X,
GamepadKey.Y,
GamepadKey.LB,
GamepadKey.RB,
GamepadKey.LT,
GamepadKey.RT,
GamepadKey.SELECT,
GamepadKey.START,
GamepadKey.HOME,
GamepadKey.L3,
GamepadKey.LS_UP,
GamepadKey.LS_DOWN,
GamepadKey.LS_LEFT,
GamepadKey.LS_RIGHT,
GamepadKey.R3,
GamepadKey.RS_UP,
GamepadKey.RS_DOWN,
GamepadKey.RS_LEFT,
GamepadKey.RS_RIGHT
];
static #instance;
static get INSTANCE() {
if (!MkbRemapper.#instance)
MkbRemapper.#instance = new MkbRemapper;
return MkbRemapper.#instance;
}
#STATE = {
currentPresetId: 0,
presets: {},
editingPresetData: null,
isEditing: !1
};
#$ = {
wrapper: null,
presetsSelect: null,
activateButton: null,
currentBindingKey: null,
allKeyElements: [],
allMouseElements: {}
};
bindingDialog;
constructor() {
this.#STATE.currentPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID), this.bindingDialog = new Dialog({
className: "bx-binding-dialog",
content: CE("div", {}, CE("p", {}, t("press-to-bind")), CE("i", {}, t("press-esc-to-cancel"))),
hideCloseButton: !0
});
}
#clearEventListeners = () => {
window.removeEventListener("keydown", this.#onKeyDown), window.removeEventListener("mousedown", this.#onMouseDown), window.removeEventListener("wheel", this.#onWheel);
};
#bindKey = ($elm, key) => {
const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot"));
if ($elm.getAttribute("data-key-code") === key.code)
return;
for (let $otherElm of this.#$.allKeyElements)
if ($otherElm.getAttribute("data-key-code") === key.code)
this.#unbindKey($otherElm);
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = key.code, $elm.textContent = key.name, $elm.setAttribute("data-key-code", key.code);
};
#unbindKey = ($elm) => {
const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot"));
this.#STATE.editingPresetData.mapping[buttonIndex][keySlot] = null, $elm.textContent = "", $elm.removeAttribute("data-key-code");
};
#onWheel = (e) => {
e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
};
#onMouseDown = (e) => {
e.preventDefault(), this.#clearEventListeners(), this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e)), window.setTimeout(() => this.bindingDialog.hide(), 200);
};
#onKeyDown = (e) => {
if (e.preventDefault(), e.stopPropagation(), this.#clearEventListeners(), e.code !== "Escape")
this.#bindKey(this.#$.currentBindingKey, KeyHelper.getKeyFromEvent(e));
window.setTimeout(() => this.bindingDialog.hide(), 200);
};
#onBindingKey = (e) => {
if (!this.#STATE.isEditing || e.button !== 0)
return;
console.log(e), this.#$.currentBindingKey = e.target, window.addEventListener("keydown", this.#onKeyDown), window.addEventListener("mousedown", this.#onMouseDown), window.addEventListener("wheel", this.#onWheel), this.bindingDialog.show({ title: this.#$.currentBindingKey.getAttribute("data-prompt") });
};
#onContextMenu = (e) => {
if (e.preventDefault(), !this.#STATE.isEditing)
return;
this.#unbindKey(e.target);
};
#getPreset = (presetId) => {
return this.#STATE.presets[presetId];
};
#getCurrentPreset = () => {
return this.#getPreset(this.#STATE.currentPresetId);
};
#switchPreset = (presetId) => {
this.#STATE.currentPresetId = presetId;
const presetData = this.#getCurrentPreset().data;
for (let $elm of this.#$.allKeyElements) {
const buttonIndex = parseInt($elm.getAttribute("data-button-index")), keySlot = parseInt($elm.getAttribute("data-key-slot")), buttonKeys = presetData.mapping[buttonIndex];
if (buttonKeys && buttonKeys[keySlot])
$elm.textContent = KeyHelper.codeToKeyName(buttonKeys[keySlot]), $elm.setAttribute("data-key-code", buttonKeys[keySlot]);
else
$elm.textContent = "", $elm.removeAttribute("data-key-code");
}
let key;
for (key in this.#$.allMouseElements) {
const $elm = this.#$.allMouseElements[key];
let value = presetData.mouse[key];
if (typeof value === "undefined")
value = MkbPreset.MOUSE_SETTINGS[key].default;
"setValue" in $elm && $elm.setValue(value);
}
const activated = getPref(PrefKey.MKB_DEFAULT_PRESET_ID) === this.#STATE.currentPresetId;
this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate");
};
#refresh() {
while (this.#$.presetsSelect.firstChild)
this.#$.presetsSelect.removeChild(this.#$.presetsSelect.firstChild);
LocalDb.INSTANCE.getPresets().then((presets) => {
this.#STATE.presets = presets;
const $fragment = document.createDocumentFragment();
let defaultPresetId;
if (this.#STATE.currentPresetId === 0)
this.#STATE.currentPresetId = parseInt(Object.keys(presets)[0]), defaultPresetId = this.#STATE.currentPresetId, setPref(PrefKey.MKB_DEFAULT_PRESET_ID, defaultPresetId), EmulatedMkbHandler.getInstance().refreshPresetData();
else
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
for (let id2 in presets) {
let name = presets[id2].name;
if (id2 === defaultPresetId)
name = "🎮 " + name;
const $options = CE("option", { value: id2 }, name);
$options.selected = parseInt(id2) === this.#STATE.currentPresetId, $fragment.appendChild($options);
}
this.#$.presetsSelect.appendChild($fragment);
const activated = defaultPresetId === this.#STATE.currentPresetId;
this.#$.activateButton.disabled = activated, this.#$.activateButton.querySelector("span").textContent = activated ? t("activated") : t("activate"), !this.#STATE.isEditing && this.#switchPreset(this.#STATE.currentPresetId);
});
}
#toggleEditing = (force) => {
if (this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing, this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing), this.#STATE.isEditing)
this.#STATE.editingPresetData = structuredClone(this.#getCurrentPreset().data);
else
this.#STATE.editingPresetData = null;
const childElements = this.#$.wrapper.querySelectorAll("select, button, input");
for (let $elm of Array.from(childElements)) {
if ($elm.parentElement.parentElement.classList.contains("bx-mkb-action-buttons"))
continue;
let disable = !this.#STATE.isEditing;
if ($elm.parentElement.classList.contains("bx-mkb-preset-tools"))
disable = !disable;
$elm.disabled = disable;
}
};
render() {
this.#$.wrapper = CE("div", { class: "bx-mkb-settings" }), this.#$.presetsSelect = CE("select", {}), this.#$.presetsSelect.addEventListener("change", (e) => {
this.#switchPreset(parseInt(e.target.value));
});
const promptNewName = (value) => {
let newName = "";
while (!newName) {
if (newName = prompt(t("prompt-preset-name"), value), newName === null)
return !1;
newName = newName.trim();
}
return newName ? newName : !1;
}, $header = CE("div", { class: "bx-mkb-preset-tools" }, this.#$.presetsSelect, createButton({
title: t("rename"),
icon: BxIcon.CURSOR_TEXT,
onClick: (e) => {
const preset = this.#getCurrentPreset();
let newName = promptNewName(preset.name);
if (!newName || newName === preset.name)
return;
preset.name = newName, LocalDb.INSTANCE.updatePreset(preset).then((id2) => this.#refresh());
}
}), createButton({
icon: BxIcon.NEW,
title: t("new"),
onClick: (e) => {
let newName = promptNewName("");
if (!newName)
return;
LocalDb.INSTANCE.newPreset(newName, MkbPreset.DEFAULT_PRESET).then((id2) => {
this.#STATE.currentPresetId = id2, this.#refresh();
});
}
}), createButton({
icon: BxIcon.COPY,
title: t("copy"),
onClick: (e) => {
const preset = this.#getCurrentPreset();
let newName = promptNewName(`${preset.name} (2)`);
if (!newName)
return;
LocalDb.INSTANCE.newPreset(newName, preset.data).then((id2) => {
this.#STATE.currentPresetId = id2, this.#refresh();
});
}
}), createButton({
icon: BxIcon.TRASH,
style: ButtonStyle.DANGER,
title: t("delete"),
onClick: (e) => {
if (!confirm(t("confirm-delete-preset")))
return;
LocalDb.INSTANCE.deletePreset(this.#STATE.currentPresetId).then((id2) => {
this.#STATE.currentPresetId = 0, this.#refresh();
});
}
}));
this.#$.wrapper.appendChild($header);
const $rows = CE("div", { class: "bx-mkb-settings-rows" }, CE("i", { class: "bx-mkb-note" }, t("right-click-to-unbind"))), keysPerButton = 2;
for (let buttonIndex of this.#BUTTON_ORDERS) {
const [buttonName, buttonPrompt] = GamepadKeyName[buttonIndex];
let $elm;
const $fragment = document.createDocumentFragment();
for (let i = 0;i < keysPerButton; i++)
$elm = CE("button", {
type: "button",
"data-prompt": buttonPrompt,
"data-button-index": buttonIndex,
"data-key-slot": i
}, " "), $elm.addEventListener("mouseup", this.#onBindingKey), $elm.addEventListener("contextmenu", this.#onContextMenu), $fragment.appendChild($elm), this.#$.allKeyElements.push($elm);
const $keyRow = CE("div", { class: "bx-mkb-key-row" }, CE("label", { title: buttonName }, buttonPrompt), $fragment);
$rows.appendChild($keyRow);
}
$rows.appendChild(CE("i", { class: "bx-mkb-note" }, t("mkb-adjust-ingame-settings")));
const $mouseSettings = document.createDocumentFragment();
for (let key in MkbPreset.MOUSE_SETTINGS) {
const setting = MkbPreset.MOUSE_SETTINGS[key], value = setting.default;
let $elm;
const onChange = (e, value2) => {
this.#STATE.editingPresetData.mouse[key] = value2;
}, $row = CE("div", { class: "bx-stream-settings-row" }, CE("label", { for: `bx_setting_${key}` }, setting.label), $elm = SettingElement.render(setting.type, key, setting, value, onChange, setting.params));
$mouseSettings.appendChild($row), this.#$.allMouseElements[key] = $elm;
}
$rows.appendChild($mouseSettings), this.#$.wrapper.appendChild($rows);
const $actionButtons = CE("div", { class: "bx-mkb-action-buttons" }, CE("div", {}, createButton({
label: t("edit"),
onClick: (e) => this.#toggleEditing(!0)
}), this.#$.activateButton = createButton({
label: t("activate"),
style: ButtonStyle.PRIMARY,
onClick: (e) => {
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId), EmulatedMkbHandler.getInstance().refreshPresetData(), this.#refresh();
}
})), CE("div", {}, createButton({
label: t("cancel"),
style: ButtonStyle.GHOST,
onClick: (e) => {
this.#switchPreset(this.#STATE.currentPresetId), this.#toggleEditing(!1);
}
}), createButton({
label: t("save"),
style: ButtonStyle.PRIMARY,
onClick: (e) => {
const updatedPreset = structuredClone(this.#getCurrentPreset());
updatedPreset.data = this.#STATE.editingPresetData, LocalDb.INSTANCE.updatePreset(updatedPreset).then((id2) => {
if (id2 === getPref(PrefKey.MKB_DEFAULT_PRESET_ID))
EmulatedMkbHandler.getInstance().refreshPresetData();
this.#toggleEditing(!1), this.#refresh();
});
}
})));
return this.#$.wrapper.appendChild($actionButtons), this.#toggleEditing(!1), this.#refresh(), this.#$.wrapper;
}
}
// src/modules/touch-controller.ts
var LOG_TAG3 = "TouchController";
class TouchController {
static #EVENT_SHOW_DEFAULT_CONTROLLER = new MessageEvent("message", {
data: JSON.stringify({
content: '{"layoutId":""}',
target: "/streaming/touchcontrols/showlayoutv2",
type: "Message"
}),
origin: "better-xcloud"
});
static #$style;
static #enable = !1;
static #dataChannel;
static #customLayouts = {};
static #baseCustomLayouts = {};
static #currentLayoutId;
static #customList;
static enable() {
TouchController.#enable = !0;
}
static disable() {
TouchController.#enable = !1;
}
static isEnabled() {
return TouchController.#enable;
}
static #showDefault() {
TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);
}
static #show() {
document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");
}
static #hide() {
document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.add("bx-offscreen");
}
static toggleVisibility(status) {
if (!TouchController.#dataChannel)
return;
status ? TouchController.#hide() : TouchController.#show();
}
static reset() {
TouchController.#enable = !1, TouchController.#dataChannel = null, TouchController.#$style && (TouchController.#$style.textContent = "");
}
static #dispatchMessage(msg) {
TouchController.#dataChannel && window.setTimeout(() => {
TouchController.#dataChannel.dispatchEvent(msg);
}, 10);
}
static #dispatchLayouts(data) {
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
data
});
}
static async getCustomLayouts(xboxTitleId, retries = 1) {
if (xboxTitleId in TouchController.#customLayouts) {
TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);
return;
}
if (retries = retries || 1, retries > 2) {
TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);
return;
}
const baseUrl = `https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts${BX_FLAGS.UseDevTouchLayout ? "/dev" : ""}`, url = `${baseUrl}/${xboxTitleId}.json`;
try {
const json = await (await NATIVE_FETCH(url)).json(), layouts = {};
json.layouts.forEach(async (layoutName) => {
let baseLayouts = {};
if (layoutName in TouchController.#baseCustomLayouts)
baseLayouts = TouchController.#baseCustomLayouts[layoutName];
else
try {
const layoutUrl = `${baseUrl}/layouts/${layoutName}.json`;
baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;
} catch (e) {
}
Object.assign(layouts, baseLayouts);
}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);
} catch (e) {
TouchController.getCustomLayouts(xboxTitleId, retries + 1);
}
}
static loadCustomLayout(xboxTitleId, layoutId, delay = 0) {
if (!window.BX_EXPOSED.touchLayoutManager) {
const listener = (e) => {
if (window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener), TouchController.#enable)
TouchController.loadCustomLayout(xboxTitleId, layoutId, 0);
};
window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
return;
}
const layoutChanged = TouchController.#currentLayoutId !== layoutId;
TouchController.#currentLayoutId = layoutId;
const layoutData = TouchController.#customLayouts[xboxTitleId];
if (!xboxTitleId || !layoutId || !layoutData) {
TouchController.#enable && TouchController.#showDefault();
return;
}
const layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];
if (!layout)
return;
let msg, html15 = !1;
if (layout.author) {
const author = `${escapeHtml(layout.author)}`;
msg = t("touch-control-layout-by", { name: author }), html15 = !0;
} else
msg = t("touch-control-layout");
layoutChanged && Toast.show(msg, layout.name, { html: html15 }), window.setTimeout(() => {
window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({
type: "showLayout",
scope: xboxTitleId,
subscope: "base",
layout: {
id: "System.Standard",
displayName: "System",
layoutFile: layout
}
});
}, delay);
}
static updateCustomList() {
TouchController.#customList = JSON.parse(window.localStorage.getItem("better_xcloud_custom_touch_layouts") || "[]"), NATIVE_FETCH("https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/touch-layouts/ids.json").then((response) => response.json()).then((json) => {
TouchController.#customList = json, window.localStorage.setItem("better_xcloud_custom_touch_layouts", JSON.stringify(json));
});
}
static getCustomList() {
return TouchController.#customList;
}
static setup() {
window.testTouchLayout = (layout) => {
const { touchLayoutManager } = window.BX_EXPOSED;
touchLayoutManager && touchLayoutManager.changeLayoutForScope({
type: "showLayout",
scope: "" + STATES.currentStream?.xboxTitleId,
subscope: "base",
layout: {
id: "System.Standard",
displayName: "Custom",
layoutFile: layout
}
});
};
const $style = document.createElement("style");
document.documentElement.appendChild($style), TouchController.#$style = $style;
const PREF_STYLE_STANDARD = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD), PREF_STYLE_CUSTOM = getPref(PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM);
window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => {
const dataChannel = e.dataChannel;
if (!dataChannel || dataChannel.label !== "message")
return;
let filter = "";
if (TouchController.#enable) {
if (PREF_STYLE_STANDARD === "white")
filter = "grayscale(1) brightness(2)";
else if (PREF_STYLE_STANDARD === "muted")
filter = "sepia(0.5)";
} else if (PREF_STYLE_CUSTOM === "muted")
filter = "sepia(0.5)";
if (filter)
$style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;
else
$style.textContent = "";
TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {
window.setTimeout(TouchController.#show, 1000);
});
let focused = !1;
dataChannel.addEventListener("message", (msg) => {
if (msg.origin === "better-xcloud" || typeof msg.data !== "string")
return;
if (msg.data.includes("touchcontrols/showtitledefault")) {
if (TouchController.#enable)
if (focused)
TouchController.getCustomLayouts(STATES.currentStream?.xboxTitleId);
else
TouchController.#showDefault();
return;
}
try {
if (msg.data.includes("/titleinfo")) {
const json = JSON.parse(JSON.parse(msg.data).content);
if (focused = json.focused, !json.focused)
TouchController.#show();
STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString();
}
} catch (e2) {
BxLogger.error(LOG_TAG3, "Load custom layout", e2);
}
});
});
}
}
// src/modules/vibration-manager.ts
var VIBRATION_DATA_MAP = {
gamepadIndex: 8,
leftMotorPercent: 8,
rightMotorPercent: 8,
leftTriggerMotorPercent: 8,
rightTriggerMotorPercent: 8,
durationMs: 16
};
class VibrationManager {
static #playDeviceVibration(data) {
if (AppInterface) {
AppInterface.vibrate(JSON.stringify(data), window.BX_VIBRATION_INTENSITY);
return;
}
const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY;
if (intensity === 0 || intensity === 100) {
window.navigator.vibrate(intensity ? data.durationMs : 0);
return;
}
const pulseDuration = 200, onDuration = Math.floor(pulseDuration * intensity / 100), offDuration = pulseDuration - onDuration, repeats = Math.ceil(data.durationMs / pulseDuration), pulses = Array(repeats).fill([onDuration, offDuration]).flat();
window.navigator.vibrate(pulses);
}
static supportControllerVibration() {
return Gamepad.prototype.hasOwnProperty("vibrationActuator");
}
static supportDeviceVibration() {
return !!window.navigator.vibrate;
}
static updateGlobalVars() {
if (window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref(PrefKey.CONTROLLER_ENABLE_VIBRATION) : !1, window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100, !VibrationManager.supportDeviceVibration()) {
window.BX_ENABLE_DEVICE_VIBRATION = !1;
return;
}
window.navigator.vibrate(0);
const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION);
let enabled;
if (value === "on")
enabled = !0;
else if (value === "auto") {
enabled = !0;
const gamepads = window.navigator.getGamepads();
for (let gamepad of gamepads)
if (gamepad) {
enabled = !1;
break;
}
} else
enabled = !1;
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
}
static #onMessage(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, messageType;
if (dataView.byteLength === 13)
messageType = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
else
messageType = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
if (!(messageType & 128))
return;
const vibrationType = dataView.getUint8(offset);
if (offset += Uint8Array.BYTES_PER_ELEMENT, vibrationType !== 0)
return;
const data = {};
let key;
for (key in VIBRATION_DATA_MAP)
if (VIBRATION_DATA_MAP[key] === 16)
data[key] = dataView.getUint16(offset, !0), offset += Uint16Array.BYTES_PER_ELEMENT;
else
data[key] = dataView.getUint8(offset), offset += Uint8Array.BYTES_PER_ELEMENT;
VibrationManager.#playDeviceVibration(data);
}
static initialSetup() {
window.addEventListener("gamepadconnected", VibrationManager.updateGlobalVars), window.addEventListener("gamepaddisconnected", VibrationManager.updateGlobalVars), VibrationManager.updateGlobalVars(), window.addEventListener(BxEvent.DATA_CHANNEL_CREATED, (e) => {
const dataChannel = e.dataChannel;
if (!dataChannel || dataChannel.label !== "input")
return;
dataChannel.addEventListener("message", VibrationManager.#onMessage);
});
}
}
// src/modules/ui/ui.ts
function localRedirect(path) {
const url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");
if (!$pageContent)
return;
const $anchor = CE("a", {
href: url,
class: "bx-hidden bx-offscreen"
}, "");
$anchor.addEventListener("click", (e) => {
window.setTimeout(() => {
$pageContent.removeChild($anchor);
}, 1000);
}), $pageContent.appendChild($anchor), $anchor.click();
}
var setupStreamSettingsDialog = function() {
const SETTINGS_UI = [
{
icon: BxIcon.DISPLAY,
group: "stream",
items: [
{
group: "audio",
label: t("audio"),
help_url: "https://better-xcloud.github.io/ingame-features/#audio",
items: [
{
pref: PrefKey.AUDIO_VOLUME,
onChange: (e, value) => {
SoundShortcut.setGainNodeVolume(value);
},
params: {
disabled: !getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL)
},
onMounted: ($elm) => {
const $range = $elm.querySelector("input[type=range");
window.addEventListener(BxEvent.GAINNODE_VOLUME_CHANGED, (e) => {
$range.value = e.volume, BxEvent.dispatch($range, "input", {
ignoreOnChange: !0
});
});
}
}
]
},
{
group: "video",
label: t("video"),
help_url: "https://better-xcloud.github.io/ingame-features/#video",
items: [
{
pref: PrefKey.VIDEO_PLAYER_TYPE,
onChange: onChangeVideoPlayerType
},
{
pref: PrefKey.VIDEO_RATIO,
onChange: updateVideoPlayer
},
{
pref: PrefKey.VIDEO_PROCESSING,
onChange: updateVideoPlayer
},
{
pref: PrefKey.VIDEO_SHARPNESS,
onChange: updateVideoPlayer
},
{
pref: PrefKey.VIDEO_SATURATION,
onChange: updateVideoPlayer
},
{
pref: PrefKey.VIDEO_CONTRAST,
onChange: updateVideoPlayer
},
{
pref: PrefKey.VIDEO_BRIGHTNESS,
onChange: updateVideoPlayer
}
]
}
]
},
{
icon: BxIcon.CONTROLLER,
group: "controller",
items: [
{
group: "controller",
label: t("controller"),
help_url: "https://better-xcloud.github.io/ingame-features/#controller",
items: [
{
pref: PrefKey.CONTROLLER_ENABLE_VIBRATION,
unsupported: !VibrationManager.supportControllerVibration(),
onChange: VibrationManager.updateGlobalVars
},
{
pref: PrefKey.CONTROLLER_DEVICE_VIBRATION,
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars
},
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY,
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars
}
]
},
STATES.userAgentHasTouchSupport && {
group: "touch-controller",
label: t("touch-controller"),
items: [
{
label: t("layout"),
content: CE("select", { disabled: !0 }, CE("option", {}, t("default"))),
onMounted: ($elm) => {
$elm.addEventListener("change", (e) => {
TouchController.loadCustomLayout(STATES.currentStream?.xboxTitleId, $elm.value, 1000);
}), window.addEventListener(BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, (e) => {
const data = e.data;
if (STATES.currentStream?.xboxTitleId && $elm.xboxTitleId === STATES.currentStream?.xboxTitleId) {
$elm.dispatchEvent(new Event("change"));
return;
}
$elm.xboxTitleId = STATES.currentStream?.xboxTitleId;
while ($elm.firstChild)
$elm.removeChild($elm.firstChild);
if ($elm.disabled = !data, !data) {
$elm.appendChild(CE("option", { value: "" }, t("default"))), $elm.value = "", $elm.dispatchEvent(new Event("change"));
return;
}
const $fragment = document.createDocumentFragment();
for (let key in data.layouts) {
const layout = data.layouts[key];
let name;
if (layout.author)
name = `${layout.name} (${layout.author})`;
else
name = layout.name;
const $option = CE("option", { value: key }, name);
$fragment.appendChild($option);
}
$elm.appendChild($fragment), $elm.value = data.default_layout, $elm.dispatchEvent(new Event("change"));
});
}
}
]
}
]
},
getPref(PrefKey.MKB_ENABLED) && {
icon: BxIcon.VIRTUAL_CONTROLLER,
group: "mkb",
items: [
{
group: "mkb",
label: t("virtual-controller"),
help_url: "https://better-xcloud.github.io/mouse-and-keyboard/",
content: MkbRemapper.INSTANCE.render()
}
]
},
AppInterface && getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" && {
icon: BxIcon.NATIVE_MKB,
group: "native-mkb",
items: [
{
group: "native-mkb",
label: t("native-mkb"),
items: [
{
pref: PrefKey.NATIVE_MKB_SCROLL_VERTICAL_SENSITIVITY,
onChange: (e, value) => {
NativeMkbHandler.getInstance().setVerticalScrollMultiplier(value / 100);
}
},
{
pref: PrefKey.NATIVE_MKB_SCROLL_HORIZONTAL_SENSITIVITY,
onChange: (e, value) => {
NativeMkbHandler.getInstance().setHorizontalScrollMultiplier(value / 100);
}
}
]
}
]
},
{
icon: BxIcon.COMMAND,
group: "shortcuts",
items: [
{
group: "shortcuts_controller",
label: t("controller-shortcuts"),
content: ControllerShortcut.renderSettings()
}
]
},
{
icon: BxIcon.STREAM_STATS,
group: "stats",
items: [
{
group: "stats",
label: t("stream-stats"),
help_url: "https://better-xcloud.github.io/stream-stats/",
items: [
{
pref: PrefKey.STATS_SHOW_WHEN_PLAYING
},
{
pref: PrefKey.STATS_QUICK_GLANCE,
onChange: (e) => {
const streamStats = StreamStats.getInstance();
e.target.checked ? streamStats.quickGlanceSetup() : streamStats.quickGlanceStop();
}
},
{
pref: PrefKey.STATS_ITEMS,
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_POSITION,
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_TEXT_SIZE,
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_OPACITY,
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_TRANSPARENT,
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_CONDITIONAL_FORMATTING,
onChange: StreamStats.refreshStyles
}
]
}
]
}
];
let $tabs, $settings;
const $wrapper = CE("div", { class: "bx-stream-settings-dialog bx-gone" }, $tabs = CE("div", { class: "bx-stream-settings-tabs" }), $settings = CE("div", { class: "bx-stream-settings-tab-contents" }));
for (let settingTab of SETTINGS_UI) {
if (!settingTab)
continue;
const $svg = createSvgIcon(settingTab.icon);
$svg.addEventListener("click", (e) => {
for (let $child of Array.from($settings.children))
if ($child.getAttribute("data-group") === settingTab.group)
$child.classList.remove("bx-gone");
else
$child.classList.add("bx-gone");
for (let $child of Array.from($tabs.children))
$child.classList.remove("bx-active");
$svg.classList.add("bx-active");
}), $tabs.appendChild($svg);
const $group = CE("div", { "data-group": settingTab.group, class: "bx-gone" });
for (let settingGroup of settingTab.items) {
if (!settingGroup)
continue;
if ($group.appendChild(CE("h2", {}, CE("span", {}, settingGroup.label), settingGroup.help_url && createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST,
url: settingGroup.help_url,
title: t("help")
}))), settingGroup.note) {
if (typeof settingGroup.note === "string")
settingGroup.note = document.createTextNode(settingGroup.note);
$group.appendChild(settingGroup.note);
}
if (settingGroup.content) {
$group.appendChild(settingGroup.content);
continue;
}
if (!settingGroup.items)
settingGroup.items = [];
for (let setting of settingGroup.items) {
if (!setting)
continue;
const pref = setting.pref;
let $control;
if (setting.content)
$control = setting.content;
else if (!setting.unsupported)
$control = toPrefElement(pref, setting.onChange, setting.params);
const label = Preferences.SETTINGS[pref]?.label || setting.label, note = Preferences.SETTINGS[pref]?.note || setting.note, $content = CE("div", { class: "bx-stream-settings-row", "data-type": settingGroup.group }, CE("label", { for: `bx_setting_${pref}` }, label, note && CE("div", { class: "bx-stream-settings-dialog-note" }, note), setting.unsupported && CE("div", { class: "bx-stream-settings-dialog-note" }, t("browser-unsupported-feature"))), !setting.unsupported && $control);
$group.appendChild($content), setting.onMounted && setting.onMounted($control);
}
}
$settings.appendChild($group);
}
$tabs.firstElementChild.dispatchEvent(new Event("click")), document.documentElement.appendChild($wrapper);
}, onChangeVideoPlayerType = function() {
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE), $videoProcessing = document.getElementById("bx_setting_video_processing"), $videoSharpness = document.getElementById("bx_setting_video_sharpness");
let isDisabled = !1;
if (playerType === StreamPlayerType.WEBGL2)
$videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !1;
else if ($videoProcessing.value = StreamVideoProcessing.USM, setPref(PrefKey.VIDEO_PROCESSING, StreamVideoProcessing.USM), $videoProcessing.querySelector(`option[value=${StreamVideoProcessing.CAS}]`).disabled = !0, UserAgent.isSafari())
isDisabled = !0;
$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), updateVideoPlayer();
};
function updateVideoPlayer() {
const streamPlayer = STATES.currentStream.streamPlayer;
if (!streamPlayer)
return;
const options = {
processing: getPref(PrefKey.VIDEO_PROCESSING),
sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
saturation: getPref(PrefKey.VIDEO_SATURATION),
contrast: getPref(PrefKey.VIDEO_CONTRAST),
brightness: getPref(PrefKey.VIDEO_BRIGHTNESS)
};
streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE)), streamPlayer.updateOptions(options), streamPlayer.refreshPlayer();
}
var preloadFonts = function() {
const $link = CE("link", {
rel: "preload",
href: "https://redphx.github.io/better-xcloud/fonts/promptfont.otf",
as: "font",
type: "font/otf",
crossorigin: ""
});
document.querySelector("head")?.appendChild($link);
};
function setupStreamUi() {
if (!document.querySelector(".bx-stream-settings-dialog"))
preloadFonts(), window.addEventListener("resize", updateVideoPlayer), setupStreamSettingsDialog(), Screenshot.setup();
onChangeVideoPlayerType();
}
// src/modules/remote-play.ts
var LOG_TAG4 = "RemotePlay", RemotePlayConsoleState;
(function(RemotePlayConsoleState2) {
RemotePlayConsoleState2["ON"] = "On";
RemotePlayConsoleState2["OFF"] = "Off";
RemotePlayConsoleState2["STANDBY"] = "ConnectedStandby";
RemotePlayConsoleState2["UNKNOWN"] = "Unknown";
})(RemotePlayConsoleState || (RemotePlayConsoleState = {}));
class RemotePlay {
static XCLOUD_TOKEN;
static XHOME_TOKEN;
static #CONSOLES;
static #REGIONS;
static #STATE_LABELS = {
[RemotePlayConsoleState.ON]: t("powered-on"),
[RemotePlayConsoleState.OFF]: t("powered-off"),
[RemotePlayConsoleState.STANDBY]: t("standby"),
[RemotePlayConsoleState.UNKNOWN]: t("unknown")
};
static BASE_DEVICE_INFO = {
appInfo: {
env: {
clientAppId: window.location.host,
clientAppType: "browser",
clientAppVersion: "21.1.98",
clientSdkVersion: "8.5.3",
httpEnvironment: "prod",
sdkInstallId: ""
}
},
dev: {
displayInfo: {
dimensions: {
widthInPixels: 1920,
heightInPixels: 1080
},
pixelDensity: {
dpiX: 1,
dpiY: 1
}
},
hw: {
make: "Microsoft",
model: "unknown",
sdktype: "web"
},
os: {
name: "windows",
ver: "22631.2715",
platform: "desktop"
},
browser: {
browserName: "chrome",
browserVersion: "119.0"
}
}
};
static #$content;
static #initialize() {
if (RemotePlay.#$content)
return;
RemotePlay.#$content = CE("div", {}, t("getting-consoles-list")), RemotePlay.#getXhomeToken(() => {
RemotePlay.#getConsolesList(() => {
BxLogger.info(LOG_TAG4, "Consoles", RemotePlay.#CONSOLES), RemotePlay.#renderConsoles(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
});
});
}
static #renderConsoles() {
const $fragment = CE("div", { class: "bx-remote-play-container" });
if (!RemotePlay.#CONSOLES || RemotePlay.#CONSOLES.length === 0) {
$fragment.appendChild(CE("span", {}, t("no-consoles-found"))), RemotePlay.#$content = CE("div", {}, $fragment);
return;
}
const $settingNote = CE("p", {}), resolutions = [1080, 720], currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION), $resolutionGroup = CE("div", {});
for (let resolution of resolutions) {
const value = `${resolution}p`, id2 = `bx_radio_xhome_resolution_${resolution}`, $radio = CE("input", {
type: "radio",
value,
id: id2,
name: "bx_radio_xhome_resolution"
}, value);
$radio.addEventListener("change", (e) => {
const value2 = e.target.value;
$settingNote.textContent = value2 === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value2);
});
const $label = CE("label", {
for: id2,
class: "bx-remote-play-resolution"
}, $radio, `${resolution}p`);
if ($resolutionGroup.appendChild($label), currentResolution === value)
$radio.checked = !0, $radio.dispatchEvent(new Event("change"));
}
const $qualitySettings = CE("div", { class: "bx-remote-play-settings" }, CE("div", {}, CE("label", {}, t("target-resolution"), $settingNote), $resolutionGroup));
$fragment.appendChild($qualitySettings);
for (let con of RemotePlay.#CONSOLES) {
const $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", {}, CE("span", { class: "bx-remote-play-device-name" }, con.deviceName), CE("span", { class: "bx-remote-play-console-type" }, con.consoleType.replace("Xbox", ""))), CE("div", { class: "bx-remote-play-power-state" }, RemotePlay.#STATE_LABELS[con.powerState])), createButton({
classes: ["bx-remote-play-connect-button"],
label: t("console-connect"),
style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE,
onClick: (e) => {
RemotePlay.play(con.serverId);
}
}));
$fragment.appendChild($child);
}
$fragment.appendChild(createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
url: "https://better-xcloud.github.io/remote-play",
label: t("help")
})), RemotePlay.#$content = CE("div", {}, $fragment);
}
static #getXhomeToken(callback) {
if (RemotePlay.XHOME_TOKEN) {
callback();
return;
}
let GSSV_TOKEN;
try {
GSSV_TOKEN = JSON.parse(localStorage.getItem("xboxcom_xbl_user_info")).tokens["http://gssv.xboxlive.com/"].token;
} catch (e) {
for (let i = 0;i < localStorage.length; i++) {
const key = localStorage.key(i);
if (!key.startsWith("Auth.User."))
continue;
const json = JSON.parse(localStorage.getItem(key));
for (let token of json.tokens) {
if (!token.relyingParty.includes("gssv.xboxlive.com"))
continue;
GSSV_TOKEN = token.tokenData.token;
break;
}
break;
}
}
const request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
method: "POST",
body: JSON.stringify({
offeringId: "xhome",
token: GSSV_TOKEN
}),
headers: {
"Content-Type": "application/json; charset=utf-8"
}
});
fetch(request).then((resp) => resp.json()).then((json) => {
RemotePlay.#REGIONS = json.offeringSettings.regions, RemotePlay.XHOME_TOKEN = json.gsToken, callback();
});
}
static async#getConsolesList(callback) {
if (RemotePlay.#CONSOLES) {
callback();
return;
}
const options = {
method: "GET",
headers: {
Authorization: `Bearer ${RemotePlay.XHOME_TOKEN}`
}
};
for (let region2 of RemotePlay.#REGIONS) {
try {
const request = new Request(`${region2.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();
RemotePlay.#CONSOLES = json.results, STATES.remotePlay.server = region2.baseUri, callback();
} catch (e) {
}
if (RemotePlay.#CONSOLES)
break;
}
if (!STATES.remotePlay.server)
RemotePlay.#CONSOLES = [];
}
static play(serverId, resolution) {
if (resolution)
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution);
STATES.remotePlay.config = {
serverId
}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"), RemotePlay.detachPopup();
}
static preload() {
RemotePlay.#initialize();
}
static detachPopup() {
const $popup = document.querySelector(".bx-remote-play-popup");
$popup && $popup.remove();
}
static togglePopup(force = null) {
if (!getPref(PrefKey.REMOTE_PLAY_ENABLED) || !RemotePlay.isReady()) {
Toast.show(t("getting-consoles-list"));
return;
}
if (RemotePlay.#initialize(), AppInterface && AppInterface.showRemotePlayDialog) {
AppInterface.showRemotePlayDialog(JSON.stringify(RemotePlay.#CONSOLES)), document.activeElement.blur();
return;
}
if (document.querySelector(".bx-remote-play-popup")) {
if (force === !1)
RemotePlay.#$content.classList.add("bx-gone");
else
RemotePlay.#$content.classList.toggle("bx-gone");
return;
}
const $header = document.querySelector("#gamepass-root header"), group = $header.firstElementChild.getAttribute("data-group");
RemotePlay.#$content.setAttribute("data-group", group), RemotePlay.#$content.classList.add("bx-remote-play-popup"), RemotePlay.#$content.classList.remove("bx-gone"), $header.insertAdjacentElement("afterend", RemotePlay.#$content);
}
static detect() {
if (!getPref(PrefKey.REMOTE_PLAY_ENABLED))
return;
if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying)
window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");
else
window.BX_REMOTE_PLAY_CONFIG = null;
}
static isReady() {
return RemotePlay.#CONSOLES !== null && RemotePlay.#CONSOLES.length > 0;
}
}
// src/enums/game-pass-gallery.ts
var GamePassCloudGallery;
(function(GamePassCloudGallery2) {
GamePassCloudGallery2["ALL"] = "29a81209-df6f-41fd-a528-2ae6b91f719c";
GamePassCloudGallery2["NATIVE_MKB"] = "8fa264dd-124f-4af3-97e8-596fcdf4b486";
GamePassCloudGallery2["TOUCH"] = "9c86f07a-f3e8-45ad-82a0-a1f759597059";
})(GamePassCloudGallery || (GamePassCloudGallery = {}));
// src/utils/feature-gates.ts
var FeatureGates = {
PwaPrompt: !1
};
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED))
FeatureGates.EnableHomeContextMenu = !1;
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES))
FeatureGates.EnableGuideChatTab = !1;
if (BX_FLAGS.FeatureGates)
FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
// src/utils/network.ts
var clearApplicationInsightsBuffers = function() {
window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");
}, clearDbLogs = function(dbName, table) {
const request = window.indexedDB.open(dbName);
request.onsuccess = (e) => {
const db = e.target.result;
try {
const objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();
objectStoreRequest.onsuccess = function() {
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
};
} catch (ex) {
}
};
}, clearAllLogs = function() {
clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");
}, updateIceCandidates = function(candidates, options) {
const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/), lst = [];
for (let item2 of candidates) {
if (item2.candidate == "a=end-of-candidates")
continue;
const groups = pattern.exec(item2.candidate).groups;
lst.push(groups);
}
if (options.preferIpv6Server)
lst.sort((a, b) => {
const firstIp = a.ip, secondIp = b.ip;
return !firstIp.includes(":") && secondIp.includes(":") ? 1 : -1;
});
const newCandidates = [];
let foundation = 1;
const newCandidate = (candidate) => {
return {
candidate,
messageType: "iceCandidate",
sdpMLineIndex: "0",
sdpMid: "0"
};
};
if (lst.forEach((item2) => {
item2.foundation = foundation, item2.priority = foundation == 1 ? 2130706431 : 1, newCandidates.push(newCandidate(`a=candidate:${item2.foundation} 1 UDP ${item2.priority} ${item2.ip} ${item2.port} ${item2.the_rest}`)), ++foundation;
}), options.consoleAddrs)
for (let ip in options.consoleAddrs) {
const port = options.consoleAddrs[ip];
newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));
}
return newCandidates.push(newCandidate("a=end-of-candidates")), console.log(newCandidates), newCandidates;
};
async function patchIceCandidates(request, consoleAddrs) {
const response = await NATIVE_FETCH(request), text = await response.clone().text();
if (!text.length)
return response;
const options = {
preferIpv6Server: getPref(PrefKey.PREFER_IPV6_SERVER),
consoleAddrs
}, obj = JSON.parse(text);
let exchangeResponse = JSON.parse(obj.exchangeResponse);
return exchangeResponse = updateIceCandidates(exchangeResponse, options), obj.exchangeResponse = JSON.stringify(exchangeResponse), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
function interceptHttpRequests() {
let BLOCKED_URLS = [];
if (getPref(PrefKey.BLOCK_TRACKING))
clearAllLogs(), BLOCKED_URLS = BLOCKED_URLS.concat([
"https://arc.msn.com",
"https://browser.events.data.microsoft.com",
"https://dc.services.visualstudio.com",
"https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io"
]);
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES))
BLOCKED_URLS = BLOCKED_URLS.concat([
"https://peoplehub.xboxlive.com/users/me/people/social",
"https://peoplehub.xboxlive.com/users/me/people/recommendations",
"https://xblmessaging.xboxlive.com/network/xbox/users/me/inbox"
]);
const xhrPrototype = XMLHttpRequest.prototype, nativeXhrOpen = xhrPrototype.open, nativeXhrSend = xhrPrototype.send;
xhrPrototype.open = function(method, url) {
return this._url = url, nativeXhrOpen.apply(this, arguments);
}, xhrPrototype.send = function(...arg) {
for (let blocked of BLOCKED_URLS)
if (this._url.startsWith(blocked)) {
if (blocked === "https://dc.services.visualstudio.com")
window.setTimeout(clearAllLogs, 1000);
return !1;
}
return nativeXhrSend.apply(this, arguments);
};
let gamepassAllGames = [];
window.BX_FETCH = window.fetch = async (request, init) => {
let url = typeof request === "string" ? request : request.url;
for (let blocked of BLOCKED_URLS) {
if (!url.startsWith(blocked))
continue;
return new Response('{"acc":1,"webResult":{}}', {
status: 200,
statusText: "200 OK"
});
}
if (url.endsWith("/play"))
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
if (url.endsWith("/configuration"))
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
if (url.startsWith("https://emerald.xboxservices.com/xboxcomfd/experimentation"))
try {
const response = await NATIVE_FETCH(request, init), json = await response.json();
for (let key in FeatureGates)
json.exp.treatments[key] = FeatureGates[key];
return response.json = () => Promise.resolve(json), response;
} catch (e) {
console.log(e);
}
if (STATES.userAgentHasTouchSupport && url.includes("catalog.gamepass.com/sigls/")) {
const response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
if (url.includes(GamePassCloudGallery.ALL))
for (let i = 1;i < obj.length; i++)
gamepassAllGames.push(obj[i].id);
else if (url.includes(GamePassCloudGallery.TOUCH))
try {
let customList = TouchController.getCustomList();
customList = customList.filter((id2) => gamepassAllGames.includes(id2));
const newCustomList = customList.map((item2) => ({ id: item2 }));
obj.push(...newCustomList);
} catch (e) {
console.log(e);
}
return response.json = () => Promise.resolve(obj), response;
}
if (BX_FLAGS.ForceNativeMkbTitles && url.includes("catalog.gamepass.com/sigls/") && url.includes(GamePassCloudGallery.NATIVE_MKB)) {
const response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
try {
const newCustomList = BX_FLAGS.ForceNativeMkbTitles.map((item2) => ({ id: item2 }));
obj.push(...newCustomList);
} catch (e) {
console.log(e);
}
return response.json = () => Promise.resolve(obj), response;
}
let requestType;
if (url.includes("/sessions/home") || url.includes("xhome.") || STATES.remotePlay.isPlaying && url.endsWith("/inputconfigs"))
requestType = RequestType.XHOME;
else
requestType = RequestType.XCLOUD;
if (requestType === RequestType.XHOME)
return XhomeInterceptor.handle(request);
return XcloudInterceptor.handle(request, init);
};
}
var RequestType;
(function(RequestType2) {
RequestType2["XCLOUD"] = "xcloud";
RequestType2["XHOME"] = "xhome";
})(RequestType || (RequestType = {}));
class XhomeInterceptor {
static #consoleAddrs = {};
static async#handleLogin(request) {
try {
const obj = await request.clone().json();
obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
method: "POST",
body: JSON.stringify(obj),
headers: {
"Content-Type": "application/json"
}
});
} catch (e) {
alert(e), console.log(e);
}
return NATIVE_FETCH(request);
}
static async#handleConfiguration(request) {
const response = await NATIVE_FETCH(request), obj = await response.clone().json();
console.log(obj);
const serverDetails = obj.serverDetails;
if (serverDetails.ipV4Address)
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = serverDetails.ipV4Port;
if (serverDetails.ipV6Address)
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = serverDetails.ipV6Port;
return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
static async#handleInputConfigs(request, opts) {
const response = await NATIVE_FETCH(request);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "all")
return response;
const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];
STATES.currentStream.xboxTitleId = xboxTitleId;
const inputConfigs = obj[0];
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
if (!hasTouchSupport) {
const supportedInputTypes = inputConfigs.supportedInputTypes;
hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
}
if (hasTouchSupport)
TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
data: null
});
else
TouchController.enable(), TouchController.getCustomLayouts(xboxTitleId);
return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
static async#handleTitles(request) {
const clone = request.clone(), headers = {};
for (let pair of clone.headers.entries())
headers[pair[0]] = pair[1];
headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`;
const index = request.url.indexOf(".xboxlive.com");
return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {
method: clone.method,
body: await clone.text(),
headers
}), NATIVE_FETCH(request);
}
static async#handlePlay(request) {
const body = await request.clone().json(), newRequest = new Request(request, {
body: JSON.stringify(body)
});
return NATIVE_FETCH(newRequest);
}
static async handle(request) {
TouchController.disable();
const clone = request.clone(), headers = {};
for (let pair of clone.headers.entries())
headers[pair[0]] = pair[1];
headers.authorization = `Bearer ${RemotePlay.XHOME_TOKEN}`;
const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === "720p")
deviceInfo.dev.os.name = "android";
headers["x-ms-device-info"] = JSON.stringify(deviceInfo);
const opts = {
method: clone.method,
headers
};
if (clone.method === "POST")
opts.body = await clone.text();
let newUrl = request.url;
if (!newUrl.includes("/servers/home")) {
const index = request.url.indexOf(".xboxlive.com");
newUrl = STATES.remotePlay.server + request.url.substring(index + 13);
}
request = new Request(newUrl, opts);
let url = typeof request === "string" ? request : request.url;
if (url.includes("/configuration"))
return XhomeInterceptor.#handleConfiguration(request);
else if (url.endsWith("/sessions/home/play"))
return XhomeInterceptor.#handlePlay(request);
else if (url.includes("inputconfigs"))
return XhomeInterceptor.#handleInputConfigs(request, opts);
else if (url.includes("/login/user"))
return XhomeInterceptor.#handleLogin(request);
else if (url.endsWith("/titles"))
return XhomeInterceptor.#handleTitles(request);
else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET")
return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs);
return await NATIVE_FETCH(request);
}
}
class XcloudInterceptor {
static async#handleLogin(request, init) {
const response = await NATIVE_FETCH(request, init), obj = await response.clone().json();
getPref(PrefKey.REMOTE_PLAY_ENABLED) && BX_FLAGS.PreloadRemotePlay && RemotePlay.preload(), RemotePlay.XCLOUD_TOKEN = obj.gsToken;
const serverEmojis = {
AustraliaEast: "🇦🇺",
AustraliaSouthEast: "🇦🇺",
BrazilSouth: "🇧🇷",
EastUS: "🇺🇸",
EastUS2: "🇺🇸",
JapanEast: "🇯🇵",
KoreaCentral: "🇰🇷",
MexicoCentral: "🇲🇽",
NorthCentralUs: "🇺🇸",
SouthCentralUS: "🇺🇸",
UKSouth: "🇬🇧",
WestEurope: "🇪🇺",
WestUS: "🇺🇸",
WestUS2: "🇺🇸"
}, serverRegex = /\/\/(\w+)\./;
for (let region3 of obj.offeringSettings.regions) {
const regionName = region3.name;
let shortName = region3.name, match = serverRegex.exec(region3.baseUri);
if (match) {
if (shortName = match[1], serverEmojis[regionName])
shortName = serverEmojis[regionName] + " " + shortName;
}
region3.shortName = shortName.toUpperCase(), STATES.serverRegions[region3.name] = Object.assign({}, region3);
}
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
const preferredRegion = getPreferredServerRegion();
if (preferredRegion in STATES.serverRegions) {
const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
tmp.isDefault = !0, obj.offeringSettings.regions = [tmp];
}
return response.json = () => Promise.resolve(obj), response;
}
static async#handlePlay(request, init) {
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION), PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url);
let badgeRegion = parsedUrl.host.split(".", 1)[0];
for (let regionName in STATES.serverRegions) {
const region3 = STATES.serverRegions[regionName];
if (parsedUrl.origin == region3.baseUri) {
badgeRegion = regionName;
break;
}
}
StreamBadges.getInstance().setRegion(badgeRegion);
const body = await request.clone().json();
if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {
const osName = PREF_STREAM_TARGET_RESOLUTION === "720p" ? "android" : "windows";
body.settings.osName = osName;
}
if (PREF_STREAM_PREFERRED_LOCALE !== "default")
body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
const newRequest = new Request(request, {
body: JSON.stringify(body)
});
return NATIVE_FETCH(newRequest);
}
static async#handleWaitTime(request, init) {
const response = await NATIVE_FETCH(request, init);
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
const json = await response.clone().json();
if (json.estimatedAllocationTimeInSeconds > 0)
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
}
return response;
}
static async#handleConfiguration(request, init) {
if (request.method !== "GET")
return NATIVE_FETCH(request, init);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all")
if (STATES.currentStream.titleInfo?.details.hasTouchSupport)
TouchController.disable();
else
TouchController.enable();
const response = await NATIVE_FETCH(request, init), text = await response.clone().text();
if (!text.length)
return response;
const obj = JSON.parse(text);
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};
overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;
let overrideMkb = null;
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" || BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId))
overrideMkb = !0;
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === "off")
overrideMkb = !1;
if (overrideMkb !== null)
overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
enableMouseInput: overrideMkb,
enableKeyboardInput: overrideMkb
});
if (overrides.videoConfiguration = overrides.videoConfiguration || {}, overrides.videoConfiguration.setCodecPreferences = !0, TouchController.isEnabled())
overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;
if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING))
overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;
return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
}
static async handle(request, init) {
let url = typeof request === "string" ? request : request.url;
if (url.endsWith("/v2/login/user"))
return XcloudInterceptor.#handleLogin(request, init);
else if (url.endsWith("/sessions/cloud/play"))
return XcloudInterceptor.#handlePlay(request, init);
else if (url.includes("xboxlive.com") && url.includes("/waittime/"))
return XcloudInterceptor.#handleWaitTime(request, init);
else if (url.endsWith("/configuration"))
return XcloudInterceptor.#handleConfiguration(request, init);
else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET")
return patchIceCandidates(request);
return NATIVE_FETCH(request, init);
}
}
// src/utils/gamepad.ts
function showGamepadToast(gamepad) {
if (gamepad.id === EmulatedMkbHandler.VIRTUAL_GAMEPAD_ID)
return;
BxLogger.info("Gamepad", gamepad);
let text = "🎮";
if (getPref(PrefKey.LOCAL_CO_OP_ENABLED))
text += ` #${gamepad.index + 1}`;
const gamepadId = gamepad.id.replace(/ \(.*?Vendor: \w+ Product: \w+\)$/, "");
text += ` - ${gamepadId}`;
let status;
if (gamepad.connected)
status = (gamepad.vibrationActuator ? "✅" : "❌") + " " + t("vibration-status");
else
status = t("disconnected");
Toast.show(text, status, { instant: !1 });
}
// src/utils/css.ts
function addCss() {
let css = `:root {
--bx-title-font: Bahnschrift, Arial, Helvetica, sans-serif;
--bx-title-font-semibold: Bahnschrift Semibold, Arial, Helvetica, sans-serif;
--bx-normal-font: "Segoe UI", Arial, Helvetica, sans-serif;
--bx-monospaced-font: Consolas, "Courier New", Courier, monospace;
--bx-promptfont-font: promptfont;
--bx-button-height: 36px;
--bx-default-button-color: #2d3036;
--bx-default-button-hover-color: #515863;
--bx-default-button-disabled-color: #8e8e8e;
--bx-primary-button-color: #008746;
--bx-primary-button-hover-color: #04b358;
--bx-primary-button-disabled-color: #448262;
--bx-danger-button-color: #c10404;
--bx-danger-button-hover-color: #e61d1d;
--bx-danger-button-disabled-color: #a26c6c;
--bx-toast-z-index: 9999;
--bx-dialog-z-index: 9101;
--bx-dialog-overlay-z-index: 9100;
--bx-remote-play-popup-z-index: 9090;
--bx-stats-bar-z-index: 9001;
--bx-stream-settings-z-index: 9000;
--bx-mkb-pointer-lock-msg-z-index: 8999;
--bx-game-bar-z-index: 8888;
--bx-wait-time-box-z-index: 100;
--bx-screenshot-animation-z-index: 1;
}
@font-face {
font-family: 'promptfont';
src: url("https://redphx.github.io/better-xcloud/fonts/promptfont.otf");
}
div[class^=HUDButton-module__hiddenContainer] ~ div:not([class^=HUDButton-module__hiddenContainer]) {
opacity: 0;
pointer-events: none !important;
position: absolute;
top: -9999px;
left: -9999px;
}
@media screen and (max-width: 600px) {
header a[href="/play"] {
display: none;
}
}
.bx-full-width {
width: 100% !important;
}
.bx-full-height {
height: 100% !important;
}
.bx-no-scroll {
overflow: hidden !important;
}
.bx-gone {
display: none !important;
}
.bx-offscreen {
position: absolute !important;
top: -9999px !important;
left: -9999px !important;
visibility: hidden !important;
}
.bx-hidden {
visibility: hidden !important;
}
.bx-pixel {
width: 1px !important;
height: 1px !important;
}
.bx-no-margin {
margin: 0 !important;
}
.bx-no-padding {
padding: 0 !important;
}
.bx-prompt {
font-family: var(--bx-promptfont-font);
}
#headerArea,
#uhfSkipToMain,
.uhf-footer {
display: none;
}
div[class*=NotFocusedDialog] {
position: absolute !important;
top: -9999px !important;
left: -9999px !important;
width: 0px !important;
height: 0px !important;
}
#game-stream video:not([src]) {
visibility: hidden;
}
.bx-button {
background-color: var(--bx-default-button-color);
user-select: none;
-webkit-user-select: none;
color: #fff;
font-family: var(--bx-title-font-semibold);
font-size: 14px;
border: none;
font-weight: 400;
height: var(--bx-button-height);
border-radius: 4px;
padding: 0 8px;
text-transform: uppercase;
cursor: pointer;
overflow: hidden;
}
.bx-button:focus {
outline: none !important;
}
.bx-button:hover,
.bx-button.bx-focusable:focus {
background-color: var(--bx-default-button-hover-color);
}
.bx-button:disabled {
cursor: default;
background-color: var(--bx-default-button-disabled-color);
}
.bx-button.bx-ghost {
background-color: transparent;
}
.bx-button.bx-ghost:hover,
.bx-button.bx-ghost.bx-focusable:focus {
background-color: var(--bx-default-button-hover-color);
}
.bx-button.bx-primary {
background-color: var(--bx-primary-button-color);
}
.bx-button.bx-primary:hover,
.bx-button.bx-primary.bx-focusable:focus {
background-color: var(--bx-primary-button-hover-color);
}
.bx-button.bx-primary:disabled {
background-color: var(--bx-primary-button-disabled-color);
}
.bx-button.bx-danger {
background-color: var(--bx-danger-button-color);
}
.bx-button.bx-danger:hover,
.bx-button.bx-danger.bx-focusable:focus {
background-color: var(--bx-danger-button-hover-color);
}
.bx-button.bx-danger:disabled {
background-color: var(--bx-danger-button-disabled-color);
}
.bx-button.bx-tall {
height: calc(var(--bx-button-height) * 1.5) !important;
}
.bx-button svg {
display: inline-block;
width: 16px;
height: var(--bx-button-height);
}
.bx-button svg:not(:only-child) {
margin-right: 4px;
}
.bx-button span {
display: inline-block;
/* height: var(--bx-button-height); */
line-height: var(--bx-button-height);
vertical-align: middle;
/* vertical-align: -webkit-baseline-middle; */
color: #fff;
overflow: hidden;
white-space: nowrap;
}
.bx-button.bx-focusable {
position: relative;
}
.bx-button.bx-focusable::after {
border: 2px solid transparent;
border-radius: 4px;
}
.bx-button.bx-focusable:focus::after {
content: '';
border-color: #fff;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
a.bx-button {
display: inline-block;
}
a.bx-button.bx-full-width {
text-align: center;
}
.bx-header-remote-play-button {
height: auto;
margin-right: 8px !important;
}
.bx-header-remote-play-button svg {
width: 24px;
height: 46px;
}
.bx-header-settings-button {
line-height: 30px;
font-size: 14px;
text-transform: uppercase;
position: relative;
}
.bx-header-settings-button[data-update-available]::before {
content: '🌟' !important;
line-height: var(--bx-button-height);
display: inline-block;
margin-left: 4px;
}
.bx-settings-reload-button {
margin-top: 10px;
}
.bx-settings-container {
background-color: #151515;
user-select: none;
-webkit-user-select: none;
color: #fff;
font-family: var(--bx-normal-font);
}
@media (hover: hover) {
.bx-settings-wrapper a.bx-settings-title:hover {
color: #83f73a;
}
}
.bx-settings-wrapper {
width: 450px;
margin: auto;
padding: 12px 6px;
}
@media screen and (max-width: 450px) {
.bx-settings-wrapper {
width: 100%;
}
}
.bx-settings-wrapper *:focus {
outline: none !important;
}
.bx-settings-wrapper .bx-settings-title-wrapper {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.bx-settings-wrapper a.bx-settings-title {
font-family: var(--bx-title-font);
font-size: 1.4rem;
text-decoration: none;
font-weight: bold;
display: block;
color: #5dc21e;
flex: 1;
}
.bx-settings-wrapper a.bx-settings-title:focus {
color: #83f73a;
}
.bx-settings-wrapper .bx-button.bx-primary {
margin-top: 8px;
}
.bx-settings-wrapper a.bx-settings-update {
display: block;
color: #ff834b;
text-decoration: none;
margin-bottom: 8px;
text-align: center;
background: #222;
border-radius: 4px;
padding: 4px;
}
@media (hover: hover) {
.bx-settings-wrapper a.bx-settings-update:hover {
color: #ff9869;
text-decoration: underline;
}
}
.bx-settings-wrapper a.bx-settings-update:focus {
color: #ff9869;
text-decoration: underline;
}
.bx-settings-group-label {
font-weight: bold;
display: block;
font-size: 1.1rem;
}
.bx-settings-row {
display: flex;
padding: 6px 12px;
position: relative;
}
.bx-settings-row label {
flex: 1;
align-self: center;
margin-bottom: 0;
}
.bx-settings-row:hover,
.bx-settings-row:focus-within {
background-color: #242424;
}
.bx-settings-row input {
align-self: center;
accent-color: var(--bx-primary-button-color);
}
.bx-settings-row input:focus {
accent-color: var(--bx-danger-button-color);
}
.bx-settings-row select:disabled {
-webkit-appearance: none;
background: transparent;
text-align-last: right;
border: none;
color: #fff;
}
.bx-settings-row input[type=checkbox]:focus,
.bx-settings-row select:focus {
filter: drop-shadow(1px 0 0 #fff) drop-shadow(-1px 0 0 #fff) drop-shadow(0 1px 0 #fff) drop-shadow(0 -1px 0 #fff);
}
.bx-settings-row:has(input:focus)::before,
.bx-settings-row:has(select:focus)::before {
content: ' ';
border-radius: 4px;
border: 2px solid #fff;
position: absolute;
top: 0;
left: 0;
bottom: 0;
}
.bx-settings-group-label b,
.bx-settings-row label b {
display: block;
font-size: 12px;
font-style: italic;
font-weight: normal;
color: #828282;
}
.bx-settings-group-label b {
margin-bottom: 8px;
}
.bx-settings-app-version {
margin-top: 10px;
text-align: center;
color: #747474;
font-size: 12px;
}
.bx-donation-link {
display: block;
text-align: center;
text-decoration: none;
height: 20px;
line-height: 20px;
font-size: 14px;
margin-top: 10px;
color: #5dc21e;
}
.bx-donation-link:hover {
color: #6dd72b;
}
.bx-donation-link:focus {
text-decoration: underline;
}
.bx-settings-custom-user-agent {
display: block;
width: 100%;
}
.bx-dialog-overlay {
position: fixed;
inset: 0;
z-index: var(--bx-dialog-overlay-z-index);
background: #000;
opacity: 50%;
}
.bx-dialog {
display: flex;
flex-flow: column;
max-height: 90vh;
position: fixed;
top: 50%;
left: 50%;
margin-right: -50%;
transform: translate(-50%, -50%);
min-width: 420px;
padding: 20px;
border-radius: 8px;
z-index: var(--bx-dialog-z-index);
background: #1a1b1e;
color: #fff;
font-weight: 400;
font-size: 16px;
font-family: var(--bx-normal-font);
box-shadow: 0 0 6px #000;
user-select: none;
-webkit-user-select: none;
}
.bx-dialog *:focus {
outline: none !important;
}
.bx-dialog h2 {
display: flex;
margin-bottom: 12px;
}
.bx-dialog h2 b {
flex: 1;
color: #fff;
display: block;
font-family: var(--bx-title-font);
font-size: 26px;
font-weight: 400;
line-height: var(--bx-button-height);
}
.bx-dialog.bx-binding-dialog h2 b {
font-family: var(--bx-promptfont-font) !important;
}
.bx-dialog > div {
overflow: auto;
padding: 2px 0;
}
.bx-dialog > button {
padding: 8px 32px;
margin: 10px auto 0;
border: none;
border-radius: 4px;
display: block;
background-color: #2d3036;
text-align: center;
color: #fff;
text-transform: uppercase;
font-family: var(--bx-title-font);
font-weight: 400;
line-height: 18px;
font-size: 14px;
}
@media (hover: hover) {
.bx-dialog > button:hover {
background-color: #515863;
}
}
.bx-dialog > button:focus {
background-color: #515863;
}
@media screen and (max-width: 450px) {
.bx-dialog {
min-width: 100%;
}
}
.bx-toast {
user-select: none;
-webkit-user-select: none;
position: fixed;
left: 50%;
top: 24px;
transform: translate(-50%, 0);
background: #000;
border-radius: 16px;
color: #fff;
z-index: var(--bx-toast-z-index);
font-family: var(--bx-normal-font);
border: 2px solid #fff;
display: flex;
align-items: center;
opacity: 0;
overflow: clip;
transition: opacity 0.2s ease-in;
}
.bx-toast.bx-show {
opacity: 0.85;
}
.bx-toast.bx-hide {
opacity: 0;
pointer-events: none;
}
.bx-toast-msg {
font-size: 14px;
display: inline-block;
padding: 12px 16px;
white-space: pre;
}
.bx-toast-status {
font-weight: bold;
font-size: 14px;
text-transform: uppercase;
display: inline-block;
background: #515863;
padding: 12px 16px;
color: #fff;
white-space: pre;
}
.bx-wait-time-box {
position: fixed;
top: 0;
right: 0;
background-color: rgba(0,0,0,0.8);
color: #fff;
z-index: var(--bx-wait-time-box-z-index);
padding: 12px;
border-radius: 0 0 0 8px;
}
.bx-wait-time-box label {
display: block;
text-transform: uppercase;
text-align: right;
font-size: 12px;
font-weight: bold;
margin: 0;
}
.bx-wait-time-box span {
display: block;
font-family: var(--bx-monospaced-font);
text-align: right;
font-size: 16px;
margin-bottom: 10px;
}
.bx-wait-time-box span:last-of-type {
margin-bottom: 0;
}
.bx-remote-play-popup {
width: 100%;
max-width: 1920px;
margin: auto;
position: relative;
height: 0.1px;
overflow: visible;
z-index: var(--bx-remote-play-popup-z-index);
}
.bx-remote-play-container {
position: absolute;
right: 10px;
top: 0;
background: #1a1b1e;
border-radius: 10px;
width: 420px;
max-width: calc(100vw - 20px);
margin: 0 0 0 auto;
padding: 20px;
box-shadow: rgba(0,0,0,0.502) 0px 0px 12px 0px;
}
@media (min-width: 480px) and (min-height: calc(480px + 1px)) {
.bx-remote-play-container {
right: calc(env(safe-area-inset-right, 0px) + 32px);
}
}
@media (min-width: 768px) and (min-height: calc(480px + 1px)) {
.bx-remote-play-container {
right: calc(env(safe-area-inset-right, 0px) + 48px);
}
}
@media (min-width: 1920px) and (min-height: calc(480px + 1px)) {
.bx-remote-play-container {
right: calc(env(safe-area-inset-right, 0px) + 80px);
}
}
.bx-remote-play-container > .bx-button {
display: table;
margin: 0 0 0 auto;
}
.bx-remote-play-settings {
margin-bottom: 12px;
padding-bottom: 12px;
border-bottom: 1px solid #2d2d2d;
}
.bx-remote-play-settings > div {
display: flex;
}
.bx-remote-play-settings label {
flex: 1;
}
.bx-remote-play-settings label p {
margin: 4px 0 0;
padding: 0;
color: #888;
font-size: 12px;
}
.bx-remote-play-settings span {
font-weight: bold;
font-size: 18px;
display: block;
margin-bottom: 8px;
text-align: center;
}
.bx-remote-play-resolution {
display: block;
}
.bx-remote-play-resolution input[type="radio"] {
accent-color: var(--bx-primary-button-color);
margin-right: 6px;
}
.bx-remote-play-resolution input[type="radio"]:focus {
accent-color: var(--bx-primary-button-hover-color);
}
.bx-remote-play-device-wrapper {
display: flex;
margin-bottom: 12px;
}
.bx-remote-play-device-wrapper:last-child {
margin-bottom: 2px;
}
.bx-remote-play-device-info {
flex: 1;
padding: 4px 0;
}
.bx-remote-play-device-name {
font-size: 20px;
font-weight: bold;
display: inline-block;
vertical-align: middle;
}
.bx-remote-play-console-type {
font-size: 12px;
background: #004c87;
color: #fff;
display: inline-block;
border-radius: 14px;
padding: 2px 10px;
margin-left: 8px;
vertical-align: middle;
}
.bx-remote-play-power-state {
color: #888;
font-size: 14px;
}
.bx-remote-play-connect-button {
min-height: 100%;
margin: 4px 0;
}
div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
overflow: visible;
}
.bx-stream-menu-button-on {
fill: #000 !important;
background-color: #2d2d2d !important;
color: #000 !important;
}
.bx-stream-refresh-button {
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px) !important;
}
body[data-media-type=default] .bx-stream-refresh-button {
left: calc(env(safe-area-inset-left, 0px) + 11px) !important;
}
body[data-media-type=tv] .bx-stream-refresh-button {
top: calc(var(--gds-focus-borderSize) + 80px) !important;
}
.bx-stream-home-button {
top: calc(env(safe-area-inset-top, 0px) + 10px + 50px * 2) !important;
}
body[data-media-type=default] .bx-stream-home-button {
left: calc(env(safe-area-inset-left, 0px) + 12px) !important;
}
body[data-media-type=tv] .bx-stream-home-button {
top: calc(var(--gds-focus-borderSize) + 80px * 2) !important;
}
div[data-testid=media-container] {
display: flex;
}
div[data-testid=media-container].bx-taking-screenshot:before {
animation: bx-anim-taking-screenshot 0.5s ease;
content: ' ';
position: absolute;
width: 100%;
height: 100%;
z-index: var(--bx-screenshot-animation-z-index);
}
#game-stream video {
margin: auto;
align-self: center;
background: #000;
}
#game-stream canvas {
position: absolute;
align-self: center;
margin: auto;
left: 0;
right: 0;
}
#gamepass-dialog-root div[class^=Guide-module__guide] .bx-button {
overflow: visible;
margin-bottom: 12px;
}
@-moz-keyframes bx-anim-taking-screenshot {
0% {
border: 0px solid rgba(255,255,255,0.502);
}
50% {
border: 8px solid rgba(255,255,255,0.502);
}
100% {
border: 0px solid rgba(255,255,255,0.502);
}
}
@-webkit-keyframes bx-anim-taking-screenshot {
0% {
border: 0px solid rgba(255,255,255,0.502);
}
50% {
border: 8px solid rgba(255,255,255,0.502);
}
100% {
border: 0px solid rgba(255,255,255,0.502);
}
}
@-o-keyframes bx-anim-taking-screenshot {
0% {
border: 0px solid rgba(255,255,255,0.502);
}
50% {
border: 8px solid rgba(255,255,255,0.502);
}
100% {
border: 0px solid rgba(255,255,255,0.502);
}
}
@keyframes bx-anim-taking-screenshot {
0% {
border: 0px solid rgba(255,255,255,0.502);
}
50% {
border: 8px solid rgba(255,255,255,0.502);
}
100% {
border: 0px solid rgba(255,255,255,0.502);
}
}
.bx-number-stepper {
text-align: center;
}
.bx-number-stepper span {
display: inline-block;
min-width: 40px;
font-family: var(--bx-monospaced-font);
font-size: 14px;
}
.bx-number-stepper button {
border: none;
width: 24px;
height: 24px;
margin: 0 4px;
line-height: 24px;
background-color: var(--bx-default-button-color);
color: #fff;
border-radius: 4px;
font-weight: bold;
font-size: 14px;
font-family: var(--bx-monospaced-font);
color: #fff;
}
@media (hover: hover) {
.bx-number-stepper button:hover {
background-color: var(--bx-default-button-hover-color);
}
}
.bx-number-stepper button:active {
background-color: var(--bx-default-button-hover-color);
}
.bx-number-stepper button:disabled + span {
font-family: var(--bx-title-font);
}
.bx-number-stepper input[type="range"] {
display: block;
margin: 12px auto 2px;
width: 180px;
color: #959595 !important;
}
.bx-number-stepper input[type=range]:disabled,
.bx-number-stepper button:disabled {
display: none;
}
.bx-number-stepper[data-disabled=true] input[type=range],
.bx-number-stepper[data-disabled=true] button {
display: none;
}
#bx-game-bar {
z-index: var(--bx-game-bar-z-index);
position: fixed;
bottom: 0;
width: 40px;
height: 90px;
overflow: visible;
cursor: pointer;
}
#bx-game-bar > svg {
display: none;
pointer-events: none;
position: absolute;
height: 28px;
margin-top: 16px;
}
@media (hover: hover) {
#bx-game-bar:hover > svg {
display: block;
}
}
#bx-game-bar .bx-game-bar-container {
opacity: 0;
position: absolute;
display: flex;
overflow: hidden;
background: rgba(26,27,30,0.91);
box-shadow: 0px 0px 6px #1c1c1c;
transition: opacity 0.1s ease-in;
/* Touch controller buttons */
/* Show enabled button */
/* Show enable button */
}
#bx-game-bar .bx-game-bar-container.bx-show {
opacity: 0.9;
}
#bx-game-bar .bx-game-bar-container.bx-show + svg {
display: none !important;
}
#bx-game-bar .bx-game-bar-container.bx-hide {
opacity: 0;
pointer-events: none;
}
#bx-game-bar .bx-game-bar-container button {
width: 60px;
height: 60px;
border-radius: 0;
}
#bx-game-bar .bx-game-bar-container button svg {
width: 28px;
height: 28px;
transition: transform 0.08s ease 0s;
}
#bx-game-bar .bx-game-bar-container button:hover {
border-radius: 0;
}
#bx-game-bar .bx-game-bar-container button:active svg {
transform: scale(0.75);
}
#bx-game-bar .bx-game-bar-container button.bx-activated {
background-color: #fff;
}
#bx-game-bar .bx-game-bar-container button.bx-activated svg {
filter: invert(1);
}
#bx-game-bar .bx-game-bar-container div[data-enabled] button {
display: none;
}
#bx-game-bar .bx-game-bar-container div[data-enabled='true'] button:first-of-type {
display: block;
}
#bx-game-bar .bx-game-bar-container div[data-enabled='false'] button:last-of-type {
display: block;
}
#bx-game-bar[data-position="bottom-left"] {
left: 0;
direction: ltr;
}
#bx-game-bar[data-position="bottom-left"] .bx-game-bar-container {
border-radius: 0 10px 10px 0;
}
#bx-game-bar[data-position="bottom-right"] {
right: 0;
direction: rtl;
}
#bx-game-bar[data-position="bottom-right"] .bx-game-bar-container {
direction: ltr;
border-radius: 10px 0 0 10px;
}
.bx-badges {
margin-left: 0px;
user-select: none;
-webkit-user-select: none;
}
.bx-badge {
border: none;
display: inline-block;
line-height: 24px;
color: #fff;
font-family: var(--bx-title-font-semibold);
font-size: 14px;
font-weight: 400;
margin: 0 8px 8px 0;
box-shadow: 0px 0px 6px #000;
border-radius: 4px;
}
.bx-badge-name {
background-color: #2d3036;
border-radius: 4px 0 0 4px;
}
.bx-badge-name svg {
width: 16px;
height: 16px;
}
.bx-badge-value {
background-color: #808080;
border-radius: 0 4px 4px 0;
}
.bx-badge-name,
.bx-badge-value {
display: inline-block;
padding: 0 8px;
/* height: 30px; */
line-height: 30px;
vertical-align: bottom;
}
.bx-badge-battery[data-charging=true] span:first-of-type::after {
content: ' ⚡️';
}
div[class^=StreamMenu-module__container] .bx-badges {
position: absolute;
max-width: 500px;
}
#gamepass-dialog-root .bx-badges {
position: fixed;
top: 60px;
left: 460px;
max-width: 500px;
}
@media (min-width: 568px) and (max-height: 480px) {
#gamepass-dialog-root .bx-badges {
position: unset;
top: unset;
left: unset;
margin: 8px 0;
}
}
.bx-stats-bar {
display: block;
user-select: none;
-webkit-user-select: none;
position: fixed;
top: 0;
background-color: #000;
color: #fff;
font-family: var(--bx-monospaced-font);
font-size: 0.9rem;
padding-left: 8px;
z-index: var(--bx-stats-bar-z-index);
text-wrap: nowrap;
}
.bx-stats-bar[data-stats*="[fps]"] > .bx-stat-fps,
.bx-stats-bar[data-stats*="[ping]"] > .bx-stat-ping,
.bx-stats-bar[data-stats*="[btr]"] > .bx-stat-btr,
.bx-stats-bar[data-stats*="[dt]"] > .bx-stat-dt,
.bx-stats-bar[data-stats*="[pl]"] > .bx-stat-pl,
.bx-stats-bar[data-stats*="[fl]"] > .bx-stat-fl {
display: inline-block;
}
.bx-stats-bar[data-stats$="[fps]"] > .bx-stat-fps,
.bx-stats-bar[data-stats$="[ping]"] > .bx-stat-ping,
.bx-stats-bar[data-stats$="[btr]"] > .bx-stat-btr,
.bx-stats-bar[data-stats$="[dt]"] > .bx-stat-dt,
.bx-stats-bar[data-stats$="[pl]"] > .bx-stat-pl,
.bx-stats-bar[data-stats$="[fl]"] > .bx-stat-fl {
margin-right: 0;
border-right: none;
}
.bx-stats-bar::before {
display: none;
content: '👀';
vertical-align: middle;
margin-right: 8px;
}
.bx-stats-bar[data-display=glancing]::before {
display: inline-block;
}
.bx-stats-bar[data-position=top-left] {
left: 0;
border-radius: 0 0 4px 0;
}
.bx-stats-bar[data-position=top-right] {
right: 0;
border-radius: 0 0 0 4px;
}
.bx-stats-bar[data-position=top-center] {
transform: translate(-50%, 0);
left: 50%;
border-radius: 0 0 4px 4px;
}
.bx-stats-bar[data-transparent=true] {
background: none;
filter: drop-shadow(1px 0 0 rgba(0,0,0,0.941)) drop-shadow(-1px 0 0 rgba(0,0,0,0.941)) drop-shadow(0 1px 0 rgba(0,0,0,0.941)) drop-shadow(0 -1px 0 rgba(0,0,0,0.941));
}
.bx-stats-bar > div {
display: none;
margin-right: 8px;
border-right: 1px solid #fff;
padding-right: 8px;
}
.bx-stats-bar label {
margin: 0 8px 0 0;
font-family: var(--bx-title-font);
font-size: inherit;
font-weight: bold;
vertical-align: middle;
cursor: help;
}
.bx-stats-bar span {
min-width: 60px;
display: inline-block;
text-align: right;
vertical-align: middle;
}
.bx-stats-bar span[data-grade=good] {
color: #6bffff;
}
.bx-stats-bar span[data-grade=ok] {
color: #fff16b;
}
.bx-stats-bar span[data-grade=bad] {
color: #ff5f5f;
}
.bx-stats-bar span:first-of-type {
min-width: 22px;
}
.bx-stream-settings-dialog {
display: flex;
position: fixed;
z-index: var(--bx-stream-settings-z-index);
opacity: 0.98;
user-select: none;
-webkit-user-select: none;
}
.bx-stream-settings-tabs {
position: fixed;
top: 0;
right: 420px;
display: flex;
flex-direction: column;
border-radius: 0 0 0 8px;
box-shadow: 0px 0px 6px #000;
overflow: clip;
}
.bx-stream-settings-tabs svg {
width: 32px;
height: 32px;
padding: 10px;
box-sizing: content-box;
background: #131313;
cursor: pointer;
border-left: 4px solid #1e1e1e;
}
.bx-stream-settings-tabs svg.bx-active {
background: #222;
border-color: #008746;
}
.bx-stream-settings-tabs svg:not(.bx-active):hover {
background: #2f2f2f;
border-color: #484848;
}
.bx-stream-settings-tab-contents {
flex-direction: column;
position: fixed;
right: 0;
top: 0;
bottom: 0;
padding: 14px 14px 0;
width: 420px;
background: #1a1b1e;
color: #fff;
font-weight: 400;
font-size: 16px;
font-family: var(--bx-title-font);
text-align: center;
box-shadow: 0px 0px 6px #000;
overflow: overlay;
}
.bx-stream-settings-tab-contents > div[data-group=mkb] {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.bx-stream-settings-tab-contents *:focus {
outline: none !important;
}
.bx-stream-settings-tab-contents h2 {
margin-bottom: 8px;
display: flex;
align-item: center;
}
.bx-stream-settings-tab-contents h2 span {
display: inline-block;
font-size: 24px;
font-weight: bold;
text-transform: uppercase;
text-align: left;
flex: 1;
height: var(--bx-button-height);
line-height: calc(var(--bx-button-height) + 4px);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.bx-stream-settings-row {
display: flex;
border-bottom: 1px solid rgba(64,64,64,0.502);
margin-bottom: 16px;
padding-bottom: 16px;
}
.bx-stream-settings-row label {
font-size: 16px;
display: block;
text-align: left;
flex: 1;
align-self: center;
margin-bottom: 0 !important;
}
.bx-stream-settings-row input {
accent-color: var(--bx-primary-button-color);
}
.bx-stream-settings-row select:disabled {
-webkit-appearance: none;
background: transparent;
text-align-last: right;
border: none;
color: #fff;
}
.bx-stream-settings-row select option:disabled {
display: none;
}
.bx-stream-settings-dialog-note {
display: block;
font-size: 12px;
font-weight: lighter;
font-style: italic;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:first-of-type {
display: none;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=true] > div:last-of-type {
display: block;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:first-of-type {
display: block;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] > div[data-has-gamepad=false] > div:last-of-type {
display: none;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-profile {
width: 100%;
height: 36px;
display: block;
margin-bottom: 10px;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-note {
font-size: 14px;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row {
display: flex;
margin-bottom: 10px;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row label.bx-prompt {
flex: 1;
font-size: 26px;
margin-bottom: 0;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions {
flex: 2;
position: relative;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select {
position: absolute;
width: 100%;
height: 100%;
display: block;
}
.bx-stream-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select:last-of-type {
opacity: 0;
z-index: calc(var(--bx-stream-settings-z-index) + 1);
}
.bx-mkb-settings {
display: flex;
flex-direction: column;
flex: 1;
padding-bottom: 10px;
overflow: hidden;
}
.bx-mkb-settings select:disabled {
-webkit-appearance: none;
background: transparent;
text-align-last: right;
text-align: right;
border: none;
color: #fff;
}
.bx-mkb-pointer-lock-msg {
user-select: none;
-webkit-user-select: none;
position: fixed;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
margin: auto;
background: #151515;
z-index: var(--bx-mkb-pointer-lock-msg-z-index);
color: #fff;
text-align: center;
font-weight: 400;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
font-size: 1.3rem;
padding: 12px;
border-radius: 8px;
align-items: center;
box-shadow: 0 0 6px #000;
min-width: 220px;
opacity: 0.9;
}
.bx-mkb-pointer-lock-msg:hover {
opacity: 1;
}
.bx-mkb-pointer-lock-msg > div:first-of-type {
display: flex;
flex-direction: column;
text-align: left;
}
.bx-mkb-pointer-lock-msg p {
margin: 0;
}
.bx-mkb-pointer-lock-msg p:first-child {
font-size: 22px;
margin-bottom: 4px;
font-weight: bold;
}
.bx-mkb-pointer-lock-msg p:last-child {
font-size: 12px;
font-style: italic;
}
.bx-mkb-pointer-lock-msg > div:last-of-type {
margin-top: 10px;
}
.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='native'] button:first-of-type {
margin-bottom: 8px;
}
.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div {
display: flex;
flex-flow: row;
margin-top: 8px;
}
.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button {
flex: 1;
}
.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:first-of-type {
margin-right: 5px;
}
.bx-mkb-pointer-lock-msg > div:last-of-type[data-type='virtual'] div button:last-of-type {
margin-left: 5px;
}
.bx-mkb-preset-tools {
display: flex;
margin-bottom: 12px;
}
.bx-mkb-preset-tools select {
flex: 1;
}
.bx-mkb-preset-tools button {
margin-left: 6px;
}
.bx-mkb-settings-rows {
flex: 1;
overflow: scroll;
}
.bx-mkb-key-row {
display: flex;
margin-bottom: 10px;
align-items: center;
}
.bx-mkb-key-row label {
margin-bottom: 0;
font-family: var(--bx-promptfont-font);
font-size: 26px;
text-align: center;
width: 26px;
height: 32px;
line-height: 32px;
}
.bx-mkb-key-row button {
flex: 1;
height: 32px;
line-height: 32px;
margin: 0 0 0 10px;
background: transparent;
border: none;
color: #fff;
border-radius: 0;
border-left: 1px solid #373737;
}
.bx-mkb-key-row button:hover {
background: transparent;
cursor: default;
}
.bx-mkb-settings.bx-editing .bx-mkb-key-row button {
background: #393939;
border-radius: 4px;
border: none;
}
.bx-mkb-settings.bx-editing .bx-mkb-key-row button:hover {
background: #333;
cursor: pointer;
}
.bx-mkb-action-buttons > div {
text-align: right;
display: none;
}
.bx-mkb-action-buttons button {
margin-left: 8px;
}
.bx-mkb-settings:not(.bx-editing) .bx-mkb-action-buttons > div:first-child {
display: block;
}
.bx-mkb-settings.bx-editing .bx-mkb-action-buttons > div:last-child {
display: block;
}
.bx-mkb-note {
display: block;
margin: 16px 0 10px;
font-size: 12px;
}
.bx-mkb-note:first-of-type {
margin-top: 0;
}
`;
if (getPref(PrefKey.BLOCK_SOCIAL_FEATURES))
css += `
/* Hide "Play with friends" section */
div[class^=HomePage-module__bottomSpacing]:has(button[class*=SocialEmptyCard]),
button[class*=SocialEmptyCard],
/* Hide "Start a party" button in the Guide menu */
#gamepass-dialog-root div[class^=AchievementsPreview-module__container] + button[class*=HomeLandingPage-module__button]
{
display: none;
}
`;
if (getPref(PrefKey.REDUCE_ANIMATIONS))
css += `
div[class*=GameCard-module__gameTitleInnerWrapper],
div[class*=GameCard-module__card],
div[class*=ScrollArrows-module] {
transition: none !important;
}
`;
if (getPref(PrefKey.HIDE_DOTS_ICON))
css += `
div[class*=Grip-module__container] {
visibility: hidden;
}
@media (hover: hover) {
button[class*=GripHandle-module__container]:hover div[class*=Grip-module__container] {
visibility: visible;
}
}
button[class*=GripHandle-module__container][aria-expanded=true] div[class*=Grip-module__container] {
visibility: visible;
}
button[class*=GripHandle-module__container][aria-expanded=false] {
background-color: transparent !important;
}
div[class*=StreamHUD-module__buttonsContainer] {
padding: 0px !important;
}
`;
if (css += `
div[class*=StreamMenu-module__menu] {
min-width: 100vw !important;
}
`, getPref(PrefKey.STREAM_SIMPLIFY_MENU))
css += `
div[class*=Menu-module__scrollable] {
--bxStreamMenuItemSize: 80px;
--streamMenuItemSize: calc(var(--bxStreamMenuItemSize) + 40px) !important;
}
.bx-badges {
top: calc(var(--streamMenuItemSize) - 20px);
}
body[data-media-type=tv] .bx-badges {
top: calc(var(--streamMenuItemSize) - 10px) !important;
}
button[class*=MenuItem-module__container] {
min-width: auto !important;
min-height: auto !important;
width: var(--bxStreamMenuItemSize) !important;
height: var(--bxStreamMenuItemSize) !important;
}
div[class*=MenuItem-module__label] {
display: none !important;
}
svg[class*=MenuItem-module__icon] {
width: 36px;
height: 100% !important;
padding: 0 !important;
margin: 0 !important;
}
`;
else
css += `
body[data-media-type=tv] .bx-badges {
top: calc(var(--streamMenuItemSize) + 30px);
}
body:not([data-media-type=tv]) .bx-badges {
top: calc(var(--streamMenuItemSize) + 20px);
}
body:not([data-media-type=tv]) button[class*=MenuItem-module__container] {
min-width: auto !important;
width: 100px !important;
}
body:not([data-media-type=tv]) button[class*=MenuItem-module__container]:nth-child(n+2) {
margin-left: 10px !important;
}
body:not([data-media-type=tv]) div[class*=MenuItem-module__label] {
margin-left: 8px !important;
margin-right: 8px !important;
}
`;
if (getPref(PrefKey.UI_SCROLLBAR_HIDE))
css += `
html {
scrollbar-width: none;
}
body::-webkit-scrollbar {
display: none;
}
`;
const $style = CE("style", {}, css);
document.documentElement.appendChild($style);
}
// src/modules/mkb/mouse-cursor-hider.ts
class MouseCursorHider {
static #timeout;
static #cursorVisible = !0;
static show() {
document.body && (document.body.style.cursor = "unset"), MouseCursorHider.#cursorVisible = !0;
}
static hide() {
document.body && (document.body.style.cursor = "none"), MouseCursorHider.#timeout = null, MouseCursorHider.#cursorVisible = !1;
}
static onMouseMove(e) {
!MouseCursorHider.#cursorVisible && MouseCursorHider.show(), MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), MouseCursorHider.#timeout = window.setTimeout(MouseCursorHider.hide, 3000);
}
static start() {
MouseCursorHider.show(), document.addEventListener("mousemove", MouseCursorHider.onMouseMove);
}
static stop() {
MouseCursorHider.#timeout && clearTimeout(MouseCursorHider.#timeout), document.removeEventListener("mousemove", MouseCursorHider.onMouseMove), MouseCursorHider.show();
}
}
// src/modules/patches/controller-shortcuts.js
var controller_shortcuts_default = "const currentGamepad = ${gamepadVar};\n\n// Share button on XS controller\nif (currentGamepad.buttons[17] && currentGamepad.buttons[17].pressed) {\n window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n}\n\nconst btnHome = currentGamepad.buttons[16];\nif (btnHome) {\n if (!this.bxHomeStates) {\n this.bxHomeStates = {};\n }\n\n let intervalMs = 0;\n let hijack = false;\n\n if (btnHome.pressed) {\n hijack = true;\n intervalMs = 16;\n this.gamepadIsIdle.set(currentGamepad.index, false);\n\n if (this.bxHomeStates[currentGamepad.index]) {\n const lastTimestamp = this.bxHomeStates[currentGamepad.index].timestamp;\n\n if (currentGamepad.timestamp !== lastTimestamp) {\n this.bxHomeStates[currentGamepad.index].timestamp = currentGamepad.timestamp;\n\n const handled = window.BX_EXPOSED.handleControllerShortcut(currentGamepad);\n if (handled) {\n this.bxHomeStates[currentGamepad.index].shortcutPressed += 1;\n }\n }\n } else {\n // First time pressing > save current timestamp\n window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index);\n this.bxHomeStates[currentGamepad.index] = {\n shortcutPressed: 0,\n timestamp: currentGamepad.timestamp,\n };\n }\n } else if (this.bxHomeStates[currentGamepad.index]) {\n hijack = true;\n const info = structuredClone(this.bxHomeStates[currentGamepad.index]);\n\n // Home button released\n this.bxHomeStates[currentGamepad.index] = null;\n\n if (info.shortcutPressed === 0) {\n const fakeGamepadMappings = [{\n GamepadIndex: currentGamepad.index,\n A: 0,\n B: 0,\n X: 0,\n Y: 0,\n LeftShoulder: 0,\n RightShoulder: 0,\n LeftTrigger: 0,\n RightTrigger: 0,\n View: 0,\n Menu: 0,\n LeftThumb: 0,\n RightThumb: 0,\n DPadUp: 0,\n DPadDown: 0,\n DPadLeft: 0,\n DPadRight: 0,\n Nexus: 1,\n LeftThumbXAxis: 0,\n LeftThumbYAxis: 0,\n RightThumbXAxis: 0,\n RightThumbYAxis: 0,\n PhysicalPhysicality: 0,\n VirtualPhysicality: 0,\n Dirty: true,\n Virtual: false,\n }];\n\n const isLongPress = (currentGamepad.timestamp - info.timestamp) >= 500;\n intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n } else {\n intervalMs = 4;\n }\n }\n\n if (hijack && intervalMs) {\n // Listen to next button press\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n }\n}\n";
// src/modules/patches/expose-stream-session.js
var expose_stream_session_default = "window.BX_EXPOSED.streamSession = this;\n\nconst orgSetMicrophoneState = this.setMicrophoneState.bind(this);\nthis.setMicrophoneState = state => {\n orgSetMicrophoneState(state);\n\n const evt = new Event(BxEvent.MICROPHONE_STATE_CHANGED);\n evt.microphoneState = state;\n\n window.dispatchEvent(evt);\n};\n\nwindow.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));\n\n// Patch updateDimensions() to make native touch work correctly with WebGL2\nlet updateDimensionsStr = this.updateDimensions.toString();\n\n// if(r){\nconst renderTargetVar = updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];\n\nupdateDimensionsStr = updateDimensionsStr.replaceAll(renderTargetVar + '.scroll', 'scroll');\n\nupdateDimensionsStr = updateDimensionsStr.replace(`if(${renderTargetVar}){`, `\nif (${renderTargetVar}) {\n const scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\n const scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);\n\neval(`this.updateDimensions = function ${updateDimensionsStr}`);\n";
// src/modules/patches/local-co-op-enable.js
var local_co_op_enable_default = "let match;\nlet onGamepadChangedStr = this.onGamepadChanged.toString();\n\nonGamepadChangedStr = onGamepadChangedStr.replaceAll('0', 'arguments[1]');\neval(`this.onGamepadChanged = function ${onGamepadChangedStr}`);\n\nlet onGamepadInputStr = this.onGamepadInput.toString();\n\nmatch = onGamepadInputStr.match(/(\\w+\\.GamepadIndex)/);\nif (match) {\n const gamepadIndexVar = match[0];\n onGamepadInputStr = onGamepadInputStr.replace('this.gamepadStates.get(', `this.gamepadStates.get(${gamepadIndexVar},`);\n eval(`this.onGamepadInput = function ${onGamepadInputStr}`);\n BxLogger.info('supportLocalCoOp', '✅ Successfully patched local co-op support');\n} else {\n BxLogger.error('supportLocalCoOp', '❌ Unable to patch local co-op support');\n}\n";
// src/modules/patches/remote-play-enable.js
var remote_play_enable_default = "connectMode: window.BX_REMOTE_PLAY_CONFIG ? \"xhome-connect\" : \"cloud-connect\",\nremotePlayServerId: (window.BX_REMOTE_PLAY_CONFIG && window.BX_REMOTE_PLAY_CONFIG.serverId) || '',\n";
// src/modules/patches/remote-play-keep-alive.js
var remote_play_keep_alive_default = "const msg = JSON.parse(e);\nif (msg.reason === 'WarningForBeingIdle' && !window.location.pathname.includes('/launch/')) {\n try {\n this.sendKeepAlive();\n return;\n } catch (ex) { console.log(ex); }\n}\n";
// src/modules/patches/vibration-adjust.js
var vibration_adjust_default = "if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {\n return void(0);\n}\n\nconst intensity = window.BX_VIBRATION_INTENSITY;\nif (intensity === 0) {\n return void(0);\n}\n\nif (intensity < 1) {\n e.leftMotorPercent *= intensity;\n e.rightMotorPercent *= intensity;\n e.leftTriggerMotorPercent *= intensity;\n e.rightTriggerMotorPercent *= intensity;\n}\n";
// src/modules/patcher.ts
var ENDING_CHUNKS_PATCH_NAME = "loadingEndingChunks", LOG_TAG5 = "Patcher", PATCHES = {
disableAiTrack(str2) {
const index = str2.indexOf(".track=function(");
if (index === -1)
return !1;
if (str2.substring(0, index + 200).includes('"AppInsightsCore'))
return !1;
return str2.substring(0, index) + ".track=function(e){},!!function(" + str2.substring(index + ".track=function(".length);
},
disableTelemetry(str2) {
if (!str2.includes(".disableTelemetry=function(){return!1}"))
return !1;
return str2.replace(".disableTelemetry=function(){return!1}", ".disableTelemetry=function(){return!0}");
},
disableTelemetryProvider(str2) {
if (!str2.includes("this.enableLightweightTelemetry=!"))
return !1;
const newCode = [
"this.trackEvent",
"this.trackPageView",
"this.trackHttpCompleted",
"this.trackHttpFailed",
"this.trackError",
"this.trackErrorLike",
"this.onTrackEvent",
"()=>{}"
].join("=");
return str2.replace("this.enableLightweightTelemetry=!", newCode + ";this.enableLightweightTelemetry=!");
},
disableIndexDbLogging(str2) {
if (!str2.includes(",this.logsDb=new"))
return !1;
let newCode = ",this.log=()=>{}";
return str2.replace(",this.logsDb=new", newCode + ",this.logsDb=new");
},
websiteLayout(str2) {
if (!str2.includes('?"tv":"default"'))
return !1;
const layout = getPref(PrefKey.UI_LAYOUT) === "tv" ? "tv" : "default";
return str2.replace('?"tv":"default"', `?"${layout}":"${layout}"`);
},
remotePlayDirectConnectUrl(str2) {
const index = str2.indexOf("/direct-connect");
if (index === -1)
return !1;
return str2.replace(str2.substring(index - 9, index + 15), "https://www.xbox.com/play");
},
remotePlayKeepAlive(str2) {
if (!str2.includes("onServerDisconnectMessage(e){"))
return !1;
return str2 = str2.replace("onServerDisconnectMessage(e){", "onServerDisconnectMessage(e){" + remote_play_keep_alive_default), str2;
},
remotePlayConnectMode(str2) {
if (!str2.includes('connectMode:"cloud-connect",'))
return !1;
return str2.replace('connectMode:"cloud-connect",', remote_play_enable_default);
},
remotePlayDisableAchievementToast(str2) {
if (!str2.includes(".AchievementUnlock:{"))
return !1;
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
return;
}
`;
return str2.replace(".AchievementUnlock:{", ".AchievementUnlock:{" + newCode);
},
disableTrackEvent(str2) {
if (!str2.includes("this.trackEvent="))
return !1;
return str2.replace("this.trackEvent=", "this.trackEvent=e=>{},this.uwuwu=");
},
blockWebRtcStatsCollector(str2) {
if (!str2.includes("this.shouldCollectStats=!0"))
return !1;
return str2.replace("this.shouldCollectStats=!0", "this.shouldCollectStats=!1");
},
patchPollGamepads(str2) {
const index = str2.indexOf("},this.pollGamepads=()=>{");
if (index === -1)
return !1;
const nextIndex = str2.indexOf("setTimeout(this.pollGamepads", index);
if (nextIndex === -1)
return !1;
let codeBlock = str2.substring(index, nextIndex);
if (getPref(PrefKey.BLOCK_TRACKING))
codeBlock = codeBlock.replaceAll("this.inputPollingIntervalStats.addValue", "");
const match = codeBlock.match(/this\.gamepadTimestamps\.set\((\w+)\.index/);
if (match) {
const gamepadVar = match[1], newCode = renderString(controller_shortcuts_default, {
gamepadVar
});
codeBlock = codeBlock.replace("this.gamepadTimestamps.set", newCode + "this.gamepadTimestamps.set");
}
return str2.substring(0, index) + codeBlock + str2.substring(nextIndex);
},
enableXcloudLogger(str2) {
if (!str2.includes("this.telemetryProvider=e}log(e,t,i){"))
return !1;
return str2 = str2.replaceAll("this.telemetryProvider=e}log(e,t,i){", "this.telemetryProvider=e}log(e,t,i){console.log(Array.from(arguments));"), str2;
},
enableConsoleLogging(str2) {
if (!str2.includes("static isConsoleLoggingAllowed(){"))
return !1;
return str2 = str2.replaceAll("static isConsoleLoggingAllowed(){", "static isConsoleLoggingAllowed(){return true;"), str2;
},
playVibration(str2) {
if (!str2.includes("}playVibration(e){"))
return !1;
return VibrationManager.updateGlobalVars(), str2 = str2.replaceAll("}playVibration(e){", "}playVibration(e){" + vibration_adjust_default), str2;
},
overrideSettings(str2) {
const index = str2.indexOf(",EnableStreamGate:");
if (index === -1)
return !1;
const endIndex = str2.indexOf("},", index);
let newSettings = JSON.stringify(FeatureGates);
newSettings = newSettings.substring(1, newSettings.length - 1);
const newCode = newSettings;
return str2 = str2.substring(0, endIndex) + "," + newCode + str2.substring(endIndex), str2;
},
disableGamepadDisconnectedScreen(str2) {
const index = str2.indexOf('"GamepadDisconnected_Title",');
if (index === -1)
return !1;
const constIndex = str2.indexOf("const", index - 30);
return str2 = str2.substring(0, constIndex) + "e.onClose();return null;" + str2.substring(constIndex), str2;
},
patchUpdateInputConfigurationAsync(str2) {
if (!str2.includes("async updateInputConfigurationAsync(e){"))
return !1;
const newCode = "e.enableTouchInput = true;";
return str2 = str2.replace("async updateInputConfigurationAsync(e){", "async updateInputConfigurationAsync(e){" + newCode), str2;
},
loadingEndingChunks(str2) {
if (!str2.includes('"FamilySagaManager"'))
return !1;
return BxLogger.info(LOG_TAG5, "Remaining patches:", PATCH_ORDERS), PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS), str2;
},
disableStreamGate(str2) {
const index = str2.indexOf('case"partially-ready":');
if (index === -1)
return !1;
const bracketIndex = str2.indexOf("=>{", index - 150) + 3;
return str2 = str2.substring(0, bracketIndex) + "return 0;" + str2.substring(bracketIndex), str2;
},
exposeTouchLayoutManager(str2) {
if (!str2.includes("this._perScopeLayoutsStream=new"))
return !1;
const newCode = `
true;
window.BX_EXPOSED["touchLayoutManager"] = this;
window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}"));
`;
return str2 = str2.replace("this._perScopeLayoutsStream=new", newCode + "this._perScopeLayoutsStream=new"), str2;
},
patchBabylonRendererClass(str2) {
let index = str2.indexOf(".current.render(),");
if (index === -1)
return !1;
index -= 1;
const newCode = `
if (window.BX_EXPOSED.stopTakRendering) {
try {
document.getElementById('BabylonCanvasContainer-main')?.parentElement.classList.add('bx-offscreen');
${str2[index]}.current.dispose();
} catch (e) {}
window.BX_EXPOSED.stopTakRendering = false;
return;
}
`;
return str2 = str2.substring(0, index) + newCode + str2.substring(index), str2;
},
supportLocalCoOp(str2) {
if (!str2.includes("this.gamepadMappingsToSend=[],"))
return !1;
const newCode = `true; ${local_co_op_enable_default}; true,`;
return str2 = str2.replace("this.gamepadMappingsToSend=[],", "this.gamepadMappingsToSend=[]," + newCode), str2;
},
forceFortniteConsole(str2) {
if (!str2.includes("sendTouchInputEnabledMessage(e){"))
return !1;
const newCode = "window.location.pathname.includes('/launch/fortnite/') && (e = false);";
return str2 = str2.replace("sendTouchInputEnabledMessage(e){", "sendTouchInputEnabledMessage(e){" + newCode), str2;
},
disableTakRenderer(str2) {
if (!str2.includes("const{TakRenderer:"))
return !1;
let remotePlayCode = "";
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF))
remotePlayCode = `
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
for (let gamepad of gamepads) {
if (gamepad && gamepad.connected) {
gamepadFound = true;
break;
}
}
if (gamepadFound) {
return;
}
`;
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
${remotePlayCode}
} else {
const titleInfo = window.BX_EXPOSED.getTitleInfo();
if (titleInfo && !titleInfo.details.hasTouchSupport && !titleInfo.details.hasFakeTouchSupport) {
return;
}
}
`;
return str2 = str2.replace("const{TakRenderer:", newCode + "const{TakRenderer:"), str2;
},
streamCombineSources(str2) {
if (!str2.includes("this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen"))
return !1;
return str2 = str2.replace("this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen", "this.useCombinedAudioVideoStream=true"), str2;
},
patchStreamHud(str2) {
if (!str2.includes("let{onCollapse"))
return !1;
let newCode = `
// Expose onShowStreamMenu
window.BX_EXPOSED.showStreamMenu = e.onShowStreamMenu;
// Restore the "..." button
e.guideUI = null;
`;
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off")
newCode += "e.canShowTakHUD = false;";
return str2 = str2.replace("let{onCollapse", newCode + "let{onCollapse"), str2;
},
broadcastPollingMode(str2) {
if (!str2.includes(".setPollingMode=e=>{"))
return !1;
const newCode = `
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e});
`;
return str2 = str2.replace(".setPollingMode=e=>{", ".setPollingMode=e=>{" + newCode), str2;
},
patchXcloudTitleInfo(str2) {
let index = str2.indexOf("async cloudConnect");
if (index === -1)
return !1;
let backetIndex = str2.indexOf("{", index);
const titleInfoVar = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[0], newCode = `
${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar});
BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
`;
return str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1), str2;
},
patchRemotePlayMkb(str2) {
let index = str2.indexOf("async homeConsoleConnect");
if (index === -1)
return !1;
let backetIndex = str2.indexOf("{", index);
const configsVar = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1].split(",")[1], newCode = `
Object.assign(${configsVar}.inputConfiguration, {
enableMouseInput: false,
enableKeyboardInput: false,
enableAbsoluteMouse: false,
});
BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
return str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1), str2;
},
patchAudioMediaStream(str2) {
if (!str2.includes(".srcObject=this.audioMediaStream,"))
return !1;
const newCode = "window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),";
return str2 = str2.replace(".srcObject=this.audioMediaStream,", ".srcObject=this.audioMediaStream," + newCode), str2;
},
patchCombinedAudioVideoMediaStream(str2) {
if (!str2.includes(".srcObject=this.combinedAudioVideoStream"))
return !1;
const newCode = ",window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)";
return str2 = str2.replace(".srcObject=this.combinedAudioVideoStream", ".srcObject=this.combinedAudioVideoStream" + newCode), str2;
},
patchTouchControlDefaultOpacity(str2) {
if (!str2.includes("opacityMultiplier:1"))
return !1;
const newCode = `opacityMultiplier: ${(getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1)}`;
return str2 = str2.replace("opacityMultiplier:1", newCode), str2;
},
patchShowSensorControls(str2) {
if (!str2.includes("{shouldShowSensorControls:"))
return !1;
const newCode = "{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||";
return str2 = str2.replace("{shouldShowSensorControls:", newCode), str2;
},
exposeStreamSession(str2) {
if (!str2.includes(",this._connectionType="))
return !1;
const newCode = `;
${expose_stream_session_default}
true` + ",this._connectionType=";
return str2 = str2.replace(",this._connectionType=", newCode), str2;
},
skipFeedbackDialog(str2) {
if (!str2.includes("&&this.shouldTransitionToFeedback("))
return !1;
return str2 = str2.replace("&&this.shouldTransitionToFeedback(", "&& false &&this.shouldTransitionToFeedback("), str2;
},
enableNativeMkb(str2) {
if (!str2.includes("e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;"))
return !1;
return str2 = str2.replace("e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;", "e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;return true;"), str2;
},
patchMouseAndKeyboardEnabled(str2) {
if (!str2.includes("get mouseAndKeyboardEnabled(){"))
return !1;
return str2 = str2.replace("get mouseAndKeyboardEnabled(){", "get mouseAndKeyboardEnabled(){return true;"), str2;
},
exposeInputSink(str2) {
if (!str2.includes("this.controlChannel=null,this.inputChannel=null"))
return !1;
const newCode = "window.BX_EXPOSED.inputSink = this;";
return str2 = str2.replace("this.controlChannel=null,this.inputChannel=null", newCode + "this.controlChannel=null,this.inputChannel=null"), str2;
},
disableNativeRequestPointerLock(str2) {
if (!str2.includes("async requestPointerLock(){"))
return !1;
return str2 = str2.replace("async requestPointerLock(){", "async requestPointerLock(){return;"), str2;
}
}, PATCH_ORDERS = [
...getPref(PrefKey.NATIVE_MKB_ENABLED) === "on" ? [
"enableNativeMkb",
"patchMouseAndKeyboardEnabled",
"disableNativeRequestPointerLock",
"exposeInputSink"
] : [],
"disableStreamGate",
"overrideSettings",
"broadcastPollingMode",
"exposeStreamSession",
getPref(PrefKey.UI_LAYOUT) !== "default" && "websiteLayout",
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && "supportLocalCoOp",
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && "forceFortniteConsole",
...getPref(PrefKey.BLOCK_TRACKING) ? [
"disableAiTrack",
"disableTelemetry",
"blockWebRtcStatsCollector",
"disableIndexDbLogging",
"disableTelemetryProvider",
"disableTrackEvent"
] : [],
...getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
"remotePlayKeepAlive",
"remotePlayDirectConnectUrl",
"remotePlayDisableAchievementToast",
STATES.userAgentHasTouchSupport && "patchUpdateInputConfigurationAsync"
] : [],
...BX_FLAGS.EnableXcloudLogging ? [
"enableConsoleLogging",
"enableXcloudLogger"
] : []
].filter((item2) => !!item2), PLAYING_PATCH_ORDERS = [
"patchXcloudTitleInfo",
"disableGamepadDisconnectedScreen",
"patchStreamHud",
"playVibration",
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && !getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchAudioMediaStream",
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && getPref(PrefKey.STREAM_COMBINE_SOURCES) && "patchCombinedAudioVideoMediaStream",
getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG) && "skipFeedbackDialog",
...STATES.userAgentHasTouchSupport ? [
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "patchShowSensorControls",
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "exposeTouchLayoutManager",
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off" || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && "disableTakRenderer",
getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && "patchTouchControlDefaultOpacity",
"patchBabylonRendererClass"
] : [],
BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
"patchPollGamepads",
getPref(PrefKey.STREAM_COMBINE_SOURCES) && "streamCombineSources",
...getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
"patchRemotePlayMkb",
"remotePlayConnectMode"
] : []
].filter((item2) => !!item2), ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
class Patcher {
static #patchFunctionBind() {
const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
let valid = !1;
if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
if (arguments[1] === 0 || typeof arguments[1] === "function")
valid = !0;
}
if (!valid)
return nativeBind.apply(this, arguments);
if (PatcherCache.init(), typeof arguments[1] === "function")
BxLogger.info(LOG_TAG5, "Restored Function.prototype.bind()"), Function.prototype.bind = nativeBind;
const orgFunc = this, newFunc = (a, item2) => {
Patcher.patch(item2), orgFunc(a, item2);
};
return nativeBind.apply(newFunc, arguments);
};
}
static patch(item) {
let patchesToCheck, appliedPatches;
const patchesMap = {};
for (let id in item[1]) {
appliedPatches = [];
const cachedPatches = PatcherCache.getPatches(id);
if (cachedPatches)
patchesToCheck = cachedPatches.slice(0), patchesToCheck.push(...PATCH_ORDERS);
else
patchesToCheck = PATCH_ORDERS.slice(0);
if (!patchesToCheck.length)
continue;
const func = item[1][id];
let str = func.toString(), modified = !1;
for (let patchIndex = 0;patchIndex < patchesToCheck.length; patchIndex++) {
const patchName = patchesToCheck[patchIndex];
if (appliedPatches.indexOf(patchName) > -1)
continue;
if (!PATCHES[patchName])
continue;
const patchedStr = PATCHES[patchName].call(null, str);
if (!patchedStr)
continue;
modified = !0, str = patchedStr, BxLogger.info(LOG_TAG5, `✅ ${patchName}`), appliedPatches.push(patchName), patchesToCheck.splice(patchIndex, 1), patchIndex--, PATCH_ORDERS = PATCH_ORDERS.filter((item2) => item2 != patchName);
}
if (modified)
item[1][id] = eval(str);
if (appliedPatches.length)
patchesMap[id] = appliedPatches;
}
if (Object.keys(patchesMap).length)
PatcherCache.saveToCache(patchesMap);
}
static init() {
Patcher.#patchFunctionBind();
}
}
class PatcherCache {
static #KEY_CACHE = "better_xcloud_patches_cache";
static #KEY_SIGNATURE = "better_xcloud_patches_cache_signature";
static #CACHE;
static #isInitialized = !1;
static #getSignature() {
const scriptVersion = SCRIPT_VERSION, webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content, patches = JSON.stringify(ALL_PATCHES);
return hashCode(scriptVersion + webVersion + patches);
}
static clear() {
window.localStorage.removeItem(PatcherCache.#KEY_CACHE), PatcherCache.#CACHE = {};
}
static checkSignature() {
const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0, currentSig = PatcherCache.#getSignature();
if (currentSig !== parseInt(storedSig))
BxLogger.warning(LOG_TAG5, "Signature changed"), window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString()), PatcherCache.clear();
else
BxLogger.info(LOG_TAG5, "Signature unchanged");
}
static #cleanupPatches(patches) {
return patches.filter((item2) => {
for (let id2 in PatcherCache.#CACHE)
if (PatcherCache.#CACHE[id2].includes(item2))
return !1;
return !0;
});
}
static getPatches(id2) {
return PatcherCache.#CACHE[id2];
}
static saveToCache(subCache) {
for (let id2 in subCache) {
const patchNames = subCache[id2];
let data = PatcherCache.#CACHE[id2];
if (!data)
PatcherCache.#CACHE[id2] = patchNames;
else
for (let patchName of patchNames)
if (!data.includes(patchName))
data.push(patchName);
}
window.localStorage.setItem(PatcherCache.#KEY_CACHE, JSON.stringify(PatcherCache.#CACHE));
}
static init() {
if (PatcherCache.#isInitialized)
return;
if (PatcherCache.#isInitialized = !0, PatcherCache.checkSignature(), PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}"), BxLogger.info(LOG_TAG5, PatcherCache.#CACHE), window.location.pathname.includes("/play/"))
PATCH_ORDERS.push(...PLAYING_PATCH_ORDERS);
else
PATCH_ORDERS.push(ENDING_CHUNKS_PATCH_NAME);
PATCH_ORDERS = PatcherCache.#cleanupPatches(PATCH_ORDERS), PLAYING_PATCH_ORDERS = PatcherCache.#cleanupPatches(PLAYING_PATCH_ORDERS), BxLogger.info(LOG_TAG5, PATCH_ORDERS.slice(0)), BxLogger.info(LOG_TAG5, PLAYING_PATCH_ORDERS.slice(0));
}
}
// src/modules/ui/global-settings.ts
function setupSettingsUi() {
if (document.querySelector(".bx-settings-container"))
return;
const PREF_PREFERRED_REGION = getPreferredServerRegion(), PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
let $btnReload;
const $container = CE("div", {
class: "bx-settings-container bx-gone"
});
let $updateAvailable;
const $wrapper = CE("div", { class: "bx-settings-wrapper" }, CE("div", { class: "bx-settings-title-wrapper" }, CE("a", {
class: "bx-settings-title",
href: "https://github.com/redphx/better-xcloud/releases",
target: "_blank"
}, "Better xCloud " + SCRIPT_VERSION), createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.FOCUSABLE,
label: t("help"),
url: "https://better-xcloud.github.io/features/"
})));
if ($updateAvailable = CE("a", {
class: "bx-settings-update bx-gone",
href: "https://github.com/redphx/better-xcloud/releases/latest",
target: "_blank"
}), $wrapper.appendChild($updateAvailable), !SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION != SCRIPT_VERSION)
$updateAvailable.textContent = `🌟 Version ${PREF_LATEST_VERSION} available`, $updateAvailable.classList.remove("bx-gone");
if (AppInterface) {
const $btn = createButton({
label: t("android-app-settings"),
icon: BxIcon.STREAM_SETTINGS,
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: (e) => {
AppInterface.openAppSettings && AppInterface.openAppSettings();
}
});
$wrapper.appendChild($btn);
} else if (UserAgent.getDefault().toLowerCase().includes("android")) {
const $btn = createButton({
label: "🔥 " + t("install-android"),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
url: "https://better-xcloud.github.io/android"
});
$wrapper.appendChild($btn);
}
const onChange = async (e) => {
PatcherCache.clear(), $btnReload.classList.add("bx-danger");
const $btnHeaderSettings = document.querySelector(".bx-header-settings-button");
if ($btnHeaderSettings && $btnHeaderSettings.classList.add("bx-danger"), e.target.id === "bx_setting_" + PrefKey.BETTER_XCLOUD_LOCALE)
Translations.refreshCurrentLocale(), await Translations.updateTranslations(), $btnReload.textContent = t("settings-reloading"), $btnReload.click();
};
for (let groupLabel in SETTINGS_UI) {
const $group = CE("span", { class: "bx-settings-group-label" }, groupLabel);
if (SETTINGS_UI[groupLabel].note) {
const $note = CE("b", {}, SETTINGS_UI[groupLabel].note);
$group.appendChild($note);
}
if ($wrapper.appendChild($group), SETTINGS_UI[groupLabel].unsupported)
continue;
const settingItems = SETTINGS_UI[groupLabel].items;
for (let settingId of settingItems) {
if (!settingId)
continue;
const setting = Preferences.SETTINGS[settingId];
if (!setting)
continue;
let settingLabel = setting.label, settingNote = setting.note || "";
if (setting.experimental)
if (settingLabel = "🧪 " + settingLabel, !settingNote)
settingNote = t("experimental");
else
settingNote = `${t("experimental")}: ${settingNote}`;
let $control, $inpCustomUserAgent, labelAttrs = {
tabindex: "-1"
};
if (settingId === PrefKey.USER_AGENT_PROFILE) {
let defaultUserAgent = window.navigator.orgUserAgent || window.navigator.userAgent;
$inpCustomUserAgent = CE("input", {
id: `bx_setting_inp_${settingId}`,
type: "text",
placeholder: defaultUserAgent,
class: "bx-settings-custom-user-agent"
}), $inpCustomUserAgent.addEventListener("change", (e) => {
const profile = $control.value, custom = e.target.value.trim();
UserAgent.updateStorage(profile, custom), onChange(e);
}), $control = toPrefElement(PrefKey.USER_AGENT_PROFILE, (e) => {
const value = e.target.value;
let isCustom = value === UserAgentProfile.CUSTOM, userAgent2 = UserAgent.get(value);
UserAgent.updateStorage(value), $inpCustomUserAgent.value = userAgent2, $inpCustomUserAgent.readOnly = !isCustom, $inpCustomUserAgent.disabled = !isCustom, !e.target.disabled && onChange(e);
});
} else if (settingId === PrefKey.SERVER_REGION) {
let selectedValue;
$control = CE("select", {
id: `bx_setting_${settingId}`,
title: settingLabel,
tabindex: 0
}), $control.name = $control.id, $control.addEventListener("change", (e) => {
setPref(settingId, e.target.value), onChange(e);
}), selectedValue = PREF_PREFERRED_REGION, setting.options = {};
for (let regionName in STATES.serverRegions) {
const region4 = STATES.serverRegions[regionName];
let value = regionName, label = `${region4.shortName} - ${regionName}`;
if (region4.isDefault) {
if (label += ` (${t("default")})`, value = "default", selectedValue === regionName)
selectedValue = "default";
}
setting.options[value] = label;
}
for (let value in setting.options) {
const label = setting.options[value], $option = CE("option", { value }, label);
$control.appendChild($option);
}
$control.value = selectedValue;
} else if (settingId === PrefKey.BETTER_XCLOUD_LOCALE)
$control = toPrefElement(settingId, (e) => {
localStorage.setItem("better_xcloud_locale", e.target.value), onChange(e);
});
else
$control = toPrefElement(settingId, onChange);
if ($control.id)
labelAttrs.for = $control.id;
else
labelAttrs.for = `bx_setting_${settingId}`;
if (setting.unsupported)
$control.disabled = !0;
if ($control.disabled && !!$control.getAttribute("tabindex"))
$control.setAttribute("tabindex", -1);
const $label = CE("label", labelAttrs, settingLabel);
if (settingNote)
$label.appendChild(CE("b", {}, settingNote));
const $elm = CE("div", { class: "bx-settings-row" }, $label, $control);
if ($wrapper.appendChild($elm), settingId === PrefKey.USER_AGENT_PROFILE)
$wrapper.appendChild($inpCustomUserAgent), $control.disabled = !0, $control.dispatchEvent(new Event("change")), $control.disabled = !1;
}
}
$btnReload = createButton({
label: t("settings-reload"),
classes: ["bx-settings-reload-button"],
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH | ButtonStyle.TALL,
onClick: (e) => {
window.location.reload(), $btnReload.disabled = !0, $btnReload.textContent = t("settings-reloading");
}
}), $btnReload.setAttribute("tabindex", "0"), $wrapper.appendChild($btnReload);
const $donationLink = CE("a", {
class: "bx-donation-link",
href: "https://ko-fi.com/redphx",
target: "_blank",
tabindex: 0
}, `❤️ ${t("support-better-xcloud")}`);
$wrapper.appendChild($donationLink);
try {
const appVersion = document.querySelector("meta[name=gamepass-app-version]").content, appDate = new Date(document.querySelector("meta[name=gamepass-app-date]").content).toISOString().substring(0, 10);
$wrapper.appendChild(CE("div", { class: "bx-settings-app-version" }, `xCloud website version ${appVersion} (${appDate})`));
} catch (e) {
}
$container.appendChild($wrapper);
const $pageContent = document.getElementById("PageContent");
$pageContent?.parentNode?.insertBefore($container, $pageContent);
}
var SETTINGS_UI = {
"Better xCloud": {
items: [
PrefKey.BETTER_XCLOUD_LOCALE,
PrefKey.REMOTE_PLAY_ENABLED
]
},
[t("server")]: {
items: [
PrefKey.SERVER_REGION,
PrefKey.STREAM_PREFERRED_LOCALE,
PrefKey.PREFER_IPV6_SERVER
]
},
[t("stream")]: {
items: [
PrefKey.STREAM_TARGET_RESOLUTION,
PrefKey.STREAM_CODEC_PROFILE,
PrefKey.BITRATE_VIDEO_MAX,
PrefKey.AUDIO_ENABLE_VOLUME_CONTROL,
PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG,
PrefKey.SCREENSHOT_APPLY_FILTERS,
PrefKey.AUDIO_MIC_ON_PLAYING,
PrefKey.GAME_FORTNITE_FORCE_CONSOLE,
PrefKey.STREAM_COMBINE_SOURCES
]
},
[t("game-bar")]: {
items: [
PrefKey.GAME_BAR_POSITION
]
},
[t("local-co-op")]: {
items: [
PrefKey.LOCAL_CO_OP_ENABLED
]
},
[t("mouse-and-keyboard")]: {
items: [
PrefKey.NATIVE_MKB_ENABLED,
PrefKey.MKB_ENABLED,
PrefKey.MKB_HIDE_IDLE_CURSOR
]
},
[t("touch-controller")]: {
note: !STATES.userAgentHasTouchSupport ? "⚠️ " + t("device-unsupported-touch") : null,
unsupported: !STATES.userAgentHasTouchSupport,
items: [
PrefKey.STREAM_TOUCH_CONTROLLER,
PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF,
PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY,
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_STANDARD,
PrefKey.STREAM_TOUCH_CONTROLLER_STYLE_CUSTOM
]
},
[t("loading-screen")]: {
items: [
PrefKey.UI_LOADING_SCREEN_GAME_ART,
PrefKey.UI_LOADING_SCREEN_WAIT_TIME,
PrefKey.UI_LOADING_SCREEN_ROCKET
]
},
[t("ui")]: {
items: [
PrefKey.UI_LAYOUT,
PrefKey.UI_HOME_CONTEXT_MENU_DISABLED,
PrefKey.STREAM_SIMPLIFY_MENU,
PrefKey.SKIP_SPLASH_VIDEO,
!AppInterface && PrefKey.UI_SCROLLBAR_HIDE,
PrefKey.HIDE_DOTS_ICON,
PrefKey.REDUCE_ANIMATIONS
]
},
[t("other")]: {
items: [
PrefKey.BLOCK_SOCIAL_FEATURES,
PrefKey.BLOCK_TRACKING
]
},
[t("advanced")]: {
items: [
PrefKey.USER_AGENT_PROFILE
]
}
};
// src/modules/ui/header.ts
var injectSettingsButton = function($parent) {
if (!$parent)
return;
const PREF_PREFERRED_REGION = getPreferredServerRegion(!0), PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION), $headerFragment = document.createDocumentFragment();
if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
const $remotePlayBtn = createButton({
classes: ["bx-header-remote-play-button"],
icon: BxIcon.REMOTE_PLAY,
title: t("remote-play"),
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
onClick: (e) => {
RemotePlay.togglePopup();
}
});
$headerFragment.appendChild($remotePlayBtn);
}
const $settingsBtn = createButton({
classes: ["bx-header-settings-button"],
label: PREF_PREFERRED_REGION,
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
onClick: (e) => {
setupSettingsUi(), document.querySelector(".bx-settings-container").classList.toggle("bx-gone"), window.scrollTo(0, 0), document.activeElement && document.activeElement.blur();
}
});
if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION)
$settingsBtn.setAttribute("data-update-available", "true");
$headerFragment.appendChild($settingsBtn), $parent.appendChild($headerFragment);
};
function checkHeader() {
if (!document.querySelector(".bx-header-settings-button")) {
const $rightHeader = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");
injectSettingsButton($rightHeader);
}
}
function watchHeader() {
const $header = document.querySelector("#PageContent header");
if (!$header)
return;
let timeout;
new MutationObserver((mutationList) => {
timeout && clearTimeout(timeout), timeout = window.setTimeout(checkHeader, 2000);
}).observe($header, { subtree: !0, childList: !0 }), checkHeader();
}
// src/utils/history.ts
function patchHistoryMethod(type) {
const orig = window.history[type];
return function(...args) {
return BxEvent.dispatch(window, BxEvent.POPSTATE, {
arguments: args
}), orig.apply(this, arguments);
};
}
function onHistoryChanged(e) {
if (e && e.arguments && e.arguments[0] && e.arguments[0].origin === "better-xcloud")
return;
window.setTimeout(RemotePlay.detect, 10);
const $settings = document.querySelector(".bx-settings-container");
if ($settings)
$settings.classList.add("bx-gone");
RemotePlay.detachPopup(), LoadingScreen.reset(), window.setTimeout(checkHeader, 2000), BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
}
// src/utils/preload-state.ts
function overridePreloadState() {
let _state;
Object.defineProperty(window, "__PRELOADED_STATE__", {
configurable: !0,
get: () => {
return _state;
},
set: (state) => {
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG6, e);
}
if (STATES.userAgentHasTouchSupport)
try {
const sigls = state.xcloud.sigls;
if (GamePassCloudGallery.TOUCH in sigls) {
let customList = TouchController.getCustomList();
const allGames = sigls[GamePassCloudGallery.ALL].data.products;
customList = customList.filter((id2) => allGames.includes(id2)), sigls[GamePassCloudGallery.TOUCH]?.data.products.push(...customList);
}
if (BX_FLAGS.ForceNativeMkbTitles && GamePassCloudGallery.NATIVE_MKB in sigls)
sigls[GamePassCloudGallery.NATIVE_MKB]?.data.products.push(...BX_FLAGS.ForceNativeMkbTitles);
} catch (e) {
BxLogger.error(LOG_TAG6, e);
}
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED))
try {
state.experiments.experimentationInfo.data.treatments.EnableHomeContextMenu = !1;
} catch (e) {
BxLogger.error(LOG_TAG6, e);
}
_state = state, STATES.appContext = structuredClone(state.appContext);
}
});
}
var LOG_TAG6 = "PreloadState";
// src/utils/sdp.ts
function patchSdpBitrate(sdp, video, audio) {
const lines = sdp.split("\n"), mediaSet = new Set;
!!video && mediaSet.add("video"), !!audio && mediaSet.add("audio");
const bitrate = {
video,
audio
};
for (let lineNumber = 0;lineNumber < lines.length; lineNumber++) {
let media = "", line = lines[lineNumber];
if (!line.startsWith("m="))
continue;
for (let m of mediaSet)
if (line.startsWith(`m=${m}`)) {
media = m, mediaSet.delete(media);
break;
}
if (!media)
continue;
const bLine = `b=AS:${bitrate[media]}`;
while (lineNumber++, lineNumber < lines.length) {
if (line = lines[lineNumber], line.startsWith("i=") || line.startsWith("c="))
continue;
if (line.startsWith("b=AS:")) {
lines[lineNumber] = bLine;
break;
}
if (line.startsWith("m=")) {
lines.splice(lineNumber, 0, bLine);
break;
}
}
}
return lines.join("\n");
}
// src/modules/player/shaders/clarity_boost.vert
var clarity_boost_default = "attribute vec2 position;\n\nvoid main() {\n gl_Position = vec4(position, 0, 1);\n}\n";
// src/modules/player/shaders/clarity_boost.fs
var clarity_boost_default2 = "const int FILTER_UNSHARP_MASKING = 1;\nconst int FILTER_CAS = 2;\n\nprecision highp float;\nuniform sampler2D data;\nuniform vec2 iResolution;\n\nuniform int filterId;\nuniform float sharpenFactor;\nuniform float brightness;\nuniform float contrast;\nuniform float saturation;\n\nvec3 textureAt(sampler2D tex, vec2 coord) {\n return texture2D(tex, coord / iResolution.xy).rgb;\n}\n\nvec3 clarityBoost(sampler2D tex, vec2 coord)\n{\n // Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.\n // a b c\n // d e f\n // g h i\n vec3 a = textureAt(tex, coord + vec2(-1, 1));\n vec3 b = textureAt(tex, coord + vec2(0, 1));\n vec3 c = textureAt(tex, coord + vec2(1, 1));\n\n vec3 d = textureAt(tex, coord + vec2(-1, 0));\n vec3 e = textureAt(tex, coord);\n vec3 f = textureAt(tex, coord + vec2(1, 0));\n\n vec3 g = textureAt(tex, coord + vec2(-1, -1));\n vec3 h = textureAt(tex, coord + vec2(0, -1));\n vec3 i = textureAt(tex, coord + vec2(1, -1));\n\n if (filterId == FILTER_CAS) {\n // Soft min and max.\n // a b c b\n // d e f * 0.5 + d e f * 0.5\n // g h i h\n // These are 2.0x bigger (factored out the extra multiply).\n vec3 minRgb = min(min(min(d, e), min(f, b)), h);\n vec3 minRgb2 = min(min(a, c), min(g, i));\n minRgb += min(minRgb, minRgb2);\n\n vec3 maxRgb = max(max(max(d, e), max(f, b)), h);\n vec3 maxRgb2 = max(max(a, c), max(g, i));\n maxRgb += max(maxRgb, maxRgb2);\n\n // Smooth minimum distance to signal limit divided by smooth max.\n vec3 reciprocalMaxRgb = 1.0 / maxRgb;\n vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);\n\n // Shaping amount of sharpening.\n amplifyRgb = inversesqrt(amplifyRgb);\n\n float contrast = 0.8;\n float peak = -3.0 * contrast + 8.0;\n vec3 weightRgb = -(1.0 / (amplifyRgb * peak));\n\n vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);\n\n // 0 w 0\n // Filter shape: w 1 w\n // 0 w 0\n vec3 window = (b + d) + (f + h);\n vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);\n\n outColor = mix(e, outColor, sharpenFactor / 2.0);\n\n return outColor;\n } else if (filterId == FILTER_UNSHARP_MASKING) {\n vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +\n d * 2.0 + e * 4.0 + f * 2.0 +\n g * 1.0 + h * 2.0 + i * 1.0) / 16.0;\n\n // Return edge detection\n return e + (e - gaussianBlur) * sharpenFactor / 3.0;\n }\n\n return e;\n}\n\nvec3 adjustBrightness(vec3 color) {\n return (1.0 + brightness) * color;\n}\n\nvec3 adjustContrast(vec3 color) {\n return 0.5 + (1.0 + contrast) * (color - 0.5);\n}\n\nvec3 adjustSaturation(vec3 color) {\n const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722);\n vec3 grayscale = vec3(dot(color, luminosityFactor));\n\n return mix(grayscale, color, 1.0 + saturation);\n}\n\nvoid main() {\n vec3 color;\n\n if (sharpenFactor > 0.0) {\n color = clarityBoost(data, gl_FragCoord.xy);\n } else {\n color = textureAt(data, gl_FragCoord.xy);\n }\n\n if (saturation != 0.0) {\n color = adjustSaturation(color);\n }\n\n if (contrast != 0.0) {\n color = adjustContrast(color);\n }\n\n if (brightness != 0.0) {\n color = adjustBrightness(color);\n }\n\n gl_FragColor = vec4(color, 1.0);\n}\n";
// src/modules/player/webgl2-player.ts
var LOG_TAG7 = "WebGL2Player";
class WebGL2Player {
#$video;
#$canvas;
#gl = null;
#resources = [];
#program = null;
#stopped = !1;
#options = {
filterId: 1,
sharpenFactor: 0,
brightness: 0,
contrast: 0,
saturation: 0
};
#animFrameId = null;
constructor($video) {
BxLogger.info(LOG_TAG7, "Initialize"), this.#$video = $video;
const $canvas = document.createElement("canvas");
$canvas.width = $video.videoWidth, $canvas.height = $video.videoHeight, this.#$canvas = $canvas, this.#setupShaders(), this.#setupRendering(), $video.insertAdjacentElement("afterend", $canvas);
}
setFilter(filterId, update = !0) {
this.#options.filterId = filterId, update && this.updateCanvas();
}
setSharpness(sharpness, update = !0) {
this.#options.sharpenFactor = sharpness, update && this.updateCanvas();
}
setBrightness(brightness, update = !0) {
this.#options.brightness = (brightness - 100) / 100, update && this.updateCanvas();
}
setContrast(contrast, update = !0) {
this.#options.contrast = (contrast - 100) / 100, update && this.updateCanvas();
}
setSaturation(saturation, update = !0) {
this.#options.saturation = (saturation - 100) / 100, update && this.updateCanvas();
}
getCanvas() {
return this.#$canvas;
}
updateCanvas() {
const gl = this.#gl, program = this.#program;
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.#$canvas.width, this.#$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.#options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.#options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.#options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.#options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.#options.saturation);
}
drawFrame() {
const gl = this.#gl, $video = this.#$video;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video), gl.drawArrays(gl.TRIANGLES, 0, 6);
}
#setupRendering() {
let animate;
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype) {
const $video = this.#$video;
animate = () => {
if (this.#stopped)
return;
this.drawFrame(), this.#animFrameId = $video.requestVideoFrameCallback(animate);
}, this.#animFrameId = $video.requestVideoFrameCallback(animate);
} else
animate = () => {
if (this.#stopped)
return;
this.drawFrame(), this.#animFrameId = requestAnimationFrame(animate);
}, this.#animFrameId = requestAnimationFrame(animate);
}
#setupShaders() {
const gl = this.#$canvas.getContext("webgl2", {
isBx: !0,
antialias: !0,
alpha: !1,
powerPreference: "high-performance"
});
this.#gl = gl, gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
const vShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vShader, clarity_boost_default), gl.compileShader(vShader);
const fShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fShader, clarity_boost_default2), gl.compileShader(fShader);
const program = gl.createProgram();
if (this.#program = program, gl.attachShader(program, vShader), gl.attachShader(program, fShader), gl.linkProgram(program), gl.useProgram(program), !gl.getProgramParameter(program, gl.LINK_STATUS))
console.error(`Link failed: ${gl.getProgramInfoLog(program)}`), console.error(`vs info-log: ${gl.getShaderInfoLog(vShader)}`), console.error(`fs info-log: ${gl.getShaderInfoLog(fShader)}`);
this.updateCanvas();
const buffer = gl.createBuffer();
this.#resources.push(buffer), gl.bindBuffer(gl.ARRAY_BUFFER, buffer), gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
-1,
-1,
1,
-1,
-1,
1,
-1,
1,
1,
-1,
1,
1
]), gl.STATIC_DRAW), gl.enableVertexAttribArray(0), gl.vertexAttribPointer(0, 2, gl.FLOAT, !1, 0, 0);
const texture = gl.createTexture();
this.#resources.push(texture), gl.bindTexture(gl.TEXTURE_2D, texture), gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, !0), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR), gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR), gl.uniform1i(gl.getUniformLocation(program, "data"), 0), gl.activeTexture(gl.TEXTURE0);
}
resume() {
this.stop(), this.#stopped = !1, BxLogger.info(LOG_TAG7, "Resume"), this.#$canvas.classList.remove("bx-gone"), this.#setupRendering();
}
stop() {
if (BxLogger.info(LOG_TAG7, "Stop"), this.#$canvas.classList.add("bx-gone"), this.#stopped = !0, this.#animFrameId) {
if ("requestVideoFrameCallback" in HTMLVideoElement.prototype)
this.#$video.cancelVideoFrameCallback(this.#animFrameId);
else
cancelAnimationFrame(this.#animFrameId);
this.#animFrameId = null;
}
}
destroy() {
BxLogger.info(LOG_TAG7, "Destroy"), this.stop();
const gl = this.#gl;
if (gl) {
gl.getExtension("WEBGL_lose_context")?.loseContext();
for (let resource of this.#resources)
if (resource instanceof WebGLProgram)
gl.useProgram(null), gl.deleteProgram(resource);
else if (resource instanceof WebGLShader)
gl.deleteShader(resource);
else if (resource instanceof WebGLTexture)
gl.deleteTexture(resource);
else if (resource instanceof WebGLBuffer)
gl.deleteBuffer(resource);
this.#gl = null;
}
if (this.#$canvas.isConnected)
this.#$canvas.parentElement?.removeChild(this.#$canvas);
this.#$canvas.width = 1, this.#$canvas.height = 1;
}
}
// src/modules/stream-player.ts
class StreamPlayer {
#$video;
#playerType = StreamPlayerType.VIDEO;
#options = {};
#webGL2Player = null;
#$videoCss = null;
#$usmMatrix = null;
constructor($video, type, options) {
this.#setupVideoElements(), this.#$video = $video, this.#options = options || {}, this.setPlayerType(type);
}
#setupVideoElements() {
if (this.#$videoCss = document.getElementById("bx-video-css"), this.#$videoCss) {
this.#$usmMatrix = this.#$videoCss.querySelector("#bx-filter-usm-matrix");
return;
}
const $fragment = document.createDocumentFragment();
this.#$videoCss = CE("style", { id: "bx-video-css" }), $fragment.appendChild(this.#$videoCss);
const $svg = CE("svg", {
id: "bx-video-filters",
xmlns: "http://www.w3.org/2000/svg",
class: "bx-gone"
}, CE("defs", { xmlns: "http://www.w3.org/2000/svg" }, CE("filter", {
id: "bx-filter-usm",
xmlns: "http://www.w3.org/2000/svg"
}, this.#$usmMatrix = CE("feConvolveMatrix", {
id: "bx-filter-usm-matrix",
order: "3",
xmlns: "http://www.w3.org/2000/svg"
}))));
$fragment.appendChild($svg), document.documentElement.appendChild($fragment);
}
#getVideoPlayerFilterStyle() {
const filters = [], sharpness = this.#options.sharpness || 0;
if (sharpness != 0) {
const matrix = `0 -1 0 -1 ${(7 - (sharpness / 2 - 1) * 0.5).toFixed(1)} -1 0 -1 0`;
this.#$usmMatrix?.setAttributeNS(null, "kernelMatrix", matrix), filters.push("url(#bx-filter-usm)");
}
const saturation = this.#options.saturation || 100;
if (saturation != 100)
filters.push(`saturate(${saturation}%)`);
const contrast = this.#options.contrast || 100;
if (contrast != 100)
filters.push(`contrast(${contrast}%)`);
const brightness = this.#options.brightness || 100;
if (brightness != 100)
filters.push(`brightness(${brightness}%)`);
return filters.join(" ");
}
#resizePlayer() {
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO), $video = this.#$video, isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
let $webGL2Canvas;
if (this.#playerType == StreamPlayerType.WEBGL2)
$webGL2Canvas = this.#webGL2Player?.getCanvas();
let targetWidth, targetHeight, targetObjectFit;
if (PREF_RATIO.includes(":")) {
const tmp = PREF_RATIO.split(":"), videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
let width = 0, height = 0;
const parentRect = $video.parentElement.getBoundingClientRect();
if (parentRect.width / parentRect.height > videoRatio)
height = parentRect.height, width = height * videoRatio;
else
width = parentRect.width, height = width / videoRatio;
width = Math.ceil(Math.min(parentRect.width, width)), height = Math.ceil(Math.min(parentRect.height, height)), $video.dataset.width = width.toString(), $video.dataset.height = height.toString(), targetWidth = `${width}px`, targetHeight = `${height}px`, targetObjectFit = PREF_RATIO === "16:9" ? "contain" : "fill";
} else
targetWidth = "100%", targetHeight = "100%", targetObjectFit = PREF_RATIO, $video.dataset.width = window.innerWidth.toString(), $video.dataset.height = window.innerHeight.toString();
if ($video.style.width = targetWidth, $video.style.height = targetHeight, $video.style.objectFit = targetObjectFit, $webGL2Canvas)
$webGL2Canvas.style.width = targetWidth, $webGL2Canvas.style.height = targetHeight, $webGL2Canvas.style.objectFit = targetObjectFit;
if (isNativeTouchGame && this.#playerType == StreamPlayerType.WEBGL2)
window.BX_EXPOSED.streamSession.updateDimensions();
}
setPlayerType(type, refreshPlayer = !1) {
if (this.#playerType !== type)
if (type === StreamPlayerType.WEBGL2) {
if (!this.#webGL2Player)
this.#webGL2Player = new WebGL2Player(this.#$video);
else
this.#webGL2Player.resume();
this.#$videoCss.textContent = "", this.#$video.classList.add("bx-pixel");
} else
this.#webGL2Player?.stop(), this.#$video.classList.remove("bx-pixel");
this.#playerType = type, refreshPlayer && this.refreshPlayer();
}
setOptions(options, refreshPlayer = !1) {
this.#options = options, refreshPlayer && this.refreshPlayer();
}
updateOptions(options, refreshPlayer = !1) {
this.#options = Object.assign(this.#options, options), refreshPlayer && this.refreshPlayer();
}
getPlayerElement(playerType) {
if (typeof playerType === "undefined")
playerType = this.#playerType;
if (playerType === StreamPlayerType.WEBGL2)
return this.#webGL2Player?.getCanvas();
return this.#$video;
}
getWebGL2Player() {
return this.#webGL2Player;
}
refreshPlayer() {
if (this.#playerType === StreamPlayerType.WEBGL2) {
const options = this.#options, webGL2Player = this.#webGL2Player;
if (options.processing === StreamVideoProcessing.USM)
webGL2Player.setFilter(1);
else
webGL2Player.setFilter(2);
Screenshot.updateCanvasFilters("none"), webGL2Player.setSharpness(options.sharpness || 0), webGL2Player.setSaturation(options.saturation || 100), webGL2Player.setContrast(options.contrast || 100), webGL2Player.setBrightness(options.brightness || 100);
} else {
let filters = this.#getVideoPlayerFilterStyle(), videoCss = "";
if (filters)
videoCss += `filter: ${filters} !important;`;
if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS))
Screenshot.updateCanvasFilters(filters);
let css = "";
if (videoCss)
css = `#game-stream video { ${videoCss} }`;
this.#$videoCss.textContent = css;
}
this.#resizePlayer();
}
destroy() {
this.#webGL2Player?.destroy(), this.#webGL2Player = null;
}
}
// src/utils/monkey-patches.ts
function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO), showFunc = function() {
if (this.style.visibility = "visible", this.removeEventListener("playing", showFunc), !this.videoWidth)
return;
const playerOptions = {
processing: getPref(PrefKey.VIDEO_PROCESSING),
sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
saturation: getPref(PrefKey.VIDEO_SATURATION),
contrast: getPref(PrefKey.VIDEO_CONTRAST),
brightness: getPref(PrefKey.VIDEO_BRIGHTNESS)
};
STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref(PrefKey.VIDEO_PLAYER_TYPE), playerOptions), BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, {
$video: this
});
}, nativePlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.nativePlay = nativePlay, HTMLMediaElement.prototype.play = function() {
if (this.className && this.className.startsWith("XboxSplashVideo")) {
if (PREF_SKIP_SPLASH_VIDEO)
return this.volume = 0, this.style.display = "none", this.dispatchEvent(new Event("ended")), new Promise(() => {
});
return nativePlay.apply(this);
}
const $parent = this.parentElement;
if (!this.src && $parent.dataset.testid === "media-container")
this.addEventListener("playing", showFunc);
return nativePlay.apply(this);
};
}
function patchRtcCodecs() {
const codecProfile = getPref(PrefKey.STREAM_CODEC_PROFILE);
if (codecProfile === "default")
return;
if (typeof RTCRtpTransceiver === "undefined" || !("setCodecPreferences" in RTCRtpTransceiver.prototype))
return !1;
const profileLevelId = `profile-level-id=${codecProfile === "high" ? "4d" : codecProfile === "low" ? "420" : "42e"}`, nativeSetCodecPreferences = RTCRtpTransceiver.prototype.setCodecPreferences;
RTCRtpTransceiver.prototype.setCodecPreferences = function(codecs) {
const newCodecs = codecs.slice();
let pos = 0;
newCodecs.forEach((codec, i) => {
if (codec.sdpFmtpLine && codec.sdpFmtpLine.includes(profileLevelId))
newCodecs.splice(i, 1), newCodecs.splice(pos, 0, codec), ++pos;
});
try {
nativeSetCodecPreferences.apply(this, [newCodecs]);
} catch (e) {
BxLogger.error("setCodecPreferences", e), nativeSetCodecPreferences.apply(this, [codecs]);
}
};
}
function patchRtcPeerConnection() {
const nativeCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = nativeCreateDataChannel.apply(this, arguments);
return BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, {
dataChannel
}), dataChannel;
};
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
if (maxVideoBitrate > 0) {
const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
RTCPeerConnection.prototype.setLocalDescription = function(description) {
try {
if (description)
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
} catch (e) {
BxLogger.error("setLocalDescription", e);
}
return nativeSetLocalDescription.apply(this, arguments);
};
}
const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() {
const conn = new OrgRTCPeerConnection;
return STATES.currentStream.peerConnection = conn, conn.addEventListener("connectionstatechange", (e) => {
BxLogger.info("connectionstatechange", conn.connectionState);
}), conn;
};
}
function patchAudioContext() {
const OrgAudioContext = window.AudioContext, nativeCreateGain = OrgAudioContext.prototype.createGain;
window.AudioContext = function(options) {
if (options && options.latencyHint)
options.latencyHint = 0;
const ctx = new OrgAudioContext(options);
return BxLogger.info("patchAudioContext", ctx, options), ctx.createGain = function() {
const gainNode = nativeCreateGain.apply(this);
return gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100, STATES.currentStream.audioGainNode = gainNode, gainNode;
}, STATES.currentStream.audioContext = ctx, ctx;
};
}
function patchMeControl() {
const overrideConfigs = {
enableAADTelemetry: !1,
enableTelemetry: !1,
telEvs: "",
oneDSUrl: ""
}, MSA = {
MeControl: {
API: {
setDisplayMode: () => {
},
setMobileState: () => {
}
}
}
}, MeControl = {}, MsaHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "MeControl" && value.Config)
value.Config = Object.assign(value.Config, overrideConfigs);
return obj[prop] = value, !0;
}
}, MeControlHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "Config")
value = Object.assign(value, overrideConfigs);
return obj[prop] = value, !0;
}
};
window.MSA = new Proxy(MSA, MsaHandler), window.MeControl = new Proxy(MeControl, MeControlHandler);
}
function patchCanvasContext() {
const nativeGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(contextType, contextAttributes) {
if (contextType.includes("webgl")) {
if (contextAttributes = contextAttributes || {}, !contextAttributes.isBx) {
if (contextAttributes.antialias = !1, contextAttributes.powerPreference === "high-performance")
contextAttributes.powerPreference = "low-power";
}
}
return nativeGetContext.apply(this, [contextType, contextAttributes]);
};
}
function patchPointerLockApi() {
Object.defineProperty(document, "fullscreenElement", {
configurable: !0,
get() {
return document.documentElement;
}
}), HTMLElement.prototype.requestFullscreen = function(options) {
return Promise.resolve();
};
let pointerLockElement = null;
Object.defineProperty(document, "pointerLockElement", {
configurable: !0,
get() {
return pointerLockElement;
}
}), HTMLElement.prototype.requestPointerLock = function() {
pointerLockElement = document.documentElement, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_REQUESTED));
}, Document.prototype.exitPointerLock = function() {
pointerLockElement = null, window.dispatchEvent(new Event(BxEvent.POINTER_LOCK_EXITED));
};
}
// src/modules/game-bar/action-base.ts
class BaseGameBarAction {
constructor() {
}
reset() {
}
}
// src/modules/game-bar/action-screenshot.ts
class ScreenshotAction extends BaseGameBarAction {
$content;
constructor() {
super();
const onClick = (e) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED), Screenshot.takeScreenshot();
};
this.$content = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.SCREENSHOT,
title: t("take-screenshot"),
onClick
});
}
render() {
return this.$content;
}
}
// src/modules/game-bar/action-touch-control.ts
class TouchControlAction extends BaseGameBarAction {
$content;
constructor() {
super();
const onClick = (e) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const $parent = e.target.closest("div[data-enabled]");
let enabled = $parent.getAttribute("data-enabled", "true") === "true";
$parent.setAttribute("data-enabled", (!enabled).toString()), TouchController.toggleVisibility(enabled);
}, $btnEnable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_ENABLE,
title: t("show-touch-controller"),
onClick,
classes: ["bx-activated"]
}), $btnDisable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_DISABLE,
title: t("hide-touch-controller"),
onClick
});
this.$content = CE("div", {}, $btnEnable, $btnDisable), this.reset();
}
render() {
return this.$content;
}
reset() {
this.$content.setAttribute("data-enabled", "true");
}
}
// src/modules/game-bar/action-microphone.ts
class MicrophoneAction extends BaseGameBarAction {
$content;
visible = !1;
constructor() {
super();
const onClick = (e) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const enabled = MicrophoneShortcut.toggle(!1);
this.$content.setAttribute("data-enabled", enabled.toString());
}, $btnDefault = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE,
title: t("show-touch-controller"),
onClick,
classes: ["bx-activated"]
}), $btnMuted = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE_MUTED,
title: t("hide-touch-controller"),
onClick
});
this.$content = CE("div", {}, $btnDefault, $btnMuted), this.reset(), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => {
const enabled = e.microphoneState === MicrophoneState.ENABLED;
this.$content.setAttribute("data-enabled", enabled.toString()), this.$content.classList.remove("bx-gone");
});
}
render() {
return this.$content;
}
reset() {
this.visible = !1, this.$content.classList.add("bx-gone"), this.$content.setAttribute("data-enabled", "false");
}
}
// src/modules/game-bar/game-bar.ts
class GameBar {
static instance;
static getInstance() {
if (!GameBar.instance)
GameBar.instance = new GameBar;
return GameBar.instance;
}
static VISIBLE_DURATION = 2000;
$gameBar;
$container;
timeout = null;
actions = [];
constructor() {
let $container;
const position = getPref(PrefKey.GAME_BAR_POSITION), $gameBar = CE("div", { id: "bx-game-bar", class: "bx-gone", "data-position": position }, $container = CE("div", { class: "bx-game-bar-container bx-offscreen" }), createSvgIcon(position === "bottom-left" ? BxIcon.CARET_RIGHT : BxIcon.CARET_LEFT));
if (this.actions = [
new ScreenshotAction,
...STATES.userAgentHasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [],
new MicrophoneAction
], position === "bottom-right")
this.actions.reverse();
for (let action of this.actions)
$container.appendChild(action.render());
$gameBar.addEventListener("click", (e) => {
if (e.target !== $gameBar)
return;
$container.classList.contains("bx-show") ? this.hideBar() : this.showBar();
}), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => {
const classList = $container.classList;
if (classList.contains("bx-hide"))
classList.remove("bx-offscreen", "bx-hide"), classList.add("bx-offscreen");
}), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref(PrefKey.GAME_BAR_POSITION) !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => {
if (!STATES.isPlaying) {
this.disable();
return;
}
e.mode !== "None" ? this.disable() : this.enable();
}).bind(this));
}
beginHideTimeout() {
this.clearHideTimeout(), this.timeout = window.setTimeout(() => {
this.timeout = null, this.hideBar();
}, GameBar.VISIBLE_DURATION);
}
clearHideTimeout() {
this.timeout && clearTimeout(this.timeout), this.timeout = null;
}
enable() {
this.$gameBar && this.$gameBar.classList.remove("bx-gone");
}
disable() {
this.hideBar(), this.$gameBar && this.$gameBar.classList.add("bx-gone");
}
showBar() {
if (!this.$container)
return;
this.$container.classList.remove("bx-offscreen", "bx-hide"), this.$container.classList.add("bx-show"), this.beginHideTimeout();
}
hideBar() {
if (!this.$container)
return;
this.$container.classList.remove("bx-show"), this.$container.classList.add("bx-hide");
}
reset() {
for (let action of this.actions)
action.reset();
}
}
// src/modules/ui/guide-menu.ts
var GuideMenuTab;
(function(GuideMenuTab2) {
GuideMenuTab2[GuideMenuTab2["HOME"] = 0] = "HOME";
})(GuideMenuTab || (GuideMenuTab = {}));
class GuideMenu {
static #injectHome($root) {
const $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");
if (!$dividers)
return;
const $lastDivider = $dividers[$dividers.length - 1];
if (AppInterface) {
const $btnQuit = createButton({
label: t("close-app"),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE | ButtonStyle.DANGER,
onClick: (e) => {
AppInterface.closeApp();
}
});
$lastDivider.insertAdjacentElement("afterend", $btnQuit);
}
}
static #injectHomePlaying($root) {
const $btnQuit = $root.querySelector("a[class*=QuitGameButton]");
if (!$btnQuit)
return;
const $btnReload = createButton({
label: t("reload-stream"),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: (e) => {
confirm(t("confirm-reload-stream")) && window.location.reload();
}
}), $btnHome = createButton({
label: t("back-to-home"),
style: ButtonStyle.FULL_WIDTH | ButtonStyle.FOCUSABLE,
onClick: (e) => {
confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));
}
});
$btnQuit.insertAdjacentElement("afterend", $btnReload), $btnReload.insertAdjacentElement("afterend", $btnHome);
const $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");
$btnXcloudHome && ($btnXcloudHome.style.display = "none");
}
static async#onShown(e) {
if (e.where === GuideMenuTab.HOME) {
const $root = document.querySelector("#gamepass-dialog-root div[role=dialog]");
if (STATES.isPlaying)
GuideMenu.#injectHomePlaying($root);
else
GuideMenu.#injectHome($root);
}
}
static observe() {
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, GuideMenu.#onShown);
}
}
// src/index.ts
var unload = function() {
if (!STATES.isPlaying)
return;
EmulatedMkbHandler.getInstance().destroy(), NativeMkbHandler.getInstance().destroy(), STATES.currentStream.streamPlayer?.destroy(), STATES.isPlaying = !1, STATES.currentStream = {}, window.BX_EXPOSED.shouldShowSensorControls = !1, window.BX_EXPOSED.stopTakRendering = !1;
const $streamSettingsDialog = document.querySelector(".bx-stream-settings-dialog");
if ($streamSettingsDialog)
$streamSettingsDialog.classList.add("bx-gone");
StreamStats.getInstance().onStoppedPlaying(), MouseCursorHider.stop(), TouchController.reset(), GameBar.getInstance().disable();
}, observeRootDialog = function($root) {
let currentShown = !1;
new MutationObserver((mutationList) => {
for (let mutation of mutationList) {
if (mutation.type !== "childList")
continue;
if (mutation.addedNodes.length === 1) {
const $addedElm = mutation.addedNodes[0];
if ($addedElm instanceof HTMLElement && $addedElm.className) {
if ($addedElm.className.startsWith("NavigationAnimation") || $addedElm.className.startsWith("DialogRoutes") || $addedElm.className.startsWith("Dialog-module__container")) {
if (document.querySelector("#gamepass-dialog-root div[class*=GuideDialog]")) {
const $selectedTab = $addedElm.querySelector("div[class^=NavigationMenu] button[aria-selected=true");
if ($selectedTab) {
let $elm = $selectedTab, index;
for (index = 0;$elm = $elm?.previousElementSibling; index++)
;
if (index === 0)
BxEvent.dispatch(window, BxEvent.XCLOUD_GUIDE_MENU_SHOWN, { where: GuideMenuTab.HOME });
}
}
}
}
}
const shown = $root.firstElementChild && $root.firstElementChild.childElementCount > 0 || !1;
if (shown !== currentShown)
currentShown = shown, BxEvent.dispatch(window, shown ? BxEvent.XCLOUD_DIALOG_SHOWN : BxEvent.XCLOUD_DIALOG_DISMISSED);
}
}).observe($root, { subtree: !0, childList: !0 });
}, waitForRootDialog = function() {
const observer = new MutationObserver((mutationList) => {
for (let mutation of mutationList) {
if (mutation.type !== "childList")
continue;
const $target = mutation.target;
if ($target.id && $target.id === "gamepass-dialog-root") {
observer.disconnect(), observeRootDialog($target);
break;
}
}
});
observer.observe(document.documentElement, { subtree: !0, childList: !0 });
}, main = function() {
if (waitForRootDialog(), patchRtcPeerConnection(), patchRtcCodecs(), interceptHttpRequests(), patchVideoApi(), patchCanvasContext(), AppInterface && patchPointerLockApi(), getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext(), getPref(PrefKey.BLOCK_TRACKING) && patchMeControl(), STATES.userAgentHasTouchSupport && TouchController.updateCustomList(), overridePreloadState(), VibrationManager.initialSetup(), BX_FLAGS.CheckForUpdate && checkForUpdate(), addCss(), Toast.setup(), getPref(PrefKey.GAME_BAR_POSITION) !== "off" && GameBar.getInstance(), BX_FLAGS.PreloadUi && setupStreamUi(), GuideMenu.observe(), StreamBadges.setupEvents(), StreamStats.setupEvents(), EmulatedMkbHandler.setupEvents(), Patcher.init(), disablePwa(), window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad)), window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad)), getPref(PrefKey.REMOTE_PLAY_ENABLED))
RemotePlay.detect();
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all")
TouchController.setup();
if (getPref(PrefKey.MKB_ENABLED) && AppInterface)
STATES.pointerServerPort = AppInterface.startPointerServer() || 9269, BxLogger.info("startPointerServer", "Port", STATES.pointerServerPort.toString());
};
if (window.location.pathname.includes("/auth/msa"))
throw window.addEventListener("load", (e) => {
window.location.search.includes("loggedIn") && window.setTimeout(() => {
const location2 = window.location;
location2.pathname.includes("/play") && location2.reload(!0);
}, 2000);
}), new Error("[Better xCloud] Refreshing the page after logging in");
BxLogger.info("readyState", document.readyState);
if (BX_FLAGS.SafariWorkaround && document.readyState !== "loading") {
window.stop();
const css2 = `
.bx-reload-overlay {
position: fixed;
top: 0;
background: #000000cc;
z-index: 9999;
width: 100%;
line-height: 100vh;
color: #fff;
text-align: center;
font-weight: 400;
font-family: "Segoe UI", Arial, Helvetica, sans-serif;
font-size: 1.3rem;
}
`, $fragment = document.createDocumentFragment();
throw $fragment.appendChild(CE("style", {}, css2)), $fragment.appendChild(CE("div", { class: "bx-reload-overlay" }, t("safari-failed-message"))), document.documentElement.appendChild($fragment), window.location.reload(!0), new Error("[Better xCloud] Executing workaround for Safari");
}
window.addEventListener("load", (e) => {
window.setTimeout(() => {
if (document.body.classList.contains("legacyBackground"))
window.stop(), window.location.reload(!0);
}, 3000);
});
window.BX_EXPOSED = BxExposed;
window.addEventListener(BxEvent.POPSTATE, onHistoryChanged);
window.addEventListener("popstate", onHistoryChanged);
window.history.pushState = patchHistoryMethod("pushState");
window.history.replaceState = patchHistoryMethod("replaceState");
window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, (e) => {
if (document.querySelector("div[class^=UnsupportedMarketPage]"))
window.setTimeout(watchHeader, 2000);
else
watchHeader();
});
window.addEventListener(BxEvent.STREAM_LOADING, (e) => {
if (window.location.pathname.includes("/launch/")) {
const matches = /\/launch\/(?[^\/]+)\/(?\w+)/.exec(window.location.pathname);
if (matches?.groups)
STATES.currentStream.titleId = matches.groups.title_id, STATES.currentStream.productId = matches.groups.product_id;
} else
STATES.currentStream.titleId = "remote-play", STATES.currentStream.productId = "";
setupStreamUi();
});
getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && window.addEventListener(BxEvent.TITLE_INFO_READY, LoadingScreen.setup);
window.addEventListener(BxEvent.STREAM_STARTING, (e) => {
if (LoadingScreen.hide(), !getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR))
MouseCursorHider.start(), MouseCursorHider.hide();
});
window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
if (STATES.isPlaying = !0, injectStreamMenuButtons(), getPref(PrefKey.GAME_BAR_POSITION) !== "off") {
const gameBar = GameBar.getInstance();
gameBar.reset(), gameBar.enable(), gameBar.showBar();
}
const $video = e.$video;
Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight), updateVideoPlayer();
});
window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
});
window.addEventListener(BxEvent.STREAM_STOPPED, unload);
window.addEventListener("pagehide", (e) => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
});
window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {
Screenshot.takeScreenshot();
});
main();