From 0b02a758db694e085cacb729d1ef0f35f7514b32 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Fri, 21 Feb 2025 07:10:54 +0700 Subject: [PATCH] Fix custom touch control not working in Remote Play (#674) --- dist/better-xcloud.pretty.user.js | 4 ++-- dist/better-xcloud.user.js | 4 ++-- src/modules/touch-controller.ts | 2 +- src/utils/xhome-interceptor.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index f9d8717..354425a 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -4981,7 +4981,7 @@ class TouchController { } if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null; if (!layoutId) { - BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); + BxLogger.warning(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault(); return; } let layoutChanged = TouchController.#currentLayoutId !== layoutId; @@ -8573,7 +8573,7 @@ class XhomeInterceptor { if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, { data: null }); - else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId); + else TouchController.enable(), TouchController.requestCustomLayouts(); return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; } static async handleTitles(request) { diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index a576375..dc955d3 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -145,7 +145,7 @@ window.addEventListener("resize", resizeVideoPlayer); class NavigationDialog {dialogManager;onMountedCallbacks = [];constructor() {this.dialogManager = NavigationDialogManager.getInstance();}isCancellable() {return !0;}isOverlayVisible() {return !0;}show(configs = {}, clearStack = !1) {if (NavigationDialogManager.getInstance().show(this, configs, clearStack), !this.getFocusedElement()) this.focusIfNeeded();}hide() {NavigationDialogManager.getInstance().hide();}getFocusedElement() {let $activeElement = document.activeElement;if (!$activeElement) return null;if (this.$container.contains($activeElement)) return $activeElement;return null;}onBeforeMount(configs = {}) {}onMounted(configs = {}) {for (let callback of this.onMountedCallbacks)callback.call(this);}onBeforeUnmount() {}onUnmounted() {}handleKeyPress(key) {return !1;}handleGamepad(button) {return !1;}} class NavigationDialogManager {static instance;static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);LOG_TAG = "NavigationDialogManager";static GAMEPAD_POLLING_INTERVAL = 50;static GAMEPAD_KEYS = [0,1,2,3,12,15,13,14,4,5,6,7,10,11,8,9];static GAMEPAD_DIRECTION_MAP = {12: 1,13: 3,14: 4,15: 2,100: 1,101: 3,102: 4,103: 2};static SIBLING_PROPERTY_MAP = {horizontal: {4: "previousElementSibling",2: "nextElementSibling"},vertical: {1: "previousElementSibling",3: "nextElementSibling"}};gamepadPollingIntervalId = null;gamepadLastStates = [];gamepadHoldingIntervalId = null;$overlay;$container;dialog = null;dialogsStack = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$overlay = CE("div", { class: "bx-navigation-dialog-overlay bx-gone" }), this.$overlay.addEventListener("click", (e) => {e.preventDefault(), e.stopPropagation(), this.dialog?.isCancellable() && this.hide();}), document.documentElement.appendChild(this.$overlay), this.$container = CE("div", { class: "bx-navigation-dialog bx-gone" }), document.documentElement.appendChild(this.$container), window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, (e) => this.hide()), new MutationObserver((mutationList) => {if (mutationList.length === 0 || mutationList[0].addedNodes.length === 0) return;let $dialog = mutationList[0].addedNodes[0];if (!$dialog || !($dialog instanceof HTMLElement)) return;calculateSelectBoxes($dialog);}).observe(this.$container, { childList: !0 });}updateActiveInput(input) {document.documentElement.dataset.activeInput = input;}handleEvent(event) {switch (event.type) {case "keydown":this.updateActiveInput("keyboard");let $target = event.target, keyboardEvent = event, keyCode = keyboardEvent.code || keyboardEvent.key, handled = this.dialog?.handleKeyPress(keyCode);if (handled) {event.preventDefault(), event.stopPropagation();return;}if (keyCode === "ArrowUp" || keyCode === "ArrowDown") handled = !0, this.focusDirection(keyCode === "ArrowUp" ? 1 : 3);else if (keyCode === "ArrowLeft" || keyCode === "ArrowRight") {if (!($target instanceof HTMLInputElement && ($target.type === "text" || $target.type === "range"))) handled = !0, this.focusDirection(keyCode === "ArrowLeft" ? 4 : 2);} else if (keyCode === "Enter" || keyCode === "NumpadEnter" || keyCode === "Space") {if (!($target instanceof HTMLInputElement && $target.type === "text")) handled = !0, $target.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));} else if (keyCode === "Escape") handled = !0, this.hide();if (handled) event.preventDefault(), event.stopPropagation();break;}}isShowing() {return this.$container && !this.$container.classList.contains("bx-gone");}pollGamepad = () => {let gamepads = window.navigator.getGamepads();for (let gamepad of gamepads) {if (!gamepad || !gamepad.connected) continue;if (gamepad.id === VIRTUAL_GAMEPAD_ID) continue;let { axes, buttons } = gamepad, releasedButton = null, heldButton = null, lastState = this.gamepadLastStates[gamepad.index], lastTimestamp, lastKey, lastKeyPressed;if (lastState) [lastTimestamp, lastKey, lastKeyPressed] = lastState;if (lastTimestamp && lastTimestamp === gamepad.timestamp) continue;for (let key of NavigationDialogManager.GAMEPAD_KEYS)if (lastKey === key && !buttons[key].pressed) {releasedButton = key;break;} else if (buttons[key].pressed) {heldButton = key;break;}if (heldButton === null && releasedButton === null && axes && axes.length >= 2) {if (lastKey) {let releasedHorizontal = Math.abs(axes[0]) < 0.1 && (lastKey === 102 || lastKey === 103), releasedVertical = Math.abs(axes[1]) < 0.1 && (lastKey === 100 || lastKey === 101);if (releasedHorizontal || releasedVertical) releasedButton = lastKey;else heldButton = lastKey;} else if (axes[0] < -0.5) heldButton = 102;else if (axes[0] > 0.5) heldButton = 103;else if (axes[1] < -0.5) heldButton = 100;else if (axes[1] > 0.5) heldButton = 101;}if (heldButton !== null) {if (this.gamepadLastStates[gamepad.index] = [gamepad.timestamp, heldButton, !1], this.clearGamepadHoldingInterval(), NavigationDialogManager.GAMEPAD_DIRECTION_MAP[heldButton]) this.gamepadHoldingIntervalId = window.setInterval(() => {let lastState2 = this.gamepadLastStates[gamepad.index];if (lastState2) {if ([lastTimestamp, lastKey, lastKeyPressed] = lastState2, lastKey === heldButton) {this.handleGamepad(gamepad, heldButton);return;}}this.clearGamepadHoldingInterval();}, 100);continue;}if (releasedButton === null) {this.clearGamepadHoldingInterval();continue;}if (this.gamepadLastStates[gamepad.index] = null, lastKeyPressed) return;if (this.updateActiveInput("gamepad"), this.handleGamepad(gamepad, releasedButton)) return;if (releasedButton === 0) {document.activeElement?.dispatchEvent(new MouseEvent("click", { bubbles: !0 }));return;} else if (releasedButton === 1) {this.hide();return;}}};handleGamepad(gamepad, key) {let handled = this.dialog?.handleGamepad(key);if (handled) return !0;let direction = NavigationDialogManager.GAMEPAD_DIRECTION_MAP[key];if (!direction) return !1;if (document.activeElement instanceof HTMLInputElement && document.activeElement.type === "range") {let $range = document.activeElement;if (direction === 4 || direction === 2) {let $numberStepper = $range.closest(".bx-number-stepper");if ($numberStepper) BxNumberStepper.change.call($numberStepper, direction === 4 ? "dec" : "inc");else $range.value = (parseInt($range.value) + parseInt($range.step) * (direction === 4 ? -1 : 1)).toString(), $range.dispatchEvent(new InputEvent("input"));handled = !0;}}if (!handled) this.focusDirection(direction);return this.gamepadLastStates[gamepad.index] && (this.gamepadLastStates[gamepad.index][2] = !0), !0;}clearGamepadHoldingInterval() {this.gamepadHoldingIntervalId && window.clearInterval(this.gamepadHoldingIntervalId), this.gamepadHoldingIntervalId = null;}show(dialog, configs = {}, clearStack = !1) {this.clearGamepadHoldingInterval(), BxEventBus.Script.emit("dialog.shown", {}), window.BX_EXPOSED.disableGamepadPolling = !0, document.body.classList.add("bx-no-scroll"), this.unmountCurrentDialog(), this.dialogsStack.push(dialog), this.dialog = dialog, dialog.onBeforeMount(configs), this.$container.appendChild(dialog.getContent()), dialog.onMounted(configs), this.$overlay.classList.remove("bx-gone"), this.$overlay.classList.toggle("bx-invisible", !dialog.isOverlayVisible()), this.$container.classList.remove("bx-gone"), this.$container.addEventListener("keydown", this), this.startGamepadPolling();}hide() {if (this.clearGamepadHoldingInterval(), !this.isShowing()) return;if (document.body.classList.remove("bx-no-scroll"), BxEventBus.Script.emit("dialog.dismissed", {}), this.$overlay.classList.add("bx-gone"), this.$overlay.classList.remove("bx-invisible"), this.$container.classList.add("bx-gone"), this.$container.removeEventListener("keydown", this), this.stopGamepadPolling(), this.dialog) {let dialogIndex = this.dialogsStack.indexOf(this.dialog);if (dialogIndex > -1) this.dialogsStack = this.dialogsStack.slice(0, dialogIndex);}if (this.unmountCurrentDialog(), window.BX_EXPOSED.disableGamepadPolling = !1, this.dialogsStack.length) this.dialogsStack[this.dialogsStack.length - 1].show();}focus($elm) {if (!$elm) return !1;if ($elm.nearby && $elm.nearby.focus) if ($elm.nearby.focus instanceof HTMLElement) return this.focus($elm.nearby.focus);else return $elm.nearby.focus();return $elm.focus(), $elm === document.activeElement;}getOrientation($elm) {let nearby = $elm.nearby || {};if (nearby.selfOrientation) return nearby.selfOrientation;let orientation, $current = $elm.parentElement;while ($current !== this.$container) {let tmp = $current.nearby?.orientation;if ($current.nearby && tmp) {orientation = tmp;break;}$current = $current.parentElement;}return orientation = orientation || "vertical", setNearby($elm, {selfOrientation: orientation}), orientation;}findNextTarget($focusing, direction, checkParent = !1, checked = []) {if (!$focusing || $focusing === this.$container) return null;if (checked.includes($focusing)) return null;checked.push($focusing);let $target = $focusing, $parent = $target.parentElement, nearby = $target.nearby || {}, orientation = this.getOrientation($target);if (nearby[1] && direction === 1) return nearby[1];else if (nearby[3] && direction === 3) return nearby[3];else if (nearby[4] && direction === 4) return nearby[4];else if (nearby[2] && direction === 2) return nearby[2];let siblingProperty = NavigationDialogManager.SIBLING_PROPERTY_MAP[orientation][direction];if (siblingProperty) {let $sibling = $target;while ($sibling[siblingProperty]) {$sibling = $sibling[siblingProperty];let $focusable = this.findFocusableElement($sibling, direction);if ($focusable) return $focusable;}}if (nearby.loop) {if (nearby.loop(direction)) return null;}if (checkParent) return this.findNextTarget($parent, direction, checkParent, checked);return null;}findFocusableElement($elm, direction) {if (!$elm) return null;if (!!$elm.disabled) return null;if (!isElementVisible($elm)) return null;if ($elm.tabIndex > -1) return $elm;let focus = $elm.nearby?.focus;if (focus) {if (focus instanceof HTMLElement) return this.findFocusableElement(focus, direction);else if (typeof focus === "function") {if (focus()) return document.activeElement;}}let children = Array.from($elm.children), orientation = $elm.nearby?.orientation || "vertical";if (orientation === "horizontal" || orientation === "vertical" && direction === 1) children.reverse();for (let $child of children) {if (!$child || !($child instanceof HTMLElement)) return null;let $target = this.findFocusableElement($child, direction);if ($target) return $target;}return null;}startGamepadPolling() {this.stopGamepadPolling(), this.gamepadPollingIntervalId = window.setInterval(this.pollGamepad, NavigationDialogManager.GAMEPAD_POLLING_INTERVAL);}stopGamepadPolling() {this.gamepadLastStates = [], this.gamepadPollingIntervalId && window.clearInterval(this.gamepadPollingIntervalId), this.gamepadPollingIntervalId = null;}focusDirection(direction) {let dialog = this.dialog;if (!dialog) return;let $focusing = dialog.getFocusedElement();if (!$focusing || !this.findFocusableElement($focusing, direction)) return dialog.focusIfNeeded(), null;let $target = this.findNextTarget($focusing, direction, !0);this.focus($target);}unmountCurrentDialog() {let dialog = this.dialog;dialog && dialog.onBeforeUnmount(), this.$container.firstChild?.remove(), dialog && dialog.onUnmounted(), this.dialog = null;}} var LOG_TAG = "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 #enabled = !1;static #dataChannel;static #customLayouts = {};static #baseCustomLayouts = {};static #currentLayoutId;static #customList;static #xboxTitleId = null;static setXboxTitleId(xboxTitleId) {TouchController.#xboxTitleId = xboxTitleId;}static getCustomLayouts() {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return null;return TouchController.#customLayouts[xboxTitleId];}static enable() {TouchController.#enabled = !0;}static disable() {TouchController.#enabled = !1;}static isEnabled() {return TouchController.#enabled;}static #showDefault() {TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);}static #show() {document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");}static toggleVisibility() {if (!TouchController.#dataChannel) return !1;let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement;if (!$container) return !1;return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen");}static reset() {TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = "");}static #dispatchMessage(msg) {TouchController.#dataChannel && window.setTimeout(() => {TouchController.#dataChannel.dispatchEvent(msg);}, 10);}static #dispatchLayouts(data) {TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);}static async requestCustomLayouts(retries = 1) {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return;if (xboxTitleId in TouchController.#customLayouts) {TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);return;}if (retries = retries || 1, retries > 2) {TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);return;}try {let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {};json.layouts.forEach(async (layoutName) => {let baseLayouts = {};if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName];else try {let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;} catch (e) {}Object.assign(layouts, baseLayouts);}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);} catch (e) {TouchController.requestCustomLayouts(retries + 1);}}static applyCustomLayout(layoutId, delay = 0) {if (!window.BX_EXPOSED.touchLayoutManager) {let listener = (e) => {if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0);};window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 });return;}let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) {BxLogger.error(LOG_TAG, "Invalid xboxTitleId");return;}if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;if (!layoutId) {BxLogger.error(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault();return;}let layoutChanged = TouchController.#currentLayoutId !== layoutId;TouchController.#currentLayoutId = layoutId;let layoutData = TouchController.#customLayouts[xboxTitleId];if (!xboxTitleId || !layoutId || !layoutData) {TouchController.#enabled && TouchController.#showDefault();return;}let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];if (!layout) return;let msg, html = !1;if (layout.author) {let author = `${escapeHtml(layout.author)}`;msg = t("touch-control-layout-by", { name: author }), html = !0;} else msg = t("touch-control-layout");layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => {window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "System",layoutFile: layout}});}, delay);}static updateCustomList() {TouchController.#customList = GhPagesUtils.getTouchControlCustomList();}static getCustomList() {return TouchController.#customList;}static setup() {window.testTouchLayout = (layout) => {let { touchLayoutManager } = window.BX_EXPOSED;touchLayoutManager && touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: "" + TouchController.#xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "Custom",layoutFile: layout}});};let $style = document.createElement("style");document.documentElement.appendChild($style), TouchController.#$style = $style;let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom");BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label !== "message") return;let filter = "";if (TouchController.#enabled) {if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)";else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)";} else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)";if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;else $style.textContent = "";TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {window.setTimeout(TouchController.#show, 1000);});let focused = !1;dataChannel.addEventListener("message", (msg) => {if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;if (msg.data.includes("touchcontrols/showtitledefault")) {if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts();else TouchController.#showDefault();return;}try {if (msg.data.includes("/titleinfo")) {let json = JSON.parse(JSON.parse(msg.data).content);if (focused = json.focused, !json.focused) TouchController.#show();TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());}} catch (e) {BxLogger.error(LOG_TAG, "Load custom layout", e);}});});}} +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 #enabled = !1;static #dataChannel;static #customLayouts = {};static #baseCustomLayouts = {};static #currentLayoutId;static #customList;static #xboxTitleId = null;static setXboxTitleId(xboxTitleId) {TouchController.#xboxTitleId = xboxTitleId;}static getCustomLayouts() {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return null;return TouchController.#customLayouts[xboxTitleId];}static enable() {TouchController.#enabled = !0;}static disable() {TouchController.#enabled = !1;}static isEnabled() {return TouchController.#enabled;}static #showDefault() {TouchController.#dispatchMessage(TouchController.#EVENT_SHOW_DEFAULT_CONTROLLER);}static #show() {document.querySelector("#BabylonCanvasContainer-main")?.parentElement?.classList.remove("bx-offscreen");}static toggleVisibility() {if (!TouchController.#dataChannel) return !1;let $container = document.querySelector("#BabylonCanvasContainer-main")?.parentElement;if (!$container) return !1;return $container.classList.toggle("bx-offscreen"), !$container.classList.contains("bx-offscreen");}static reset() {TouchController.#enabled = !1, TouchController.#dataChannel = null, TouchController.#xboxTitleId = null, TouchController.#$style && (TouchController.#$style.textContent = "");}static #dispatchMessage(msg) {TouchController.#dataChannel && window.setTimeout(() => {TouchController.#dataChannel.dispatchEvent(msg);}, 10);}static #dispatchLayouts(data) {TouchController.applyCustomLayout(null, 1000), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED);}static async requestCustomLayouts(retries = 1) {let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) return;if (xboxTitleId in TouchController.#customLayouts) {TouchController.#dispatchLayouts(TouchController.#customLayouts[xboxTitleId]);return;}if (retries = retries || 1, retries > 2) {TouchController.#customLayouts[xboxTitleId] = null, window.setTimeout(() => TouchController.#dispatchLayouts(null), 1000);return;}try {let json = await (await NATIVE_FETCH(GhPagesUtils.getUrl(`touch-layouts/${xboxTitleId}.json`))).json(), layouts = {};json.layouts.forEach(async (layoutName) => {let baseLayouts = {};if (layoutName in TouchController.#baseCustomLayouts) baseLayouts = TouchController.#baseCustomLayouts[layoutName];else try {let layoutUrl = GhPagesUtils.getUrl(`touch-layouts/layouts/${layoutName}.json`);baseLayouts = (await (await NATIVE_FETCH(layoutUrl)).json()).layouts, TouchController.#baseCustomLayouts[layoutName] = baseLayouts;} catch (e) {}Object.assign(layouts, baseLayouts);}), json.layouts = layouts, TouchController.#customLayouts[xboxTitleId] = json, window.setTimeout(() => TouchController.#dispatchLayouts(json), 1000);} catch (e) {TouchController.requestCustomLayouts(retries + 1);}}static applyCustomLayout(layoutId, delay = 0) {if (!window.BX_EXPOSED.touchLayoutManager) {let listener = (e) => {if (TouchController.#enabled) TouchController.applyCustomLayout(layoutId, 0);};window.addEventListener(BxEvent.TOUCH_LAYOUT_MANAGER_READY, listener, { once: !0 });return;}let xboxTitleId = TouchController.#xboxTitleId;if (!xboxTitleId) {BxLogger.error(LOG_TAG, "Invalid xboxTitleId");return;}if (!layoutId) layoutId = TouchController.#customLayouts[xboxTitleId]?.default_layout || null;if (!layoutId) {BxLogger.warning(LOG_TAG, "Invalid layoutId, show default controller"), TouchController.#enabled && TouchController.#showDefault();return;}let layoutChanged = TouchController.#currentLayoutId !== layoutId;TouchController.#currentLayoutId = layoutId;let layoutData = TouchController.#customLayouts[xboxTitleId];if (!xboxTitleId || !layoutId || !layoutData) {TouchController.#enabled && TouchController.#showDefault();return;}let layout = layoutData.layouts[layoutId] || layoutData.layouts[layoutData.default_layout];if (!layout) return;let msg, html = !1;if (layout.author) {let author = `${escapeHtml(layout.author)}`;msg = t("touch-control-layout-by", { name: author }), html = !0;} else msg = t("touch-control-layout");layoutChanged && Toast.show(msg, layout.name, { html }), window.setTimeout(() => {window.BX_EXPOSED.shouldShowSensorControls = JSON.stringify(layout).includes("gyroscope"), window.BX_EXPOSED.touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "System",layoutFile: layout}});}, delay);}static updateCustomList() {TouchController.#customList = GhPagesUtils.getTouchControlCustomList();}static getCustomList() {return TouchController.#customList;}static setup() {window.testTouchLayout = (layout) => {let { touchLayoutManager } = window.BX_EXPOSED;touchLayoutManager && touchLayoutManager.changeLayoutForScope({type: "showLayout",scope: "" + TouchController.#xboxTitleId,subscope: "base",layout: {id: "System.Standard",displayName: "Custom",layoutFile: layout}});};let $style = document.createElement("style");document.documentElement.appendChild($style), TouchController.#$style = $style;let PREF_STYLE_STANDARD = getGlobalPref("touchController.style.standard"), PREF_STYLE_CUSTOM = getGlobalPref("touchController.style.custom");BxEventBus.Stream.on("dataChannelCreated", (payload) => {let { dataChannel } = payload;if (dataChannel?.label !== "message") return;let filter = "";if (TouchController.#enabled) {if (PREF_STYLE_STANDARD === "white") filter = "grayscale(1) brightness(2)";else if (PREF_STYLE_STANDARD === "muted") filter = "sepia(0.5)";} else if (PREF_STYLE_CUSTOM === "muted") filter = "sepia(0.5)";if (filter) $style.textContent = `#babylon-canvas { filter: ${filter} !important; }`;else $style.textContent = "";TouchController.#dataChannel = dataChannel, dataChannel.addEventListener("open", () => {window.setTimeout(TouchController.#show, 1000);});let focused = !1;dataChannel.addEventListener("message", (msg) => {if (msg.origin === "better-xcloud" || typeof msg.data !== "string") return;if (msg.data.includes("touchcontrols/showtitledefault")) {if (TouchController.#enabled) if (focused) TouchController.requestCustomLayouts();else TouchController.#showDefault();return;}try {if (msg.data.includes("/titleinfo")) {let json = JSON.parse(JSON.parse(msg.data).content);if (focused = json.focused, !json.focused) TouchController.#show();TouchController.setXboxTitleId(parseInt(json.titleid, 16).toString());}} catch (e) {BxLogger.error(LOG_TAG, "Load custom layout", e);}});});}} var controller_customization_default = "var shareButtonPressed=currentGamepad.buttons[17]?.pressed,shareButtonHandled=!1,xCloudGamepad=$xCloudGamepadVar$;if(currentGamepad.id in window.BX_STREAM_SETTINGS.controllers){let controller=window.BX_STREAM_SETTINGS.controllers[currentGamepad.id];if(controller?.customization){let{mapping,ranges}=controller.customization,pressedButtons={},releasedButtons={},isModified=!1;if(ranges.LeftTrigger){let[from,to]=ranges.LeftTrigger;xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTrigger>to?1:xCloudGamepad.LeftTrigger,xCloudGamepad.LeftTrigger=xCloudGamepad.LeftTriggerto?1:xCloudGamepad.RightTrigger,xCloudGamepad.RightTrigger=xCloudGamepad.RightTriggerto?1:range;if(newRange=newRangeto?1:range;if(newRange=newRange=0.1)pressedButtons[targetX]=rangeX,pressedButtons[targetY]=rangeY}releasedButtons[sourceX]=0,releasedButtons[sourceY]=0,isModified=!0}else if(typeof mappedKey===\"string\"){let pressed=!1,value=0;if(key===\"LeftTrigger\"||key===\"RightTrigger\"){let currentRange=xCloudGamepad[key];if(mappedKey===\"LeftTrigger\"||mappedKey===\"RightTrigger\")pressed=currentRange>=0.1,value=currentRange;else pressed=!0,value=currentRange>=0.9?1:0}else if(xCloudGamepad[key])pressed=!0,value=xCloudGamepad[key];if(pressed)pressedButtons[mappedKey]=value,releasedButtons[key]=0,isModified=!0}else if(mappedKey===!1)pressedButtons[key]=0,isModified=!0}isModified&&Object.assign(xCloudGamepad,releasedButtons,pressedButtons)}}if(shareButtonPressed&&!shareButtonHandled)window.dispatchEvent(new Event(BxEvent.CAPTURE_SCREENSHOT));\n"; var poll_gamepad_default = "var self=this;if(window.BX_EXPOSED.disableGamepadPolling){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(50):self.pollGamepadssetTimeoutTimerID=window.setTimeout(self.pollGamepads,50);return}var currentGamepad=$gamepadVar$,btnHome=currentGamepad.buttons[16];if(btnHome){if(!self.bxHomeStates)self.bxHomeStates={};let intervalMs=0,hijack=!1;if(btnHome.pressed)if(hijack=!0,intervalMs=16,self.gamepadIsIdle.set(currentGamepad.index,!1),self.bxHomeStates[currentGamepad.index]){let lastTimestamp=self.bxHomeStates[currentGamepad.index].timestamp;if(currentGamepad.timestamp!==lastTimestamp){if(self.bxHomeStates[currentGamepad.index].timestamp=currentGamepad.timestamp,window.BX_EXPOSED.handleControllerShortcut(currentGamepad))self.bxHomeStates[currentGamepad.index].shortcutPressed+=1}}else window.BX_EXPOSED.resetControllerShortcut(currentGamepad.index),self.bxHomeStates[currentGamepad.index]={shortcutPressed:0,timestamp:currentGamepad.timestamp};else if(self.bxHomeStates[currentGamepad.index]){hijack=!0;let info=structuredClone(self.bxHomeStates[currentGamepad.index]);if(self.bxHomeStates[currentGamepad.index]=null,info.shortcutPressed===0){let fakeGamepadMappings=[{GamepadIndex:currentGamepad.index,A:0,B:0,X:0,Y:0,LeftShoulder:0,RightShoulder:0,LeftTrigger:0,RightTrigger:0,View:0,Menu:0,LeftThumb:0,RightThumb:0,DPadUp:0,DPadDown:0,DPadLeft:0,DPadRight:0,Nexus:1,LeftThumbXAxis:0,LeftThumbYAxis:0,RightThumbXAxis:0,RightThumbYAxis:0,PhysicalPhysicality:0,VirtualPhysicality:0,Dirty:!0,Virtual:!1}];intervalMs=currentGamepad.timestamp-info.timestamp>=500?500:100,self.inputSink.onGamepadInput(performance.now()-intervalMs,fakeGamepadMappings)}else intervalMs=window.BX_STREAM_SETTINGS.controllerPollingRate}if(hijack&&intervalMs){self.inputConfiguration.useIntervalWorkerThreadForInput&&self.intervalWorker?self.intervalWorker.scheduleTimer(intervalMs):self.pollGamepadssetTimeoutTimerID=setTimeout(self.pollGamepads,intervalMs);return}}\n"; var expose_stream_session_default = 'var self=this;window.BX_EXPOSED.streamSession=self;var orgSetMicrophoneState=self.setMicrophoneState.bind(self);self.setMicrophoneState=(state)=>{orgSetMicrophoneState(state),window.BxEventBus.Stream.emit("microphone.state.changed",{state})};window.dispatchEvent(new Event(BxEvent.STREAM_SESSION_READY));var updateDimensionsStr=self.updateDimensions.toString();if(updateDimensionsStr.startsWith("function "))updateDimensionsStr=updateDimensionsStr.substring(9);var renderTargetVar=updateDimensionsStr.match(/if\\((\\w+)\\){/)[1];updateDimensionsStr=updateDimensionsStr.replaceAll(renderTargetVar+".scroll","scroll");updateDimensionsStr=updateDimensionsStr.replace(`if(${renderTargetVar}){`,`\nif (${renderTargetVar}) {\nconst scrollWidth = ${renderTargetVar}.dataset.width ? parseInt(${renderTargetVar}.dataset.width) : ${renderTargetVar}.scrollWidth;\nconst scrollHeight = ${renderTargetVar}.dataset.height ? parseInt(${renderTargetVar}.dataset.height) : ${renderTargetVar}.scrollHeight;\n`);eval(`this.updateDimensions = function ${updateDimensionsStr}`);\n'; @@ -238,7 +238,7 @@ function getPreferredServerRegion(shortName = !1) {let preferredRegion = getGlob class HeaderSection {static instance;static getInstance = () => HeaderSection.instance ?? (HeaderSection.instance = new HeaderSection);LOG_TAG = "HeaderSection";$btnRemotePlay;$btnSettings;$buttonsWrapper;constructor() {BxLogger.info(this.LOG_TAG, "constructor()"), this.$btnRemotePlay = createButton({classes: ["bx-header-remote-play-button", "bx-gone"],icon: BxIcon.REMOTE_PLAY,title: t("remote-play"),style: 8 | 64 | 2048,onClick: (e) => RemotePlayManager.getInstance()?.togglePopup()});let $btnSettings = this.$btnSettings = createButton({classes: ["bx-header-settings-button", "bx-gone"],label: t("better-xcloud"),style: 16 | 32 | 64 | 256,onClick: (e) => SettingsDialog.getInstance().show()});this.$buttonsWrapper = CE("div", !1, getGlobalPref("xhome.enabled") ? this.$btnRemotePlay : null, this.$btnSettings), BxEventBus.Script.on("xcloud.server", ({ status }) => {if (status === "ready") {STATES.isSignedIn = !0, $btnSettings.querySelector("span").textContent = getPreferredServerRegion(!0) || t("better-xcloud");let PREF_LATEST_VERSION = getGlobalPref("version.latest");if (!SCRIPT_VERSION.includes("beta") && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) $btnSettings.setAttribute("data-update-available", "true");} else if (status === "unavailable") {if (STATES.supportedRegion = !1, document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show();}$btnSettings.classList.remove("bx-gone");});}checkHeader = () => {let $target = document.querySelector("#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]");if (!$target) $target = document.querySelector("div[class^=UnsupportedMarketPage-module__buttons]");if ($target?.appendChild(this.$buttonsWrapper), !STATES.isSignedIn) BxEventBus.Script.emit("xcloud.server", { status: "signed-out" });};showRemotePlayButton() {this.$btnRemotePlay?.classList.remove("bx-gone");}} class RemotePlayDialog extends NavigationDialog {static instance;static getInstance = () => RemotePlayDialog.instance ?? (RemotePlayDialog.instance = new RemotePlayDialog);LOG_TAG = "RemotePlayNavigationDialog";STATE_LABELS = {On: t("powered-on"),Off: t("powered-off"),ConnectedStandby: t("standby"),Unknown: t("unknown")};$container;constructor() {super();BxLogger.info(this.LOG_TAG, "constructor()"), this.setupDialog();}setupDialog() {let $fragment = CE("div", { class: "bx-centered-dialog" }, CE("div", { class: "bx-dialog-title" }, CE("p", !1, t("remote-play")))), $settingNote = CE("p", {}), currentResolution = getGlobalPref("xhome.video.resolution"), $resolutions = CE("select", !1, CE("option", { value: "720p" }, "720p"), CE("option", { value: "1080p" }, "1080p"), CE("option", { value: "1080p-hq" }, "1080p (HQ)"));$resolutions = BxSelectElement.create($resolutions), $resolutions.addEventListener("input", (e) => {let value = e.target.value;$settingNote.textContent = value === "1080p" ? "✅ " + t("can-stream-xbox-360-games") : "❌ " + t("cant-stream-xbox-360-games"), setGlobalPref("xhome.video.resolution", value, "ui");}), $resolutions.value = currentResolution, BxEvent.dispatch($resolutions, "input", {manualTrigger: !0});let $qualitySettings = CE("div", {class: "bx-remote-play-settings"}, CE("div", !1, CE("label", !1, t("target-resolution"), $settingNote), $resolutions));$fragment.appendChild($qualitySettings);let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles();for (let con of consoles) {let $child = CE("div", { class: "bx-remote-play-device-wrapper" }, CE("div", { class: "bx-remote-play-device-info" }, CE("div", !1, 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" }, this.STATE_LABELS[con.powerState])), createButton({classes: ["bx-remote-play-connect-button"],label: t("console-connect"),style: 1 | 64,onClick: (e) => manager.play(con.serverId)}));$fragment.appendChild($child);}$fragment.appendChild(CE("div", {class: "bx-remote-play-buttons",_nearby: {orientation: "horizontal"}}, createButton({icon: BxIcon.QUESTION,style: 8 | 64,url: "https://better-xcloud.github.io/remote-play",label: t("help")}), createButton({style: 8 | 64,label: t("close"),onClick: (e) => this.hide()}))), this.$container = $fragment;}getDialog() {return this;}getContent() {return this.$container;}focusIfNeeded() {let $btnConnect = this.$container.querySelector(".bx-remote-play-device-wrapper button");$btnConnect && $btnConnect.focus();}} class RemotePlayManager {static instance;static getInstance() {if (typeof RemotePlayManager.instance === "undefined") if (getGlobalPref("xhome.enabled")) RemotePlayManager.instance = new RemotePlayManager;else RemotePlayManager.instance = null;return RemotePlayManager.instance;}LOG_TAG = "RemotePlayManager";isInitialized = !1;XCLOUD_TOKEN;XHOME_TOKEN;consoles;regions = [];constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}initialize() {if (this.isInitialized) return;this.isInitialized = !0, this.requestXhomeToken(() => {this.getConsolesList(() => {BxLogger.info(this.LOG_TAG, "Consoles", this.consoles), STATES.supportedRegion && HeaderSection.getInstance().showRemotePlayButton(), BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);});});}getXcloudToken() {return this.XCLOUD_TOKEN;}setXcloudToken(token) {this.XCLOUD_TOKEN = token;}getXhomeToken() {return this.XHOME_TOKEN;}getConsoles() {return this.consoles;}requestXhomeToken(callback) {if (this.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++) {let key = localStorage.key(i);if (!key.startsWith("Auth.User.")) continue;let json = JSON.parse(localStorage.getItem(key));for (let token of json.tokens) {if (!token.relyingParty.includes("gssv.xboxlive.com")) continue;GSSV_TOKEN = token.tokenData.token;break;}break;}}let 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) => {this.regions = json.offeringSettings.regions, this.XHOME_TOKEN = json.gsToken, callback();});}async getConsolesList(callback) {if (this.consoles) {callback();return;}let options = {method: "GET",headers: {Authorization: `Bearer ${this.XHOME_TOKEN}`}};for (let region of this.regions)try {let request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options), json = await (await fetch(request)).json();if (json.results.length === 0) continue;this.consoles = json.results, STATES.remotePlay.server = region.baseUri;break;} catch (e) {}if (!STATES.remotePlay.server) this.consoles = [];callback();}play(serverId, resolution) {if (resolution) setGlobalPref("xhome.video.resolution", resolution, "ui");STATES.remotePlay.config = {serverId}, window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, localRedirect("/launch/fortnite/BT5P2X999VH2#remote-play"), setTimeout(() => localRedirect("/consoles/launch/" + serverId), 100);}togglePopup(force = null) {if (!this.isReady()) {Toast.show(t("getting-consoles-list"));return;}if (this.consoles.length === 0) {Toast.show(t("no-consoles-found"), "", { instant: !0 });return;}RemotePlayDialog.getInstance().show();}static detect() {if (!getGlobalPref("xhome.enabled")) return;if (STATES.remotePlay.isPlaying = window.location.pathname.includes("/launch/") && window.location.hash.startsWith("#remote-play"), STATES.remotePlay?.isPlaying) window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config, window.history.replaceState({ origin: "better-xcloud" }, "", "https://www.xbox.com/" + location.pathname.substring(1, 6) + "/play");else window.BX_REMOTE_PLAY_CONFIG = null;}isReady() {return this.consoles !== null;}} -class XhomeInterceptor {static consoleAddrs = {};static async handleLogin(request) {try {let obj = await request.clone().json();obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify(obj),headers: {"Content-Type": "application/json"}});} catch (e) {alert(e), console.log(e);}return NATIVE_FETCH(request);}static async handleConfiguration(request) {BxEventBus.Stream.emit("state.starting", {});let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [["ipAddress", "port"],["ipV4Address", "ipV4Port"],["ipV6Address", "ipV6Port"]];XhomeInterceptor.consoleAddrs = {};for (let pair of pairs) {let [keyAddr, keyPort] = pair;if (keyAddr && keyPort && serverDetails[keyAddr]) {let port = serverDetails[keyPort], ports = new Set;port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);}}return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleInputConfigs(request, opts) {let response = await NATIVE_FETCH(request);if (getGlobalPref("touchController.mode") !== "all") return response;let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];TouchController.setXboxTitleId(xboxTitleId);let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;if (!hasTouchSupport) {let supportedInputTypes = inputConfigs.supportedInputTypes;hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");}if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {data: null});else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleTitles(request) {let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;let index = request.url.indexOf(".xboxlive.com");return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {method: clone.method,body: await clone.text(),headers}), NATIVE_FETCH(request);}static async handlePlay(request) {BxEventBus.Stream.emit("state.loading", {});let body = await request.clone().json(), newRequest = new Request(request, {body: JSON.stringify(body)});return NATIVE_FETCH(newRequest);}static async handle(request) {TouchController.disable();let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));let opts = {method: clone.method,headers};if (clone.method === "POST") opts.body = await clone.text();let url = request.url;if (!url.includes("/servers/home")) {let parsed = new URL(url);url = STATES.remotePlay.server + parsed.pathname;}if (request = new Request(url, opts), 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 XhomeInterceptor {static consoleAddrs = {};static async handleLogin(request) {try {let obj = await request.clone().json();obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {method: "POST",body: JSON.stringify(obj),headers: {"Content-Type": "application/json"}});} catch (e) {alert(e), console.log(e);}return NATIVE_FETCH(request);}static async handleConfiguration(request) {BxEventBus.Stream.emit("state.starting", {});let response = await NATIVE_FETCH(request), obj = await response.clone().json(), serverDetails = obj.serverDetails, pairs = [["ipAddress", "port"],["ipV4Address", "ipV4Port"],["ipV6Address", "ipV6Port"]];XhomeInterceptor.consoleAddrs = {};for (let pair of pairs) {let [keyAddr, keyPort] = pair;if (keyAddr && keyPort && serverDetails[keyAddr]) {let port = serverDetails[keyPort], ports = new Set;port && ports.add(port), ports.add(9002), XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);}}return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleInputConfigs(request, opts) {let response = await NATIVE_FETCH(request);if (getGlobalPref("touchController.mode") !== "all") return response;let obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];TouchController.setXboxTitleId(xboxTitleId);let inputConfigs = obj[0], hasTouchSupport = inputConfigs.supportedTabs.length > 0;if (!hasTouchSupport) {let supportedInputTypes = inputConfigs.supportedInputTypes;hasTouchSupport = supportedInputTypes.includes("NativeTouch") || supportedInputTypes.includes("CustomTouchOverlay");}if (hasTouchSupport) TouchController.disable(), BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {data: null});else TouchController.enable(), TouchController.requestCustomLayouts();return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handleTitles(request) {let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXcloudToken()}`;let index = request.url.indexOf(".xboxlive.com");return request = new Request("https://wus.core.gssv-play-prod" + request.url.substring(index), {method: clone.method,body: await clone.text(),headers}), NATIVE_FETCH(request);}static async handlePlay(request) {BxEventBus.Stream.emit("state.loading", {});let body = await request.clone().json(), newRequest = new Request(request, {body: JSON.stringify(body)});return NATIVE_FETCH(newRequest);}static async handle(request) {TouchController.disable();let clone = request.clone(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];headers.authorization = `Bearer ${RemotePlayManager.getInstance().getXhomeToken()}`;let osName = getOsNameFromResolution(getGlobalPref("xhome.video.resolution"));headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName));let opts = {method: clone.method,headers};if (clone.method === "POST") opts.body = await clone.text();let url = request.url;if (!url.includes("/servers/home")) {let parsed = new URL(url);url = STATES.remotePlay.server + parsed.pathname;}if (request = new Request(url, opts), 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 LoadingScreen {static $bgStyle;static $waitTimeBox;static waitTimeInterval = null;static orgWebTitle;static secondsToString(seconds) {let m = Math.floor(seconds / 60), s = Math.floor(seconds % 60), mDisplay = m > 0 ? `${m}m` : "", sDisplay = `${s}s`.padStart(s >= 0 ? 3 : 4, "0");return mDisplay + sDisplay;}static setup() {let titleInfo = STATES.currentStream.titleInfo;if (!titleInfo) return;if (!LoadingScreen.$bgStyle) {let $bgStyle = CE("style");document.documentElement.appendChild($bgStyle), LoadingScreen.$bgStyle = $bgStyle;}if (titleInfo.productInfo) LoadingScreen.setBackground(titleInfo.productInfo.heroImageUrl || titleInfo.productInfo.titledHeroImageUrl || titleInfo.productInfo.tileImageUrl);if (getGlobalPref("loadingScreen.rocket") === "hide") LoadingScreen.hideRocket();}static hideRocket() {let $bgStyle = LoadingScreen.$bgStyle;$bgStyle.textContent += "#game-stream div[class*=RocketAnimation-module__container] > svg{display:none}#game-stream video[class*=RocketAnimationVideo-module__video]{display:none}";}static setBackground(imageUrl) {let $bgStyle = LoadingScreen.$bgStyle;imageUrl = imageUrl + "?w=1920";let imageQuality = getGlobalPref("ui.imageQuality");if (imageQuality !== 90) imageUrl += "&q=" + imageQuality;$bgStyle.textContent += '#game-stream{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 .3s ease-in-out !important}' + `#game-stream {background-image: linear-gradient(#00000033, #000000e6), url(${imageUrl}) !important;}`;let bg = new Image;bg.onload = (e) => {$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:0 !important}';}, bg.src = imageUrl;}static setupWaitTime(waitTime) {if (getGlobalPref("loadingScreen.rocket") === "hide-queue") LoadingScreen.hideRocket();let secondsLeft = waitTime, $countDown, $estimated;LoadingScreen.orgWebTitle = document.title;let endDate = new Date, timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;endDate.setSeconds(endDate.getSeconds() + waitTime - timeZoneOffsetSeconds);let endDateStr = endDate.toISOString().slice(0, 19);endDateStr = endDateStr.substring(0, 10) + " " + endDateStr.substring(11, 19), endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;let $waitTimeBox = LoadingScreen.$waitTimeBox;if (!$waitTimeBox) $waitTimeBox = CE("div", { class: "bx-wait-time-box" }, CE("label", !1, t("server")), CE("span", !1, getPreferredServerRegion()), CE("label", !1, t("wait-time-estimated")), $estimated = CE("span", {}), CE("label", !1, t("wait-time-countdown")), $countDown = CE("span", {})), document.documentElement.appendChild($waitTimeBox), LoadingScreen.$waitTimeBox = $waitTimeBox;else $waitTimeBox.classList.remove("bx-gone"), $estimated = $waitTimeBox.querySelector(".bx-wait-time-estimated"), $countDown = $waitTimeBox.querySelector(".bx-wait-time-countdown");$estimated.textContent = endDateStr, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, LoadingScreen.waitTimeInterval = window.setInterval(() => {if (secondsLeft--, $countDown.textContent = LoadingScreen.secondsToString(secondsLeft), document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`, secondsLeft <= 0) LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}, 1000);}static hide() {if (LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), getGlobalPref("loadingScreen.gameArt.show") && LoadingScreen.$bgStyle) {let $rocketBg = document.querySelector('#game-stream rect[width="800"]');$rocketBg && $rocketBg.addEventListener("transitionend", (e) => {LoadingScreen.$bgStyle.textContent += "#game-stream{background:#000 !important}";}), LoadingScreen.$bgStyle.textContent += '#game-stream rect[width="800"]{opacity:1 !important}';}setTimeout(LoadingScreen.reset, 2000);}static reset() {LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = ""), LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add("bx-gone"), LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval), LoadingScreen.waitTimeInterval = null;}} class GuideMenu {static instance;static getInstance = () => GuideMenu.instance ?? (GuideMenu.instance = new GuideMenu);$renderedButtons;closeGuideMenu() {if (window.BX_EXPOSED.dialogRoutes) {window.BX_EXPOSED.dialogRoutes.closeAll();return;}let $btnClose = document.querySelector("#gamepass-dialog-root button[class^=Header-module__closeButton]");$btnClose && $btnClose.click();}renderButtons() {if (this.$renderedButtons) return this.$renderedButtons;let buttons = {scriptSettings: createButton({label: t("better-xcloud"),icon: BxIcon.BETTER_XCLOUD,style: 128 | 64 | 1,onClick: () => {BxEventBus.Script.once("dialog.dismissed", () => {setTimeout(() => SettingsDialog.getInstance().show(), 50);}), this.closeGuideMenu();}}),closeApp: AppInterface && createButton({icon: BxIcon.POWER,label: t("close-app"),title: t("close-app"),style: 128 | 64 | 4,onClick: (e) => {AppInterface.closeApp();},attributes: {"data-state": "normal"}}),reloadPage: createButton({icon: BxIcon.REFRESH,label: t("reload-page"),title: t("reload-page"),style: 128 | 64,onClick: () => {if (this.closeGuideMenu(), STATES.isPlaying) confirm(t("confirm-reload-stream")) && window.location.reload();else window.location.reload();}}),backToHome: createButton({icon: BxIcon.HOME,label: t("back-to-home"),title: t("back-to-home"),style: 128 | 64,onClick: () => {this.closeGuideMenu(), confirm(t("back-to-home-confirm")) && (window.location.href = window.location.href.substring(0, 31));},attributes: {"data-state": "playing"}})}, buttonsLayout = [buttons.scriptSettings,[buttons.backToHome,buttons.reloadPage,buttons.closeApp]], $div = CE("div", {class: "bx-guide-home-buttons"});if (STATES.userAgent.isTv || getGlobalPref("ui.layout") === "tv") document.body.dataset.bxMediaType = "tv";for (let $button of buttonsLayout) {if (!$button) continue;if ($button instanceof HTMLElement) $div.appendChild($button);else if (Array.isArray($button)) {let $wrapper = CE("div", {});for (let $child of $button)$child && $wrapper.appendChild($child);$div.appendChild($wrapper);}}return this.$renderedButtons = $div, $div;}injectHome($root, isPlaying = !1) {let $buttons = this.renderButtons();if ($root.contains($buttons)) return;let $target = null;if (isPlaying) {$target = $root.querySelector("a[class*=QuitGameButton]");let $btnXcloudHome = $root.querySelector("div[class^=HomeButtonWithDivider]");$btnXcloudHome && ($btnXcloudHome.style.display = "none");} else {let $dividers = $root.querySelectorAll("div[class*=Divider-module__divider]");if ($dividers) $target = $dividers[$dividers.length - 1];}if (!$target) return !1;$buttons.dataset.isPlaying = isPlaying.toString(), $target.insertAdjacentElement("afterend", $buttons);}} class StreamBadges {static instance;static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);LOG_TAG = "StreamBadges";serverInfo = {};badges = {playtime: {name: t("playtime"),icon: BxIcon.PLAYTIME,color: "#ff004d"},battery: {name: t("battery"),icon: BxIcon.BATTERY,color: "#00b543"},download: {name: t("download"),icon: BxIcon.DOWNLOAD,color: "#29adff"},upload: {name: t("upload"),icon: BxIcon.UPLOAD,color: "#ff77a8"},server: {name: t("server"),icon: BxIcon.SERVER,color: "#ff6c24"},video: {name: t("video"),icon: BxIcon.DISPLAY,color: "#742f29"},audio: {name: t("audio"),icon: BxIcon.AUDIO,color: "#5f574f"}};$container;intervalId;REFRESH_INTERVAL = 3000;constructor() {BxLogger.info(this.LOG_TAG, "constructor()");}setRegion(region) {this.serverInfo.server = {region};}renderBadge(name, value) {let badgeInfo = this.badges[name], $badge;if (badgeInfo.$element) return $badge = badgeInfo.$element, $badge.lastElementChild.textContent = value, $badge;if ($badge = CE("div", { class: "bx-badge", title: badgeInfo.name }, CE("span", { class: "bx-badge-name" }, createSvgIcon(badgeInfo.icon)), CE("span", { class: "bx-badge-value", style: `background-color: ${badgeInfo.color}` }, value)), name === "battery") $badge.classList.add("bx-badge-battery");return this.badges[name].$element = $badge, $badge;}updateBadges = async (forceUpdate = !1) => {if (!this.$container || !forceUpdate && !this.$container.isConnected) {this.stop();return;}let statsCollector = StreamStatsCollector.getInstance();await statsCollector.collect();let play = statsCollector.getStat("play"), batt = statsCollector.getStat("batt"), dl = statsCollector.getStat("dl"), ul = statsCollector.getStat("ul"), badges = {download: dl.toString(),upload: ul.toString(),playtime: play.toString(),battery: batt.toString()}, name;for (name in badges) {let value = badges[name];if (value === null) continue;let $elm = this.badges[name].$element;if (!$elm) continue;if ($elm.lastElementChild.textContent = value, name === "battery") if (batt.current === 100 && batt.start === 100) $elm.classList.add("bx-gone");else $elm.dataset.charging = batt.isCharging.toString(), $elm.classList.remove("bx-gone");}};async start() {await this.updateBadges(!0), this.stop(), this.intervalId = window.setInterval(this.updateBadges, this.REFRESH_INTERVAL);}stop() {this.intervalId && clearInterval(this.intervalId), this.intervalId = null;}destroy() {this.serverInfo = {}, delete this.$container;}async render() {if (this.$container) return this.start(), this.$container;await this.getServerStats();let batteryLevel = "";if (STATES.browser.capabilities.batteryApi) batteryLevel = "100%";let BADGES = [["playtime", "1m"],["battery", batteryLevel],["download", humanFileSize(0)],["upload", humanFileSize(0)],this.badges.server.$element ?? ["server", "?"],this.serverInfo.video ? this.badges.video.$element : ["video", "?"],this.serverInfo.audio ? this.badges.audio.$element : ["audio", "?"]], $container = CE("div", { class: "bx-badges" });for (let item2 of BADGES) {if (!item2) continue;let $badge;if (!(item2 instanceof HTMLElement)) $badge = this.renderBadge(...item2);else $badge = item2;$container.appendChild($badge);}return this.$container = $container, await this.start(), $container;}async getServerStats() {let stats = await STATES.currentStream.peerConnection.getStats(), allVideoCodecs = {}, videoCodecId, videoWidth = 0, videoHeight = 0, allAudioCodecs = {}, audioCodecId, allCandidatePairs = {}, allRemoteCandidates = {}, candidatePairId;if (stats.forEach((stat) => {if (stat.type === "codec") {let mimeType = stat.mimeType.split("/")[0];if (mimeType === "video") allVideoCodecs[stat.id] = stat;else if (mimeType === "audio") allAudioCodecs[stat.id] = stat;} else if (stat.type === "inbound-rtp" && stat.packetsReceived > 0) {if (stat.kind === "video") videoCodecId = stat.codecId, videoWidth = stat.frameWidth, videoHeight = stat.frameHeight;else if (stat.kind === "audio") audioCodecId = stat.codecId;} else if (stat.type === "transport" && stat.selectedCandidatePairId) candidatePairId = stat.selectedCandidatePairId;else if (stat.type === "candidate-pair") allCandidatePairs[stat.id] = stat.remoteCandidateId;else if (stat.type === "remote-candidate") allRemoteCandidates[stat.id] = stat.address;}), videoCodecId) {let videoStat = allVideoCodecs[videoCodecId], video = {width: videoWidth,height: videoHeight,codec: videoStat.mimeType.substring(6)};if (video.codec === "H264") {let match = /profile-level-id=([0-9a-f]{6})/.exec(videoStat.sdpFmtpLine);match && (video.profile = match[1]);}let text = videoHeight + "p";if (text && (text += "/"), text += video.codec, video.profile) {let profile = video.profile, 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");text += ` (${quality})`;}this.badges.video.$element = this.renderBadge("video", text), this.serverInfo.video = video;}if (audioCodecId) {let audioStat = allAudioCodecs[audioCodecId], audio = {codec: audioStat.mimeType.substring(6),bitrate: audioStat.clockRate}, bitrate = audio.bitrate / 1000, text = `${audio.codec} (${bitrate} kHz)`;this.badges.audio.$element = this.renderBadge("audio", text), this.serverInfo.audio = audio;}if (candidatePairId) {BxLogger.info("candidate", candidatePairId, allCandidatePairs);let text = "", isIpv6 = allRemoteCandidates[allCandidatePairs[candidatePairId]].includes(":"), server = this.serverInfo.server;if (server && server.region) text += server.region;text += "@" + (isIpv6 ? "IPv6" : "IPv4"), this.badges.server.$element = this.renderBadge("server", text);}}static setupEvents() {}} diff --git a/src/modules/touch-controller.ts b/src/modules/touch-controller.ts index c1c83af..c5971a0 100755 --- a/src/modules/touch-controller.ts +++ b/src/modules/touch-controller.ts @@ -209,7 +209,7 @@ export class TouchController { } if (!layoutId) { - BxLogger.error(LOG_TAG, 'Invalid layoutId, show default controller'); + BxLogger.warning(LOG_TAG, 'Invalid layoutId, show default controller'); TouchController.#enabled && TouchController.#showDefault(); return; } diff --git a/src/utils/xhome-interceptor.ts b/src/utils/xhome-interceptor.ts index db60d00..f9c08b5 100755 --- a/src/utils/xhome-interceptor.ts +++ b/src/utils/xhome-interceptor.ts @@ -95,7 +95,7 @@ export class XhomeInterceptor { }); } else { TouchController.enable(); - TouchController.requestCustomLayouts(xboxTitleId); + TouchController.requestCustomLayouts(); } response.json = () => Promise.resolve(obj);