From 88151c94260f0d8486f5a310d77ae1d51f785751 Mon Sep 17 00:00:00 2001 From: redphx <96280+redphx@users.noreply.github.com> Date: Sat, 21 Jun 2025 18:12:58 +0700 Subject: [PATCH] Show error when not able to get the server list --- dist/better-xcloud.pretty.user.js | 11 +++++++++-- dist/better-xcloud.user.js | 4 ++-- src/modules/ui/header.ts | 3 +++ src/utils/bx-event-bus.ts | 2 +- src/utils/xcloud-interceptor.ts | 8 +++++++- 5 files changed, 22 insertions(+), 6 deletions(-) diff --git a/dist/better-xcloud.pretty.user.js b/dist/better-xcloud.pretty.user.js index a358196..2a8689b 100644 --- a/dist/better-xcloud.pretty.user.js +++ b/dist/better-xcloud.pretty.user.js @@ -8565,7 +8565,8 @@ class HeaderSection { 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") { + } else if (status === "error") Toast.show(t("server-list-error"), "❌", { instant: !0 }); + else if (status === "unavailable") { if (STATES.supportedRegion = !1, document.querySelector("div[class^=UnsupportedMarketPage-module__container]")) SettingsDialog.getInstance().show(); } $btnSettings.classList.remove("bx-gone"); @@ -9063,7 +9064,13 @@ class XcloudInterceptor { let ip = BypassServerIps[bypassServer]; ip && request.headers.set("X-Forwarded-For", ip); } - let response = await NATIVE_FETCH(request, init); + let response; + try { + response = await NATIVE_FETCH(request, init); + } catch (e) { + BxEventBus.Script.emit("xcloud.server", { status: "error" }); + return; + } if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server", { status: "unavailable" }), response; let obj = await response.clone().json(); RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken); diff --git a/dist/better-xcloud.user.js b/dist/better-xcloud.user.js index b9ac602..fa64f56 100755 --- a/dist/better-xcloud.user.js +++ b/dist/better-xcloud.user.js @@ -235,12 +235,12 @@ function getPreferredServerRegion(shortName = !1) {let preferredRegion = getGlob 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;}} function localRedirect(path) {let url = window.location.href.substring(0, 31) + path, $pageContent = document.getElementById("PageContent");if (!$pageContent) return;let $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();} window.localRedirect = localRedirect; -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("block.features").includes("remote-play") ? 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 $header = document.querySelector("#gamepass-root header[class^=Header-module__header]");if (!$header) return;let $target = $header.querySelector("div[class*=EdgewaterHeader-module__rightSectionSpacing], div[class*=RemotePlayHeader-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 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("block.features").includes("remote-play") ? 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 === "error") Toast.show(t("server-list-error"), "❌", { instant: !0 });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 $header = document.querySelector("#gamepass-root header[class^=Header-module__header]");if (!$header) return;let $target = $header.querySelector("div[class*=EdgewaterHeader-module__rightSectionSpacing], div[class*=RemotePlayHeader-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 = `✅ ${t("xbox-360-games")} ${value === "1080p-hq" ? "❌" : "✅"} ${t("xbox-apps")}`, 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), CE("div", !1, CE("label", { for: `bx_setting_${escapeCssSelector("xhome.ipv6.prefer")}` }, t("prefer-ipv6-server")), SettingElement.fromPref("xhome.ipv6.prefer")));$fragment.appendChild($qualitySettings);let manager = RemotePlayManager.getInstance(), consoles = manager.getConsoles(), createConsoleShortcut = (e) => {let { serverId, deviceName } = e.target.dataset, optionsJson = JSON.stringify({resolution: getGlobalPref("xhome.video.resolution")});AppInterface?.createConsoleShortcut(serverId, deviceName, optionsJson);};for (let con of consoles) {let $connect, $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])), AppInterface ? createButton({attributes: {"data-server-id": con.serverId,"data-device-name": con.deviceName},icon: BxIcon.CREATE_SHORTCUT,style: 8 | 64,title: t("create-shortcut"),onClick: createConsoleShortcut}) : null, $connect = createButton({classes: ["bx-remote-play-connect-button"],label: t("console-connect"),style: 1 | 64,onClick: (e) => manager.play(con.serverId)}));setNearby($child, {orientation: "horizontal",focus: $connect}), $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:last-of-type");$btnConnect && $btnConnect.focus();}} class RemotePlayManager {static instance;static getInstance() {if (typeof RemotePlayManager.instance === "undefined") if (!getGlobalPref("block.features").includes("remote-play")) 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}`}};this.regions.sort((a, b) => {return a.isDefault ? -1 : 0;});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");localRedirect("/consoles/launch/" + serverId);}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();}isReady() {return this.consoles !== 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() {}} -class XcloudInterceptor {static SERVER_EXTRA_INFO = {EastUS: ["🇺🇸", "america-north"],EastUS2: ["🇺🇸", "america-north"],NorthCentralUs: ["🇺🇸", "america-north"],SouthCentralUS: ["🇺🇸", "america-north"],WestUS: ["🇺🇸", "america-north"],WestUS2: ["🇺🇸", "america-north"],MexicoCentral: ["🇲🇽", "america-north"],BrazilSouth: ["🇧🇷", "america-south"],JapanEast: ["🇯🇵", "asia"],KoreaCentral: ["🇰🇷", "asia"],AustraliaEast: ["🇦🇺", "australia"],AustraliaSouthEast: ["🇦🇺", "australia"],SwedenCentral: ["🇸🇪", "europe"],UKSouth: ["🇬🇧", "europe"],WestEurope: ["🇪🇺", "europe"]};static async handleLogin(request, init) {let bypassServer = getGlobalPref("server.bypassRestriction");if (bypassServer !== "off") {let ip = BypassServerIps[bypassServer];ip && request.headers.set("X-Forwarded-For", ip);}let response = await NATIVE_FETCH(request, init);if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server", { status: "unavailable" }), response;let obj = await response.clone().json();RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;for (region of obj.offeringSettings.regions) {let { name: regionName, name: shortName } = region;if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);let match = serverRegex.exec(region.baseUri);if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);}let preferredRegion = getPreferredServerRegion();if (preferredRegion && preferredRegion in STATES.serverRegions) {let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;}return STATES.gsToken = obj.gsToken, BxEventBus.Script.emit("xcloud.server", { status: "ready" }), response.json = () => Promise.resolve(obj), response;}static async handlePlay(request, init) {BxEventBus.Stream.emit("state.loading", {});let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName];if (region && parsedUrl.origin === region.baseUri) {badgeRegion = regionName;break;}}StreamBadges.getInstance().setRegion(badgeRegion);let clone = request.clone(), body = await clone.json(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;}if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;let newRequest = new Request(request, {body: JSON.stringify(body),headers});return NATIVE_FETCH(newRequest);}static async handleWaitTime(request, init) {let response = await NATIVE_FETCH(request, init);if (getGlobalPref("loadingScreen.waitTime.show")) {let 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 (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();else TouchController.enable();let response = await NATIVE_FETCH(request, init), text = await response.clone().text();if (!text.length) return response;BxEventBus.Stream.emit("state.starting", {});let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;let overrideMkb = null;if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {enableMouseInput: overrideMkb,enableKeyboardInput: overrideMkb});if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handle(request, init) {let url = typeof request === "string" ? request : request.url;if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);return NATIVE_FETCH(request, init);}} +class XcloudInterceptor {static SERVER_EXTRA_INFO = {EastUS: ["🇺🇸", "america-north"],EastUS2: ["🇺🇸", "america-north"],NorthCentralUs: ["🇺🇸", "america-north"],SouthCentralUS: ["🇺🇸", "america-north"],WestUS: ["🇺🇸", "america-north"],WestUS2: ["🇺🇸", "america-north"],MexicoCentral: ["🇲🇽", "america-north"],BrazilSouth: ["🇧🇷", "america-south"],JapanEast: ["🇯🇵", "asia"],KoreaCentral: ["🇰🇷", "asia"],AustraliaEast: ["🇦🇺", "australia"],AustraliaSouthEast: ["🇦🇺", "australia"],SwedenCentral: ["🇸🇪", "europe"],UKSouth: ["🇬🇧", "europe"],WestEurope: ["🇪🇺", "europe"]};static async handleLogin(request, init) {let bypassServer = getGlobalPref("server.bypassRestriction");if (bypassServer !== "off") {let ip = BypassServerIps[bypassServer];ip && request.headers.set("X-Forwarded-For", ip);}let response;try {response = await NATIVE_FETCH(request, init);} catch (e) {BxEventBus.Script.emit("xcloud.server", { status: "error" });return;}if (response.status !== 200) return BxEventBus.Script.emit("xcloud.server", { status: "unavailable" }), response;let obj = await response.clone().json();RemotePlayManager.getInstance()?.setXcloudToken(obj.gsToken);let serverRegex = /\/\/(\w+)\./, serverExtra = XcloudInterceptor.SERVER_EXTRA_INFO, region;for (region of obj.offeringSettings.regions) {let { name: regionName, name: shortName } = region;if (region.isDefault) STATES.selectedRegion = Object.assign({}, region);let match = serverRegex.exec(region.baseUri);if (match) if (shortName = match[1], serverExtra[regionName]) shortName = serverExtra[regionName][0] + " " + shortName, region.contintent = serverExtra[regionName][1];else region.contintent = "other", BX_FLAGS.Debug && alert("New server: " + shortName);region.shortName = shortName.toUpperCase(), STATES.serverRegions[region.name] = Object.assign({}, region);}let preferredRegion = getPreferredServerRegion();if (preferredRegion && preferredRegion in STATES.serverRegions) {let tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);tmp.isDefault = !0, obj.offeringSettings.regions = [tmp], STATES.selectedRegion = tmp;}return STATES.gsToken = obj.gsToken, BxEventBus.Script.emit("xcloud.server", { status: "ready" }), response.json = () => Promise.resolve(obj), response;}static async handlePlay(request, init) {BxEventBus.Stream.emit("state.loading", {});let PREF_STREAM_TARGET_RESOLUTION = getGlobalPref("stream.video.resolution"), PREF_STREAM_PREFERRED_LOCALE = getGlobalPref("stream.locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url), badgeRegion = parsedUrl.host.split(".", 1)[0];for (let regionName in STATES.serverRegions) {let region = STATES.serverRegions[regionName];if (region && parsedUrl.origin === region.baseUri) {badgeRegion = regionName;break;}}StreamBadges.getInstance().setRegion(badgeRegion);let clone = request.clone(), body = await clone.json(), headers = {};for (let pair of clone.headers.entries())headers[pair[0]] = pair[1];if (PREF_STREAM_TARGET_RESOLUTION !== "auto") {let osName = getOsNameFromResolution(PREF_STREAM_TARGET_RESOLUTION);headers["x-ms-device-info"] = JSON.stringify(generateMsDeviceInfo(osName)), body.settings.osName = osName;}if (PREF_STREAM_PREFERRED_LOCALE !== "default") body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;let newRequest = new Request(request, {body: JSON.stringify(body),headers});return NATIVE_FETCH(newRequest);}static async handleWaitTime(request, init) {let response = await NATIVE_FETCH(request, init);if (getGlobalPref("loadingScreen.waitTime.show")) {let 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 (getGlobalPref("touchController.mode") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();else TouchController.enable();let response = await NATIVE_FETCH(request, init), text = await response.clone().text();if (!text.length) return response;BxEventBus.Stream.emit("state.starting", {});let obj = JSON.parse(text), overrides = JSON.parse(obj.clientStreamingConfigOverrides || "{}") || {};overrides.inputConfiguration = overrides.inputConfiguration || {}, overrides.inputConfiguration.enableVibration = !0;let overrideMkb = null;if (getGlobalPref("nativeMkb.mode") === "on" || STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId)) overrideMkb = !0;if (getGlobalPref("nativeMkb.mode") === "off") overrideMkb = !1;if (overrideMkb !== null) overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {enableMouseInput: overrideMkb,enableKeyboardInput: overrideMkb});if (TouchController.isEnabled()) overrides.inputConfiguration.enableTouchInput = !0, overrides.inputConfiguration.maxTouchPoints = 10;if (getGlobalPref("audio.mic.onPlaying")) overrides.audioConfiguration = overrides.audioConfiguration || {}, overrides.audioConfiguration.enableMicrophone = !0;return obj.clientStreamingConfigOverrides = JSON.stringify(overrides), response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;}static async handle(request, init) {let url = typeof request === "string" ? request : request.url;if (url.endsWith("/v2/login/user")) return XcloudInterceptor.handleLogin(request, init);else if (url.endsWith("/sessions/cloud/play")) return XcloudInterceptor.handlePlay(request, init);else if (url.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);else if (url.endsWith("/configuration")) return XcloudInterceptor.handleConfiguration(request, init);else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);return NATIVE_FETCH(request, init);}} function clearApplicationInsightsBuffers() {window.sessionStorage.removeItem("AI_buffer"), window.sessionStorage.removeItem("AI_sentBuffer");} function clearDbLogs(dbName, table) {let request = window.indexedDB.open(dbName);request.onsuccess = (e) => {let db = e.target.result;try {let objectStoreRequest = db.transaction(table, "readwrite").objectStore(table).clear();objectStoreRequest.onsuccess = () => BxLogger.info("clearDbLogs", `Cleared ${dbName}.${table}`);} catch (ex) {}};} function clearAllLogs() {clearApplicationInsightsBuffers(), clearDbLogs("StreamClientLogHandler", "logs"), clearDbLogs("XCloudAppLogs", "logs");} diff --git a/src/modules/ui/header.ts b/src/modules/ui/header.ts index af72393..0ed38e2 100755 --- a/src/modules/ui/header.ts +++ b/src/modules/ui/header.ts @@ -12,6 +12,7 @@ import { getGlobalPref } from "@/utils/pref-utils"; import { BxLogger } from "@/utils/bx-logger"; import { BxEventBus } from "@/utils/bx-event-bus"; import { BlockFeature } from "@/enums/pref-values"; +import { Toast } from "@/utils/toast"; export class HeaderSection { private static instance: HeaderSection; @@ -60,6 +61,8 @@ export class HeaderSection { if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) { $btnSettings.setAttribute('data-update-available', 'true'); } + } else if (status === 'error') { + Toast.show(t('server-list-error'), '❌', { instant: true }); } else if (status === 'unavailable') { STATES.supportedRegion = false; diff --git a/src/utils/bx-event-bus.ts b/src/utils/bx-event-bus.ts index 3fbdda5..c8254c1 100644 --- a/src/utils/bx-event-bus.ts +++ b/src/utils/bx-event-bus.ts @@ -9,7 +9,7 @@ type EventCallback = (payload: T) => void; export type ScriptEvents = { 'xcloud.server': { - status: 'ready' | 'unavailable' | 'signed-out', + status: 'ready' | 'unavailable' | 'signed-out' | 'error', }; 'dialog.shown': {}; diff --git a/src/utils/xcloud-interceptor.ts b/src/utils/xcloud-interceptor.ts index 607be1b..7b6643e 100755 --- a/src/utils/xcloud-interceptor.ts +++ b/src/utils/xcloud-interceptor.ts @@ -49,7 +49,13 @@ export class XcloudInterceptor { ip && (request as Request).headers.set('X-Forwarded-For', ip); } - const response = await NATIVE_FETCH(request, init); + let response; + try { + response = await NATIVE_FETCH(request, init); + } catch (e) { + BxEventBus.Script.emit('xcloud.server', { status: 'error' }); + return; + } if (response.status !== 200) { // Unsupported region BxEventBus.Script.emit('xcloud.server', { status: 'unavailable' });