// ==UserScript==
// @name Better xCloud
// @namespace https://github.com/redphx
// @version 4.4.0
// @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/utils/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 = {}));
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 #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() {
UserAgent.#config = JSON.parse(window.localStorage.getItem(UserAgent.STORAGE_KEY) || "{}");
if (!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);
clonedConfig.profile = profile;
if (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(mobile = false) {
const userAgent = UserAgent.getDefault().toLowerCase();
let result = userAgent.includes("safari") && !userAgent.includes("chrom");
if (result && mobile) {
result = userAgent.includes("mobile");
}
return result;
}
static isMobile() {
const userAgent = UserAgent.getDefault().toLowerCase();
return /iphone|ipad|android/.test(userAgent);
}
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 = "4.4.0";
var SCRIPT_HOME = "https://github.com/redphx/better-xcloud";
var AppInterface = window.AppInterface;
UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase();
var isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent);
var isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser");
var browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0;
var hasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
var STATES = {
isPlaying: false,
appContext: {},
serverRegions: {},
hasTouchSupport,
browserHasTouchSupport,
currentStream: {},
remotePlay: {}
};
// 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_MENU_SHOWN"] = "bx-stream-menu-shown";
BxEvent2["STREAM_MENU_HIDDEN"] = "bx-stream-menu-hidden";
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";
})(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 (const key in data) {
event[key] = data[key];
}
}
AppInterface && AppInterface.onEvent(eventName);
target.dispatchEvent(event);
}
BxEvent.dispatch = dispatch;
})(BxEvent || (BxEvent = {}));
window.BxEvent = BxEvent;
// src/utils/bx-flags.ts
/* ADDITIONAL CODE */
var DEFAULT_FLAGS = {
CheckForUpdate: true,
PreloadRemotePlay: true,
PreloadUi: false,
EnableXcloudLogging: false,
SafariWorkaround: true,
UseDevTouchLayout: false
};
var BX_FLAGS = Object.assign(DEFAULT_FLAGS, window.BX_FLAGS || {});
try {
delete window.BX_FLAGS;
} catch (e) {
}
var NATIVE_FETCH = window.fetch;
// 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 (const 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];
const 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);
const $span = document.createElement("span");
$span.appendChild(text);
return $span.innerHTML;
}
var CE = createElement;
var svgParser = (svg) => new DOMParser().parseFromString(svg, "image/svg+xml").documentElement;
var createSvgIcon = (icon) => {
return svgParser(icon.toString());
};
var 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";
var ButtonStyleIndices = Object.keys(ButtonStyle).splice(0, Object.keys(ButtonStyle).length / 2).map((i) => parseInt(i));
var 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;
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 = true);
options.onClick && $btn.addEventListener("click", options.onClick);
return $btn;
};
var CTN = document.createTextNode.bind(document);
window.BX_CE = createElement;
// 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: false,
willReadFrequently: false
});
}
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;
const $video = currentStream.$video;
const $canvas = Screenshot.#$canvas;
if (!$video || !$canvas) {
return;
}
$video.parentElement?.addEventListener("animationend", this.onAnimationEnd);
$video.parentElement?.classList.add("bx-taking-screenshot");
const canvasContext = Screenshot.#canvasContext;
canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
if (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;
const $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/utils/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/modules/mkb/definitions.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]
};
var 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";
MkbPresetKey2["MOUSE_STICK_DECAY_STRENGTH"] = "stick_decay_strength";
MkbPresetKey2["MOUSE_STICK_DECAY_MIN"] = "stick_decay_min";
})(MkbPresetKey || (MkbPresetKey = {}));
// src/utils/translation.ts
var SUPPORTED_LANGUAGES = {
"en-US": "English (United States)",
"en-ID": "Bahasa Indonesia",
"de-DE": "Deutsch",
"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": "中文(简体)"
};
var Texts = {
activate: "Activate",
activated: "Activated",
active: "Active",
advanced: "Advanced",
"android-app-settings": "Android app settings",
apply: "Apply",
audio: "Audio",
auto: "Auto",
"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: "Clarity",
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
clear: "Clear",
close: "Close",
"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": "The Xbox button is the one that opens 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-native-mkb": "Disable native Mouse & Keyboard feature",
"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-sensitivity": "Horizontal sensitivity",
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",
muted: "Muted",
name: "Name",
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 the Mouse and Keyboard feature`,
(e) => `${e.key}: Maus- und Tastaturunterstützung an-/ausschalten`,
(e) => `Tekan ${e.key} untuk mengaktifkan fitur Mouse dan Keyboard`,
(e) => `Pulsa ${e.key} para activar la función de ratón y teclado`,
(e) => `Appuyez sur ${e.key} pour activer/désactiver la fonction Souris et Clavier`,
(e) => `Premi ${e.key} per attivare o disattivare la funzione Mouse e Tastiera`,
(e) => `${e.key} キーでマウスとキーボードの機能を切り替える`,
(e) => `${e.key} 키를 눌러 마우스와 키보드 기능을 활성화 하십시오`,
(e) => `Naciśnij ${e.key}, aby przełączyć funkcję myszy i klawiatury`,
(e) => `Pressione ${e.key} para ativar/desativar a função de Mouse e Teclado`,
(e) => `Нажмите ${e.key} для переключения функции мыши и клавиатуры`,
,
(e) => `Klavye ve fare özelliğini açmak için ${e.key} tuşuna basın`,
(e) => `Натисніть ${e.key}, щоб увімкнути або вимкнути функцію миші та клавіатури`,
(e) => `Nhấn ${e.key} để bật/tắt tính năng Chuột và Bàn phím`,
(e) => `按下 ${e.key} 切换键鼠模式`
],
"press-to-bind": "Press a key or do a mouse click to bind...",
"prompt-preset-name": "Preset's name:",
ratio: "Ratio",
"reduce-animations": "Reduce UI animations",
region: "Region",
"remote-play": "Remote Play",
rename: "Rename",
"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-reload": "Reload page to reflect changes",
"settings-reloading": "Reloading...",
"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",
"stretch-note": "Don't use with native touch games",
"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) => `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",
"use-mouse-absolute-position": "Use mouse's absolute position",
"user-agent-profile": "User-Agent profile",
"vertical-sensitivity": "Vertical sensitivity",
"vibration-intensity": "Vibration intensity",
"vibration-status": "Vibration",
video: "Video",
"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"
};
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) {
locale = window.navigator.language || Translations.#EN_US;
if (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)) {
translation = text[Translations.#selectedLocaleIndex] || text[Translations.#enUsIndex];
return translation(values);
}
translation = text;
return 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 = false) {
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 resp = await NATIVE_FETCH(`https://raw.githubusercontent.com/redphx/better-xcloud/gh-pages/translations/${locale}.json`);
const translations = await resp.json();
window.localStorage.setItem(Translations.#KEY_TRANSLATIONS, JSON.stringify(translations));
Translations.#foreignTranslations = translations;
return true;
} catch (e) {
debugger;
}
return false;
}
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];
const $option = CE("option", { value }, label);
$control.appendChild($option);
}
$control.value = currentValue;
onChange && $control.addEventListener("change", (e) => {
const target = e.target;
const value = setting.type && setting.type === "number" ? parseInt(target.value) : target.value;
onChange(e, value);
});
$control.setValue = (value) => {
$control.value = value;
};
return $control;
}
static #renderMultipleOptions(key, setting, currentValue, onChange, params = {}) {
const $control = CE("select", {
title: setting.label,
multiple: true,
tabindex: 0
});
if (params && params.size) {
$control.setAttribute("size", params.size.toString());
}
for (let value in setting.multipleOptions) {
const label = setting.multipleOptions[value];
const $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);
}
$control.addEventListener("mousedown", function(e) {
const self = this;
const orgScrollTop = self.scrollTop;
window.setTimeout(() => self.scrollTop = orgScrollTop, 0);
});
$control.addEventListener("mousemove", (e) => e.preventDefault());
onChange && $control.addEventListener("change", (e) => {
const target = e.target;
const values = Array.from(target.selectedOptions).map((i) => i.value);
onChange(e, values);
});
return $control;
}
static #renderNumber(key, setting, currentValue, onChange) {
const $control = CE("input", { tabindex: 0, type: "number", min: setting.min, max: setting.max });
$control.value = currentValue;
onChange && $control.addEventListener("change", (e) => {
const target = e.target;
const value = Math.max(setting.min, Math.min(setting.max, parseInt(target.value)));
target.value = value.toString();
onChange(e, value);
});
return $control;
}
static #renderCheckbox(key, setting, currentValue, onChange) {
const $control = CE("input", { type: "checkbox", tabindex: 0 });
$control.checked = currentValue;
onChange && $control.addEventListener("change", (e) => {
onChange(e, e.target.checked);
});
return $control;
}
static #renderNumberStepper(key, setting, value, onChange, options = {}) {
options = options || {};
options.suffix = options.suffix || "";
options.disabled = !!options.disabled;
options.hideSlider = !!options.hideSlider;
let $text;
let $decBtn;
let $incBtn;
let $range;
const MIN = setting.min;
const MAX = setting.max;
const STEPS = Math.max(setting.steps || 1, 1);
const 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;
};
const $wrapper = CE("div", { class: "bx-number-stepper" }, $decBtn = CE("button", {
"data-type": "dec",
type: "button",
tabindex: -1
}, "-"), $text = CE("span", {}, renderTextValue(value)), $incBtn = CE("button", {
"data-type": "inc",
type: "button",
tabindex: -1
}, "+"));
if (!options.disabled && !options.hideSlider) {
$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);
$text.textContent = renderTextValue(value);
!e.ignoreOnChange && onChange && onChange(e, value);
});
$wrapper.appendChild($range);
if (options.ticks || options.exactTicks) {
const markersId = `markers-${key}`;
const $markers = CE("datalist", { id: markersId });
$range.setAttribute("list", markersId);
if (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) {
$incBtn.disabled = true;
$incBtn.classList.add("bx-hidden");
$decBtn.disabled = true;
$decBtn.classList.add("bx-hidden");
return $wrapper;
}
let interval;
let isHolding = false;
const onClick = (e) => {
if (isHolding) {
e.preventDefault();
isHolding = false;
return;
}
let value2;
if ($range) {
value2 = parseInt($range.value);
} else {
value2 = parseInt($text.textContent);
}
const btnType = e.target.getAttribute("data-type");
if (btnType === "dec") {
value2 = Math.max(MIN, value2 - STEPS);
} else {
value2 = Math.min(MAX, value2 + STEPS);
}
$text.textContent = renderTextValue(value2);
$range && ($range.value = value2.toString());
isHolding = false;
onChange && onChange(e, value2);
};
const onMouseDown = (e) => {
e.preventDefault();
isHolding = true;
const args = arguments;
interval && clearInterval(interval);
interval = window.setInterval(() => {
const event = new Event("click");
event.arguments = args;
e.target?.dispatchEvent(event);
}, 200);
};
const onMouseUp = (e) => {
e.preventDefault();
interval && clearInterval(interval);
isHolding = false;
};
const onContextMenu = (e) => e.preventDefault();
$wrapper.setValue = (value2) => {
$text.textContent = renderTextValue(value2);
$range && ($range.value = value2);
};
$decBtn.addEventListener("click", onClick);
$decBtn.addEventListener("pointerdown", onMouseDown);
$decBtn.addEventListener("pointerup", onMouseUp);
$decBtn.addEventListener("contextmenu", onContextMenu);
$incBtn.addEventListener("click", onClick);
$incBtn.addEventListener("pointerdown", onMouseDown);
$incBtn.addEventListener("pointerup", onMouseUp);
$incBtn.addEventListener("contextmenu", onContextMenu);
return $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];
const $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/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: 200,
params: {
suffix: "%",
exactTicks: 20
}
},
[MkbPresetKey.MOUSE_SENSITIVITY_X]: {
label: t("vertical-sensitivity"),
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 200,
params: {
suffix: "%",
exactTicks: 20
}
},
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: {
label: t("deadzone-counterweight"),
type: SettingElementType.NUMBER_STEPPER,
default: 20,
min: 1,
max: 100,
params: {
suffix: "%",
exactTicks: 10
}
},
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: {
label: t("stick-decay-strength"),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 10,
max: 100,
params: {
suffix: "%",
exactTicks: 10
}
},
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: {
label: t("stick-decay-minimum"),
type: SettingElementType.NUMBER_STEPPER,
default: 10,
min: 1,
max: 10,
params: {
suffix: "%"
}
}
};
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]: 50,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 100,
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 10
}
};
static convert(preset) {
const obj = {
mapping: {},
mouse: Object.assign({}, preset.mouse)
};
for (const buttonIndex in preset.mapping) {
for (const keyName of preset.mapping[parseInt(buttonIndex)]) {
obj.mapping[keyName] = parseInt(buttonIndex);
}
}
const mouse = obj.mouse;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01;
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;
}
console.log(obj);
return obj;
}
}
// src/modules/stream/stream-badges.ts
var StreamBadge;
(function(StreamBadge2) {
StreamBadge2["PLAYTIME"] = "playtime";
StreamBadge2["BATTERY"] = "battery";
StreamBadge2["IN"] = "in";
StreamBadge2["OUT"] = "out";
StreamBadge2["SERVER"] = "server";
StreamBadge2["VIDEO"] = "video";
StreamBadge2["AUDIO"] = "audio";
StreamBadge2["BREAK"] = "break";
})(StreamBadge || (StreamBadge = {}));
class StreamBadges {
static ipv6 = false;
static resolution = null;
static video = null;
static audio = null;
static fps = 0;
static region = "";
static startBatteryLevel = 100;
static startTimestamp = 0;
static #cachedDoms = {};
static #interval;
static #REFRESH_INTERVAL = 3000;
static #renderBadge(name, value, color) {
if (name === StreamBadge.BREAK) {
return CE("div", { style: "display: block" });
}
let $badge;
if (StreamBadges.#cachedDoms[name]) {
$badge = StreamBadges.#cachedDoms[name];
$badge.lastElementChild.textContent = value;
return $badge;
}
$badge = CE("div", { class: "bx-badge" }, CE("span", { class: "bx-badge-name" }, t(`badge-${name}`)), CE("span", { class: "bx-badge-value", style: `background-color: ${color}` }, value));
if (name === StreamBadge.BATTERY) {
$badge.classList.add("bx-badge-battery");
}
StreamBadges.#cachedDoms[name] = $badge;
return $badge;
}
static async#updateBadges(forceUpdate) {
if (!forceUpdate && !document.querySelector(".bx-badges")) {
StreamBadges.#stop();
return;
}
let now = +new Date;
const diffSeconds = Math.ceil((now - StreamBadges.startTimestamp) / 1000);
const playtime = StreamBadges.#secondsToHm(diffSeconds);
let batteryLevel = "100%";
let batteryLevelInt = 100;
let isCharging = false;
if ("getBattery" in navigator) {
try {
const bm = await navigator.getBattery();
isCharging = bm.charging;
batteryLevelInt = Math.round(bm.level * 100);
batteryLevel = `${batteryLevelInt}%`;
if (batteryLevelInt != StreamBadges.startBatteryLevel) {
const diffLevel = Math.round(batteryLevelInt - StreamBadges.startBatteryLevel);
const sign = diffLevel > 0 ? "+" : "";
batteryLevel += ` (${sign}${diffLevel}%)`;
}
} catch (e) {
}
}
const stats = await STATES.currentStream.peerConnection?.getStats();
let totalIn = 0;
let 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.IN]: totalIn ? StreamBadges.#humanFileSize(totalIn) : null,
[StreamBadge.OUT]: totalOut ? StreamBadges.#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 = StreamBadges.#cachedDoms[name];
$elm && ($elm.lastElementChild.textContent = value);
if (name === StreamBadge.BATTERY) {
$elm.setAttribute("data-charging", isCharging.toString());
if (StreamBadges.startBatteryLevel === 100 && batteryLevelInt === 100) {
$elm.style.display = "none";
} else {
$elm.removeAttribute("style");
}
}
}
}
static #stop() {
StreamBadges.#interval && clearInterval(StreamBadges.#interval);
StreamBadges.#interval = null;
}
static #secondsToHm(seconds) {
const h = Math.floor(seconds / 3600);
const m = Math.floor(seconds % 3600 / 60) + 1;
const hDisplay = h > 0 ? `${h}h` : "";
const mDisplay = m > 0 ? `${m}m` : "";
return hDisplay + mDisplay;
}
static #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];
}
static async render() {
let video = "";
if (StreamBadges.resolution) {
video = `${StreamBadges.resolution.height}p`;
}
if (StreamBadges.video) {
video && (video += "/");
video += StreamBadges.video.codec;
if (StreamBadges.video.profile) {
const profile = StreamBadges.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 (StreamBadges.audio) {
audio = StreamBadges.audio.codec;
const bitrate = StreamBadges.audio.bitrate / 1000;
audio += ` (${bitrate} kHz)`;
}
let batteryLevel = "";
if ("getBattery" in navigator) {
batteryLevel = "100%";
}
let server = StreamBadges.region;
server += "@" + (StreamBadges.ipv6 ? "IPv6" : "IPv4");
const BADGES = [
[StreamBadge.PLAYTIME, "1m", "#ff004d"],
[StreamBadge.BATTERY, batteryLevel, "#00b543"],
[StreamBadge.IN, StreamBadges.#humanFileSize(0), "#29adff"],
[StreamBadge.OUT, StreamBadges.#humanFileSize(0), "#ff77a8"],
[StreamBadge.BREAK],
[StreamBadge.SERVER, server, "#ff6c24"],
video ? [StreamBadge.VIDEO, video, "#742f29"] : null,
audio ? [StreamBadge.AUDIO, audio, "#5f574f"] : null
];
const $wrapper = CE("div", { class: "bx-badges" });
BADGES.forEach((item2) => {
if (!item2) {
return;
}
const $badge = StreamBadges.#renderBadge(...item2);
$wrapper.appendChild($badge);
});
await StreamBadges.#updateBadges(true);
StreamBadges.#stop();
StreamBadges.#interval = window.setInterval(StreamBadges.#updateBadges, StreamBadges.#REFRESH_INTERVAL);
return $wrapper;
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
const $video = e.$video;
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/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/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 #interval;
static #updateInterval = 1000;
static #$container;
static #$fps;
static #$ping;
static #$dt;
static #$pl;
static #$fl;
static #$br;
static #lastStat;
static #quickGlanceObserver;
static start(glancing = false) {
if (!StreamStats.isHidden() || glancing && StreamStats.isGlancing()) {
return;
}
StreamStats.#$container.classList.remove("bx-gone");
StreamStats.#$container.setAttribute("data-display", glancing ? "glancing" : "fixed");
StreamStats.#interval = window.setInterval(StreamStats.update, StreamStats.#updateInterval);
}
static stop(glancing = false) {
if (glancing && !StreamStats.isGlancing()) {
return;
}
StreamStats.#interval && clearInterval(StreamStats.#interval);
StreamStats.#interval = null;
StreamStats.#lastStat = null;
if (StreamStats.#$container) {
StreamStats.#$container.removeAttribute("data-display");
StreamStats.#$container.classList.add("bx-gone");
}
}
static toggle() {
if (StreamStats.isGlancing()) {
StreamStats.#$container.setAttribute("data-display", "fixed");
} else {
StreamStats.isHidden() ? StreamStats.start() : StreamStats.stop();
}
}
static onStoppedPlaying() {
StreamStats.stop();
StreamStats.quickGlanceStop();
StreamStats.hideSettingsUi();
}
static isHidden = () => StreamStats.#$container && StreamStats.#$container.classList.contains("bx-gone");
static isGlancing = () => StreamStats.#$container && StreamStats.#$container.getAttribute("data-display") === "glancing";
static quickGlanceSetup() {
if (StreamStats.#quickGlanceObserver) {
return;
}
const $uiContainer = document.querySelector("div[data-testid=ui-container]");
StreamStats.#quickGlanceObserver = new MutationObserver((mutationList, observer) => {
for (let record of mutationList) {
if (record.attributeName && record.attributeName === "aria-expanded") {
const expanded = record.target.ariaExpanded;
if (expanded === "true") {
StreamStats.isHidden() && StreamStats.start(true);
} else {
StreamStats.stop(true);
}
}
}
});
StreamStats.#quickGlanceObserver.observe($uiContainer, {
attributes: true,
attributeFilter: ["aria-expanded"],
subtree: true
});
}
static quickGlanceStop() {
StreamStats.#quickGlanceObserver && StreamStats.#quickGlanceObserver.disconnect();
StreamStats.#quickGlanceObserver = null;
}
static update() {
if (StreamStats.isHidden() || !STATES.currentStream.peerConnection) {
StreamStats.onStoppedPlaying();
return;
}
const PREF_STATS_CONDITIONAL_FORMATTING = getPref(PrefKey.STATS_CONDITIONAL_FORMATTING);
STATES.currentStream.peerConnection.getStats().then((stats) => {
stats.forEach((stat) => {
let grade = "";
if (stat.type === "inbound-rtp" && stat.kind === "video") {
StreamStats.#$fps.textContent = stat.framesPerSecond || 0;
const packetsLost = stat.packetsLost;
const packetsReceived = stat.packetsReceived;
const packetsLostPercentage = (packetsLost * 100 / (packetsLost + packetsReceived || 1)).toFixed(2);
StreamStats.#$pl.textContent = packetsLostPercentage === "0.00" ? packetsLost : `${packetsLost} (${packetsLostPercentage}%)`;
const framesDropped = stat.framesDropped;
const framesReceived = stat.framesReceived;
const framesDroppedPercentage = (framesDropped * 100 / (framesDropped + framesReceived || 1)).toFixed(2);
StreamStats.#$fl.textContent = framesDroppedPercentage === "0.00" ? framesDropped : `${framesDropped} (${framesDroppedPercentage}%)`;
if (StreamStats.#lastStat) {
const lastStat = StreamStats.#lastStat;
const timeDiff = stat.timestamp - lastStat.timestamp;
const bitrate = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000;
StreamStats.#$br.textContent = `${bitrate.toFixed(2)} Mbps`;
const totalDecodeTimeDiff = stat.totalDecodeTime - lastStat.totalDecodeTime;
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded;
const currentDecodeTime = totalDecodeTimeDiff / framesDecodedDiff * 1000;
StreamStats.#$dt.textContent = `${currentDecodeTime.toFixed(2)}ms`;
if (PREF_STATS_CONDITIONAL_FORMATTING) {
grade = currentDecodeTime > 12 ? "bad" : currentDecodeTime > 9 ? "ok" : currentDecodeTime > 6 ? "good" : "";
}
StreamStats.#$dt.setAttribute("data-grade", grade);
}
StreamStats.#lastStat = stat;
} else if (stat.type === "candidate-pair" && stat.packetsReceived > 0 && stat.state === "succeeded") {
const roundTripTime = typeof stat.currentRoundTripTime !== "undefined" ? stat.currentRoundTripTime * 1000 : -1;
StreamStats.#$ping.textContent = roundTripTime === -1 ? "???" : roundTripTime.toString();
if (PREF_STATS_CONDITIONAL_FORMATTING) {
grade = roundTripTime > 100 ? "bad" : roundTripTime > 75 ? "ok" : roundTripTime > 40 ? "good" : "";
}
StreamStats.#$ping.setAttribute("data-grade", grade);
}
});
});
}
static refreshStyles() {
const PREF_ITEMS = getPref(PrefKey.STATS_ITEMS);
const PREF_POSITION = getPref(PrefKey.STATS_POSITION);
const PREF_TRANSPARENT = getPref(PrefKey.STATS_TRANSPARENT);
const PREF_OPACITY = getPref(PrefKey.STATS_OPACITY);
const PREF_TEXT_SIZE = getPref(PrefKey.STATS_TEXT_SIZE);
const $container = StreamStats.#$container;
$container.setAttribute("data-stats", "[" + PREF_ITEMS.join("][") + "]");
$container.setAttribute("data-position", PREF_POSITION);
$container.setAttribute("data-transparent", PREF_TRANSPARENT);
$container.style.opacity = PREF_OPACITY + "%";
$container.style.fontSize = PREF_TEXT_SIZE;
}
static hideSettingsUi() {
if (StreamStats.isGlancing() && !getPref(PrefKey.STATS_QUICK_GLANCE)) {
StreamStats.stop();
}
}
static render() {
if (StreamStats.#$container) {
return;
}
const STATS = {
[StreamStat.PING]: [t("stat-ping"), StreamStats.#$ping = CE("span", {}, "0")],
[StreamStat.FPS]: [t("stat-fps"), StreamStats.#$fps = CE("span", {}, "0")],
[StreamStat.BITRATE]: [t("stat-bitrate"), StreamStats.#$br = CE("span", {}, "0 Mbps")],
[StreamStat.DECODE_TIME]: [t("stat-decode-time"), StreamStats.#$dt = CE("span", {}, "0ms")],
[StreamStat.PACKETS_LOST]: [t("stat-packets-lost"), StreamStats.#$pl = CE("span", {}, "0")],
[StreamStat.FRAMES_LOST]: [t("stat-frames-lost"), StreamStats.#$fl = CE("span", {}, "0")]
};
const $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);
}
StreamStats.#$container = CE("div", { class: "bx-stats-bar bx-gone" }, $barFragment);
document.documentElement.appendChild(StreamStats.#$container);
StreamStats.refreshStyles();
}
static getServerStats() {
STATES.currentStream.peerConnection && STATES.currentStream.peerConnection.getStats().then((stats) => {
const allVideoCodecs = {};
let videoCodecId;
const allAudioCodecs = {};
let audioCodecId;
const allCandidates = {};
let candidateId;
stats.forEach((stat) => {
if (stat.type === "codec") {
const mimeType = stat.mimeType.split("/");
if (mimeType[0] === "video") {
allVideoCodecs[stat.id] = stat;
} else if (mimeType[0] === "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;
}
});
if (videoCodecId) {
const videoStat = allVideoCodecs[videoCodecId];
const 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;
}
StreamBadges.video = video;
}
if (audioCodecId) {
const audioStat = allAudioCodecs[audioCodecId];
StreamBadges.audio = {
codec: audioStat.mimeType.substring(6),
bitrate: audioStat.clockRate
};
}
if (candidateId) {
BxLogger.info("candidate", candidateId, allCandidates);
StreamBadges.ipv6 = allCandidates[candidateId].includes(":");
}
if (getPref(PrefKey.STATS_SHOW_WHEN_PLAYING)) {
StreamStats.start();
}
});
}
static setupEvents() {
window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
const PREF_STATS_QUICK_GLANCE = getPref(PrefKey.STATS_QUICK_GLANCE);
const PREF_STATS_SHOW_WHEN_PLAYING = getPref(PrefKey.STATS_SHOW_WHEN_PLAYING);
StreamStats.getServerStats();
if (PREF_STATS_QUICK_GLANCE) {
StreamStats.quickGlanceSetup();
!PREF_STATS_SHOW_WHEN_PLAYING && StreamStats.start(true);
}
});
}
}
// 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_DISABLED"] = "native_mkb_disabled";
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_CLARITY"] = "video_clarity";
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 = false;
let hasNormalCodec = false;
let hasHighCodec = false;
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 = true;
} else if (!hasNormalCodec && fmtp.includes("profile-level-id=42e")) {
hasNormalCodec = true;
} else if (!hasLowCodec && fmtp.includes("profile-level-id=420")) {
hasLowCodec = true;
}
}
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;
const keys = Object.keys(options);
if (keys.length <= 1) {
setting.unsupported = true;
setting.note = "⚠️ " + t("browser-unsupported-feature");
} else {
}
}
},
[PrefKey.PREFER_IPV6_SERVER]: {
label: t("prefer-ipv6-server"),
default: false
},
[PrefKey.SCREENSHOT_APPLY_FILTERS]: {
label: t("screenshot-apply-filters"),
default: false
},
[PrefKey.SKIP_SPLASH_VIDEO]: {
label: t("skip-splash-video"),
default: false
},
[PrefKey.HIDE_DOTS_ICON]: {
label: t("hide-system-menu-icon"),
default: false
},
[PrefKey.STREAM_COMBINE_SOURCES]: {
label: t("combine-audio-video-streams"),
default: false,
experimental: true,
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.hasTouchSupport,
ready: (setting) => {
if (setting.unsupported) {
setting.default = "default";
}
}
},
[PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF]: {
label: t("tc-auto-off"),
default: false,
unsupported: !STATES.hasTouchSupport
},
[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: true
},
unsupported: !STATES.hasTouchSupport
},
[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.hasTouchSupport
},
[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.hasTouchSupport
},
[PrefKey.STREAM_SIMPLIFY_MENU]: {
label: t("simplify-stream-menu"),
default: false
},
[PrefKey.MKB_HIDE_IDLE_CURSOR]: {
label: t("hide-idle-cursor"),
default: false
},
[PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG]: {
label: t("disable-post-stream-feedback-dialog"),
default: false
},
[PrefKey.BITRATE_VIDEO_MAX]: {
type: SettingElementType.NUMBER_STEPPER,
label: t("bitrate-video-maximum"),
note: "⚠️ " + t("unexpected-behavior"),
default: 0,
min: 0,
max: 14,
steps: 1,
params: {
suffix: " Mb/s",
exactTicks: 5,
customTextValue: (value) => {
value = parseInt(value);
if (value === 0) {
return t("unlimited");
}
return null;
}
}
},
[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: false,
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: false
},
[PrefKey.CONTROLLER_ENABLE_VIBRATION]: {
default: true
},
[PrefKey.CONTROLLER_DEVICE_VIBRATION]: {
default: "off",
options: {
on: t("on"),
auto: t("device-vibration-not-using-gamepad"),
off: t("off")
}
},
[PrefKey.CONTROLLER_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: false,
unsupported: (() => {
const userAgent2 = (window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase();
return userAgent2.match(/(android|iphone|ipad)/) ? t("browser-unsupported-feature") : false;
})(),
ready: (setting) => {
let note;
let 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_DISABLED]: {
label: t("disable-native-mkb"),
default: false
},
[PrefKey.MKB_DEFAULT_PRESET_ID]: {
default: 0
},
[PrefKey.MKB_ABSOLUTE_MOUSE]: {
default: false
},
[PrefKey.REDUCE_ANIMATIONS]: {
label: t("reduce-animations"),
default: false
},
[PrefKey.UI_LOADING_SCREEN_GAME_ART]: {
label: t("show-game-art"),
default: true
},
[PrefKey.UI_LOADING_SCREEN_WAIT_TIME]: {
label: t("show-wait-time"),
default: true
},
[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: false
},
[PrefKey.UI_HOME_CONTEXT_MENU_DISABLED]: {
label: t("disable-home-context-menu"),
default: false
},
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
label: t("disable-social-features"),
default: false
},
[PrefKey.BLOCK_TRACKING]: {
label: t("disable-xcloud-analytics"),
default: false
},
[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_CLARITY]: {
type: SettingElementType.NUMBER_STEPPER,
default: 0,
min: 0,
max: 5,
params: {
hideSlider: true
}
},
[PrefKey.VIDEO_RATIO]: {
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]: {
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 50,
max: 150,
params: {
suffix: "%",
ticks: 25
}
},
[PrefKey.VIDEO_CONTRAST]: {
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 50,
max: 150,
params: {
suffix: "%",
ticks: 25
}
},
[PrefKey.VIDEO_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: false
},
[PrefKey.AUDIO_ENABLE_VOLUME_CONTROL]: {
label: t("enable-volume-control"),
default: false
},
[PrefKey.AUDIO_VOLUME]: {
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 0,
max: 600,
params: {
suffix: "%",
ticks: 100
}
},
[PrefKey.STATS_ITEMS]: {
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]: {
default: false
},
[PrefKey.STATS_QUICK_GLANCE]: {
default: true
},
[PrefKey.STATS_POSITION]: {
default: "top-right",
options: {
"top-left": t("top-left"),
"top-center": t("top-center"),
"top-right": t("top-right")
}
},
[PrefKey.STATS_TEXT_SIZE]: {
default: "0.9rem",
options: {
"0.9rem": t("small"),
"1.0rem": t("normal"),
"1.1rem": t("large")
}
},
[PrefKey.STATS_TRANSPARENT]: {
default: false
},
[PrefKey.STATS_OPACITY]: {
type: SettingElementType.NUMBER_STEPPER,
default: 80,
min: 50,
max: 100,
params: {
suffix: "%",
ticks: 10
}
},
[PrefKey.STATS_CONDITIONAL_FORMATTING]: {
default: false
},
[PrefKey.REMOTE_PLAY_ENABLED]: {
label: t("enable-remote-play-feature"),
default: false
},
[PrefKey.REMOTE_PLAY_RESOLUTION]: {
default: "1080p",
options: {
"1080p": "1080p",
"720p": "720p"
}
},
[PrefKey.GAME_FORTNITE_FORCE_CONSOLE]: {
label: "🎮 " + t("fortnite-force-console-version"),
default: false,
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];
setting.ready && setting.ready.call(this, setting);
if (setting.migrate && settingId in savedPrefs) {
setting.migrate.call(this, savedPrefs, savedPrefs[settingId]);
}
}
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) {
value = this.#validateValue(key, value);
this.#prefs[key] = value;
this.#updateStorage();
return 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);
let 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;
}
const $control = SettingElement.render(type, key, setting, currentValue, (e, value) => {
this.set(key, value);
onChange && onChange(e, value);
}, params);
return $control;
}
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;
var getPref = prefs.get.bind(prefs);
var setPref = prefs.set.bind(prefs);
var toPrefElement = prefs.toElement.bind(prefs);
// src/utils/toast.ts
class Toast {
static #$wrapper;
static #$msg;
static #$status;
static #stack = [];
static #isShowing = false;
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 = false;
return;
}
Toast.#isShowing = true;
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: {
const presets = db.createObjectStore(LocalDb.TABLE_PRESETS, { keyPath: "id", autoIncrement: true });
presets.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 transaction = this.#DB.transaction(name, type || "readonly");
const table = transaction.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;
let name;
if (e instanceof KeyboardEvent) {
code = e.code;
} 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 {
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/mouse-settings.svg
var mouse_settings_default = "\n";
// src/assets/svg/mouse.svg
var mouse_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/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/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,
MOUSE: mouse_default,
MOUSE_SETTINGS: mouse_settings_default,
NEW: new_default,
COPY: copy_default,
TRASH: trash_default,
CURSOR_TEXT: cursor_text_default,
QUESTION: question_default,
REFRESH: refresh_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
};
// src/modules/stream/stream-ui.ts
var cloneStreamHudButton = function($orgButton, label, svgIcon) {
const $container = $orgButton.cloneNode(true);
let timeout;
const onTransitionStart = (e) => {
if (e.propertyName !== "opacity") {
return;
}
timeout && clearTimeout(timeout);
$container.style.pointerEvents = "none";
};
const onTransitionEnd = (e) => {
if (e.propertyName !== "opacity") {
return;
}
const left = document.getElementById("StreamHud")?.style.left;
if (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");
const $svg = createSvgIcon(svgIcon);
$svg.style.fill = "none";
$svg.setAttribute("class", $orgSvg.getAttribute("class") || "");
$svg.ariaHidden = "true";
$orgSvg.replaceWith($svg);
return $container;
};
function injectStreamMenuButtons() {
const $screen = document.querySelector("#PageContent section[class*=PureScreens]");
if (!$screen) {
return;
}
if ($screen.xObserving) {
return;
}
$screen.xObserving = true;
const $quickBar = document.querySelector(".bx-quick-settings-bar");
const $parent = $screen.parentElement;
const hideQuickBarFunc = (e) => {
if (e) {
const $target = e.target;
e.stopPropagation();
if ($target != $parent && $target.id !== "MultiTouchSurface" && !$target.querySelector("#BabylonCanvasContainer-main")) {
return;
}
if ($target.id === "MultiTouchSurface") {
$target.removeEventListener("touchstart", hideQuickBarFunc);
}
}
$quickBar.classList.add("bx-gone");
$parent?.removeEventListener("click", hideQuickBarFunc);
};
let $btnStreamSettings;
let $btnStreamStats;
const PREF_DISABLE_FEEDBACK_DIALOG = getPref(PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG);
const observer = new MutationObserver((mutationList) => {
mutationList.forEach((item2) => {
if (item2.type !== "childList") {
return;
}
item2.removedNodes.forEach(($node) => {
if (!$node || $node.nodeType !== Node.ELEMENT_NODE) {
return;
}
if (!$node.className || !$node.className.startsWith) {
return;
}
if ($node.className.startsWith("StreamMenu")) {
if (!document.querySelector("div[class^=PureInStreamConfirmationModal]")) {
BxEvent.dispatch(window, BxEvent.STREAM_MENU_HIDDEN);
}
}
});
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 (PREF_DISABLE_FEEDBACK_DIALOG && $elm.className.startsWith("PostStreamFeedbackScreen")) {
const $btnClose = $elm.querySelector("button");
$btnClose && $btnClose.click();
return;
}
if ($elm.className?.startsWith("StreamMenu-module__container")) {
BxEvent.dispatch(window, BxEvent.STREAM_MENU_SHOWN);
const $btnCloseHud = document.querySelector("button[class*=StreamMenu-module__backButton]");
if (!$btnCloseHud) {
return;
}
$btnCloseHud && $btnCloseHud.addEventListener("click", (e) => {
$quickBar.classList.add("bx-gone");
});
const $btnRefresh = $btnCloseHud.cloneNode(true);
const $svgRefresh = createSvgIcon(BxIcon.REFRESH);
$svgRefresh.setAttribute("class", $btnRefresh.firstElementChild.getAttribute("class") || "");
$svgRefresh.style.fill = "none";
$btnRefresh.classList.add("bx-stream-refresh-button");
$btnRefresh.removeChild($btnRefresh.firstElementChild);
$btnRefresh.appendChild($svgRefresh);
$btnRefresh.addEventListener("click", (e) => {
confirm(t("confirm-reload-stream")) && window.location.reload();
});
$btnCloseHud.insertAdjacentElement("afterend", $btnRefresh);
const $menu = document.querySelector("div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module]");
$menu?.appendChild(await StreamBadges.render());
hideQuickBarFunc();
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]");
const hideGripHandle = () => {
if (!$gripHandle) {
return;
}
$gripHandle.dispatchEvent(new PointerEvent("pointerdown"));
$gripHandle.click();
$gripHandle.dispatchEvent(new PointerEvent("pointerdown"));
$gripHandle.click();
};
const $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();
$quickBar.classList.remove("bx-gone");
$parent?.addEventListener("click", hideQuickBarFunc);
const $touchSurface = document.getElementById("MultiTouchSurface");
$touchSurface && $touchSurface.style.display != "none" && $touchSurface.addEventListener("touchstart", hideQuickBarFunc);
});
}
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();
$btnStreamStats.classList.toggle("bx-stream-menu-button-on", btnStreamStatsOn);
if ($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);
}
});
});
});
observer.observe($screen, { subtree: true, childList: true });
}
function showStreamSettings(tabId) {
const $wrapper = document.querySelector(".bx-quick-settings-bar");
if (!$wrapper) {
return;
}
if (tabId) {
const $tab = $wrapper.querySelector(`.bx-quick-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 = true;
const onClick = (e) => {
$wrapper.classList.add("bx-gone");
$parent.bxClick = false;
$parent.removeEventListener("click", onClick);
};
$parent.addEventListener("click", onClick);
}
}
// src/modules/mkb/mkb-handler.ts
var LOG_TAG = "MkbHandler";
class MkbHandler {
static #instance;
static get INSTANCE() {
if (!MkbHandler.#instance) {
MkbHandler.#instance = new MkbHandler;
}
return MkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static DEFAULT_PANNING_SENSITIVITY = 0.001;
static DEFAULT_STICK_SENSITIVITY = 0.0006;
static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
static MAXIMUM_STICK_RANGE = 1.1;
static VIRTUAL_GAMEPAD_ID = "Xbox 360 Controller";
#VIRTUAL_GAMEPAD = {
id: MkbHandler.VIRTUAL_GAMEPAD_ID,
index: 3,
connected: false,
hapticActuators: null,
mapping: "standard",
axes: [0, 0, 0, 0],
buttons: new Array(17).fill(null).map(() => ({ pressed: false, value: 0 })),
timestamp: performance.now(),
vibrationActuator: null
};
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
#enabled = false;
#isPolling = false;
#prevWheelCode = null;
#wheelStoppedTimeout;
#detectMouseStoppedTimeout;
#allowStickDecaying = false;
#$message;
#STICK_MAP;
#LEFT_STICK_X = [];
#LEFT_STICK_Y = [];
#RIGHT_STICK_X = [];
#RIGHT_STICK_Y = [];
constructor() {
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]
};
}
#patchedGetGamepads = () => {
const gamepads = this.#nativeGetGamepads() || [];
gamepads[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
return 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();
}
#getStickAxes(stick) {
const virtualGamepad = this.#getVirtualGamepad();
return {
x: virtualGamepad.axes[stick * 2],
y: virtualGamepad.axes[stick * 2 + 1]
};
}
#vectorLength = (x, y) => Math.sqrt(x ** 2 + y ** 2);
#disableContextMenu = (e) => e.preventDefault();
#resetGamepad = () => {
const gamepad = this.#getVirtualGamepad();
gamepad.axes = [0, 0, 0, 0];
for (const button of gamepad.buttons) {
button.pressed = false;
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 (isKeyDown) {
if (e.code === "F8") {
e.preventDefault();
this.toggle();
return;
}
if (!this.#isPolling) {
return;
}
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code];
if (typeof buttonIndex === "undefined") {
return;
}
if (e.repeat) {
return;
}
e.preventDefault();
this.#pressButton(buttonIndex, isKeyDown);
};
#onMouseEvent = (e) => {
const isMouseDown = e.type === "mousedown";
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === "undefined") {
return;
}
e.preventDefault();
this.#pressButton(buttonIndex, isMouseDown);
};
#onWheelEvent = (e) => {
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code];
if (typeof buttonIndex === "undefined") {
return;
}
e.preventDefault();
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
this.#pressButton(buttonIndex, true);
}
this.#wheelStoppedTimeout = window.setTimeout(() => {
this.#prevWheelCode = null;
this.#pressButton(buttonIndex, false);
}, 20);
};
#decayStick = () => {
if (!this.#allowStickDecaying) {
return;
}
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
return;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
let { x, y } = this.#getStickAxes(analog);
const length = this.#vectorLength(x, y);
const clampedLength = Math.min(1, length);
const decayStrength = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH];
const decay = 1 - clampedLength * clampedLength * decayStrength;
const minDecay = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN];
const clampedDecay = Math.min(1 - minDecay, decay);
x *= clampedDecay;
y *= clampedDecay;
const deadzoneCounterweight = 20 * MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
if (Math.abs(x) <= deadzoneCounterweight && Math.abs(y) <= deadzoneCounterweight) {
x = 0;
y = 0;
}
if (this.#allowStickDecaying) {
this.#updateStick(analog, x, y);
(x !== 0 || y !== 0) && requestAnimationFrame(this.#decayStick);
}
};
#onMouseStopped = () => {
this.#allowStickDecaying = true;
requestAnimationFrame(this.#decayStick);
};
#onMouseMoveEvent = (e) => {
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
return;
}
this.#allowStickDecaying = false;
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 10);
const deltaX = e.movementX;
const deltaY = e.movementY;
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
let x = deltaX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = deltaY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let length = this.#vectorLength(x, y);
if (length !== 0 && length < deadzoneCounterweight) {
x *= deadzoneCounterweight / length;
y *= deadzoneCounterweight / length;
} else if (length > MkbHandler.MAXIMUM_STICK_RANGE) {
x *= MkbHandler.MAXIMUM_STICK_RANGE / length;
y *= MkbHandler.MAXIMUM_STICK_RANGE / length;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, x, y);
};
toggle = () => {
this.#enabled = !this.#enabled;
this.#enabled ? document.pointerLockElement && this.start() : this.stop();
Toast.show(t("mouse-and-keyboard"), t(this.#enabled ? "enabled" : "disabled"), { instant: true });
if (this.#enabled) {
!document.pointerLockElement && this.#waitForPointerLock(true);
} else {
this.#waitForPointerLock(false);
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();
});
};
#onPointerLockChange = () => {
if (this.#enabled && !document.pointerLockElement) {
this.stop();
this.#waitForPointerLock(true);
}
};
#onPointerLockError = (e) => {
console.log(e);
this.stop();
};
#onActivatePointerLock = () => {
if (!document.pointerLockElement) {
document.body.requestPointerLock();
}
this.#waitForPointerLock(false);
this.start();
};
#waitForPointerLock = (wait) => {
this.#$message && this.#$message.classList.toggle("bx-gone", !wait);
};
#onStreamMenuShown = () => {
this.#enabled && this.#waitForPointerLock(false);
};
#onStreamMenuHidden = () => {
this.#enabled && this.#waitForPointerLock(true);
};
init = () => {
this.refreshPresetData();
this.#enabled = true;
window.addEventListener("keydown", this.#onKeyboardEvent);
document.addEventListener("pointerlockchange", this.#onPointerLockChange);
document.addEventListener("pointerlockerror", this.#onPointerLockError);
this.#$message = CE("div", { class: "bx-mkb-pointer-lock-msg bx-gone" }, createButton({
icon: BxIcon.MOUSE_SETTINGS,
style: ButtonStyle.PRIMARY,
onClick: (e) => {
e.preventDefault();
e.stopPropagation();
showStreamSettings("mkb");
}
}), CE("div", {}, CE("p", {}, t("mkb-click-to-activate")), CE("p", {}, t("press-key-to-toggle-mkb", { key: "F8" }))));
this.#$message.addEventListener("click", this.#onActivatePointerLock);
document.documentElement.appendChild(this.#$message);
window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.addEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
this.#waitForPointerLock(true);
};
destroy = () => {
this.#isPolling = false;
this.#enabled = false;
this.stop();
this.#waitForPointerLock(false);
document.pointerLockElement && document.exitPointerLock();
window.removeEventListener("keydown", this.#onKeyboardEvent);
document.removeEventListener("pointerlockchange", this.#onPointerLockChange);
document.removeEventListener("pointerlockerror", this.#onPointerLockError);
window.removeEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
};
start = () => {
this.#isPolling = true;
window.navigator.getGamepads = this.#patchedGetGamepads;
this.#resetGamepad();
window.addEventListener("keyup", this.#onKeyboardEvent);
window.addEventListener("mousemove", this.#onMouseMoveEvent);
window.addEventListener("mousedown", this.#onMouseEvent);
window.addEventListener("mouseup", this.#onMouseEvent);
window.addEventListener("wheel", this.#onWheelEvent);
window.addEventListener("contextmenu", this.#disableContextMenu);
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = true;
virtualGamepad.timestamp = performance.now();
BxEvent.dispatch(window, "gamepadconnected", {
gamepad: virtualGamepad
});
};
stop = () => {
this.#isPolling = false;
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now();
BxEvent.dispatch(window, "gamepaddisconnected", {
gamepad: virtualGamepad
});
window.navigator.getGamepads = this.#nativeGetGamepads;
this.#resetGamepad();
window.removeEventListener("keyup", this.#onKeyboardEvent);
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);
};
static setupEvents() {
getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) {
BxLogger.info(LOG_TAG, "Emulate MKB");
MkbHandler.INSTANCE.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 = true) {
if (!window.BX_EXPOSED.streamSession) {
return false;
}
const state = window.BX_EXPOSED.streamSession._microphoneState;
const enableMic = state === MicrophoneState.ENABLED ? false : true;
try {
window.BX_EXPOSED.streamSession.tryEnableChatAsync(enableMic);
showToast && Toast.show(t("microphone"), t(enableMic ? "unmuted" : "muted"), { instant: true });
return enableMic;
} catch (e) {
console.log(e);
}
return false;
}
}
// 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() {
const CHECK_INTERVAL_SECONDS = 7200;
const currentVersion = getPref(PrefKey.CURRENT_VERSION);
const lastCheck = getPref(PrefKey.LAST_UPDATE_CHECK);
const 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(true);
}
function disablePwa() {
const userAgent2 = (window.navigator.orgUserAgent || window.navigator.userAgent || "").toLowerCase();
if (!userAgent2) {
return;
}
if (!!AppInterface || UserAgent.isSafari(true)) {
Object.defineProperty(window.navigator, "standalone", {
value: true
});
}
}
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;
}
newValue = setPref(PrefKey.AUDIO_VOLUME, newValue);
SoundShortcut.setGainNodeVolume(newValue);
Toast.show(`${t("stream")} ❯ ${t("volume")}`, newValue + "%", { instant: true });
BxEvent.dispatch(window, BxEvent.GAINNODE_VOLUME_CHANGED, {
volume: newValue
});
return 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;
const 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: true });
return;
}
let $media;
$media = document.querySelector("div[data-testid=media-container] audio");
if (!$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: true });
}
}
}
// src/modules/controller-shortcut.ts
var ShortcutAction;
(function(ShortcutAction2) {
ShortcutAction2["STREAM_SCREENSHOT_CAPTURE"] = "stream-screenshot-capture";
ShortcutAction2["STREAM_MENU_TOGGLE"] = "stream-menu-toggle";
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_VOLUME_INC"] = "device-volume-inc";
ShortcutAction2["DEVICE_VOLUME_DEC"] = "device-volume-dec";
ShortcutAction2["SCREEN_BRIGHTNESS_INC"] = "screen-brightness-inc";
ShortcutAction2["SCREEN_BRIGHTNESS_DEC"] = "screen-brightness-dec";
})(ShortcutAction || (ShortcutAction = {}));
class ControllerShortcut {
static #STORAGE_KEY = "better_xcloud_controller_shortcuts";
static #buttonsCache = {};
static #buttonsStatus = {};
static #$selectProfile;
static #$selectActions = {};
static #$remap;
static #ACTIONS = {};
static reset(index) {
ControllerShortcut.#buttonsCache[index] = [];
ControllerShortcut.#buttonsStatus[index] = [];
}
static handle(gamepad) {
const gamepadIndex = gamepad.index;
const actions = ControllerShortcut.#ACTIONS[gamepad.id];
if (!actions) {
return false;
}
ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0);
ControllerShortcut.#buttonsStatus[gamepadIndex] = [];
const pressed = [];
let otherButtonPressed = false;
gamepad.buttons.forEach((button, index) => {
if (button.pressed && index !== GamepadKey.HOME) {
otherButtonPressed = true;
pressed[index] = true;
if (actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index]) {
ControllerShortcut.#runAction(actions[index]);
}
}
});
ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed;
return otherButtonPressed;
}
static #runAction(action) {
switch (action) {
case ShortcutAction.STREAM_SCREENSHOT_CAPTURE:
Screenshot.takeScreenshot();
break;
case ShortcutAction.STREAM_STATS_TOGGLE:
StreamStats.toggle();
break;
case ShortcutAction.STREAM_MICROPHONE_TOGGLE:
MicrophoneShortcut.toggle();
break;
case ShortcutAction.STREAM_MENU_TOGGLE:
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;
}
}
static #updateAction(profile, button, action) {
if (!(profile in ControllerShortcut.#ACTIONS)) {
ControllerShortcut.#ACTIONS[profile] = [];
}
if (!action) {
action = null;
}
ControllerShortcut.#ACTIONS[profile][button] = action;
for (const key in ControllerShortcut.#ACTIONS) {
let empty = true;
for (const value of ControllerShortcut.#ACTIONS[key]) {
if (!!value) {
empty = false;
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;
const $remap = ControllerShortcut.#$remap;
const $fragment = document.createDocumentFragment();
while ($select.firstElementChild) {
$select.firstElementChild.remove();
}
const gamepads = navigator.getGamepads();
let hasGamepad = false;
for (const gamepad of gamepads) {
if (!gamepad || !gamepad.connected) {
continue;
}
if (gamepad.id === MkbHandler.VIRTUAL_GAMEPAD_ID) {
continue;
}
hasGamepad = true;
const $option = CE("option", { value: gamepad.id }, gamepad.id);
$fragment.appendChild($option);
}
if (hasGamepad) {
$select.appendChild($fragment);
$remap.classList.remove("bx-gone");
$select.disabled = false;
$select.selectedIndex = 0;
$select.dispatchEvent(new Event("change"));
} else {
$remap.classList.add("bx-gone");
$select.disabled = true;
const $option = CE("option", {}, "---");
$fragment.appendChild($option);
$select.appendChild($fragment);
}
}
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: true
});
}
}
static renderSettings() {
ControllerShortcut.#ACTIONS = JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || "{}");
const buttons = {
[GamepadKey.A]: PrompFont.A,
[GamepadKey.B]: PrompFont.B,
[GamepadKey.X]: PrompFont.X,
[GamepadKey.Y]: PrompFont.Y,
[GamepadKey.LB]: PrompFont.LB,
[GamepadKey.RB]: PrompFont.RB,
[GamepadKey.LT]: PrompFont.LT,
[GamepadKey.RT]: PrompFont.RT,
[GamepadKey.SELECT]: PrompFont.SELECT,
[GamepadKey.START]: PrompFont.START,
[GamepadKey.UP]: PrompFont.UP,
[GamepadKey.DOWN]: PrompFont.DOWN,
[GamepadKey.LEFT]: PrompFont.LEFT,
[GamepadKey.RIGHT]: PrompFont.RIGHT
};
const actions = {
[t("stream")]: {
[ShortcutAction.STREAM_SCREENSHOT_CAPTURE]: t("take-screenshot"),
[ShortcutAction.STREAM_STATS_TOGGLE]: [t("stats"), t("show-hide")],
[ShortcutAction.STREAM_MICROPHONE_TOGGLE]: [t("microphone"), t("toggle")],
[ShortcutAction.STREAM_MENU_TOGGLE]: [t("menu"), t("show")],
[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")]
}
};
const $baseSelect = CE("select", { autocomplete: "off" }, CE("option", { value: "" }, "---"));
for (const groupLabel in actions) {
const items = actions[groupLabel];
if (!items) {
continue;
}
const $optGroup = CE("optgroup", { label: groupLabel });
for (const 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);
}
const $container = CE("div", {});
const $profile = CE("select", { class: "bx-shortcut-profile", autocomplete: "off" });
$profile.addEventListener("change", (e) => {
ControllerShortcut.#switchProfile($profile.value);
});
$container.appendChild($profile);
const onActionChanged = (e) => {
const $target = e.target;
const profile = $profile.value;
const button2 = $target.dataset.button;
const action = $target.value;
const $fakeSelect = $target.previousElementSibling;
let fakeText = "---";
if (action) {
const $selectedOption = $target.options[$target.selectedIndex];
const $optGroup = $selectedOption.parentElement;
fakeText = $optGroup.label + " ❯ " + $selectedOption.text;
}
$fakeSelect.firstElementChild.text = fakeText;
!e.ignoreOnChange && ControllerShortcut.#updateAction(profile, button2, action);
};
const $remap = CE("div", { class: "bx-gone" });
let button;
for (button in buttons) {
const $row = CE("div", { class: "bx-shortcut-row" });
const prompt2 = buttons[button];
const $label = CE("label", { class: "bx-prompt" }, `${PrompFont.HOME} + ${prompt2}`);
const $div = CE("div", { class: "bx-shortcut-actions" });
const $fakeSelect = CE("select", { autocomplete: "off" }, CE("option", {}, "---"));
$div.appendChild($fakeSelect);
const $select = $baseSelect.cloneNode(true);
$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);
}
$container.appendChild($remap);
ControllerShortcut.#$selectProfile = $profile;
ControllerShortcut.#$remap = $remap;
window.addEventListener("gamepadconnected", ControllerShortcut.#updateProfileList);
window.addEventListener("gamepaddisconnected", ControllerShortcut.#updateProfileList);
ControllerShortcut.#updateProfileList();
return $container;
}
}
// 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/touch-controller.ts
var LOG_TAG2 = "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 = false;
static #dataChannel;
static #customLayouts = {};
static #baseCustomLayouts = {};
static #currentLayoutId;
static #customList;
static enable() {
TouchController.#enable = true;
}
static disable() {
TouchController.#enable = false;
}
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 = false;
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;
}
retries = retries || 1;
if (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" : ""}`;
const url = `${baseUrl}/${xboxTitleId}.json`;
try {
const resp = await NATIVE_FETCH(url);
const json = await resp.json();
const 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`;
const resp2 = await NATIVE_FETCH(layoutUrl);
const json2 = await resp2.json();
baseLayouts = json2.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) => {
window.removeEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener);
if (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;
let html12 = false;
if (layout.author) {
const author = `${escapeHtml(layout.author)}`;
msg = t("touch-control-layout-by", { name: author });
html12 = true;
} else {
msg = t("touch-control-layout");
}
layoutChanged && Toast.show(msg, layout.name, { html: html12 });
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() {
const key = "better_xcloud_custom_touch_layouts";
TouchController.#customList = JSON.parse(window.localStorage.getItem(key) || "[]");
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(key, 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);
const 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 = false;
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);
focused = json.focused;
if (!json.focused) {
TouchController.#show();
}
STATES.currentStream.xboxTitleId = parseInt(json.titleid, 16).toString();
}
} catch (e2) {
BxLogger.error(LOG_TAG2, "Load custom layout", e2);
}
});
});
}
}
// 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);
};
const $btnEnable = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_ENABLE,
title: t("show-touch-controller"),
onClick,
classes: ["bx-activated"]
});
const $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 = false;
constructor() {
super();
const onClick = (e) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const enabled = MicrophoneShortcut.toggle(false);
this.$content.setAttribute("data-enabled", enabled.toString());
};
const $btnDefault = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE,
title: t("show-touch-controller"),
onClick,
classes: ["bx-activated"]
});
const $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 microphoneState = e.microphoneState;
const enabled = microphoneState === MicrophoneState.ENABLED;
this.$content.setAttribute("data-enabled", enabled.toString());
this.$content.classList.remove("bx-gone");
});
}
render() {
return this.$content;
}
reset() {
this.visible = false;
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);
const $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));
this.actions = [
new ScreenshotAction,
...STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== "off" ? [new TouchControlAction] : [],
new MicrophoneAction
];
if (position === "bottom-right") {
this.actions.reverse();
}
for (const 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;
}
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 (const action of this.actions) {
action.reset();
}
}
}
// 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 = {
onPollingModeChanged: (mode) => {
if (getPref(PrefKey.GAME_BAR_POSITION) === "off") {
return;
}
const gameBar = GameBar.getInstance();
if (!STATES.isPlaying) {
gameBar.disable();
return;
}
mode !== "None" ? gameBar.disable() : gameBar.enable();
},
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyTitleInfo: (titleInfo) => {
titleInfo = structuredClone(titleInfo);
let supportedInputTypes = titleInfo.details.supportedInputTypes;
if (getPref(PrefKey.NATIVE_MKB_DISABLED) || UserAgent.isMobile()) {
supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.MKB);
}
titleInfo.details.hasMkbSupport = supportedInputTypes.includes(InputType.MKB);
if (STATES.hasTouchSupport) {
let touchControllerAvailability = getPref(PrefKey.STREAM_TOUCH_CONTROLLER);
if (touchControllerAvailability !== "off" && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) {
const gamepads = window.navigator.getGamepads();
let gamepadFound = false;
for (let gamepad of gamepads) {
if (gamepad && gamepad.connected) {
gamepadFound = true;
break;
}
}
gamepadFound && (touchControllerAvailability = "off");
}
if (touchControllerAvailability === "off") {
supportedInputTypes = supportedInputTypes.filter((i) => i !== InputType.CUSTOM_TOUCH_OVERLAY && i !== InputType.GENERIC_TOUCH);
}
titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) || supportedInputTypes.includes(InputType.GENERIC_TOUCH);
if (!titleInfo.details.hasTouchSupport && touchControllerAvailability === "all") {
titleInfo.details.hasFakeTouchSupport = true;
supportedInputTypes.push(InputType.GENERIC_TOUCH);
}
}
titleInfo.details.supportedInputTypes = supportedInputTypes;
STATES.currentStream.titleInfo = titleInfo;
BxEvent.dispatch(window, BxEvent.TITLE_INFO_READY);
return titleInfo;
},
setupGainNode: ($media, audioStream) => {
if ($media instanceof HTMLAudioElement) {
$media.muted = true;
$media.addEventListener("playing", (e) => {
$media.muted = true;
$media.pause();
});
} else {
$media.muted = true;
$media.addEventListener("playing", (e) => {
$media.muted = true;
});
}
try {
const audioCtx = STATES.currentStream.audioContext;
const source = audioCtx.createMediaStreamSource(audioStream);
const 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 = false) {
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);
const s = Math.floor(seconds % 60);
const mDisplay = m > 0 ? `${m}m` : "";
const 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;
}
LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
if (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;
}
`;
$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;
let $countDown;
let $estimated;
LoadingScreen.#orgWebTitle = document.title;
const endDate = new Date;
const 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(() => {
secondsLeft--;
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft);
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`;
if (secondsLeft <= 0) {
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval);
LoadingScreen.#waitTimeInterval = null;
}
}, 1000);
}
static hide() {
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle);
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone");
if (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;
}
`;
}
LoadingScreen.reset();
}
static reset() {
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add("bx-gone");
LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = "");
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;
const $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) {
document.activeElement && document.activeElement.blur();
if (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: false
};
#$ = {
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: true
});
}
#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"));
const keySlot = parseInt($elm.getAttribute("data-key-slot"));
if ($elm.getAttribute("data-key-code") === key.code) {
return;
}
for (const $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"));
const 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) => {
e.preventDefault();
e.stopPropagation();
this.#clearEventListeners();
if (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) => {
e.preventDefault();
if (!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 (const $elm of this.#$.allKeyElements) {
const buttonIndex = parseInt($elm.getAttribute("data-button-index"));
const keySlot = parseInt($elm.getAttribute("data-key-slot"));
const 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);
MkbHandler.INSTANCE.refreshPresetData();
} else {
defaultPresetId = getPref(PrefKey.MKB_DEFAULT_PRESET_ID);
}
for (let id2 in presets) {
const preset = presets[id2];
let name = preset.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) => {
this.#STATE.isEditing = typeof force !== "undefined" ? force : !this.#STATE.isEditing;
this.#$.wrapper.classList.toggle("bx-editing", this.#STATE.isEditing);
if (this.#STATE.isEditing) {
this.#STATE.editingPresetData = structuredClone(this.#getCurrentPreset().data);
} else {
this.#STATE.editingPresetData = null;
}
const childElements = this.#$.wrapper.querySelectorAll("select, button, input");
for (const $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) {
newName = prompt(t("prompt-preset-name"), value);
if (newName === null) {
return false;
}
newName = newName.trim();
}
return newName ? newName : false;
};
const $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")));
const keysPerButton = 2;
for (const 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 (const key in MkbPreset.MOUSE_SETTINGS) {
const setting = MkbPreset.MOUSE_SETTINGS[key];
const value = setting.default;
let $elm;
const onChange = (e, value2) => {
this.#STATE.editingPresetData.mouse[key] = value2;
};
const $row = CE("div", { class: "bx-quick-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(true)
}), this.#$.activateButton = createButton({
label: t("activate"),
style: ButtonStyle.PRIMARY,
onClick: (e) => {
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, this.#STATE.currentPresetId);
MkbHandler.INSTANCE.refreshPresetData();
this.#refresh();
}
})), CE("div", {}, createButton({
label: t("cancel"),
style: ButtonStyle.GHOST,
onClick: (e) => {
this.#switchPreset(this.#STATE.currentPresetId);
this.#toggleEditing(false);
}
}), 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)) {
MkbHandler.INSTANCE.refreshPresetData();
}
this.#toggleEditing(false);
this.#refresh();
});
}
})));
this.#$.wrapper.appendChild($actionButtons);
this.#toggleEditing(false);
this.#refresh();
return this.#$.wrapper;
}
}
// 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;
const onDuration = Math.floor(pulseDuration * intensity / 100);
const offDuration = pulseDuration - onDuration;
const repeats = Math.ceil(data.durationMs / pulseDuration);
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
window.navigator.vibrate(pulses);
}
static supportControllerVibration() {
return Gamepad.prototype.hasOwnProperty("vibrationActuator");
}
static supportDeviceVibration() {
return !!window.navigator.vibrate;
}
static updateGlobalVars() {
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? getPref(PrefKey.CONTROLLER_ENABLE_VIBRATION) : false;
window.BX_VIBRATION_INTENSITY = getPref(PrefKey.CONTROLLER_VIBRATION_INTENSITY) / 100;
if (!VibrationManager.supportDeviceVibration()) {
window.BX_ENABLE_DEVICE_VIBRATION = false;
return;
}
window.navigator.vibrate(0);
const value = getPref(PrefKey.CONTROLLER_DEVICE_VIBRATION);
let enabled;
if (value === "on") {
enabled = true;
} else if (value === "auto") {
enabled = true;
const gamepads = window.navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
enabled = false;
break;
}
}
} else {
enabled = false;
}
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
}
static #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;
let messageType;
if (dataView.byteLength === 13) {
messageType = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
messageType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
if (!(messageType & 128)) {
return;
}
const vibrationType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
if (vibrationType !== 0) {
return;
}
const data = {};
let key;
for (key in VIBRATION_DATA_MAP) {
if (VIBRATION_DATA_MAP[key] === 16) {
data[key] = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
data[key] = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
}
VibrationManager.#playDeviceVibration(data);
}
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;
const $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 getVideoPlayerFilterStyle = function() {
const filters = [];
const clarity = getPref(PrefKey.VIDEO_CLARITY);
if (clarity != 0) {
const level = (7 - (clarity - 1) * 0.5).toFixed(1);
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
document.getElementById("bx-filter-clarity-matrix").setAttributeNS(null, "kernelMatrix", matrix);
filters.push(`url(#bx-filter-clarity)`);
}
const saturation = getPref(PrefKey.VIDEO_SATURATION);
if (saturation != 100) {
filters.push(`saturate(${saturation}%)`);
}
const contrast = getPref(PrefKey.VIDEO_CONTRAST);
if (contrast != 100) {
filters.push(`contrast(${contrast}%)`);
}
const brightness = getPref(PrefKey.VIDEO_BRIGHTNESS);
if (brightness != 100) {
filters.push(`brightness(${brightness}%)`);
}
return filters.join(" ");
};
var setupQuickSettingsBar = function() {
const isSafari = UserAgent.isSafari();
const SETTINGS_UI = [
getPref(PrefKey.MKB_ENABLED) && {
icon: BxIcon.MOUSE,
group: "mkb",
items: [
{
group: "mkb",
label: t("mouse-and-keyboard"),
help_url: "https://better-xcloud.github.io/mouse-and-keyboard/",
content: MkbRemapper.INSTANCE.render()
}
]
},
{
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,
label: t("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: true
});
});
}
}
]
},
{
group: "video",
label: t("video"),
help_url: "https://better-xcloud.github.io/ingame-features/#video",
items: [
{
pref: PrefKey.VIDEO_RATIO,
label: t("ratio"),
onChange: updateVideoPlayerCss
},
{
pref: PrefKey.VIDEO_CLARITY,
label: t("clarity"),
onChange: updateVideoPlayerCss,
unsupported: isSafari
},
{
pref: PrefKey.VIDEO_SATURATION,
label: t("saturation"),
onChange: updateVideoPlayerCss
},
{
pref: PrefKey.VIDEO_CONTRAST,
label: t("contrast"),
onChange: updateVideoPlayerCss
},
{
pref: PrefKey.VIDEO_BRIGHTNESS,
label: t("brightness"),
onChange: updateVideoPlayerCss
}
]
}
]
},
{
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,
label: t("controller-vibration"),
unsupported: !VibrationManager.supportControllerVibration(),
onChange: VibrationManager.updateGlobalVars
},
{
pref: PrefKey.CONTROLLER_DEVICE_VIBRATION,
label: t("device-vibration"),
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars
},
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) && {
pref: PrefKey.CONTROLLER_VIBRATION_INTENSITY,
label: t("vibration-intensity"),
unsupported: !VibrationManager.supportDeviceVibration(),
onChange: VibrationManager.updateGlobalVars
}
]
},
STATES.hasTouchSupport && {
group: "touch-controller",
label: t("touch-controller"),
items: [
{
label: t("layout"),
content: CE("select", { disabled: true }, 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);
}
$elm.disabled = !data;
if (!data) {
$elm.appendChild(CE("option", { value: "" }, t("default")));
$elm.value = "";
$elm.dispatchEvent(new Event("change"));
return;
}
const $fragment = document.createDocumentFragment();
for (const 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"));
});
}
}
]
}
]
},
{
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,
label: t("show-stats-on-startup")
},
{
pref: PrefKey.STATS_QUICK_GLANCE,
label: "👀 " + t("enable-quick-glance-mode"),
onChange: (e) => {
e.target.checked ? StreamStats.quickGlanceSetup() : StreamStats.quickGlanceStop();
}
},
{
pref: PrefKey.STATS_ITEMS,
label: t("stats"),
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_POSITION,
label: t("position"),
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_TEXT_SIZE,
label: t("text-size"),
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_OPACITY,
label: t("opacity"),
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_TRANSPARENT,
label: t("transparent-background"),
onChange: StreamStats.refreshStyles
},
{
pref: PrefKey.STATS_CONDITIONAL_FORMATTING,
label: t("conditional-formatting"),
onChange: StreamStats.refreshStyles
}
]
}
]
}
];
let $tabs;
let $settings;
const $wrapper = CE("div", { class: "bx-quick-settings-bar bx-gone" }, $tabs = CE("div", { class: "bx-quick-settings-tabs" }), $settings = CE("div", { class: "bx-quick-settings-tab-contents" }));
for (const settingTab of SETTINGS_UI) {
if (!settingTab) {
continue;
}
const $svg = createSvgIcon(settingTab.icon);
$svg.addEventListener("click", (e) => {
for (const $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 (const $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 (const settingGroup of settingTab.items) {
if (!settingGroup) {
continue;
}
$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")
})));
if (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 (const 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 $content = CE("div", { class: "bx-quick-settings-row", "data-type": settingGroup.group }, CE("label", { for: `bx_setting_${pref}` }, setting.label, setting.unsupported && CE("div", { class: "bx-quick-settings-bar-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);
};
function updateVideoPlayerCss() {
let $elm = document.getElementById("bx-video-css");
if (!$elm) {
const $fragment = document.createDocumentFragment();
$elm = CE("style", { id: "bx-video-css" });
$fragment.appendChild($elm);
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-clarity", xmlns: "http://www.w3.org/2000/svg" }, CE("feConvolveMatrix", { id: "bx-filter-clarity-matrix", order: "3", xmlns: "http://www.w3.org/2000/svg" }))));
$fragment.appendChild($svg);
document.documentElement.appendChild($fragment);
}
let filters = getVideoPlayerFilterStyle();
let 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}
}
`;
}
$elm.textContent = css;
resizeVideoPlayer();
}
var resizeVideoPlayer = function() {
const $video = STATES.currentStream.$video;
if (!$video || !$video.parentElement) {
return;
}
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
if (PREF_RATIO.includes(":")) {
const tmp = PREF_RATIO.split(":");
const videoRatio = parseFloat(tmp[0]) / parseFloat(tmp[1]);
let width = 0;
let height = 0;
const parentRect = $video.parentElement.getBoundingClientRect();
const parentRatio = parentRect.width / parentRect.height;
if (parentRatio > videoRatio) {
height = parentRect.height;
width = height * videoRatio;
} else {
width = parentRect.width;
height = width / videoRatio;
}
width = Math.floor(width);
height = Math.floor(height);
$video.style.width = `${width}px`;
$video.style.height = `${height}px`;
$video.style.objectFit = "fill";
} else {
$video.style.width = "100%";
$video.style.height = "100%";
$video.style.objectFit = PREF_RATIO;
}
};
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-quick-settings-bar")) {
preloadFonts();
window.addEventListener("resize", updateVideoPlayerCss);
setupQuickSettingsBar();
StreamStats.render();
Screenshot.setup();
}
updateVideoPlayerCss();
}
// src/modules/remote-play.ts
var LOG_TAG3 = "RemotePlay";
var 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_TAG3, "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", {});
const resolutions = [1080, 720];
const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION);
const $resolutionGroup = CE("div", {});
for (const resolution of resolutions) {
const value = `${resolution}p`;
const id2 = `bx_radio_xhome_resolution_${resolution}`;
const $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`);
$resolutionGroup.appendChild($label);
if (currentResolution === value) {
$radio.checked = true;
$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 (const 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 (const region2 of RemotePlay.#REGIONS) {
try {
const request = new Request(`${region2.baseUri}/v6/servers/home?mr=50`, options);
const resp = await fetch(request);
const json = await resp.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;
}
RemotePlay.#initialize();
if (AppInterface && AppInterface.showRemotePlayDialog) {
AppInterface.showRemotePlayDialog(JSON.stringify(RemotePlay.#CONSOLES));
document.activeElement.blur();
return;
}
if (document.querySelector(".bx-remote-play-popup")) {
if (force === false) {
RemotePlay.#$content.classList.add("bx-gone");
} else {
RemotePlay.#$content.classList.toggle("bx-gone");
}
return;
}
const $header = document.querySelector("#gamepass-root header");
const 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;
}
STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play");
if (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/utils/gamepass-gallery.ts
var GamePassCloudGallery;
(function(GamePassCloudGallery2) {
GamePassCloudGallery2["TOUCH"] = "9c86f07a-f3e8-45ad-82a0-a1f759597059";
GamePassCloudGallery2["ALL"] = "29a81209-df6f-41fd-a528-2ae6b91f719c";
})(GamePassCloudGallery || (GamePassCloudGallery = {}));
// src/utils/network.ts
var clearApplicationInsightsBuffers = function() {
window.sessionStorage.removeItem("AI_buffer");
window.sessionStorage.removeItem("AI_sentBuffer");
};
var clearDbLogs = function(dbName, table) {
const request = window.indexedDB.open(dbName);
request.onsuccess = (e) => {
const db = e.target.result;
try {
const objectStore = db.transaction(table, "readwrite").objectStore(table);
const objectStoreRequest = objectStore.clear();
objectStoreRequest.onsuccess = function() {
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
};
} catch (ex) {
}
};
};
var clearAllLogs = function() {
clearApplicationInsightsBuffers();
clearDbLogs("StreamClientLogHandler", "logs");
clearDbLogs("XCloudAppLogs", "logs");
};
var updateIceCandidates = function(candidates, options) {
const pattern = new RegExp(/a=candidate:(?\d+) (?\d+) UDP (?\d+) (?[^\s]+) (?\d+) (?.*)/);
const 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;
const 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"
};
};
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;
});
if (options.consoleAddrs) {
for (const ip in options.consoleAddrs) {
const port = options.consoleAddrs[ip];
newCandidates.push(newCandidate(`a=candidate:${newCandidates.length + 1} 1 UDP 1 ${ip} ${port} typ host`));
}
}
newCandidates.push(newCandidate("a=end-of-candidates"));
console.log(newCandidates);
return newCandidates;
};
async function patchIceCandidates(request, consoleAddrs) {
const response = await NATIVE_FETCH(request);
const text = await response.clone().text();
if (!text.length) {
return response;
}
const options = {
preferIpv6Server: getPref(PrefKey.PREFER_IPV6_SERVER),
consoleAddrs
};
const obj = JSON.parse(text);
let exchangeResponse = JSON.parse(obj.exchangeResponse);
exchangeResponse = updateIceCandidates(exchangeResponse, options);
obj.exchangeResponse = JSON.stringify(exchangeResponse);
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return 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;
const nativeXhrOpen = xhrPrototype.open;
const nativeXhrSend = xhrPrototype.send;
xhrPrototype.open = function(method, url) {
this._url = url;
return nativeXhrOpen.apply(this, arguments);
};
xhrPrototype.send = function(...arg) {
for (const blocked of BLOCKED_URLS) {
if (this._url.startsWith(blocked)) {
if (blocked === "https://dc.services.visualstudio.com") {
window.setTimeout(clearAllLogs, 1000);
}
return false;
}
}
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);
const json = await response.json();
const overrideTreatments = {};
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
overrideTreatments["EnableHomeContextMenu"] = false;
}
for (const key in overrideTreatments) {
json.exp.treatments[key] = overrideTreatments[key];
}
response.json = () => Promise.resolve(json);
return response;
} catch (e) {
console.log(e);
}
}
if (STATES.hasTouchSupport && url.includes("catalog.gamepass.com/sigls/")) {
const response = await NATIVE_FETCH(request, init);
const 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);
}
}
response.json = () => Promise.resolve(obj);
return 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 clone = request.clone();
const obj = await 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);
const 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;
}
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return 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();
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
STATES.currentStream.xboxTitleId = xboxTitleId;
const inputConfigs = obj[0];
let hasTouchSupport2 = inputConfigs.supportedTabs.length > 0;
if (!hasTouchSupport2) {
const supportedInputTypes = inputConfigs.supportedInputTypes;
hasTouchSupport2 = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
}
if (hasTouchSupport2) {
TouchController.disable();
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
data: null
});
} else {
TouchController.enable();
TouchController.getCustomLayouts(xboxTitleId);
}
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return response;
}
static async#handleTitles(request) {
const clone = request.clone();
const headers = {};
for (const pair of clone.headers.entries()) {
headers[pair[0]] = pair[1];
}
headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`;
const index = request.url.indexOf(".xboxlive.com");
request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {
method: clone.method,
body: await clone.text(),
headers
});
return NATIVE_FETCH(request);
}
static async#handlePlay(request) {
const clone = request.clone();
const body = await clone.json();
const newRequest = new Request(request, {
body: JSON.stringify(body)
});
return NATIVE_FETCH(newRequest);
}
static async handle(request) {
TouchController.disable();
const clone = request.clone();
const headers = {};
for (const 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);
const 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: "🇺🇸"
};
const serverRegex = /\/\/(\w+)\./;
for (let region3 of obj.offeringSettings.regions) {
const regionName = region3.name;
let shortName = region3.name;
let match = serverRegex.exec(region3.baseUri);
if (match) {
shortName = match[1];
if (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 = true;
obj.offeringSettings.regions = [tmp];
}
response.json = () => Promise.resolve(obj);
return response;
}
static async#handlePlay(request, init) {
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
const url = typeof request === "string" ? request : request.url;
const parsedUrl = new URL(url);
StreamBadges.region = parsedUrl.host.split(".", 1)[0];
for (let regionName in STATES.serverRegions) {
const region3 = STATES.serverRegions[regionName];
if (parsedUrl.origin == region3.baseUri) {
StreamBadges.region = regionName;
break;
}
}
const clone = request.clone();
const body = await 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") {
const titleInfo = STATES.currentStream.titleInfo;
if (titleInfo?.details.hasTouchSupport) {
TouchController.disable();
} else {
TouchController.enable();
}
}
const response = await NATIVE_FETCH(request, init);
const 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 = true;
if (getPref(PrefKey.NATIVE_MKB_DISABLED) || UserAgent.isMobile()) {
overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
enableMouseInput: false,
enableAbsoluteMouse: false,
enableKeyboardInput: false
});
}
overrides.videoConfiguration = overrides.videoConfiguration || {};
overrides.videoConfiguration.setCodecPreferences = true;
if (TouchController.isEnabled()) {
overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10;
}
if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING)) {
overrides.audioConfiguration = overrides.audioConfiguration || {};
overrides.audioConfiguration.enableMicrophone = true;
}
obj.clientStreamingConfigOverrides = JSON.stringify(overrides);
response.json = () => Promise.resolve(obj);
response.text = () => Promise.resolve(JSON.stringify(obj));
return 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 === MkbHandler.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) {
const supportVibration = !!gamepad.vibrationActuator;
status = (supportVibration ? "✅" : "❌") + " " + t("vibration-status");
} else {
status = t("disconnected");
}
Toast.show(text, status, { instant: false });
}
// 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-no-margin {
margin: 0 !important;
}
.bx-no-padding {
padding: 0 !important;
}
#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 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;
height: calc(var(--bx-button-height) * 1.5);
}
.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;
}
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;
}
@-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-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 {
position: absolute;
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;
display: inline-block;
padding: 2px 8px;
border-radius: 4px 0 0 4px;
text-transform: uppercase;
}
.bx-badge-value {
background-color: #808080;
display: inline-block;
padding: 2px 8px;
border-radius: 0 4px 4px 0;
}
.bx-badge-battery[data-charging=true] span:first-of-type::after {
content: ' ⚡️';
}
.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-quick-settings-bar {
display: flex;
position: fixed;
z-index: var(--bx-stream-settings-z-index);
opacity: 0.98;
user-select: none;
-webkit-user-select: none;
}
.bx-quick-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-quick-settings-tabs svg {
width: 32px;
height: 32px;
padding: 10px;
box-sizing: content-box;
background: #131313;
cursor: pointer;
border-left: 4px solid #1e1e1e;
}
.bx-quick-settings-tabs svg.bx-active {
background: #222;
border-color: #008746;
}
.bx-quick-settings-tabs svg:not(.bx-active):hover {
background: #2f2f2f;
border-color: #484848;
}
.bx-quick-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-quick-settings-tab-contents > div[data-group=mkb] {
display: flex;
flex-direction: column;
height: 100%;
overflow: hidden;
}
.bx-quick-settings-tab-contents *:focus {
outline: none !important;
}
.bx-quick-settings-tab-contents h2 {
margin-bottom: 8px;
display: flex;
align-item: center;
}
.bx-quick-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-quick-settings-row {
display: flex;
border-bottom: 1px solid rgba(64,64,64,0.502);
margin-bottom: 16px;
padding-bottom: 16px;
}
.bx-quick-settings-row label {
font-size: 16px;
display: block;
text-align: left;
flex: 1;
align-self: center;
margin-bottom: 0 !important;
}
.bx-quick-settings-row input {
accent-color: var(--bx-primary-button-color);
}
.bx-quick-settings-row select:disabled {
-webkit-appearance: none;
background: transparent;
text-align-last: right;
border: none;
}
.bx-quick-settings-bar-note {
display: block;
text-align: center;
font-size: 12px;
font-weight: lighter;
font-style: italic;
padding-top: 16px;
}
.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-profile {
width: 100%;
height: 36px;
display: block;
margin-bottom: 10px;
}
.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row {
display: flex;
margin-bottom: 10px;
}
.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row label.bx-prompt {
flex: 1;
font-family: var(--bx-promptfont-font);
font-size: 26px;
margin-bottom: 0;
}
.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions {
flex: 2;
position: relative;
}
.bx-quick-settings-tab-contents div[data-group="shortcuts"] .bx-shortcut-row .bx-shortcut-actions select {
position: absolute;
width: 100%;
height: 100%;
display: block;
}
.bx-quick-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 {
display: flex;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
position: fixed;
left: 50%;
top: 50%;
transform: translateX(-50%) translateY(-50%);
margin: auto;
background: rgba(0,0,0,0.898);
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;
}
.bx-mkb-pointer-lock-msg:hover {
background: #151515;
}
.bx-mkb-pointer-lock-msg button {
margin-right: 12px;
height: 60px;
}
.bx-mkb-pointer-lock-msg svg {
width: 32px;
}
.bx-mkb-pointer-lock-msg div {
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: 8px;
}
.bx-mkb-pointer-lock-msg p:last-child {
font-size: 14px;
font-style: italic;
}
.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 += `
div[class^=HomePage-module__bottomSpacing]:has(button[class*=SocialEmptyCard]),
button[class*=SocialEmptyCard] {
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;
}
`;
}
css += `
div[class*=StreamMenu-module__menu] {
min-width: 100vw !important;
}
`;
if (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 = true;
static show() {
document.body && (document.body.style.cursor = "unset");
MouseCursorHider.#cursorVisible = true;
}
static hide() {
document.body && (document.body.style.cursor = "none");
MouseCursorHider.#timeout = null;
MouseCursorHider.#cursorVisible = false;
}
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].value === 1) {\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 if (btnHome.pressed) {\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\n // Listen to next button press\n const intervalMs = 50;\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n\n // Hijack this button\n return;\n } else if (this.bxHomeStates[currentGamepad.index]) {\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 const intervalMs = isLongPress ? 500 : 100;\n\n this.inputSink.onGamepadInput(performance.now() - intervalMs, fakeGamepadMappings);\n this.inputConfiguration.useIntervalWorkerThreadForInput && this.intervalWorker ? this.intervalWorker.scheduleTimer(intervalMs) : this.pollGamepadssetTimeoutTimerID = setTimeout(this.pollGamepads, intervalMs);\n return;\n }\n }\n}\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 && 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";
var LOG_TAG4 = "Patcher";
var PATCHES = {
disableAiTrack(str2) {
const text = ".track=function(";
const index = str2.indexOf(text);
if (index === -1) {
return false;
}
if (str2.substring(0, index + 200).includes('"AppInsightsCore')) {
return false;
}
return str2.substring(0, index) + ".track=function(e){},!!function(" + str2.substring(index + text.length);
},
disableTelemetry(str2) {
const text = ".disableTelemetry=function(){return!1}";
if (!str2.includes(text)) {
return false;
}
return str2.replace(text, ".disableTelemetry=function(){return!0}");
},
disableTelemetryProvider(str2) {
const text = "this.enableLightweightTelemetry=!";
if (!str2.includes(text)) {
return false;
}
const newCode = [
"this.trackEvent",
"this.trackPageView",
"this.trackHttpCompleted",
"this.trackHttpFailed",
"this.trackError",
"this.trackErrorLike",
"this.onTrackEvent",
"()=>{}"
].join("=");
return str2.replace(text, newCode + ";" + text);
},
disableIndexDbLogging(str2) {
const text = ",this.logsDb=new";
if (!str2.includes(text)) {
return false;
}
let newCode = ",this.log=()=>{}";
return str2.replace(text, newCode + text);
},
websiteLayout(str2) {
const text = '?"tv":"default"';
if (!str2.includes(text)) {
return false;
}
const layout = getPref(PrefKey.UI_LAYOUT) === "tv" ? "tv" : "default";
return str2.replace(text, `?"${layout}":"${layout}"`);
},
remotePlayDirectConnectUrl(str2) {
const index = str2.indexOf("/direct-connect");
if (index === -1) {
return false;
}
return str2.replace(str2.substring(index - 9, index + 15), "https://www.xbox.com/play");
},
remotePlayKeepAlive(str2) {
const text = "onServerDisconnectMessage(e){";
if (!str2.includes(text)) {
return false;
}
str2 = str2.replace(text, text + remote_play_keep_alive_default);
return str2;
},
remotePlayConnectMode(str2) {
const text = 'connectMode:"cloud-connect",';
if (!str2.includes(text)) {
return false;
}
return str2.replace(text, remote_play_enable_default);
},
remotePlayDisableAchievementToast(str2) {
const text = ".AchievementUnlock:{";
if (!str2.includes(text)) {
return false;
}
const newCode = `
if (!!window.BX_REMOTE_PLAY_CONFIG) {
return;
}
`;
return str2.replace(text, text + newCode);
},
disableTrackEvent(str2) {
const text = "this.trackEvent=";
if (!str2.includes(text)) {
return false;
}
return str2.replace(text, "this.trackEvent=e=>{},this.uwuwu=");
},
blockWebRtcStatsCollector(str2) {
const text = "this.shouldCollectStats=!0";
if (!str2.includes(text)) {
return false;
}
return str2.replace(text, "this.shouldCollectStats=!1");
},
patchPollGamepads(str2) {
const index = str2.indexOf("},this.pollGamepads=()=>{");
if (index === -1) {
return false;
}
const nextIndex = str2.indexOf("setTimeout(this.pollGamepads", index);
if (nextIndex === -1) {
return false;
}
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];
const 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) {
const text = "this.telemetryProvider=e}log(e,t,i){";
if (!str2.includes(text)) {
return false;
}
str2 = str2.replaceAll(text, text + "console.log(Array.from(arguments));");
return str2;
},
enableConsoleLogging(str2) {
const text = "static isConsoleLoggingAllowed(){";
if (!str2.includes(text)) {
return false;
}
str2 = str2.replaceAll(text, text + "return true;");
return str2;
},
playVibration(str2) {
const text = "}playVibration(e){";
if (!str2.includes(text)) {
return false;
}
VibrationManager.updateGlobalVars();
str2 = str2.replaceAll(text, text + vibration_adjust_default);
return str2;
},
overrideSettings(str2) {
const index = str2.indexOf(",EnableStreamGate:");
if (index === -1) {
return false;
}
const endIndex = str2.indexOf("},", index);
const newSettings = [
"PwaPrompt: false"
];
const newCode = newSettings.join(",");
str2 = str2.substring(0, endIndex) + "," + newCode + str2.substring(endIndex);
return str2;
},
disableGamepadDisconnectedScreen(str2) {
const index = str2.indexOf('"GamepadDisconnected_Title",');
if (index === -1) {
return false;
}
const constIndex = str2.indexOf("const", index - 30);
str2 = str2.substring(0, constIndex) + "e.onClose();return null;" + str2.substring(constIndex);
return str2;
},
patchUpdateInputConfigurationAsync(str2) {
const text = "async updateInputConfigurationAsync(e){";
if (!str2.includes(text)) {
return false;
}
const newCode = "e.enableTouchInput = true;";
str2 = str2.replace(text, text + newCode);
return str2;
},
loadingEndingChunks(str2) {
const text = '"FamilySagaManager"';
if (!str2.includes(text)) {
return false;
}
BxLogger.info(LOG_TAG4, "Remaining patches:", PATCH_ORDERS);
PATCH_ORDERS = PATCH_ORDERS.concat(PLAYING_PATCH_ORDERS);
return str2;
},
disableStreamGate(str2) {
const index = str2.indexOf('case"partially-ready":');
if (index === -1) {
return false;
}
const bracketIndex = str2.indexOf("=>{", index - 150) + 3;
str2 = str2.substring(0, bracketIndex) + "return 0;" + str2.substring(bracketIndex);
return str2;
},
exposeTouchLayoutManager(str2) {
const text = "this._perScopeLayoutsStream=new";
if (!str2.includes(text)) {
return false;
}
const newCode = `
true;
window.BX_EXPOSED["touchLayoutManager"] = this;
window.dispatchEvent(new Event("${BxEvent.TOUCH_LAYOUT_MANAGER_READY}"));
`;
str2 = str2.replace(text, newCode + text);
return str2;
},
supportLocalCoOp(str2) {
const text = "this.gamepadMappingsToSend=[],";
if (!str2.includes(text)) {
return false;
}
const newCode = `true; ${local_co_op_enable_default}; true,`;
str2 = str2.replace(text, text + newCode);
return str2;
},
forceFortniteConsole(str2) {
const text = "sendTouchInputEnabledMessage(e){";
if (!str2.includes(text)) {
return false;
}
const newCode = `window.location.pathname.includes('/launch/fortnite/') && (e = false);`;
str2 = str2.replace(text, text + newCode);
return str2;
},
disableTakRenderer(str2) {
const text = "const{TakRenderer:";
if (!str2.includes(text)) {
return false;
}
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;
}
}
`;
str2 = str2.replace(text, newCode + text);
return str2;
},
streamCombineSources(str2) {
const text = "this.useCombinedAudioVideoStream=!!this.deviceInformation.isTizen";
if (!str2.includes(text)) {
return false;
}
str2 = str2.replace(text, "this.useCombinedAudioVideoStream=true");
return str2;
},
patchStreamHud(str2) {
const text = "let{onCollapse";
if (!str2.includes(text)) {
return false;
}
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;";
}
str2 = str2.replace(text, newCode + text);
return str2;
},
broadcastPollingMode(str2) {
const text = ".setPollingMode=e=>{";
if (!str2.includes(text)) {
return false;
}
const newCode = `
window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged(e);
`;
str2 = str2.replace(text, text + newCode);
return str2;
},
patchXcloudTitleInfo(str2) {
const text = "async cloudConnect";
let index = str2.indexOf(text);
if (index === -1) {
return false;
}
let backetIndex = str2.indexOf("{", index);
const params = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
const titleInfoVar = params.split(",")[0];
const newCode = `
${titleInfoVar} = window.BX_EXPOSED.modifyTitleInfo(${titleInfoVar});
BxLogger.info('patchXcloudTitleInfo', ${titleInfoVar});
`;
str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1);
return str2;
},
patchRemotePlayMkb(str2) {
const text = "async homeConsoleConnect";
let index = str2.indexOf(text);
if (index === -1) {
return false;
}
let backetIndex = str2.indexOf("{", index);
const params = str2.substring(index, backetIndex).match(/\(([^)]+)\)/)[1];
const configsVar = params.split(",")[1];
const newCode = `
Object.assign(${configsVar}.inputConfiguration, {
enableMouseInput: false,
enableKeyboardInput: false,
enableAbsoluteMouse: false,
});
BxLogger.info('patchRemotePlayMkb', ${configsVar});
`;
str2 = str2.substring(0, backetIndex + 1) + newCode + str2.substring(backetIndex + 1);
return str2;
},
patchAudioMediaStream(str2) {
const text = ".srcObject=this.audioMediaStream,";
if (!str2.includes(text)) {
return false;
}
const newCode = `window.BX_EXPOSED.setupGainNode(arguments[1], this.audioMediaStream),`;
str2 = str2.replace(text, text + newCode);
return str2;
},
patchCombinedAudioVideoMediaStream(str2) {
const text = ".srcObject=this.combinedAudioVideoStream";
if (!str2.includes(text)) {
return false;
}
const newCode = `,window.BX_EXPOSED.setupGainNode(arguments[0], this.combinedAudioVideoStream)`;
str2 = str2.replace(text, text + newCode);
return str2;
},
patchTouchControlDefaultOpacity(str2) {
const text = "opacityMultiplier:1";
if (!str2.includes(text)) {
return false;
}
const opacity = (getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) / 100).toFixed(1);
const newCode = `opacityMultiplier: ${opacity}`;
str2 = str2.replace(text, newCode);
return str2;
},
patchShowSensorControls(str2) {
const text = "{shouldShowSensorControls:";
if (!str2.includes(text)) {
return false;
}
const newCode = `{shouldShowSensorControls: (window.BX_EXPOSED && window.BX_EXPOSED.shouldShowSensorControls) ||`;
str2 = str2.replace(text, newCode);
return str2;
},
exposeStreamSession(str2) {
const text = ",this._connectionType=";
if (!str2.includes(text)) {
return false;
}
const newCode = `;
window.BX_EXPOSED.streamSession = this;
const orgSetMicrophoneState = this.setMicrophoneState.bind(this);
this.setMicrophoneState = state => {
orgSetMicrophoneState(state);
const evt = new Event('${BxEvent.MICROPHONE_STATE_CHANGED}');
evt.microphoneState = state;
window.dispatchEvent(evt);
};
window.dispatchEvent(new Event('${BxEvent.STREAM_SESSION_READY}'))
true` + text;
str2 = str2.replace(text, newCode);
return str2;
}
};
var PATCH_ORDERS = [
"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.hasTouchSupport && "patchUpdateInputConfigurationAsync"
] : [],
...BX_FLAGS.EnableXcloudLogging ? [
"enableConsoleLogging",
"enableXcloudLogger"
] : []
].filter((item2) => !!item2);
var 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",
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "patchShowSensorControls",
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all" && "exposeTouchLayoutManager",
STATES.hasTouchSupport && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "off" || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && "disableTakRenderer",
STATES.hasTouchSupport && getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && "patchTouchControlDefaultOpacity",
BX_FLAGS.EnableXcloudLogging && "enableConsoleLogging",
"patchPollGamepads",
getPref(PrefKey.STREAM_COMBINE_SOURCES) && "streamCombineSources",
...getPref(PrefKey.REMOTE_PLAY_ENABLED) ? [
"patchRemotePlayMkb",
"remotePlayConnectMode"
] : []
].filter((item2) => !!item2);
var ALL_PATCHES = [...PATCH_ORDERS, ...PLAYING_PATCH_ORDERS];
class Patcher {
static #patchFunctionBind() {
const nativeBind = Function.prototype.bind;
Function.prototype.bind = function() {
let valid = false;
if (this.name.length <= 2 && arguments.length === 2 && arguments[0] === null) {
if (arguments[1] === 0 || typeof arguments[1] === "function") {
valid = true;
}
}
if (!valid) {
return nativeBind.apply(this, arguments);
}
PatcherCache.init();
if (typeof arguments[1] === "function") {
BxLogger.info(LOG_TAG4, "Restored Function.prototype.bind()");
Function.prototype.bind = nativeBind;
}
const orgFunc = this;
const newFunc = (a, item2) => {
Patcher.patch(item2);
orgFunc(a, item2);
};
return nativeBind.apply(newFunc, arguments);
};
}
static patch(item) {
let patchesToCheck;
let 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();
let modified = false;
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 = true;
str = patchedStr;
BxLogger.info(LOG_TAG4, `✅ ${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 = false;
static #getSignature() {
const scriptVersion = SCRIPT_VERSION;
const webVersion = document.querySelector("meta[name=gamepass-app-version]")?.content;
const patches = JSON.stringify(ALL_PATCHES);
const sig = hashCode(scriptVersion + webVersion + patches);
return sig;
}
static clear() {
window.localStorage.removeItem(PatcherCache.#KEY_CACHE);
PatcherCache.#CACHE = {};
}
static checkSignature() {
const storedSig = window.localStorage.getItem(PatcherCache.#KEY_SIGNATURE) || 0;
const currentSig = PatcherCache.#getSignature();
if (currentSig !== parseInt(storedSig)) {
BxLogger.warning(LOG_TAG4, "Signature changed");
window.localStorage.setItem(PatcherCache.#KEY_SIGNATURE, currentSig.toString());
PatcherCache.clear();
} else {
BxLogger.info(LOG_TAG4, "Signature unchanged");
}
}
static #cleanupPatches(patches) {
return patches.filter((item2) => {
for (const id2 in PatcherCache.#CACHE) {
const cached = PatcherCache.#CACHE[id2];
if (cached.includes(item2)) {
return false;
}
}
return true;
});
}
static getPatches(id2) {
return PatcherCache.#CACHE[id2];
}
static saveToCache(subCache) {
for (const id2 in subCache) {
const patchNames = subCache[id2];
let data = PatcherCache.#CACHE[id2];
if (!data) {
PatcherCache.#CACHE[id2] = patchNames;
} else {
for (const 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;
}
PatcherCache.#isInitialized = true;
PatcherCache.checkSignature();
PatcherCache.#CACHE = JSON.parse(window.localStorage.getItem(PatcherCache.#KEY_CACHE) || "{}");
BxLogger.info(LOG_TAG4, PatcherCache.#CACHE);
if (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_TAG4, PATCH_ORDERS.slice(0));
BxLogger.info(LOG_TAG4, 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();
const 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: SCRIPT_HOME,
target: "_blank"
}, "Better xCloud " + SCRIPT_VERSION), createButton({
icon: BxIcon.QUESTION,
style: ButtonStyle.FOCUSABLE,
label: t("help"),
url: "https://better-xcloud.github.io/features/"
})));
$updateAvailable = CE("a", {
class: "bx-settings-update bx-gone",
href: "https://github.com/redphx/better-xcloud/releases",
target: "_blank"
});
$wrapper.appendChild($updateAvailable);
if (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 {
const userAgent2 = UserAgent.getDefault().toLowerCase();
if (userAgent2.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");
$btnHeaderSettings && $btnHeaderSettings.classList.add("bx-danger");
if (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);
}
$wrapper.appendChild($group);
if (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;
let settingNote = setting.note || "";
if (setting.experimental) {
settingLabel = "🧪 " + settingLabel;
if (!settingNote) {
settingNote = t("experimental");
} else {
settingNote = `${t("experimental")}: ${settingNote}`;
}
}
let $control;
let $inpCustomUserAgent;
let 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;
const 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;
let 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;
let label = `${region4.shortName} - ${regionName}`;
if (region4.isDefault) {
label += ` (${t("default")})`;
value = "default";
if (selectedValue === regionName) {
selectedValue = "default";
}
}
setting.options[value] = label;
}
for (let value in setting.options) {
const label = setting.options[value];
const $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 = true;
}
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);
$wrapper.appendChild($elm);
if (settingId === PrefKey.USER_AGENT_PROFILE) {
$wrapper.appendChild($inpCustomUserAgent);
$control.disabled = true;
$control.dispatchEvent(new Event("change"));
$control.disabled = false;
}
}
}
$btnReload = createButton({
label: t("settings-reload"),
classes: ["bx-settings-reload-button"],
style: ButtonStyle.FOCUSABLE | ButtonStyle.FULL_WIDTH,
onClick: (e) => {
window.location.reload();
$btnReload.disabled = true;
$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;
const 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.AUDIO_MIC_ON_PLAYING,
PrefKey.STREAM_DISABLE_FEEDBACK_DIALOG,
PrefKey.SCREENSHOT_APPLY_FILTERS,
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_DISABLED,
PrefKey.MKB_ENABLED,
PrefKey.MKB_HIDE_IDLE_CURSOR
]
},
[t("touch-controller")]: {
note: !STATES.hasTouchSupport ? "⚠️ " + t("device-unsupported-touch") : null,
unsupported: !STATES.hasTouchSupport,
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(true);
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
const $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();
const $settings = document.querySelector(".bx-settings-container");
$settings.classList.toggle("bx-gone");
window.scrollTo(0, 0);
document.activeElement && document.activeElement.blur();
}
});
if (PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
$settingsBtn.setAttribute("data-update-available", "true");
}
$headerFragment.appendChild($settingsBtn);
$parent.appendChild($headerFragment);
};
function checkHeader() {
const $button = document.querySelector(".bx-header-settings-button");
if (!$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;
const observer = new MutationObserver((mutationList) => {
timeout && clearTimeout(timeout);
timeout = window.setTimeout(checkHeader, 2000);
});
observer.observe($header, { subtree: true, childList: true });
checkHeader();
}
// src/utils/history.ts
function patchHistoryMethod(type) {
const orig = window.history[type];
return function(...args) {
BxEvent.dispatch(window, BxEvent.POPSTATE, {
arguments: args
});
return 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: true,
get: () => {
return _state;
},
set: (state) => {
try {
state.appContext.requestInfo.userAgent = window.navigator.userAgent;
} catch (e) {
BxLogger.error(LOG_TAG5, e);
}
if (STATES.hasTouchSupport) {
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);
}
} catch (e) {
BxLogger.error(LOG_TAG5, e);
}
}
if (getPref(PrefKey.UI_HOME_CONTEXT_MENU_DISABLED)) {
try {
state.experiments.experimentationInfo.data.treatments.EnableHomeContextMenu = false;
} catch (e) {
BxLogger.error(LOG_TAG5, e);
}
}
_state = state;
STATES.appContext = structuredClone(state.appContext);
}
});
}
var LOG_TAG5 = "PreloadState";
// src/utils/sdp.ts
function patchSdpBitrate(sdp, video, audio) {
const lines = sdp.split("\n");
const 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 = "";
let line = lines[lineNumber];
if (!line.startsWith("m=")) {
continue;
}
for (const 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) {
line = lines[lineNumber];
if (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/utils/monkey-patches.ts
function patchVideoApi() {
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
const showFunc = function() {
this.style.visibility = "visible";
this.removeEventListener("playing", showFunc);
if (!this.videoWidth) {
return;
}
BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, {
$video: this
});
};
const nativePlay = HTMLMediaElement.prototype.play;
HTMLMediaElement.prototype.play = function() {
if (this.className && this.className.startsWith("XboxSplashVideo")) {
if (PREF_SKIP_SPLASH_VIDEO) {
this.volume = 0;
this.style.display = "none";
this.dispatchEvent(new Event("ended"));
return new Promise(() => {
});
}
return nativePlay.apply(this);
}
if (!!this.src) {
return nativePlay.apply(this);
}
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 false;
}
const profilePrefix = codecProfile === "high" ? "4d" : codecProfile === "low" ? "420" : "42e";
const profileLevelId = `profile-level-id=${profilePrefix}`;
const 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);
BxEvent.dispatch(window, BxEvent.DATA_CHANNEL_CREATED, {
dataChannel
});
return dataChannel;
};
const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
RTCPeerConnection.prototype.setLocalDescription = function(description) {
try {
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
if (maxVideoBitrate > 0) {
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, maxVideoBitrate * 1000);
}
} catch (e) {
BxLogger.error("setLocalDescription", e);
}
return nativeSetLocalDescription.apply(this, arguments);
};
const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() {
const conn = new OrgRTCPeerConnection;
STATES.currentStream.peerConnection = conn;
conn.addEventListener("connectionstatechange", (e) => {
BxLogger.info("connectionstatechange", conn.connectionState);
});
return conn;
};
}
function patchAudioContext() {
const OrgAudioContext = window.AudioContext;
const nativeCreateGain = OrgAudioContext.prototype.createGain;
window.AudioContext = function(options) {
const ctx = new OrgAudioContext(options);
BxLogger.info("patchAudioContext", ctx, options);
ctx.createGain = function() {
const gainNode = nativeCreateGain.apply(this);
gainNode.gain.value = getPref(PrefKey.AUDIO_VOLUME) / 100;
STATES.currentStream.audioGainNode = gainNode;
return gainNode;
};
STATES.currentStream.audioContext = ctx;
return ctx;
};
}
function patchMeControl() {
const overrideConfigs = {
enableAADTelemetry: false,
enableTelemetry: false,
telEvs: "",
oneDSUrl: ""
};
const MSA = {
MeControl: {}
};
const MeControl = {};
const MsaHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "MeControl" && value.Config) {
value.Config = Object.assign(value.Config, overrideConfigs);
}
obj[prop] = value;
return true;
}
};
const MeControlHandler = {
get(target, prop, receiver) {
return target[prop];
},
set(obj, prop, value) {
if (prop === "Config") {
value = Object.assign(value, overrideConfigs);
}
obj[prop] = value;
return true;
}
};
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")) {
contextAttributes = contextAttributes || {};
contextAttributes.antialias = false;
if (contextAttributes.powerPreference === "high-performance") {
contextAttributes.powerPreference = "low-power";
}
}
return nativeGetContext.apply(this, [contextType, contextAttributes]);
};
}
// src/index.ts
var main = function() {
patchRtcPeerConnection();
patchRtcCodecs();
interceptHttpRequests();
patchVideoApi();
patchCanvasContext();
getPref(PrefKey.AUDIO_ENABLE_VOLUME_CONTROL) && patchAudioContext();
getPref(PrefKey.BLOCK_TRACKING) && patchMeControl();
STATES.hasTouchSupport && TouchController.updateCustomList();
overridePreloadState();
VibrationManager.initialSetup();
BX_FLAGS.CheckForUpdate && checkForUpdate();
addCss();
Toast.setup();
getPref(PrefKey.GAME_BAR_POSITION) !== "off" && GameBar.getInstance();
BX_FLAGS.PreloadUi && setupStreamUi();
StreamBadges.setupEvents();
StreamStats.setupEvents();
MkbHandler.setupEvents();
Patcher.init();
disablePwa();
window.addEventListener("gamepadconnected", (e) => showGamepadToast(e.gamepad));
window.addEventListener("gamepaddisconnected", (e) => showGamepadToast(e.gamepad));
if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
RemotePlay.detect();
}
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === "all") {
TouchController.setup();
}
};
if (window.location.pathname.includes("/auth/msa")) {
window.addEventListener("load", (e) => {
window.location.search.includes("loggedIn") && window.setTimeout(() => {
const location2 = window.location;
location2.pathname.includes("/play") && location2.reload(true);
}, 2000);
});
throw 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;
}
`;
const $fragment = document.createDocumentFragment();
$fragment.appendChild(CE("style", {}, css2));
$fragment.appendChild(CE("div", { class: "bx-reload-overlay" }, t("safari-failed-message")));
document.documentElement.appendChild($fragment);
window.location.reload(true);
throw 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(true);
}
}, 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) => {
LoadingScreen.hide();
if (!getPref(PrefKey.MKB_ENABLED) && getPref(PrefKey.MKB_HIDE_IDLE_CURSOR)) {
MouseCursorHider.start();
MouseCursorHider.hide();
}
});
window.addEventListener(BxEvent.STREAM_PLAYING, (e) => {
const $video = e.$video;
STATES.currentStream.$video = $video;
STATES.isPlaying = true;
injectStreamMenuButtons();
if (getPref(PrefKey.GAME_BAR_POSITION) !== "off") {
const gameBar = GameBar.getInstance();
gameBar.reset();
gameBar.enable();
gameBar.showBar();
}
Screenshot.updateCanvasSize($video.videoWidth, $video.videoHeight);
updateVideoPlayerCss();
});
window.addEventListener(BxEvent.STREAM_ERROR_PAGE, (e) => {
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
});
window.addEventListener(BxEvent.STREAM_STOPPED, (e) => {
if (!STATES.isPlaying) {
return;
}
STATES.isPlaying = false;
STATES.currentStream = {};
window.BX_EXPOSED.shouldShowSensorControls = false;
getPref(PrefKey.MKB_ENABLED) && MkbHandler.INSTANCE.destroy();
const $quickBar = document.querySelector(".bx-quick-settings-bar");
if ($quickBar) {
$quickBar.classList.add("bx-gone");
}
STATES.currentStream.audioGainNode = null;
STATES.currentStream.$video = null;
StreamStats.onStoppedPlaying();
MouseCursorHider.stop();
TouchController.reset();
GameBar.getInstance().disable();
});
window.addEventListener(BxEvent.CAPTURE_SCREENSHOT, (e) => {
Screenshot.takeScreenshot();
});
main();