Compare commits

...

11 Commits

Author SHA1 Message Date
fa19a5a68e Bump version to 5.8.5 2024-10-15 19:48:18 +07:00
3f834f74b6 Update "skipFeedbackDialog" patch 2024-10-15 16:49:38 +07:00
749d5d720e Update dist 2024-10-14 21:08:35 +07:00
b969d52a3c Show max FPS value in Stats bar 2024-10-14 21:06:52 +07:00
e5bd7e64a7 Refactor xCloud & xHome interceptors 2024-10-14 20:08:47 +07:00
82ee00b4ae Update dist 2024-10-14 17:17:32 +07:00
8e88af5f8c Set indent of built scripts to 1 space 2024-10-14 17:14:43 +07:00
927eae3f2f Refactor getInstance() methods 2024-10-14 16:56:05 +07:00
9f440e9cf4 Don't call animate() when hiding renderer 2024-10-14 16:47:03 +07:00
1acb30e3af Refactor Game Bar 2024-10-14 16:45:57 +07:00
34159fad22 Update better-xcloud.lite.user.js 2024-10-13 20:04:42 +07:00
31 changed files with 12805 additions and 12989 deletions

View File

@ -87,6 +87,15 @@ const postProcess = (str: string): string => {
return p1.toUpperCase(); return p1.toUpperCase();
}); });
// Replace " (e) =>" to " e =>"
// str = str.replaceAll(/ \(([^\s,.$()]+)\) =>/g, ' $1 =>');
// Set indent to 1 space
str = str.replaceAll(/\n(\s+)/g, (match, p1) => {
const len = p1.length / 2;
return '\n' + ' '.repeat(len);
});
assert(str.includes('/* ADDITIONAL CODE */')); assert(str.includes('/* ADDITIONAL CODE */'));
assert(str.includes('window.BX_EXPOSED = BxExposed')); assert(str.includes('window.BX_EXPOSED = BxExposed'));
assert(str.includes('window.BxEvent = BxEvent')); assert(str.includes('window.BxEvent = BxEvent'));

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud (Lite) // @name Better xCloud (Lite)
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 5.8.4-beta // @version 5.8.5
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -111,7 +111,7 @@ function deepClone(obj) {
if (!obj) return {}; if (!obj) return {};
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
var SCRIPT_VERSION = "5.8.4-beta", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface; var SCRIPT_VERSION = "5.8.5", SCRIPT_VARIANT = "lite", AppInterface = window.AppInterface;
UserAgent.init(); UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0, supportedRegion: !0,
@ -936,10 +936,7 @@ class BaseSettingsStore {
} }
class StreamStatsCollector { class StreamStatsCollector {
static instance; static instance;
static getInstance() { static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
if (!StreamStatsCollector.instance) StreamStatsCollector.instance = new StreamStatsCollector;
return StreamStatsCollector.instance;
}
static INTERVAL_BACKGROUND = 60000; static INTERVAL_BACKGROUND = 60000;
calculateGrade(value, grades) { calculateGrade(value, grades) {
return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : ""; return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
@ -962,7 +959,8 @@ class StreamStatsCollector {
fps: { fps: {
current: 0, current: 0,
toString() { toString() {
return this.current.toString(); const maxFps = getPref("video_max_fps");
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
} }
}, },
btr: { btr: {
@ -1807,10 +1805,7 @@ var MouseMapTo;
})(MouseMapTo ||= {}); })(MouseMapTo ||= {});
class StreamStats { class StreamStats {
static instance; static instance;
static getInstance() { static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
if (!StreamStats.instance) StreamStats.instance = new StreamStats;
return StreamStats.instance;
}
intervalId; intervalId;
REFRESH_INTERVAL = 1000; REFRESH_INTERVAL = 1000;
stats = { stats = {
@ -2141,14 +2136,13 @@ function onChangeVideoPlayerType() {
else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer();
} }
function limitVideoPlayerFps() { function limitVideoPlayerFps(targetFps) {
const targetFps = getPref("video_max_fps");
STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
} }
function updateVideoPlayer() { function updateVideoPlayer() {
const streamPlayer = STATES.currentStream.streamPlayer; const streamPlayer = STATES.currentStream.streamPlayer;
if (!streamPlayer) return; if (!streamPlayer) return;
limitVideoPlayerFps(); limitVideoPlayerFps(getPref("video_max_fps"));
const options = { const options = {
processing: getPref("video_processing"), processing: getPref("video_processing"),
sharpness: getPref("video_sharpness"), sharpness: getPref("video_sharpness"),
@ -2381,10 +2375,7 @@ class KeyHelper {
var LOG_TAG = "PointerClient"; var LOG_TAG = "PointerClient";
class PointerClient { class PointerClient {
static instance; static instance;
static getInstance() { static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
if (!PointerClient.instance) PointerClient.instance = new PointerClient;
return PointerClient.instance;
}
socket; socket;
mkbHandler; mkbHandler;
start(port, mkbHandler) { start(port, mkbHandler) {
@ -2516,11 +2507,8 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
#disableContextMenu = (e) => e.preventDefault(); #disableContextMenu = (e) => e.preventDefault();
} }
class EmulatedMkbHandler extends MkbHandler { class EmulatedMkbHandler extends MkbHandler {
static #instance; static instance;
static getInstance() { static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler);
if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler;
return EmulatedMkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static DEFAULT_PANNING_SENSITIVITY = 0.001; static DEFAULT_PANNING_SENSITIVITY = 0.001;
static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
@ -2798,10 +2786,7 @@ class NavigationDialog {
} }
class NavigationDialogManager { class NavigationDialogManager {
static instance; static instance;
static getInstance() { static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
if (!NavigationDialogManager.instance) NavigationDialogManager.instance = new NavigationDialogManager;
return NavigationDialogManager.instance;
}
static GAMEPAD_POLLING_INTERVAL = 50; static GAMEPAD_POLLING_INTERVAL = 50;
static GAMEPAD_KEYS = [ static GAMEPAD_KEYS = [
12, 12,
@ -3171,10 +3156,7 @@ if (getPref("block_social_features")) FeatureGates.EnableGuideChatTab = !1;
if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates); if (BX_FLAGS.FeatureGates) FeatureGates = Object.assign(BX_FLAGS.FeatureGates, FeatureGates);
class FullscreenText { class FullscreenText {
static instance; static instance;
static getInstance() { static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
if (!FullscreenText.instance) FullscreenText.instance = new FullscreenText;
return FullscreenText.instance;
}
$text; $text;
constructor() { constructor() {
this.$text = CE("div", { this.$text = CE("div", {
@ -3190,10 +3172,7 @@ class FullscreenText {
} }
class SettingsNavigationDialog extends NavigationDialog { class SettingsNavigationDialog extends NavigationDialog {
static instance; static instance;
static getInstance() { static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog);
if (!SettingsNavigationDialog.instance) SettingsNavigationDialog.instance = new SettingsNavigationDialog;
return SettingsNavigationDialog.instance;
}
$container; $container;
$tabs; $tabs;
$settings; $settings;
@ -3458,7 +3437,9 @@ class SettingsNavigationDialog extends NavigationDialog {
onChange: onChangeVideoPlayerType onChange: onChangeVideoPlayerType
}, { }, {
pref: "video_max_fps", pref: "video_max_fps",
onChange: limitVideoPlayerFps onChange: (e) => {
limitVideoPlayerFps(parseInt(e.target.value));
}
}, { }, {
pref: "video_power_preference", pref: "video_power_preference",
onChange: () => { onChange: () => {
@ -4185,10 +4166,7 @@ class HeaderSection {
} }
class RemotePlayNavigationDialog extends NavigationDialog { class RemotePlayNavigationDialog extends NavigationDialog {
static instance; static instance;
static getInstance() { static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog);
if (!RemotePlayNavigationDialog.instance) RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog;
return RemotePlayNavigationDialog.instance;
}
STATE_LABELS = { STATE_LABELS = {
On: t("powered-on"), On: t("powered-on"),
Off: t("powered-off"), Off: t("powered-off"),
@ -4254,10 +4232,7 @@ class RemotePlayNavigationDialog extends NavigationDialog {
var LOG_TAG2 = "RemotePlay"; var LOG_TAG2 = "RemotePlay";
class RemotePlayManager { class RemotePlayManager {
static instance; static instance;
static getInstance() { static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager);
if (!this.instance) this.instance = new RemotePlayManager;
return this.instance;
}
isInitialized = !1; isInitialized = !1;
XCLOUD_TOKEN; XCLOUD_TOKEN;
XHOME_TOKEN; XHOME_TOKEN;
@ -4544,10 +4519,7 @@ class GuideMenu {
} }
class StreamBadges { class StreamBadges {
static instance; static instance;
static getInstance() { static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
if (!StreamBadges.instance) StreamBadges.instance = new StreamBadges;
return StreamBadges.instance;
}
serverInfo = {}; serverInfo = {};
badges = { badges = {
playtime: { playtime: {
@ -4718,17 +4690,7 @@ class StreamBadges {
} }
} }
class XcloudInterceptor { class XcloudInterceptor {
static async#handleLogin(request, init) { static SERVER_EMOJIS = {
const bypassServer = getPref("server_bypass_restriction");
if (bypassServer !== "off") {
const ip = BypassServerIps[bypassServer];
ip && request.headers.set("X-Forwarded-For", ip);
}
const response = await NATIVE_FETCH(request, init);
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
const obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
const serverEmojis = {
AustraliaEast: "🇦🇺", AustraliaEast: "🇦🇺",
AustraliaSouthEast: "🇦🇺", AustraliaSouthEast: "🇦🇺",
BrazilSouth: "🇧🇷", BrazilSouth: "🇧🇷",
@ -4743,7 +4705,18 @@ class XcloudInterceptor {
WestEurope: "🇪🇺", WestEurope: "🇪🇺",
WestUS: "🇺🇸", WestUS: "🇺🇸",
WestUS2: "🇺🇸" WestUS2: "🇺🇸"
}, serverRegex = /\/\/(\w+)\./; };
static async handleLogin(request, init) {
const bypassServer = getPref("server_bypass_restriction");
if (bypassServer !== "off") {
const ip = BypassServerIps[bypassServer];
ip && request.headers.set("X-Forwarded-For", ip);
}
const response = await NATIVE_FETCH(request, init);
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
const obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
const serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) { for (let region of obj.offeringSettings.regions) {
const regionName = region.name; const regionName = region.name;
let shortName = region.name; let shortName = region.name;
@ -4762,7 +4735,7 @@ class XcloudInterceptor {
} }
return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
} }
static async#handlePlay(request, init) { static async handlePlay(request, init) {
const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url);
let badgeRegion = parsedUrl.host.split(".", 1)[0]; let badgeRegion = parsedUrl.host.split(".", 1)[0];
for (let regionName in STATES.serverRegions) { for (let regionName in STATES.serverRegions) {
@ -4784,7 +4757,7 @@ class XcloudInterceptor {
}); });
return NATIVE_FETCH(newRequest); return NATIVE_FETCH(newRequest);
} }
static async#handleWaitTime(request, init) { static async handleWaitTime(request, init) {
const response = await NATIVE_FETCH(request, init); const response = await NATIVE_FETCH(request, init);
if (getPref("ui_loading_screen_wait_time")) { if (getPref("ui_loading_screen_wait_time")) {
const json = await response.clone().json(); const json = await response.clone().json();
@ -4792,7 +4765,7 @@ class XcloudInterceptor {
} }
return response; return response;
} }
static async#handleConfiguration(request, init) { static async handleConfiguration(request, init) {
if (request.method !== "GET") return NATIVE_FETCH(request, init); if (request.method !== "GET") return NATIVE_FETCH(request, init);
const response = await NATIVE_FETCH(request, init), text = await response.clone().text(); const response = await NATIVE_FETCH(request, init), text = await response.clone().text();
if (!text.length) return response; if (!text.length) return response;
@ -4811,10 +4784,10 @@ class XcloudInterceptor {
} }
static async handle(request, init) { static async handle(request, init) {
let url = typeof request === "string" ? request : request.url; let url = typeof request === "string" ? request : request.url;
if (url.endsWith("/v2/login/user")) return XcloudInterceptor.#handleLogin(request, init); 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.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.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
else if (url.endsWith("/configuration")) return XcloudInterceptor.#handleConfiguration(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); else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
return NATIVE_FETCH(request, init); return NATIVE_FETCH(request, init);
} }
@ -5083,7 +5056,7 @@ class WebGL2Player {
saturation: 0 saturation: 0
}; };
targetFps = 60; targetFps = 60;
frameInterval = Math.ceil(1000 / this.targetFps); frameInterval = 0;
lastFrameTime = 0; lastFrameTime = 0;
animFrameId = null; animFrameId = null;
constructor($video) { constructor($video) {
@ -5107,7 +5080,7 @@ class WebGL2Player {
this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas(); this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
} }
setTargetFps(target) { setTargetFps(target) {
this.targetFps = target, this.frameInterval = Math.ceil(1000 / target); this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
} }
getCanvas() { getCanvas() {
return this.$canvas; return this.$canvas;
@ -5117,6 +5090,7 @@ class WebGL2Player {
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation); gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
} }
drawFrame() { drawFrame() {
if (this.targetFps === 0) return;
if (this.targetFps < 60) { if (this.targetFps < 60) {
const currentTime = performance.now(); const currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) return; if (currentTime - this.lastFrameTime < this.frameInterval) return;
@ -5171,9 +5145,9 @@ class WebGL2Player {
BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
const gl = this.gl; const gl = this.gl;
if (gl) { if (gl) {
gl.getExtension("WEBGL_lose_context")?.loseContext(); gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
for (let resource of this.resources) for (let resource of this.resources)
if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource); if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
else if (resource instanceof WebGLShader) gl.deleteShader(resource); else if (resource instanceof WebGLShader) gl.deleteShader(resource);
else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 5.8.4 // @version 5.8.5
// ==/UserScript== // ==/UserScript==

View File

@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 5.8.4 // @version 5.8.5
// @description Improve Xbox Cloud Gaming (xCloud) experience // @description Improve Xbox Cloud Gaming (xCloud) experience
// @author redphx // @author redphx
// @license MIT // @license MIT
@ -113,7 +113,7 @@ function deepClone(obj) {
if (!obj) return {}; if (!obj) return {};
return JSON.parse(JSON.stringify(obj)); return JSON.parse(JSON.stringify(obj));
} }
var SCRIPT_VERSION = "5.8.4", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface; var SCRIPT_VERSION = "5.8.5", SCRIPT_VARIANT = "full", AppInterface = window.AppInterface;
UserAgent.init(); UserAgent.init();
var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = { var userAgent = window.navigator.userAgent.toLowerCase(), isTv = userAgent.includes("smart-tv") || userAgent.includes("smarttv") || /\baft.*\b/.test(userAgent), isVr = window.navigator.userAgent.includes("VR") && window.navigator.userAgent.includes("OculusBrowser"), browserHasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0, userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport, supportMkb = AppInterface || !userAgent.match(/(android|iphone|ipad)/), STATES = {
supportedRegion: !0, supportedRegion: !0,
@ -963,10 +963,7 @@ class BaseSettingsStore {
} }
class StreamStatsCollector { class StreamStatsCollector {
static instance; static instance;
static getInstance() { static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector);
if (!StreamStatsCollector.instance) StreamStatsCollector.instance = new StreamStatsCollector;
return StreamStatsCollector.instance;
}
static INTERVAL_BACKGROUND = 60000; static INTERVAL_BACKGROUND = 60000;
calculateGrade(value, grades) { calculateGrade(value, grades) {
return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : ""; return value > grades[2] ? "bad" : value > grades[1] ? "ok" : value > grades[0] ? "good" : "";
@ -989,7 +986,8 @@ class StreamStatsCollector {
fps: { fps: {
current: 0, current: 0,
toString() { toString() {
return this.current.toString(); const maxFps = getPref("video_max_fps");
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
} }
}, },
btr: { btr: {
@ -1878,10 +1876,7 @@ var MouseMapTo;
})(MouseMapTo ||= {}); })(MouseMapTo ||= {});
class StreamStats { class StreamStats {
static instance; static instance;
static getInstance() { static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats);
if (!StreamStats.instance) StreamStats.instance = new StreamStats;
return StreamStats.instance;
}
intervalId; intervalId;
REFRESH_INTERVAL = 1000; REFRESH_INTERVAL = 1000;
stats = { stats = {
@ -2258,14 +2253,13 @@ function onChangeVideoPlayerType() {
else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0; else if ($videoProcessing.value = "usm", setPref("video_processing", "usm"), $optCas && ($optCas.disabled = !0), UserAgent.isSafari()) isDisabled = !0;
$videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer(); $videoProcessing.disabled = isDisabled, $videoSharpness.dataset.disabled = isDisabled.toString(), $videoPowerPreference.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), $videoMaxFps.closest(".bx-settings-row").classList.toggle("bx-gone", playerType !== "webgl2"), updateVideoPlayer();
} }
function limitVideoPlayerFps() { function limitVideoPlayerFps(targetFps) {
const targetFps = getPref("video_max_fps");
STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); STATES.currentStream.streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
} }
function updateVideoPlayer() { function updateVideoPlayer() {
const streamPlayer = STATES.currentStream.streamPlayer; const streamPlayer = STATES.currentStream.streamPlayer;
if (!streamPlayer) return; if (!streamPlayer) return;
limitVideoPlayerFps(); limitVideoPlayerFps(getPref("video_max_fps"));
const options = { const options = {
processing: getPref("video_processing"), processing: getPref("video_processing"),
sharpness: getPref("video_sharpness"), sharpness: getPref("video_sharpness"),
@ -2498,10 +2492,7 @@ class KeyHelper {
var LOG_TAG = "PointerClient"; var LOG_TAG = "PointerClient";
class PointerClient { class PointerClient {
static instance; static instance;
static getInstance() { static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient);
if (!PointerClient.instance) PointerClient.instance = new PointerClient;
return PointerClient.instance;
}
socket; socket;
mkbHandler; mkbHandler;
start(port, mkbHandler) { start(port, mkbHandler) {
@ -2575,6 +2566,7 @@ class MouseDataProvider {
class MkbHandler {} class MkbHandler {}
class NativeMkbHandler extends MkbHandler { class NativeMkbHandler extends MkbHandler {
static instance; static instance;
static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler);
#pointerClient; #pointerClient;
#enabled = !1; #enabled = !1;
#mouseButtonsPressed = 0; #mouseButtonsPressed = 0;
@ -2584,10 +2576,6 @@ class NativeMkbHandler extends MkbHandler {
#mouseHorizontalMultiply = 0; #mouseHorizontalMultiply = 0;
#inputSink; #inputSink;
#$message; #$message;
static getInstance() {
if (!NativeMkbHandler.instance) NativeMkbHandler.instance = new NativeMkbHandler;
return NativeMkbHandler.instance;
}
#onKeyboardEvent(e) { #onKeyboardEvent(e) {
if (e.type === "keyup" && e.code === "F8") { if (e.type === "keyup" && e.code === "F8") {
e.preventDefault(), this.toggle(); e.preventDefault(), this.toggle();
@ -2793,11 +2781,8 @@ class PointerLockMouseDataProvider extends MouseDataProvider {
#disableContextMenu = (e) => e.preventDefault(); #disableContextMenu = (e) => e.preventDefault();
} }
class EmulatedMkbHandler extends MkbHandler { class EmulatedMkbHandler extends MkbHandler {
static #instance; static instance;
static getInstance() { static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler);
if (!EmulatedMkbHandler.#instance) EmulatedMkbHandler.#instance = new EmulatedMkbHandler;
return EmulatedMkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static DEFAULT_PANNING_SENSITIVITY = 0.001; static DEFAULT_PANNING_SENSITIVITY = 0.001;
static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01; static DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
@ -3081,10 +3066,7 @@ class NavigationDialog {
} }
class NavigationDialogManager { class NavigationDialogManager {
static instance; static instance;
static getInstance() { static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager);
if (!NavigationDialogManager.instance) NavigationDialogManager.instance = new NavigationDialogManager;
return NavigationDialogManager.instance;
}
static GAMEPAD_POLLING_INTERVAL = 50; static GAMEPAD_POLLING_INTERVAL = 50;
static GAMEPAD_KEYS = [ static GAMEPAD_KEYS = [
12, 12,
@ -4257,9 +4239,9 @@ true` + text;
return str = str.replace(text, newCode), str; return str = str.replace(text, newCode), str;
}, },
skipFeedbackDialog(str) { skipFeedbackDialog(str) {
let text = "&&this.shouldTransitionToFeedback("; let text = "shouldTransitionToFeedback(e){";
if (!str.includes(text)) return !1; if (!str.includes(text)) return !1;
return str = str.replace(text, "&& false " + text), str; return str = str.replace(text, text + "return !1;"), str;
}, },
enableNativeMkb(str) { enableNativeMkb(str) {
let text = "e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;"; let text = "e.mouseSupported&&e.keyboardSupported&&e.fullscreenSupported;";
@ -4551,10 +4533,7 @@ class PatcherCache {
} }
class FullscreenText { class FullscreenText {
static instance; static instance;
static getInstance() { static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText);
if (!FullscreenText.instance) FullscreenText.instance = new FullscreenText;
return FullscreenText.instance;
}
$text; $text;
constructor() { constructor() {
this.$text = CE("div", { this.$text = CE("div", {
@ -4570,10 +4549,7 @@ class FullscreenText {
} }
class SettingsNavigationDialog extends NavigationDialog { class SettingsNavigationDialog extends NavigationDialog {
static instance; static instance;
static getInstance() { static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog);
if (!SettingsNavigationDialog.instance) SettingsNavigationDialog.instance = new SettingsNavigationDialog;
return SettingsNavigationDialog.instance;
}
$container; $container;
$tabs; $tabs;
$settings; $settings;
@ -4838,7 +4814,9 @@ class SettingsNavigationDialog extends NavigationDialog {
onChange: onChangeVideoPlayerType onChange: onChangeVideoPlayerType
}, { }, {
pref: "video_max_fps", pref: "video_max_fps",
onChange: limitVideoPlayerFps onChange: (e) => {
limitVideoPlayerFps(parseInt(e.target.value));
}
}, { }, {
pref: "video_power_preference", pref: "video_power_preference",
onChange: () => { onChange: () => {
@ -5832,10 +5810,7 @@ class HeaderSection {
} }
class RemotePlayNavigationDialog extends NavigationDialog { class RemotePlayNavigationDialog extends NavigationDialog {
static instance; static instance;
static getInstance() { static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog);
if (!RemotePlayNavigationDialog.instance) RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog;
return RemotePlayNavigationDialog.instance;
}
STATE_LABELS = { STATE_LABELS = {
On: t("powered-on"), On: t("powered-on"),
Off: t("powered-off"), Off: t("powered-off"),
@ -5901,10 +5876,7 @@ class RemotePlayNavigationDialog extends NavigationDialog {
var LOG_TAG5 = "RemotePlay"; var LOG_TAG5 = "RemotePlay";
class RemotePlayManager { class RemotePlayManager {
static instance; static instance;
static getInstance() { static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager);
if (!this.instance) this.instance = new RemotePlayManager;
return this.instance;
}
isInitialized = !1; isInitialized = !1;
XCLOUD_TOKEN; XCLOUD_TOKEN;
XHOME_TOKEN; XHOME_TOKEN;
@ -6017,7 +5989,7 @@ class RemotePlayManager {
} }
} }
class XhomeInterceptor { class XhomeInterceptor {
static #consoleAddrs = {}; static consoleAddrs = {};
static BASE_DEVICE_INFO = { static BASE_DEVICE_INFO = {
appInfo: { appInfo: {
env: { env: {
@ -6056,7 +6028,7 @@ class XhomeInterceptor {
} }
} }
}; };
static async#handleLogin(request) { static async handleLogin(request) {
try { try {
const obj = await request.clone().json(); const obj = await request.clone().json();
obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", { obj.offeringId = "xhome", request = new Request("https://xhome.gssv-play-prod.xboxlive.com/v2/login/user", {
@ -6071,19 +6043,19 @@ class XhomeInterceptor {
} }
return NATIVE_FETCH(request); return NATIVE_FETCH(request);
} }
static async#handleConfiguration(request) { static async handleConfiguration(request) {
const response = await NATIVE_FETCH(request), obj = await response.clone().json(); const response = await NATIVE_FETCH(request), obj = await response.clone().json();
console.log(obj); console.log(obj);
const processPorts = (port) => { const processPorts = (port) => {
const ports = new Set; const ports = new Set;
return port && ports.add(port), ports.add(9002), Array.from(ports); return port && ports.add(port), ports.add(9002), Array.from(ports);
}, serverDetails = obj.serverDetails; }, serverDetails = obj.serverDetails;
if (serverDetails.ipAddress) XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); if (serverDetails.ipAddress) XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
if (serverDetails.ipV4Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); if (serverDetails.ipV4Address) XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
if (serverDetails.ipV6Address) XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); if (serverDetails.ipV6Address) XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
} }
static async#handleInputConfigs(request, opts) { static async handleInputConfigs(request, opts) {
const response = await NATIVE_FETCH(request); const response = await NATIVE_FETCH(request);
if (getPref("stream_touch_controller") !== "all") return response; if (getPref("stream_touch_controller") !== "all") return response;
const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0]; const obj = await response.clone().json(), xboxTitleId = JSON.parse(opts.body).titleIds[0];
@ -6100,7 +6072,7 @@ class XhomeInterceptor {
else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId); else TouchController.enable(), TouchController.requestCustomLayouts(xboxTitleId);
return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response; return response.json = () => Promise.resolve(obj), response.text = () => Promise.resolve(JSON.stringify(obj)), response;
} }
static async#handleTitles(request) { static async handleTitles(request) {
const clone = request.clone(), headers = {}; const clone = request.clone(), headers = {};
for (let pair of clone.headers.entries()) for (let pair of clone.headers.entries())
headers[pair[0]] = pair[1]; headers[pair[0]] = pair[1];
@ -6112,7 +6084,7 @@ class XhomeInterceptor {
headers headers
}), NATIVE_FETCH(request); }), NATIVE_FETCH(request);
} }
static async#handlePlay(request) { static async handlePlay(request) {
const body = await request.clone().json(), newRequest = new Request(request, { const body = await request.clone().json(), newRequest = new Request(request, {
body: JSON.stringify(body) body: JSON.stringify(body)
}); });
@ -6139,12 +6111,12 @@ class XhomeInterceptor {
} }
request = new Request(newUrl, opts); request = new Request(newUrl, opts);
let url = typeof request === "string" ? request : request.url; let url = typeof request === "string" ? request : request.url;
if (url.includes("/configuration")) return XhomeInterceptor.#handleConfiguration(request); if (url.includes("/configuration")) return XhomeInterceptor.handleConfiguration(request);
else if (url.endsWith("/sessions/home/play")) return XhomeInterceptor.#handlePlay(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("inputconfigs")) return XhomeInterceptor.handleInputConfigs(request, opts);
else if (url.includes("/login/user")) return XhomeInterceptor.#handleLogin(request); else if (url.includes("/login/user")) return XhomeInterceptor.handleLogin(request);
else if (url.endsWith("/titles")) return XhomeInterceptor.#handleTitles(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); else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
return await NATIVE_FETCH(request); return await NATIVE_FETCH(request);
} }
} }
@ -6413,10 +6385,7 @@ class GuideMenu {
} }
class StreamBadges { class StreamBadges {
static instance; static instance;
static getInstance() { static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges);
if (!StreamBadges.instance) StreamBadges.instance = new StreamBadges;
return StreamBadges.instance;
}
serverInfo = {}; serverInfo = {};
badges = { badges = {
playtime: { playtime: {
@ -6581,17 +6550,7 @@ class StreamBadges {
static setupEvents() {} static setupEvents() {}
} }
class XcloudInterceptor { class XcloudInterceptor {
static async#handleLogin(request, init) { static SERVER_EMOJIS = {
const bypassServer = getPref("server_bypass_restriction");
if (bypassServer !== "off") {
const ip = BypassServerIps[bypassServer];
ip && request.headers.set("X-Forwarded-For", ip);
}
const response = await NATIVE_FETCH(request, init);
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
const obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
const serverEmojis = {
AustraliaEast: "🇦🇺", AustraliaEast: "🇦🇺",
AustraliaSouthEast: "🇦🇺", AustraliaSouthEast: "🇦🇺",
BrazilSouth: "🇧🇷", BrazilSouth: "🇧🇷",
@ -6606,7 +6565,18 @@ class XcloudInterceptor {
WestEurope: "🇪🇺", WestEurope: "🇪🇺",
WestUS: "🇺🇸", WestUS: "🇺🇸",
WestUS2: "🇺🇸" WestUS2: "🇺🇸"
}, serverRegex = /\/\/(\w+)\./; };
static async handleLogin(request, init) {
const bypassServer = getPref("server_bypass_restriction");
if (bypassServer !== "off") {
const ip = BypassServerIps[bypassServer];
ip && request.headers.set("X-Forwarded-For", ip);
}
const response = await NATIVE_FETCH(request, init);
if (response.status !== 200) return BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE), response;
const obj = await response.clone().json();
RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
const serverRegex = /\/\/(\w+)\./, serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) { for (let region of obj.offeringSettings.regions) {
const regionName = region.name; const regionName = region.name;
let shortName = region.name; let shortName = region.name;
@ -6625,7 +6595,7 @@ class XcloudInterceptor {
} }
return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response; return STATES.gsToken = obj.gsToken, response.json = () => Promise.resolve(obj), response;
} }
static async#handlePlay(request, init) { static async handlePlay(request, init) {
const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url); const PREF_STREAM_TARGET_RESOLUTION = getPref("stream_target_resolution"), PREF_STREAM_PREFERRED_LOCALE = getPref("stream_preferred_locale"), url = typeof request === "string" ? request : request.url, parsedUrl = new URL(url);
let badgeRegion = parsedUrl.host.split(".", 1)[0]; let badgeRegion = parsedUrl.host.split(".", 1)[0];
for (let regionName in STATES.serverRegions) { for (let regionName in STATES.serverRegions) {
@ -6647,7 +6617,7 @@ class XcloudInterceptor {
}); });
return NATIVE_FETCH(newRequest); return NATIVE_FETCH(newRequest);
} }
static async#handleWaitTime(request, init) { static async handleWaitTime(request, init) {
const response = await NATIVE_FETCH(request, init); const response = await NATIVE_FETCH(request, init);
if (getPref("ui_loading_screen_wait_time")) { if (getPref("ui_loading_screen_wait_time")) {
const json = await response.clone().json(); const json = await response.clone().json();
@ -6655,7 +6625,7 @@ class XcloudInterceptor {
} }
return response; return response;
} }
static async#handleConfiguration(request, init) { static async handleConfiguration(request, init) {
if (request.method !== "GET") return NATIVE_FETCH(request, init); if (request.method !== "GET") return NATIVE_FETCH(request, init);
if (getPref("stream_touch_controller") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable(); if (getPref("stream_touch_controller") === "all") if (STATES.currentStream.titleInfo?.details.hasTouchSupport) TouchController.disable();
else TouchController.enable(); else TouchController.enable();
@ -6677,10 +6647,10 @@ class XcloudInterceptor {
} }
static async handle(request, init) { static async handle(request, init) {
let url = typeof request === "string" ? request : request.url; let url = typeof request === "string" ? request : request.url;
if (url.endsWith("/v2/login/user")) return XcloudInterceptor.#handleLogin(request, init); 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.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.includes("xboxlive.com") && url.includes("/waittime/")) return XcloudInterceptor.handleWaitTime(request, init);
else if (url.endsWith("/configuration")) return XcloudInterceptor.#handleConfiguration(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); else if (url && url.endsWith("/ice") && url.includes("/sessions/") && request.method === "GET") return patchIceCandidates(request);
return NATIVE_FETCH(request, init); return NATIVE_FETCH(request, init);
} }
@ -6991,7 +6961,7 @@ class WebGL2Player {
saturation: 0 saturation: 0
}; };
targetFps = 60; targetFps = 60;
frameInterval = Math.ceil(1000 / this.targetFps); frameInterval = 0;
lastFrameTime = 0; lastFrameTime = 0;
animFrameId = null; animFrameId = null;
constructor($video) { constructor($video) {
@ -7015,7 +6985,7 @@ class WebGL2Player {
this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas(); this.options.saturation = 1 + (saturation - 100) / 100, update && this.updateCanvas();
} }
setTargetFps(target) { setTargetFps(target) {
this.targetFps = target, this.frameInterval = Math.ceil(1000 / target); this.targetFps = target, this.lastFrameTime = 0, this.frameInterval = target ? Math.floor(1000 / target) : 0;
} }
getCanvas() { getCanvas() {
return this.$canvas; return this.$canvas;
@ -7025,6 +6995,7 @@ class WebGL2Player {
gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation); gl.uniform2f(gl.getUniformLocation(program, "iResolution"), this.$canvas.width, this.$canvas.height), gl.uniform1i(gl.getUniformLocation(program, "filterId"), this.options.filterId), gl.uniform1f(gl.getUniformLocation(program, "sharpenFactor"), this.options.sharpenFactor), gl.uniform1f(gl.getUniformLocation(program, "brightness"), this.options.brightness), gl.uniform1f(gl.getUniformLocation(program, "contrast"), this.options.contrast), gl.uniform1f(gl.getUniformLocation(program, "saturation"), this.options.saturation);
} }
drawFrame() { drawFrame() {
if (this.targetFps === 0) return;
if (this.targetFps < 60) { if (this.targetFps < 60) {
const currentTime = performance.now(); const currentTime = performance.now();
if (currentTime - this.lastFrameTime < this.frameInterval) return; if (currentTime - this.lastFrameTime < this.frameInterval) return;
@ -7079,9 +7050,9 @@ class WebGL2Player {
BxLogger.info(this.LOG_TAG, "Destroy"), this.stop(); BxLogger.info(this.LOG_TAG, "Destroy"), this.stop();
const gl = this.gl; const gl = this.gl;
if (gl) { if (gl) {
gl.getExtension("WEBGL_lose_context")?.loseContext(); gl.getExtension("WEBGL_lose_context")?.loseContext(), gl.useProgram(null);
for (let resource of this.resources) for (let resource of this.resources)
if (resource instanceof WebGLProgram) gl.useProgram(null), gl.deleteProgram(resource); if (resource instanceof WebGLProgram) gl.deleteProgram(resource);
else if (resource instanceof WebGLShader) gl.deleteShader(resource); else if (resource instanceof WebGLShader) gl.deleteShader(resource);
else if (resource instanceof WebGLTexture) gl.deleteTexture(resource); else if (resource instanceof WebGLTexture) gl.deleteTexture(resource);
else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource); else if (resource instanceof WebGLBuffer) gl.deleteBuffer(resource);
@ -7343,6 +7314,9 @@ class BaseGameBarAction {
onClick(e) { onClick(e) {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
} }
render() {
return this.$content;
}
} }
class ScreenshotAction extends BaseGameBarAction { class ScreenshotAction extends BaseGameBarAction {
$content; $content;
@ -7358,9 +7332,6 @@ class ScreenshotAction extends BaseGameBarAction {
onClick(e) { onClick(e) {
super.onClick(e), Screenshot.takeScreenshot(); super.onClick(e), Screenshot.takeScreenshot();
} }
render() {
return this.$content;
}
} }
class TouchControlAction extends BaseGameBarAction { class TouchControlAction extends BaseGameBarAction {
$content; $content;
@ -7378,23 +7349,19 @@ class TouchControlAction extends BaseGameBarAction {
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
classes: ["bx-activated"] classes: ["bx-activated"]
}); });
this.$content = CE("div", {}, $btnEnable, $btnDisable), this.reset(); this.$content = CE("div", {}, $btnEnable, $btnDisable);
} }
onClick(e) { onClick(e) {
super.onClick(e); super.onClick(e);
const isVisible = TouchController.toggleVisibility(); const isVisible = TouchController.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render() {
return this.$content;
}
reset() { reset() {
this.$content.dataset.activated = "false"; this.$content.dataset.activated = "false";
} }
} }
class MicrophoneAction extends BaseGameBarAction { class MicrophoneAction extends BaseGameBarAction {
$content; $content;
visible = !1;
constructor() { constructor() {
super(); super();
const $btnDefault = createButton({ const $btnDefault = createButton({
@ -7407,7 +7374,7 @@ class MicrophoneAction extends BaseGameBarAction {
icon: BxIcon.MICROPHONE_MUTED, icon: BxIcon.MICROPHONE_MUTED,
onClick: this.onClick.bind(this) onClick: this.onClick.bind(this)
}); });
this.$content = CE("div", {}, $btnMuted, $btnDefault), this.reset(), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => { this.$content = CE("div", {}, $btnMuted, $btnDefault), window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, (e) => {
const enabled = e.microphoneState === "Enabled"; const enabled = e.microphoneState === "Enabled";
this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone"); this.$content.dataset.activated = enabled.toString(), this.$content.classList.remove("bx-gone");
}); });
@ -7417,11 +7384,8 @@ class MicrophoneAction extends BaseGameBarAction {
const enabled = MicrophoneShortcut.toggle(!1); const enabled = MicrophoneShortcut.toggle(!1);
this.$content.dataset.activated = enabled.toString(); this.$content.dataset.activated = enabled.toString();
} }
render() {
return this.$content;
}
reset() { reset() {
this.visible = !1, this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false"; this.$content.classList.add("bx-gone"), this.$content.dataset.activated = "false";
} }
} }
class TrueAchievementsAction extends BaseGameBarAction { class TrueAchievementsAction extends BaseGameBarAction {
@ -7431,16 +7395,12 @@ class TrueAchievementsAction extends BaseGameBarAction {
this.$content = createButton({ this.$content = createButton({
style: 4, style: 4,
icon: BxIcon.TRUE_ACHIEVEMENTS, icon: BxIcon.TRUE_ACHIEVEMENTS,
title: t("true-achievements"),
onClick: this.onClick.bind(this) onClick: this.onClick.bind(this)
}); });
} }
onClick(e) { onClick(e) {
super.onClick(e), TrueAchievements.open(!1); super.onClick(e), TrueAchievements.open(!1);
} }
render() {
return this.$content;
}
} }
class SpeakerAction extends BaseGameBarAction { class SpeakerAction extends BaseGameBarAction {
$content; $content;
@ -7456,7 +7416,7 @@ class SpeakerAction extends BaseGameBarAction {
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
classes: ["bx-activated"] classes: ["bx-activated"]
}); });
this.$content = CE("div", {}, $btnEnable, $btnMuted), this.reset(), window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, (e) => { this.$content = CE("div", {}, $btnEnable, $btnMuted), window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, (e) => {
const enabled = e.speakerState === 0; const enabled = e.speakerState === 0;
this.$content.dataset.activated = (!enabled).toString(); this.$content.dataset.activated = (!enabled).toString();
}); });
@ -7464,9 +7424,6 @@ class SpeakerAction extends BaseGameBarAction {
onClick(e) { onClick(e) {
super.onClick(e), SoundShortcut.muteUnmute(); super.onClick(e), SoundShortcut.muteUnmute();
} }
render() {
return this.$content;
}
reset() { reset() {
this.$content.dataset.activated = "false"; this.$content.dataset.activated = "false";
} }
@ -7475,7 +7432,9 @@ class RendererShortcut {
static toggleVisibility() { static toggleVisibility() {
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
if (!$mediaContainer) return !0; if (!$mediaContainer) return !0;
return $mediaContainer.classList.toggle("bx-gone"), !$mediaContainer.classList.contains("bx-gone"); $mediaContainer.classList.toggle("bx-gone");
const isShowing = !$mediaContainer.classList.contains("bx-gone");
return limitVideoPlayerFps(isShowing ? getPref("video_max_fps") : 0), isShowing;
} }
} }
class RendererAction extends BaseGameBarAction { class RendererAction extends BaseGameBarAction {
@ -7492,26 +7451,20 @@ class RendererAction extends BaseGameBarAction {
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
classes: ["bx-activated"] classes: ["bx-activated"]
}); });
this.$content = CE("div", {}, $btnDefault, $btnActivated), this.reset(); this.$content = CE("div", {}, $btnDefault, $btnActivated);
} }
onClick(e) { onClick(e) {
super.onClick(e); super.onClick(e);
const isVisible = RendererShortcut.toggleVisibility(); const isVisible = RendererShortcut.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render() {
return this.$content;
}
reset() { reset() {
this.$content.dataset.activated = "false"; this.$content.dataset.activated = "false";
} }
} }
class GameBar { class GameBar {
static instance; static instance;
static getInstance() { static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar);
if (!GameBar.instance) GameBar.instance = new GameBar;
return GameBar.instance;
}
static VISIBLE_DURATION = 2000; static VISIBLE_DURATION = 2000;
$gameBar; $gameBar;
$container; $container;
@ -7535,8 +7488,7 @@ class GameBar {
if (e.target !== $gameBar) return; if (e.target !== $gameBar) return;
$container.classList.contains("bx-show") ? this.hideBar() : this.showBar(); $container.classList.contains("bx-show") ? this.hideBar() : this.showBar();
}), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => { }), window.addEventListener(BxEvent.GAME_BAR_ACTION_ACTIVATED, this.hideBar.bind(this)), $container.addEventListener("pointerover", this.clearHideTimeout.bind(this)), $container.addEventListener("pointerout", this.beginHideTimeout.bind(this)), $container.addEventListener("transitionend", (e) => {
const classList = $container.classList; $container.classList.replace("bx-hide", "bx-offscreen");
if (classList.contains("bx-hide")) classList.remove("bx-hide"), classList.add("bx-offscreen");
}), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref("game_bar_position") !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => { }), document.documentElement.appendChild($gameBar), this.$gameBar = $gameBar, this.$container = $container, getPref("game_bar_position") !== "off" && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e) => {
if (!STATES.isPlaying) { if (!STATES.isPlaying) {
this.disable(); this.disable();
@ -7554,30 +7506,24 @@ class GameBar {
this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null; this.timeoutId && clearTimeout(this.timeoutId), this.timeoutId = null;
} }
enable() { enable() {
this.$gameBar && this.$gameBar.classList.remove("bx-gone"); this.$gameBar.classList.remove("bx-gone");
} }
disable() { disable() {
this.hideBar(), this.$gameBar && this.$gameBar.classList.add("bx-gone"); this.hideBar(), this.$gameBar.classList.add("bx-gone");
} }
showBar() { showBar() {
if (!this.$container) return;
this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout(); this.$container.classList.remove("bx-offscreen", "bx-hide", "bx-gone"), this.$container.classList.add("bx-show"), this.beginHideTimeout();
} }
hideBar() { hideBar() {
if (this.clearHideTimeout(), clearFocus(), !this.$container) return; this.clearHideTimeout(), clearFocus(), this.$container.classList.replace("bx-show", "bx-hide");
this.$container.classList.remove("bx-show"), this.$container.classList.add("bx-hide");
} }
reset() { reset() {
for (let action of this.actions) this.actions.forEach((action) => action.reset());
action.reset();
} }
} }
class XcloudApi { class XcloudApi {
static instance; static instance;
static getInstance() { static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi);
if (!XcloudApi.instance) XcloudApi.instance = new XcloudApi;
return XcloudApi.instance;
}
CACHE_TITLES = {}; CACHE_TITLES = {};
CACHE_WAIT_TIME = {}; CACHE_WAIT_TIME = {};
async getTitleInfo(id2) { async getTitleInfo(id2) {

View File

@ -1,6 +1,8 @@
import { BxEvent } from "@/utils/bx-event"; import { BxEvent } from "@/utils/bx-event";
export abstract class BaseGameBarAction { export abstract class BaseGameBarAction {
abstract $content: HTMLElement;
constructor() {} constructor() {}
reset() {} reset() {}
@ -8,5 +10,7 @@ export abstract class BaseGameBarAction {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED); BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
}; };
abstract render(): HTMLElement; render(): HTMLElement {
return this.$content;
};
} }

View File

@ -8,8 +8,6 @@ import { MicrophoneShortcut, MicrophoneState } from "../shortcuts/shortcut-micro
export class MicrophoneAction extends BaseGameBarAction { export class MicrophoneAction extends BaseGameBarAction {
$content: HTMLElement; $content: HTMLElement;
visible: boolean = false;
constructor() { constructor() {
super(); super();
@ -26,12 +24,7 @@ export class MicrophoneAction extends BaseGameBarAction {
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnMuted, $btnDefault);
$btnMuted,
$btnDefault,
);
this.reset();
window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => { window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => {
const microphoneState = (e as any).microphoneState; const microphoneState = (e as any).microphoneState;
@ -49,12 +42,7 @@ export class MicrophoneAction extends BaseGameBarAction {
this.$content.dataset.activated = enabled.toString(); this.$content.dataset.activated = enabled.toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.visible = false;
this.$content.classList.add('bx-gone'); this.$content.classList.add('bx-gone');
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -23,12 +23,7 @@ export class RendererAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnDefault, $btnActivated);
$btnDefault,
$btnActivated,
);
this.reset();
} }
onClick(e: Event) { onClick(e: Event) {
@ -37,10 +32,6 @@ export class RendererAction extends BaseGameBarAction {
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -22,8 +22,4 @@ export class ScreenshotAction extends BaseGameBarAction {
super.onClick(e); super.onClick(e);
Screenshot.takeScreenshot(); Screenshot.takeScreenshot();
} }
render(): HTMLElement {
return this.$content;
}
} }

View File

@ -24,12 +24,7 @@ export class SpeakerAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnEnable, $btnMuted);
$btnEnable,
$btnMuted,
);
this.reset();
window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => { window.addEventListener(BxEvent.SPEAKER_STATE_CHANGED, e => {
const speakerState = (e as any).speakerState; const speakerState = (e as any).speakerState;
@ -44,10 +39,6 @@ export class SpeakerAction extends BaseGameBarAction {
SoundShortcut.muteUnmute(); SoundShortcut.muteUnmute();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -25,12 +25,7 @@ export class TouchControlAction extends BaseGameBarAction {
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
this.$content = CE('div', {}, this.$content = CE('div', {}, $btnEnable, $btnDisable);
$btnEnable,
$btnDisable,
);
this.reset();
} }
onClick(e: Event) { onClick(e: Event) {
@ -39,10 +34,6 @@ export class TouchControlAction extends BaseGameBarAction {
this.$content.dataset.activated = (!isVisible).toString(); this.$content.dataset.activated = (!isVisible).toString();
} }
render(): HTMLElement {
return this.$content;
}
reset(): void { reset(): void {
this.$content.dataset.activated = 'false'; this.$content.dataset.activated = 'false';
} }

View File

@ -1,6 +1,5 @@
import { BxIcon } from "@/utils/bx-icon"; import { BxIcon } from "@/utils/bx-icon";
import { createButton, ButtonStyle } from "@/utils/html"; import { createButton, ButtonStyle } from "@/utils/html";
import { t } from "@/utils/translation";
import { BaseGameBarAction } from "./action-base"; import { BaseGameBarAction } from "./action-base";
import { TrueAchievements } from "@/utils/true-achievements"; import { TrueAchievements } from "@/utils/true-achievements";
@ -13,7 +12,6 @@ export class TrueAchievementsAction extends BaseGameBarAction {
this.$content = createButton({ this.$content = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.TRUE_ACHIEVEMENTS, icon: BxIcon.TRUE_ACHIEVEMENTS,
title: t('true-achievements'),
onClick: this.onClick.bind(this), onClick: this.onClick.bind(this),
}); });
} }
@ -22,8 +20,4 @@ export class TrueAchievementsAction extends BaseGameBarAction {
super.onClick(e); super.onClick(e);
TrueAchievements.open(false); TrueAchievements.open(false);
} }
render(): HTMLElement {
return this.$content;
}
} }

View File

@ -7,7 +7,7 @@ import type { BaseGameBarAction } from "./action-base";
import { STATES } from "@utils/global"; import { STATES } from "@utils/global";
import { MicrophoneAction } from "./action-microphone"; import { MicrophoneAction } from "./action-microphone";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage"; import { getPref, StreamTouchController, type GameBarPosition } from "@/utils/settings-storages/global-settings-storage";
import { TrueAchievementsAction } from "./action-true-achievements"; import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker"; import { SpeakerAction } from "./action-speaker";
import { RendererAction } from "./action-renderer"; import { RendererAction } from "./action-renderer";
@ -15,13 +15,7 @@ import { RendererAction } from "./action-renderer";
export class GameBar { export class GameBar {
private static instance: GameBar; private static instance: GameBar;
public static getInstance(): GameBar { public static getInstance = () => GameBar.instance ?? (GameBar.instance = new GameBar());
if (!GameBar.instance) {
GameBar.instance = new GameBar();
}
return GameBar.instance;
}
private static readonly VISIBLE_DURATION = 2000; private static readonly VISIBLE_DURATION = 2000;
@ -35,7 +29,7 @@ export class GameBar {
private constructor() { private constructor() {
let $container; let $container;
const position = getPref(PrefKey.GAME_BAR_POSITION); const position = getPref(PrefKey.GAME_BAR_POSITION) as GameBarPosition;
const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position}, const $gameBar = CE('div', {id: 'bx-game-bar', class: 'bx-gone', 'data-position': position},
$container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}), $container = CE('div', {class: 'bx-game-bar-container bx-offscreen'}),
@ -78,11 +72,7 @@ export class GameBar {
// Add animation when hiding game bar // Add animation when hiding game bar
$container.addEventListener('transitionend', e => { $container.addEventListener('transitionend', e => {
const classList = $container.classList; $container.classList.replace('bx-hide', 'bx-offscreen');
if (classList.contains('bx-hide')) {
classList.remove('bx-hide');
classList.add('bx-offscreen');
}
}); });
document.documentElement.appendChild($gameBar); document.documentElement.appendChild($gameBar);
@ -117,19 +107,15 @@ export class GameBar {
} }
enable() { enable() {
this.$gameBar && this.$gameBar.classList.remove('bx-gone'); this.$gameBar.classList.remove('bx-gone');
} }
disable() { disable() {
this.hideBar(); this.hideBar();
this.$gameBar && this.$gameBar.classList.add('bx-gone'); this.$gameBar.classList.add('bx-gone');
} }
showBar() { showBar() {
if (!this.$container) {
return;
}
this.$container.classList.remove('bx-offscreen', 'bx-hide' , 'bx-gone'); this.$container.classList.remove('bx-offscreen', 'bx-hide' , 'bx-gone');
this.$container.classList.add('bx-show'); this.$container.classList.add('bx-show');
@ -142,18 +128,11 @@ export class GameBar {
// Stop focusing Game Bar // Stop focusing Game Bar
clearFocus(); clearFocus();
if (!this.$container) { this.$container.classList.replace('bx-show', 'bx-hide');
return;
}
this.$container.classList.remove('bx-show');
this.$container.classList.add('bx-hide');
} }
// Reset all states // Reset all states
reset() { reset() {
for (const action of this.actions) { this.actions.forEach(action => action.reset());
action.reset();
}
} }
} }

View File

@ -124,14 +124,8 @@ This class uses some code from Yuzu emulator to handle mouse's movements
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
*/ */
export class EmulatedMkbHandler extends MkbHandler { export class EmulatedMkbHandler extends MkbHandler {
static #instance: EmulatedMkbHandler; private static instance: EmulatedMkbHandler;
public static getInstance(): EmulatedMkbHandler { public static getInstance = () => EmulatedMkbHandler.instance ?? (EmulatedMkbHandler.instance = new EmulatedMkbHandler());
if (!EmulatedMkbHandler.#instance) {
EmulatedMkbHandler.#instance = new EmulatedMkbHandler();
}
return EmulatedMkbHandler.#instance;
}
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET); #CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);

View File

@ -23,6 +23,8 @@ type XcloudInputSink = {
export class NativeMkbHandler extends MkbHandler { export class NativeMkbHandler extends MkbHandler {
private static instance: NativeMkbHandler; private static instance: NativeMkbHandler;
public static getInstance = () => NativeMkbHandler.instance ?? (NativeMkbHandler.instance = new NativeMkbHandler());
#pointerClient: PointerClient | undefined; #pointerClient: PointerClient | undefined;
#enabled: boolean = false; #enabled: boolean = false;
@ -37,14 +39,6 @@ export class NativeMkbHandler extends MkbHandler {
#$message?: HTMLElement; #$message?: HTMLElement;
public static getInstance(): NativeMkbHandler {
if (!NativeMkbHandler.instance) {
NativeMkbHandler.instance = new NativeMkbHandler();
}
return NativeMkbHandler.instance;
}
#onKeyboardEvent(e: KeyboardEvent) { #onKeyboardEvent(e: KeyboardEvent) {
if (e.type === 'keyup' && e.code === 'F8') { if (e.type === 'keyup' && e.code === 'F8') {
e.preventDefault(); e.preventDefault();

View File

@ -15,13 +15,7 @@ enum PointerAction {
export class PointerClient { export class PointerClient {
private static instance: PointerClient; private static instance: PointerClient;
public static getInstance(): PointerClient { public static getInstance = () => PointerClient.instance ?? (PointerClient.instance = new PointerClient());
if (!PointerClient.instance) {
PointerClient.instance = new PointerClient();
}
return PointerClient.instance;
}
private socket: WebSocket | undefined | null; private socket: WebSocket | undefined | null;
private mkbHandler: MkbHandler | undefined; private mkbHandler: MkbHandler | undefined;

View File

@ -632,12 +632,12 @@ true` + text;
}, },
skipFeedbackDialog(str: string) { skipFeedbackDialog(str: string) {
let text = '&&this.shouldTransitionToFeedback('; let text = 'shouldTransitionToFeedback(e){';
if (!str.includes(text)) { if (!str.includes(text)) {
return false; return false;
} }
str = str.replace(text, '&& false ' + text); str = str.replace(text, text + 'return !1;');
return str; return str;
}, },

View File

@ -26,7 +26,7 @@ export class WebGL2Player {
}; };
private targetFps = 60; private targetFps = 60;
private frameInterval = Math.ceil(1000 / this.targetFps); private frameInterval = 0;
private lastFrameTime = 0; private lastFrameTime = 0;
private animFrameId: number | null = null; private animFrameId: number | null = null;
@ -73,7 +73,8 @@ export class WebGL2Player {
setTargetFps(target: number) { setTargetFps(target: number) {
this.targetFps = target; this.targetFps = target;
this.frameInterval = Math.ceil(1000 / target); this.lastFrameTime = 0;
this.frameInterval = target ? Math.floor(1000 / target) : 0;
} }
getCanvas() { getCanvas() {
@ -94,6 +95,11 @@ export class WebGL2Player {
} }
drawFrame() { drawFrame() {
// Don't draw when FPS is 0
if (this.targetFps === 0) {
return;
}
// Limit FPS // Limit FPS
if (this.targetFps < 60) { if (this.targetFps < 60) {
const currentTime = performance.now(); const currentTime = performance.now();
@ -233,10 +239,10 @@ export class WebGL2Player {
const gl = this.gl; const gl = this.gl;
if (gl) { if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext(); gl.getExtension('WEBGL_lose_context')?.loseContext();
gl.useProgram(null);
for (const resource of this.resources) { for (const resource of this.resources) {
if (resource instanceof WebGLProgram) { if (resource instanceof WebGLProgram) {
gl.useProgram(null);
gl.deleteProgram(resource); gl.deleteProgram(resource);
} else if (resource instanceof WebGLShader) { } else if (resource instanceof WebGLShader) {
gl.deleteShader(resource); gl.deleteShader(resource);

View File

@ -37,13 +37,7 @@ type RemotePlayConsole = {
export class RemotePlayManager { export class RemotePlayManager {
private static instance: RemotePlayManager; private static instance: RemotePlayManager;
public static getInstance(): RemotePlayManager { public static getInstance = () => RemotePlayManager.instance ?? (RemotePlayManager.instance = new RemotePlayManager());
if (!this.instance) {
this.instance = new RemotePlayManager();
}
return this.instance;
}
private isInitialized = false; private isInitialized = false;

View File

@ -1,3 +1,7 @@
import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { limitVideoPlayerFps } from "../stream/stream-settings-utils";
export class RendererShortcut { export class RendererShortcut {
static toggleVisibility(): boolean { static toggleVisibility(): boolean {
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]'); const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
@ -6,6 +10,9 @@ export class RendererShortcut {
} }
$mediaContainer.classList.toggle('bx-gone'); $mediaContainer.classList.toggle('bx-gone');
return !$mediaContainer.classList.contains('bx-gone'); const isShowing = !$mediaContainer.classList.contains('bx-gone');
// Switch FPS
limitVideoPlayerFps(isShowing ? getPref(PrefKey.VIDEO_MAX_FPS) : 0);
return isShowing;
} }
} }

View File

@ -50,13 +50,7 @@ enum StreamBadge {
export class StreamBadges { export class StreamBadges {
private static instance: StreamBadges; private static instance: StreamBadges;
public static getInstance(): StreamBadges { public static getInstance = () => StreamBadges.instance ?? (StreamBadges.instance = new StreamBadges());
if (!StreamBadges.instance) {
StreamBadges.instance = new StreamBadges();
}
return StreamBadges.instance;
}
private serverInfo: StreamServerInfo = {}; private serverInfo: StreamServerInfo = {};

View File

@ -45,8 +45,7 @@ export function onChangeVideoPlayerType() {
} }
export function limitVideoPlayerFps() { export function limitVideoPlayerFps(targetFps: number) {
const targetFps = getPref(PrefKey.VIDEO_MAX_FPS);
const streamPlayer = STATES.currentStream.streamPlayer; const streamPlayer = STATES.currentStream.streamPlayer;
streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps); streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
} }
@ -58,7 +57,7 @@ export function updateVideoPlayer() {
return; return;
} }
limitVideoPlayerFps(); limitVideoPlayerFps(getPref(PrefKey.VIDEO_MAX_FPS));
const options = { const options = {
processing: getPref(PrefKey.VIDEO_PROCESSING), processing: getPref(PrefKey.VIDEO_PROCESSING),

View File

@ -9,13 +9,7 @@ import { StreamStat, StreamStatsCollector, type StreamStatGrade } from "@/utils/
export class StreamStats { export class StreamStats {
private static instance: StreamStats; private static instance: StreamStats;
public static getInstance(): StreamStats { public static getInstance = () => StreamStats.instance ?? (StreamStats.instance = new StreamStats());
if (!StreamStats.instance) {
StreamStats.instance = new StreamStats();
}
return StreamStats.instance;
}
private intervalId?: number | null; private intervalId?: number | null;
private readonly REFRESH_INTERVAL = 1 * 1000; private readonly REFRESH_INTERVAL = 1 * 1000;

View File

@ -88,12 +88,7 @@ export abstract class NavigationDialog {
export class NavigationDialogManager { export class NavigationDialogManager {
private static instance: NavigationDialogManager; private static instance: NavigationDialogManager;
public static getInstance(): NavigationDialogManager { public static getInstance = () => NavigationDialogManager.instance ?? (NavigationDialogManager.instance = new NavigationDialogManager());
if (!NavigationDialogManager.instance) {
NavigationDialogManager.instance = new NavigationDialogManager();
}
return NavigationDialogManager.instance;
}
private static readonly GAMEPAD_POLLING_INTERVAL = 50; private static readonly GAMEPAD_POLLING_INTERVAL = 50;
private static readonly GAMEPAD_KEYS = [ private static readonly GAMEPAD_KEYS = [

View File

@ -11,12 +11,7 @@ import { BxEvent } from "@/utils/bx-event";
export class RemotePlayNavigationDialog extends NavigationDialog { export class RemotePlayNavigationDialog extends NavigationDialog {
private static instance: RemotePlayNavigationDialog; private static instance: RemotePlayNavigationDialog;
public static getInstance(): RemotePlayNavigationDialog { public static getInstance = () => RemotePlayNavigationDialog.instance ?? (RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog());
if (!RemotePlayNavigationDialog.instance) {
RemotePlayNavigationDialog.instance = new RemotePlayNavigationDialog();
}
return RemotePlayNavigationDialog.instance;
}
private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = { private readonly STATE_LABELS: Record<RemotePlayConsoleState, string> = {
[RemotePlayConsoleState.ON]: t('powered-on'), [RemotePlayConsoleState.ON]: t('powered-on'),

View File

@ -64,12 +64,7 @@ type SettingTab = {
export class SettingsNavigationDialog extends NavigationDialog { export class SettingsNavigationDialog extends NavigationDialog {
private static instance: SettingsNavigationDialog; private static instance: SettingsNavigationDialog;
public static getInstance(): SettingsNavigationDialog { public static getInstance = () => SettingsNavigationDialog.instance ?? (SettingsNavigationDialog.instance = new SettingsNavigationDialog());
if (!SettingsNavigationDialog.instance) {
SettingsNavigationDialog.instance = new SettingsNavigationDialog();
}
return SettingsNavigationDialog.instance;
}
$container!: HTMLElement; $container!: HTMLElement;
private $tabs!: HTMLElement; private $tabs!: HTMLElement;
@ -409,7 +404,9 @@ export class SettingsNavigationDialog extends NavigationDialog {
onChange: onChangeVideoPlayerType, onChange: onChangeVideoPlayerType,
}, { }, {
pref: PrefKey.VIDEO_MAX_FPS, pref: PrefKey.VIDEO_MAX_FPS,
onChange: limitVideoPlayerFps, onChange: e => {
limitVideoPlayerFps(parseInt(e.target.value));
},
}, { }, {
pref: PrefKey.VIDEO_POWER_PREFERENCE, pref: PrefKey.VIDEO_POWER_PREFERENCE,
onChange: () => { onChange: () => {

View File

@ -2,12 +2,7 @@ import { CE } from "@/utils/html";
export class FullscreenText { export class FullscreenText {
private static instance: FullscreenText; private static instance: FullscreenText;
public static getInstance(): FullscreenText { public static getInstance = () => FullscreenText.instance ?? (FullscreenText.instance = new FullscreenText());
if (!FullscreenText.instance) {
FullscreenText.instance = new FullscreenText();
}
return FullscreenText.instance;
}
$text: HTMLElement; $text: HTMLElement;

View File

@ -39,6 +39,10 @@ export const enum ControllerDeviceVibration {
} }
export type GameBarPosition = 'bottom-left' | 'bottom-right' | 'off';
export type GameBarPositionOptions = Record<GameBarPosition, string>;
function getSupportedCodecProfiles() { function getSupportedCodecProfiles() {
const options: PartialRecord<CodecProfile, string> = { const options: PartialRecord<CodecProfile, string> = {
default: t('default'), default: t('default'),
@ -323,12 +327,12 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
[PrefKey.GAME_BAR_POSITION]: { [PrefKey.GAME_BAR_POSITION]: {
requiredVariants: 'full', requiredVariants: 'full',
label: t('position'), label: t('position'),
default: 'bottom-left', default: 'bottom-left' satisfies GameBarPosition,
options: { options: {
'bottom-left': t('bottom-left'), 'bottom-left': t('bottom-left'),
'bottom-right': t('bottom-right'), 'bottom-right': t('bottom-right'),
'off': t('off'), 'off': t('off'),
}, } satisfies GameBarPositionOptions,
}, },
[PrefKey.LOCAL_CO_OP_ENABLED]: { [PrefKey.LOCAL_CO_OP_ENABLED]: {

View File

@ -1,6 +1,8 @@
import { PrefKey } from "@/enums/pref-keys";
import { BxEvent } from "./bx-event"; import { BxEvent } from "./bx-event";
import { STATES } from "./global"; import { STATES } from "./global";
import { humanFileSize, secondsToHm } from "./html"; import { humanFileSize, secondsToHm } from "./html";
import { getPref } from "./settings-storages/global-settings-storage";
export enum StreamStat { export enum StreamStat {
PING = 'ping', PING = 'ping',
@ -92,13 +94,7 @@ type CurrentStats = {
export class StreamStatsCollector { export class StreamStatsCollector {
private static instance: StreamStatsCollector; private static instance: StreamStatsCollector;
public static getInstance(): StreamStatsCollector { public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
if (!StreamStatsCollector.instance) {
StreamStatsCollector.instance = new StreamStatsCollector();
}
return StreamStatsCollector.instance;
}
// Collect in background - 60 seconds // Collect in background - 60 seconds
static readonly INTERVAL_BACKGROUND = 60 * 1000; static readonly INTERVAL_BACKGROUND = 60 * 1000;
@ -127,7 +123,8 @@ export class StreamStatsCollector {
[StreamStat.FPS]: { [StreamStat.FPS]: {
current: 0, current: 0,
toString() { toString() {
return this.current.toString(); const maxFps = getPref(PrefKey.VIDEO_MAX_FPS);
return maxFps < 60 ? `${maxFps}/${this.current}` : this.current.toString();
}, },
}, },

View File

@ -3,13 +3,7 @@ import { STATES } from "./global";
export class XcloudApi { export class XcloudApi {
private static instance: XcloudApi; private static instance: XcloudApi;
public static getInstance(): XcloudApi { public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
if (!XcloudApi.instance) {
XcloudApi.instance = new XcloudApi();
}
return XcloudApi.instance;
}
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {}; private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {}; private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};

View File

@ -13,9 +13,25 @@ import { BypassServerIps } from "@/enums/bypass-servers";
import { PrefKey } from "@/enums/pref-keys"; import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage"; import { getPref, StreamResolution, StreamTouchController } from "./settings-storages/global-settings-storage";
export export class XcloudInterceptor {
class XcloudInterceptor { private static readonly SERVER_EMOJIS = {
static async #handleLogin(request: RequestInfo | URL, init?: RequestInit) { AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
};
private static async handleLogin(request: RequestInfo | URL, init?: RequestInit) {
const bypassServer = getPref(PrefKey.SERVER_BYPASS_RESTRICTION); const bypassServer = getPref(PrefKey.SERVER_BYPASS_RESTRICTION);
if (bypassServer !== 'off') { if (bypassServer !== 'off') {
const ip = BypassServerIps[bypassServer as keyof typeof BypassServerIps]; const ip = BypassServerIps[bypassServer as keyof typeof BypassServerIps];
@ -35,24 +51,8 @@ class XcloudInterceptor {
RemotePlayManager.getInstance().xcloudToken = obj.gsToken; RemotePlayManager.getInstance().xcloudToken = obj.gsToken;
// Get server list // Get server list
const serverEmojis = {
AustraliaEast: '🇦🇺',
AustraliaSouthEast: '🇦🇺',
BrazilSouth: '🇧🇷',
EastUS: '🇺🇸',
EastUS2: '🇺🇸',
JapanEast: '🇯🇵',
KoreaCentral: '🇰🇷',
MexicoCentral: '🇲🇽',
NorthCentralUs: '🇺🇸',
SouthCentralUS: '🇺🇸',
UKSouth: '🇬🇧',
WestEurope: '🇪🇺',
WestUS: '🇺🇸',
WestUS2: '🇺🇸',
};
const serverRegex = /\/\/(\w+)\./; const serverRegex = /\/\/(\w+)\./;
const serverEmojis = XcloudInterceptor.SERVER_EMOJIS;
for (let region of obj.offeringSettings.regions) { for (let region of obj.offeringSettings.regions) {
const regionName = region.name as keyof typeof serverEmojis; const regionName = region.name as keyof typeof serverEmojis;
@ -91,7 +91,7 @@ class XcloudInterceptor {
return response; return response;
} }
static async #handlePlay(request: RequestInfo | URL, init?: RequestInit) { private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION); const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE); const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
@ -129,7 +129,7 @@ class XcloudInterceptor {
return NATIVE_FETCH(newRequest); return NATIVE_FETCH(newRequest);
} }
static async #handleWaitTime(request: RequestInfo | URL, init?: RequestInit) { private static async handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
const response = await NATIVE_FETCH(request, init); const response = await NATIVE_FETCH(request, init);
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) { if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
@ -143,7 +143,7 @@ class XcloudInterceptor {
return response; return response;
} }
static async #handleConfiguration(request: RequestInfo | URL, init?: RequestInit) { private static async handleConfiguration(request: RequestInfo | URL, init?: RequestInit) {
if ((request as Request).method !== 'GET') { if ((request as Request).method !== 'GET') {
return NATIVE_FETCH(request, init); return NATIVE_FETCH(request, init);
} }
@ -213,13 +213,13 @@ class XcloudInterceptor {
// Server list // Server list
if (url.endsWith('/v2/login/user')) { if (url.endsWith('/v2/login/user')) {
return XcloudInterceptor.#handleLogin(request, init); return XcloudInterceptor.handleLogin(request, init);
} else if (url.endsWith('/sessions/cloud/play')) { // Get session } else if (url.endsWith('/sessions/cloud/play')) { // Get session
return XcloudInterceptor.#handlePlay(request, init); return XcloudInterceptor.handlePlay(request, init);
} else if (url.includes('xboxlive.com') && url.includes('/waittime/')) { } else if (url.includes('xboxlive.com') && url.includes('/waittime/')) {
return XcloudInterceptor.#handleWaitTime(request, init); return XcloudInterceptor.handleWaitTime(request, init);
} else if (url.endsWith('/configuration')) { } else if (url.endsWith('/configuration')) {
return XcloudInterceptor.#handleConfiguration(request, init); return XcloudInterceptor.handleConfiguration(request, init);
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') { } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
return patchIceCandidates(request as Request); return patchIceCandidates(request as Request);
} }

View File

@ -10,7 +10,7 @@ import type { RemotePlayConsoleAddresses } from "@/types/network";
import { RemotePlayManager } from "@/modules/remote-play-manager"; import { RemotePlayManager } from "@/modules/remote-play-manager";
export class XhomeInterceptor { export class XhomeInterceptor {
static #consoleAddrs: RemotePlayConsoleAddresses = {}; private static consoleAddrs: RemotePlayConsoleAddresses = {};
private static readonly BASE_DEVICE_INFO = { private static readonly BASE_DEVICE_INFO = {
appInfo: { appInfo: {
@ -52,7 +52,7 @@ export class XhomeInterceptor {
}, },
}; };
static async #handleLogin(request: Request) { private static async handleLogin(request: Request) {
try { try {
const clone = (request as Request).clone(); const clone = (request as Request).clone();
@ -74,7 +74,7 @@ export class XhomeInterceptor {
return NATIVE_FETCH(request); return NATIVE_FETCH(request);
} }
static async #handleConfiguration(request: Request | URL) { private static async handleConfiguration(request: Request | URL) {
const response = await NATIVE_FETCH(request); const response = await NATIVE_FETCH(request);
const obj = await response.clone().json() const obj = await response.clone().json()
@ -90,15 +90,15 @@ export class XhomeInterceptor {
const serverDetails = obj.serverDetails; const serverDetails = obj.serverDetails;
if (serverDetails.ipAddress) { if (serverDetails.ipAddress) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port); XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
} }
if (serverDetails.ipV4Address) { if (serverDetails.ipV4Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port); XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
} }
if (serverDetails.ipV6Address) { if (serverDetails.ipV6Address) {
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port); XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
} }
response.json = () => Promise.resolve(obj); response.json = () => Promise.resolve(obj);
@ -107,7 +107,7 @@ export class XhomeInterceptor {
return response; return response;
} }
static async #handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) { private static async handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
const response = await NATIVE_FETCH(request); const response = await NATIVE_FETCH(request);
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.ALL) { if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.ALL) {
@ -144,7 +144,7 @@ export class XhomeInterceptor {
return response; return response;
} }
static async #handleTitles(request: Request) { private static async handleTitles(request: Request) {
const clone = request.clone(); const clone = request.clone();
const headers: {[index: string]: any} = {}; const headers: {[index: string]: any} = {};
@ -163,7 +163,7 @@ export class XhomeInterceptor {
return NATIVE_FETCH(request); return NATIVE_FETCH(request);
} }
static async #handlePlay(request: RequestInfo | URL) { private static async handlePlay(request: RequestInfo | URL) {
const clone = (request as Request).clone(); const clone = (request as Request).clone();
const body = await clone.json(); const body = await clone.json();
@ -216,17 +216,17 @@ export class XhomeInterceptor {
// Get console IP // Get console IP
if (url.includes('/configuration')) { if (url.includes('/configuration')) {
return XhomeInterceptor.#handleConfiguration(request); return XhomeInterceptor.handleConfiguration(request);
} else if (url.endsWith('/sessions/home/play')) { } else if (url.endsWith('/sessions/home/play')) {
return XhomeInterceptor.#handlePlay(request); return XhomeInterceptor.handlePlay(request);
} else if (url.includes('inputconfigs')) { } else if (url.includes('inputconfigs')) {
return XhomeInterceptor.#handleInputConfigs(request, opts); return XhomeInterceptor.handleInputConfigs(request, opts);
} else if (url.includes('/login/user')) { } else if (url.includes('/login/user')) {
return XhomeInterceptor.#handleLogin(request); return XhomeInterceptor.handleLogin(request);
} else if (url.endsWith('/titles')) { } else if (url.endsWith('/titles')) {
return XhomeInterceptor.#handleTitles(request); return XhomeInterceptor.handleTitles(request);
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') { } else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs); return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
} }
return await NATIVE_FETCH(request); return await NATIVE_FETCH(request);