mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-29 10:51:44 +02:00
Compare commits
37 Commits
Author | SHA1 | Date | |
---|---|---|---|
55a56837c8 | |||
df713136d8 | |||
29dfdaf72e | |||
04cf66a466 | |||
1d55026c6d | |||
fcfecf7ff9 | |||
5e22bf097a | |||
542079d53e | |||
1d00d793b8 | |||
2a9da6f827 | |||
b6089a61f9 | |||
0fe6608be9 | |||
9e39e80309 | |||
5bfcf3a044 | |||
66d5d9edc6 | |||
9d00082c67 | |||
ef2e0892bc | |||
ce1901b300 | |||
18a8b8330c | |||
b9e78f09d3 | |||
33b2b36e2b | |||
61ed68c40f | |||
4ad0d44929 | |||
422442071e | |||
8d1ae0656c | |||
a78de2ca37 | |||
db1da22c0a | |||
91ab57fa29 | |||
416307e23a | |||
e7c94f3ece | |||
ea9ad16770 | |||
9a2e7de68d | |||
962f4dec6d | |||
10d0dedc0a | |||
c6acc251ae | |||
a06d061409 | |||
6b2412ff27 |
3
build.ts
3
build.ts
@ -4,6 +4,7 @@ import { parseArgs } from "node:util";
|
||||
import { sys } from "typescript";
|
||||
import txtScriptHeader from "./src/assets/header_script.txt" with { type: "text" };
|
||||
import txtMetaHeader from "./src/assets/header_meta.txt" with { type: "text" };
|
||||
import { assert } from "node:console";
|
||||
|
||||
enum BuildTarget {
|
||||
ALL = 'all',
|
||||
@ -24,6 +25,8 @@ const postProcess = (str: string): string => {
|
||||
// Add ADDITIONAL CODE block
|
||||
str = str.replace('var DEFAULT_FLAGS', '\n/* ADDITIONAL CODE */\n\nvar DEFAULT_FLAGS');
|
||||
|
||||
assert(str.includes('/* ADDITIONAL CODE */'));
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
|
2
dist/better-xcloud.meta.js
vendored
2
dist/better-xcloud.meta.js
vendored
@ -1,5 +1,5 @@
|
||||
// ==UserScript==
|
||||
// @name Better xCloud
|
||||
// @namespace https://github.com/redphx
|
||||
// @version 5.1.2
|
||||
// @version 5.3.0
|
||||
// ==/UserScript==
|
||||
|
2557
dist/better-xcloud.user.js
vendored
2557
dist/better-xcloud.user.js
vendored
File diff suppressed because one or more lines are too long
@ -6,8 +6,8 @@
|
||||
"build": "build.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.1.5",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/bun": "^1.1.6",
|
||||
"@types/node": "^20.14.10",
|
||||
"@types/stylus": "^0.48.42",
|
||||
"stylus": "^0.63.0"
|
||||
},
|
||||
|
@ -48,11 +48,18 @@
|
||||
text-decoration: none;
|
||||
font-weight: bold;
|
||||
display: block;
|
||||
color: #5dc21e;
|
||||
flex: 1;
|
||||
text-transform: none;
|
||||
margin-right: 10px;
|
||||
|
||||
span {
|
||||
color: #5dc21e !important;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
color: #83f73a;
|
||||
span {
|
||||
color: #83f73a !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,3 +132,35 @@ div[class*=SupportedInputsBadge] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.bx-game-tile-wait-time {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
background: #0000008c;
|
||||
display: none;
|
||||
border-radius: 0 0 4px 0;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
|
||||
a[class^=BaseItem-module__container]:focus &,
|
||||
button[class^=BaseItem-module__container]:focus & {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 14px;
|
||||
height: 16px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
height: 16px;
|
||||
line-height: 16px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
export enum UserAgentProfile {
|
||||
WINDOWS_EDGE = 'windows-edge',
|
||||
MACOS_SAFARI = 'macos-safari',
|
||||
SMARTTV_GENERIC = 'smarttv-generic',
|
||||
SMARTTV_TIZEN = 'smarttv-tizen',
|
||||
SMART_TV_GENERIC = 'smarttv-generic',
|
||||
SMART_TV_TIZEN = 'smarttv-tizen',
|
||||
VR_OCULUS = 'vr-oculus',
|
||||
DEFAULT = 'default',
|
||||
CUSTOM = 'custom',
|
||||
|
21
src/index.ts
21
src/index.ts
@ -16,7 +16,6 @@ import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { MouseCursorHider } from "@modules/mkb/mouse-cursor-hider";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { watchHeader } from "@modules/ui/header";
|
||||
import { checkForUpdate, disablePwa } from "@utils/utils";
|
||||
import { Patcher } from "@modules/patcher";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
@ -34,6 +33,8 @@ import { GuideMenu, GuideMenuTab } from "./modules/ui/guide-menu";
|
||||
import { StreamSettings } from "./modules/stream/stream-settings";
|
||||
import { updateVideoPlayer } from "./modules/stream/stream-settings-utils";
|
||||
import { UiSection } from "./enums/ui-sections";
|
||||
import { HeaderSection } from "./modules/ui/header";
|
||||
import { GameTile } from "./modules/ui/game-tile";
|
||||
|
||||
|
||||
// Handle login page
|
||||
@ -125,13 +126,13 @@ window.addEventListener('popstate', onHistoryChanged);
|
||||
window.history.pushState = patchHistoryMethod('pushState');
|
||||
window.history.replaceState = patchHistoryMethod('replaceState');
|
||||
|
||||
window.addEventListener(BxEvent.XCLOUD_SERVERS_UNAVAILABLE, e => {
|
||||
STATES.supportedRegion = false;
|
||||
window.setTimeout(HeaderSection.watchHeader, 2000);
|
||||
});
|
||||
|
||||
window.addEventListener(BxEvent.XCLOUD_SERVERS_READY, e => {
|
||||
// Start rendering UI
|
||||
if (document.querySelector('div[class^=UnsupportedMarketPage]')) {
|
||||
window.setTimeout(watchHeader, 2000);
|
||||
} else {
|
||||
watchHeader();
|
||||
}
|
||||
HeaderSection.watchHeader();
|
||||
});
|
||||
|
||||
window.addEventListener(BxEvent.STREAM_LOADING, e => {
|
||||
@ -340,6 +341,12 @@ function main() {
|
||||
STATES.pointerServerPort = AppInterface.startPointerServer() || 9269;
|
||||
BxLogger.info('startPointerServer', 'Port', STATES.pointerServerPort.toString());
|
||||
}
|
||||
|
||||
// Preload Remote Play
|
||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) && BX_FLAGS.PreloadRemotePlay && RemotePlay.preload();
|
||||
|
||||
// Show wait time in game card
|
||||
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && GameTile.setup();
|
||||
}
|
||||
|
||||
main();
|
||||
|
@ -9,6 +9,7 @@ import { BxEvent } from "@/utils/bx-event";
|
||||
import codeControllerShortcuts from "./patches/controller-shortcuts.js" with { type: "text" };
|
||||
import codeExposeStreamSession from "./patches/expose-stream-session.js" with { type: "text" };
|
||||
import codeLocalCoOpEnable from "./patches/local-co-op-enable.js" with { type: "text" };
|
||||
import codeSetCurrentlyFocusedInteractable from "./patches/set-currently-focused-interactable.js" with { type: "text" };
|
||||
import codeRemotePlayEnable from "./patches/remote-play-enable.js" with { type: "text" };
|
||||
import codeRemotePlayKeepAlive from "./patches/remote-play-keep-alive.js" with { type: "text" };
|
||||
import codeVibrationAdjust from "./patches/vibration-adjust.js" with { type: "text" };
|
||||
@ -738,6 +739,55 @@ true` + text;
|
||||
str = str.substring(0, index) + 'true ? null :' + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
|
||||
// Override Storage.getSettings()
|
||||
overrideStorageGetSettings(str: string) {
|
||||
const text = '}getSetting(e){';
|
||||
if (!str.includes(text)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newCode = `
|
||||
// console.log('setting', this.baseStorageKey, e);
|
||||
if (this.baseStorageKey in window.BX_EXPOSED.overrideSettings) {
|
||||
const settings = window.BX_EXPOSED.overrideSettings[this.baseStorageKey];
|
||||
if (e in settings) {
|
||||
return settings[e];
|
||||
}
|
||||
}
|
||||
`;
|
||||
str = str.replace(text, text + newCode);
|
||||
return str;
|
||||
},
|
||||
|
||||
// game-stream.js 24.16.4
|
||||
alwaysShowStreamHud(str: string) {
|
||||
let index = str.indexOf(',{onShowStreamMenu:');
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('&&(0,', index - 100);
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const commaIndex = str.indexOf(',', index - 10);
|
||||
str = str.substring(0, commaIndex) + ',true' + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
|
||||
// 24225.js#4127, 24.17.11
|
||||
patchSetCurrentlyFocusedInteractable(str: string) {
|
||||
let index = str.indexOf('.setCurrentlyFocusedInteractable=(');
|
||||
if (index === -1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
index = str.indexOf('{', index) + 1;
|
||||
str = str.substring(0, index) + codeSetCurrentlyFocusedInteractable + str.substring(index);
|
||||
return str;
|
||||
},
|
||||
};
|
||||
|
||||
let PATCH_ORDERS: PatchArray = [
|
||||
@ -759,6 +809,9 @@ let PATCH_ORDERS: PatchArray = [
|
||||
|
||||
'enableTvRoutes',
|
||||
|
||||
'overrideStorageGetSettings',
|
||||
getPref(PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME) && 'patchSetCurrentlyFocusedInteractable',
|
||||
|
||||
getPref(PrefKey.UI_LAYOUT) !== 'default' && 'websiteLayout',
|
||||
getPref(PrefKey.LOCAL_CO_OP_ENABLED) && 'supportLocalCoOp',
|
||||
getPref(PrefKey.GAME_FORTNITE_FORCE_CONSOLE) && 'forceFortniteConsole',
|
||||
@ -797,6 +850,8 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
'patchStreamHud',
|
||||
'playVibration',
|
||||
|
||||
'alwaysShowStreamHud',
|
||||
|
||||
// 'exposeEventTarget',
|
||||
|
||||
// Patch volume control for normal stream
|
||||
@ -810,7 +865,7 @@ let PLAYING_PATCH_ORDERS: PatchArray = [
|
||||
...(STATES.userAgent.capabilities.touch ? [
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'patchShowSensorControls',
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all' && 'exposeTouchLayoutManager',
|
||||
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF)) && 'disableTakRenderer',
|
||||
(getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'off' || getPref(PrefKey.STREAM_TOUCH_CONTROLLER_AUTO_OFF) || !STATES.userAgent.capabilities.touch) && 'disableTakRenderer',
|
||||
getPref(PrefKey.STREAM_TOUCH_CONTROLLER_DEFAULT_OPACITY) !== 100 && 'patchTouchControlDefaultOpacity',
|
||||
'patchBabylonRendererClass',
|
||||
] : []),
|
||||
|
@ -0,0 +1 @@
|
||||
e && BxEvent.dispatch(window, BxEvent.NAVIGATION_FOCUS_CHANGED, {element: e});
|
@ -7,6 +7,7 @@ import { getPref, PrefKey, setPref } from "@utils/preferences";
|
||||
import { t } from "@utils/translation";
|
||||
import { localRedirect } from "@modules/ui/ui";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { HeaderSection } from "./ui/header";
|
||||
|
||||
const LOG_TAG = 'RemotePlay';
|
||||
|
||||
@ -97,6 +98,10 @@ export class RemotePlay {
|
||||
RemotePlay.#getXhomeToken(() => {
|
||||
RemotePlay.#getConsolesList(() => {
|
||||
BxLogger.info(LOG_TAG, 'Consoles', RemotePlay.#CONSOLES);
|
||||
if (RemotePlay.#CONSOLES && RemotePlay.#CONSOLES.length > 0) {
|
||||
STATES.supportedRegion && HeaderSection.showRemotePlayButton();
|
||||
}
|
||||
|
||||
RemotePlay.#renderConsoles();
|
||||
BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
|
||||
});
|
||||
|
@ -240,6 +240,9 @@ export class StreamSettings {
|
||||
|
||||
constructor() {
|
||||
this.#setupDialog();
|
||||
|
||||
// Hide dialog when the Guide menu is shown
|
||||
window.addEventListener(BxEvent.XCLOUD_GUIDE_MENU_SHOWN, e => this.hide());
|
||||
}
|
||||
|
||||
show(tabId?: string) {
|
||||
|
@ -142,10 +142,10 @@ export class StreamStats {
|
||||
this.#$fps!.textContent = stat.framesPerSecond || 0;
|
||||
|
||||
// Packets Lost
|
||||
const packetsLost = stat.packetsLost;
|
||||
const packetsLost = Math.max(0, stat.packetsLost); // packetsLost can be negative, but we don't care about that
|
||||
const packetsReceived = stat.packetsReceived;
|
||||
const packetsLostPercentage = (packetsLost * 100 / ((packetsLost + packetsReceived) || 1)).toFixed(2);
|
||||
this.#$pl!.textContent = packetsLostPercentage === '0.00' ? packetsLost : `${packetsLost} (${packetsLostPercentage}%)`;
|
||||
this.#$pl!.textContent = packetsLostPercentage === '0.00' ? packetsLost.toString() : `${packetsLost} (${packetsLostPercentage}%)`;
|
||||
|
||||
// Frames dropped
|
||||
const framesDropped = stat.framesDropped;
|
||||
|
87
src/modules/ui/game-tile.ts
Normal file
87
src/modules/ui/game-tile.ts
Normal file
@ -0,0 +1,87 @@
|
||||
import { BxEvent } from "@/utils/bx-event";
|
||||
import { BxIcon } from "@/utils/bx-icon";
|
||||
import { CE, createSvgIcon, getReactProps } from "@/utils/html";
|
||||
import { XcloudApi } from "@/utils/xcloud-api";
|
||||
|
||||
export class GameTile {
|
||||
static #timeout: number | null;
|
||||
|
||||
static #secondsToHms(seconds: number) {
|
||||
let h = Math.floor(seconds / 3600);
|
||||
seconds %= 3600;
|
||||
let m = Math.floor(seconds / 60);
|
||||
let s = seconds % 60;
|
||||
|
||||
const output = [];
|
||||
h > 0 && output.push(`${h}h`);
|
||||
m > 0 && output.push(`${m}m`);
|
||||
if (s > 0 || output.length === 0) {
|
||||
output.push(`${s}s`);
|
||||
}
|
||||
|
||||
return output.join(' ');
|
||||
}
|
||||
|
||||
static async #showWaitTime($elm: HTMLElement, productId: string) {
|
||||
let totalWaitTime;
|
||||
|
||||
const api = XcloudApi.getInstance();
|
||||
const info = await api.getTitleInfo(productId);
|
||||
if (info) {
|
||||
const waitTime = await api.getWaitTime(info.titleId);
|
||||
if (waitTime) {
|
||||
totalWaitTime = waitTime.estimatedAllocationTimeInSeconds;
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof totalWaitTime === 'number' && $elm.isConnected) {
|
||||
const $div = CE('div', {'class': 'bx-game-tile-wait-time'},
|
||||
createSvgIcon(BxIcon.PLAYTIME),
|
||||
CE('span', {}, GameTile.#secondsToHms(totalWaitTime)),
|
||||
);
|
||||
$elm.insertAdjacentElement('afterbegin', $div);
|
||||
}
|
||||
}
|
||||
|
||||
static requestWaitTime($elm: HTMLElement, productId: string) {
|
||||
GameTile.#timeout && clearTimeout(GameTile.#timeout);
|
||||
GameTile.#timeout = window.setTimeout(async () => {
|
||||
if (!($elm as any).hasWaitTime) {
|
||||
($elm as any).hasWaitTime = true;
|
||||
GameTile.#showWaitTime($elm, productId);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
static setup() {
|
||||
window.addEventListener(BxEvent.NAVIGATION_FOCUS_CHANGED, e => {
|
||||
let productId;
|
||||
const $elm = (e as any).element;
|
||||
try {
|
||||
if (($elm.tagName === 'BUTTON' && $elm.className.includes('MruGameCard')) || (($elm.tagName === 'A' && $elm.className.includes('GameCard')))) {
|
||||
let props = getReactProps($elm.parentElement);
|
||||
|
||||
// When context menu is enabled
|
||||
if (Array.isArray(props.children)) {
|
||||
productId = props.children[0].props.productId;
|
||||
} else {
|
||||
productId = props.children.props.productId;
|
||||
}
|
||||
} else if ($elm.tagName === 'A' && $elm.className.includes('GameItem')) {
|
||||
let props = getReactProps($elm.parentElement);
|
||||
props = props.children.props;
|
||||
if (props.location !== 'NonStreamableGameItem') {
|
||||
if ('productId' in props) {
|
||||
productId = props.productId;
|
||||
} else {
|
||||
// Search page
|
||||
productId = props.children.props.productId;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
productId && GameTile.requestWaitTime($elm, productId);
|
||||
});
|
||||
}
|
||||
}
|
@ -15,6 +15,7 @@ const SETTINGS_UI = {
|
||||
'Better xCloud': {
|
||||
items: [
|
||||
PrefKey.BETTER_XCLOUD_LOCALE,
|
||||
PrefKey.SERVER_BYPASS_RESTRICTION,
|
||||
PrefKey.REMOTE_PLAY_ENABLED,
|
||||
],
|
||||
},
|
||||
@ -89,6 +90,7 @@ const SETTINGS_UI = {
|
||||
items: [
|
||||
PrefKey.UI_LAYOUT,
|
||||
PrefKey.UI_HOME_CONTEXT_MENU_DISABLED,
|
||||
PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME,
|
||||
PrefKey.CONTROLLER_SHOW_CONNECTION_STATUS,
|
||||
PrefKey.STREAM_SIMPLIFY_MENU,
|
||||
PrefKey.SKIP_SPLASH_VIDEO,
|
||||
@ -132,11 +134,12 @@ export function setupSettingsUi() {
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-settings-wrapper'},
|
||||
CE('div', {'class': 'bx-settings-title-wrapper'},
|
||||
CE('a', {
|
||||
'class': 'bx-settings-title',
|
||||
'href': 'https://github.com/redphx/better-xcloud/releases',
|
||||
'target': '_blank',
|
||||
}, 'Better xCloud ' + SCRIPT_VERSION),
|
||||
createButton({
|
||||
classes: ['bx-settings-title'],
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST,
|
||||
label: 'Better xCloud ' + SCRIPT_VERSION,
|
||||
url: 'https://github.com/redphx/better-xcloud/releases',
|
||||
}),
|
||||
createButton({
|
||||
icon: BxIcon.QUESTION,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
@ -259,9 +262,9 @@ export function setupSettingsUi() {
|
||||
if (setting.experimental) {
|
||||
settingLabel = '🧪 ' + settingLabel;
|
||||
if (!settingNote) {
|
||||
settingNote = t('experimental')
|
||||
settingNote = t('experimental');
|
||||
} else {
|
||||
settingNote = `${t('experimental')}: ${settingNote}`
|
||||
settingNote = `${t('experimental')}: ${settingNote}`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,6 +345,8 @@ export function setupSettingsUi() {
|
||||
$control.appendChild($option);
|
||||
}
|
||||
|
||||
$control.disabled = Object.keys(STATES.serverRegions).length === 0;
|
||||
|
||||
// Select preferred region
|
||||
$control.value = selectedValue;
|
||||
} else {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SCRIPT_VERSION } from "@utils/global";
|
||||
import { createButton, ButtonStyle } from "@utils/html";
|
||||
import { createButton, ButtonStyle, CE } from "@utils/html";
|
||||
import { BxIcon } from "@utils/bx-icon";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
@ -7,35 +7,20 @@ import { RemotePlay } from "@modules/remote-play";
|
||||
import { t } from "@utils/translation";
|
||||
import { setupSettingsUi } from "./global-settings";
|
||||
|
||||
export class HeaderSection {
|
||||
static #$remotePlayBtn = createButton({
|
||||
classes: ['bx-header-remote-play-button', 'bx-gone'],
|
||||
icon: BxIcon.REMOTE_PLAY,
|
||||
title: t('remote-play'),
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
RemotePlay.togglePopup();
|
||||
},
|
||||
});
|
||||
|
||||
function injectSettingsButton($parent?: HTMLElement) {
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_PREFERRED_REGION = getPreferredServerRegion(true);
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||
|
||||
const $headerFragment = document.createDocumentFragment();
|
||||
|
||||
// Remote Play button
|
||||
if (getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
|
||||
const $remotePlayBtn = createButton({
|
||||
classes: ['bx-header-remote-play-button'],
|
||||
icon: BxIcon.REMOTE_PLAY,
|
||||
title: t('remote-play'),
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
||||
onClick: e => {
|
||||
RemotePlay.togglePopup();
|
||||
},
|
||||
});
|
||||
$headerFragment.appendChild($remotePlayBtn);
|
||||
}
|
||||
|
||||
// Setup Settings button
|
||||
const $settingsBtn = createButton({
|
||||
static #$settingsBtn = createButton({
|
||||
classes: ['bx-header-settings-button'],
|
||||
label: PREF_PREFERRED_REGION,
|
||||
label: '???',
|
||||
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE | ButtonStyle.FULL_HEIGHT,
|
||||
onClick: e => {
|
||||
setupSettingsUi();
|
||||
@ -47,39 +32,58 @@ function injectSettingsButton($parent?: HTMLElement) {
|
||||
},
|
||||
});
|
||||
|
||||
// Show new update status
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
|
||||
$settingsBtn.setAttribute('data-update-available', 'true');
|
||||
static #$buttonsWrapper = CE('div', {},
|
||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) ? HeaderSection.#$remotePlayBtn : null,
|
||||
HeaderSection.#$settingsBtn,
|
||||
);
|
||||
|
||||
static #observer: MutationObserver;
|
||||
static #timeout: number | null;
|
||||
|
||||
static #injectSettingsButton($parent?: HTMLElement) {
|
||||
if (!$parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_LATEST_VERSION = getPref(PrefKey.LATEST_VERSION);
|
||||
|
||||
// Setup Settings button
|
||||
const $settingsBtn = HeaderSection.#$settingsBtn;
|
||||
$settingsBtn.querySelector('span')!.textContent = getPreferredServerRegion(true);
|
||||
|
||||
// Show new update status
|
||||
if (!SCRIPT_VERSION.includes('beta') && PREF_LATEST_VERSION && PREF_LATEST_VERSION !== SCRIPT_VERSION) {
|
||||
$settingsBtn.setAttribute('data-update-available', 'true');
|
||||
}
|
||||
|
||||
// Add the Settings button to the web page
|
||||
$parent.appendChild(HeaderSection.#$buttonsWrapper);
|
||||
}
|
||||
|
||||
// Add the Settings button to the web page
|
||||
$headerFragment.appendChild($settingsBtn);
|
||||
$parent.appendChild($headerFragment);
|
||||
}
|
||||
|
||||
|
||||
export function checkHeader() {
|
||||
const $button = document.querySelector('.bx-header-settings-button');
|
||||
|
||||
if (!$button) {
|
||||
const $rightHeader = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||
injectSettingsButton($rightHeader as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function watchHeader() {
|
||||
const $header = document.querySelector('#PageContent header');
|
||||
if (!$header) {
|
||||
return;
|
||||
static checkHeader() {
|
||||
if (!HeaderSection.#$buttonsWrapper.isConnected) {
|
||||
const $rightHeader = document.querySelector('#PageContent div[class*=EdgewaterHeader-module__rightSectionSpacing]');
|
||||
HeaderSection.#injectSettingsButton($rightHeader as HTMLElement);
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: number | null;
|
||||
const observer = new MutationObserver(mutationList => {
|
||||
timeout && clearTimeout(timeout);
|
||||
timeout = window.setTimeout(checkHeader, 2000);
|
||||
});
|
||||
observer.observe($header, {subtree: true, childList: true});
|
||||
static showRemotePlayButton() {
|
||||
HeaderSection.#$remotePlayBtn.classList.remove('bx-gone');
|
||||
}
|
||||
|
||||
checkHeader();
|
||||
static watchHeader() {
|
||||
const $header = document.querySelector('#PageContent header');
|
||||
if (!$header) {
|
||||
return;
|
||||
}
|
||||
|
||||
HeaderSection.#observer && HeaderSection.#observer.disconnect();
|
||||
HeaderSection.#observer = new MutationObserver(mutationList => {
|
||||
HeaderSection.#timeout && clearTimeout(HeaderSection.#timeout);
|
||||
HeaderSection.#timeout = window.setTimeout(HeaderSection.checkHeader, 2000);
|
||||
});
|
||||
HeaderSection.#observer.observe($header, {subtree: true, childList: true});
|
||||
|
||||
HeaderSection.checkHeader();
|
||||
}
|
||||
}
|
||||
|
16
src/types/index.d.ts
vendored
16
src/types/index.d.ts
vendored
@ -24,9 +24,14 @@ interface NavigatorBattery extends Navigator {
|
||||
}
|
||||
|
||||
type BxStates = {
|
||||
supportedRegion: boolean;
|
||||
serverRegions: any;
|
||||
selectedRegion: any;
|
||||
gsToken: string;
|
||||
|
||||
|
||||
isPlaying: boolean;
|
||||
appContext: any | null;
|
||||
serverRegions: any;
|
||||
|
||||
browser: {
|
||||
capabilities: {
|
||||
@ -36,6 +41,7 @@ type BxStates = {
|
||||
};
|
||||
|
||||
userAgent: {
|
||||
isTv: boolean;
|
||||
capabilities: {
|
||||
touch: boolean;
|
||||
};
|
||||
@ -68,6 +74,8 @@ type BxStates = {
|
||||
type DualEnum = {[index: string]: number} & {[index: number]: string};
|
||||
|
||||
type XcloudTitleInfo = {
|
||||
titleId: string,
|
||||
|
||||
details: {
|
||||
productId: string;
|
||||
supportedInputTypes: InputType[];
|
||||
@ -85,6 +93,12 @@ type XcloudTitleInfo = {
|
||||
};
|
||||
};
|
||||
|
||||
type XcloudWaitTimeInfo = Partial<{
|
||||
estimatedAllocationTimeInSeconds: number,
|
||||
estimatedProvisioningTimeInSeconds: number,
|
||||
estimatedTotalWaitTimeInSeconds: number,
|
||||
}>;
|
||||
|
||||
declare module '*.js';
|
||||
declare module '*.svg';
|
||||
declare module '*.styl';
|
||||
|
@ -26,6 +26,7 @@ export enum BxEvent {
|
||||
REMOTE_PLAY_FAILED = 'bx-remote-play-failed',
|
||||
|
||||
XCLOUD_SERVERS_READY = 'bx-servers-ready',
|
||||
XCLOUD_SERVERS_UNAVAILABLE = 'bx-servers-unavailable',
|
||||
|
||||
DATA_CHANNEL_CREATED = 'bx-data-channel-created',
|
||||
|
||||
@ -38,6 +39,8 @@ export enum BxEvent {
|
||||
POINTER_LOCK_REQUESTED = 'bx-pointer-lock-requested',
|
||||
POINTER_LOCK_EXITED = 'bx-pointer-lock-exited',
|
||||
|
||||
NAVIGATION_FOCUS_CHANGED = 'bx-nav-focus-changed',
|
||||
|
||||
// xCloud Dialog events
|
||||
XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown',
|
||||
XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed',
|
||||
|
@ -109,4 +109,10 @@ export const BxExposed = {
|
||||
|
||||
handleControllerShortcut: ControllerShortcut.handle,
|
||||
resetControllerShortcut: ControllerShortcut.reset,
|
||||
|
||||
overrideSettings: {
|
||||
'Tv_settings': {
|
||||
hasCompletedOnboarding: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -3,6 +3,7 @@ import { getPref, PrefKey } from "./preferences";
|
||||
|
||||
export let FeatureGates: {[key: string]: boolean} = {
|
||||
'PwaPrompt': false,
|
||||
'EnableWifiWarnings': false,
|
||||
};
|
||||
|
||||
// Disable context menu in Home page
|
||||
|
@ -13,9 +13,13 @@ const browserHasTouchSupport = 'ontouchstart' in window || navigator.maxTouchPoi
|
||||
const userAgentHasTouchSupport = !isTv && !isVr && browserHasTouchSupport;
|
||||
|
||||
export const STATES: BxStates = {
|
||||
supportedRegion: true,
|
||||
serverRegions: {},
|
||||
selectedRegion: {},
|
||||
gsToken: '',
|
||||
|
||||
isPlaying: false,
|
||||
appContext: {},
|
||||
serverRegions: {},
|
||||
|
||||
browser: {
|
||||
capabilities: {
|
||||
@ -25,6 +29,7 @@ export const STATES: BxStates = {
|
||||
},
|
||||
|
||||
userAgent: {
|
||||
isTv: isTv,
|
||||
capabilities: {
|
||||
touch: userAgentHasTouchSupport,
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { checkHeader } from "@modules/ui/header";
|
||||
import { HeaderSection } from "@/modules/ui/header";
|
||||
|
||||
export function patchHistoryMethod(type: 'pushState' | 'replaceState') {
|
||||
const orig = window.history[type];
|
||||
@ -34,7 +34,7 @@ export function onHistoryChanged(e: PopStateEvent) {
|
||||
RemotePlay.detachPopup();
|
||||
|
||||
LoadingScreen.reset();
|
||||
window.setTimeout(checkHeader, 2000);
|
||||
window.setTimeout(HeaderSection.watchHeader, 2000);
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STOPPED);
|
||||
}
|
||||
|
@ -104,6 +104,16 @@ export const createButton = <T=HTMLButtonElement>(options: BxButton): T => {
|
||||
return $btn as T;
|
||||
}
|
||||
|
||||
export function getReactProps($elm: HTMLElement): any | null {
|
||||
for (const key in $elm) {
|
||||
if (key.startsWith('__reactProps')) {
|
||||
return ($elm as any)[key];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function escapeHtml(html: string): string {
|
||||
const text = document.createTextNode(html);
|
||||
const $span = document.createElement('span');
|
||||
|
@ -1,16 +1,13 @@
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { PrefKey, getPref } from "@utils/preferences";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { StreamBadges } from "@modules/stream/stream-badges";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { STATES } from "@utils/global";
|
||||
import { getPreferredServerRegion } from "@utils/region";
|
||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||
import { InputType } from "./bx-exposed";
|
||||
import { FeatureGates } from "./feature-gates";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { XhomeInterceptor } from "./xhome-interceptor";
|
||||
import { XcloudInterceptor } from "./xcloud-interceptor";
|
||||
|
||||
enum RequestType {
|
||||
XCLOUD = 'xcloud',
|
||||
@ -101,7 +98,7 @@ function updateIceCandidates(candidates: any, options: any) {
|
||||
}
|
||||
|
||||
|
||||
async function patchIceCandidates(request: Request, consoleAddrs?: {[index: string]: number}) {
|
||||
export async function patchIceCandidates(request: Request, consoleAddrs?: {[index: string]: number}) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
const text = await response.clone().text();
|
||||
|
||||
@ -125,383 +122,6 @@ async function patchIceCandidates(request: Request, consoleAddrs?: {[index: stri
|
||||
return response;
|
||||
}
|
||||
|
||||
|
||||
class XhomeInterceptor {
|
||||
static #consoleAddrs: {[index: string]: number} = {};
|
||||
|
||||
static async #handleLogin(request: Request) {
|
||||
try {
|
||||
const clone = (request as Request).clone();
|
||||
|
||||
const obj = await clone.json();
|
||||
obj.offeringId = 'xhome';
|
||||
|
||||
request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return NATIVE_FETCH(request);
|
||||
}
|
||||
|
||||
static async #handleConfiguration(request: Request | URL) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
const obj = await response.clone().json()
|
||||
console.log(obj);
|
||||
|
||||
const serverDetails = obj.serverDetails;
|
||||
if (serverDetails.ipAddress) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = serverDetails.port;
|
||||
}
|
||||
|
||||
if (serverDetails.ipV4Address) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = serverDetails.ipV4Port;
|
||||
}
|
||||
|
||||
if (serverDetails.ipV6Address) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = serverDetails.ipV6Port;
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'all') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = await response.clone().json() as any;
|
||||
|
||||
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
|
||||
STATES.currentStream.xboxTitleId = xboxTitleId;
|
||||
|
||||
const inputConfigs = obj[0];
|
||||
|
||||
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
|
||||
if (!hasTouchSupport) {
|
||||
const supportedInputTypes = inputConfigs.supportedInputTypes;
|
||||
hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
|
||||
}
|
||||
|
||||
if (hasTouchSupport) {
|
||||
TouchController.disable();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
|
||||
data: null,
|
||||
});
|
||||
} else {
|
||||
TouchController.enable();
|
||||
TouchController.getCustomLayouts(xboxTitleId);
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleTitles(request: Request) {
|
||||
const clone = request.clone();
|
||||
|
||||
const headers: {[index: string]: any} = {};
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`;
|
||||
|
||||
const index = request.url.indexOf('.xboxlive.com');
|
||||
request = new Request('https://wus.core.gssv-play-prod' + request.url.substring(index), {
|
||||
method: clone.method,
|
||||
body: await clone.text(),
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(request);
|
||||
}
|
||||
|
||||
static async #handlePlay(request: RequestInfo | URL) {
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
// body.settings.useIceConnection = true;
|
||||
|
||||
const newRequest = new Request(request, {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(newRequest);
|
||||
}
|
||||
|
||||
static async handle(request: Request) {
|
||||
TouchController.disable();
|
||||
|
||||
const clone = request.clone();
|
||||
|
||||
const headers: {[index: string]: string} = {};
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
// Add xHome token to headers
|
||||
headers.authorization = `Bearer ${RemotePlay.XHOME_TOKEN}`;
|
||||
|
||||
// Patch resolution
|
||||
const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
|
||||
if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === '720p') {
|
||||
deviceInfo.dev.os.name = 'android';
|
||||
}
|
||||
|
||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||
|
||||
const opts: {[index: string]: any} = {
|
||||
method: clone.method,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
if (clone.method === 'POST') {
|
||||
opts.body = await clone.text();
|
||||
}
|
||||
|
||||
let newUrl = request.url;
|
||||
if (!newUrl.includes('/servers/home')) {
|
||||
const index = request.url.indexOf('.xboxlive.com');
|
||||
newUrl = STATES.remotePlay.server + request.url.substring(index + 13);
|
||||
}
|
||||
|
||||
request = new Request(newUrl, opts);
|
||||
let url = (typeof request === 'string') ? request : request.url;
|
||||
|
||||
// Get console IP
|
||||
if (url.includes('/configuration')) {
|
||||
return XhomeInterceptor.#handleConfiguration(request);
|
||||
} else if (url.endsWith('/sessions/home/play')) {
|
||||
return XhomeInterceptor.#handlePlay(request);
|
||||
} else if (url.includes('inputconfigs')) {
|
||||
return XhomeInterceptor.#handleInputConfigs(request, opts);
|
||||
} else if (url.includes('/login/user')) {
|
||||
return XhomeInterceptor.#handleLogin(request);
|
||||
} else if (url.endsWith('/titles')) {
|
||||
return XhomeInterceptor.#handleTitles(request);
|
||||
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
|
||||
return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs);
|
||||
}
|
||||
|
||||
return await NATIVE_FETCH(request);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class XcloudInterceptor {
|
||||
static async #handleLogin(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
const obj = await response.clone().json();
|
||||
|
||||
// Preload Remote Play
|
||||
getPref(PrefKey.REMOTE_PLAY_ENABLED) && BX_FLAGS.PreloadRemotePlay && RemotePlay.preload();
|
||||
|
||||
// Store xCloud token
|
||||
RemotePlay.XCLOUD_TOKEN = obj.gsToken;
|
||||
|
||||
// Get server list
|
||||
const serverEmojis = {
|
||||
AustraliaEast: '🇦🇺',
|
||||
AustraliaSouthEast: '🇦🇺',
|
||||
BrazilSouth: '🇧🇷',
|
||||
EastUS: '🇺🇸',
|
||||
EastUS2: '🇺🇸',
|
||||
JapanEast: '🇯🇵',
|
||||
KoreaCentral: '🇰🇷',
|
||||
MexicoCentral: '🇲🇽',
|
||||
NorthCentralUs: '🇺🇸',
|
||||
SouthCentralUS: '🇺🇸',
|
||||
UKSouth: '🇬🇧',
|
||||
WestEurope: '🇪🇺',
|
||||
WestUS: '🇺🇸',
|
||||
WestUS2: '🇺🇸',
|
||||
};
|
||||
|
||||
const serverRegex = /\/\/(\w+)\./;
|
||||
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
const regionName = region.name as keyof typeof serverEmojis;
|
||||
let shortName = region.name;
|
||||
|
||||
let match = serverRegex.exec(region.baseUri);
|
||||
if (match) {
|
||||
shortName = match[1];
|
||||
if (serverEmojis[regionName]) {
|
||||
shortName = serverEmojis[regionName] + ' ' + shortName;
|
||||
}
|
||||
}
|
||||
|
||||
region.shortName = shortName.toUpperCase();
|
||||
STATES.serverRegions[region.name] = Object.assign({}, region);
|
||||
}
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
|
||||
|
||||
const preferredRegion = getPreferredServerRegion();
|
||||
if (preferredRegion in STATES.serverRegions) {
|
||||
const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
|
||||
tmp.isDefault = true;
|
||||
|
||||
obj.offeringSettings.regions = [tmp];
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handlePlay(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||
|
||||
const url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
let badgeRegion: string = parsedUrl.host.split('.', 1)[0];
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
if (parsedUrl.origin == region.baseUri) {
|
||||
badgeRegion = regionName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
StreamBadges.getInstance().setRegion(badgeRegion);
|
||||
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
// Force stream's resolution
|
||||
if (PREF_STREAM_TARGET_RESOLUTION !== 'auto') {
|
||||
const osName = (PREF_STREAM_TARGET_RESOLUTION === '720p') ? 'android' : 'windows';
|
||||
body.settings.osName = osName;
|
||||
}
|
||||
|
||||
// Override "locale" value
|
||||
if (PREF_STREAM_PREFERRED_LOCALE !== 'default') {
|
||||
body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
|
||||
}
|
||||
|
||||
const newRequest = new Request(request, {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(newRequest);
|
||||
}
|
||||
|
||||
static async #handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
|
||||
const json = await response.clone().json();
|
||||
if (json.estimatedAllocationTimeInSeconds > 0) {
|
||||
// Setup wait time overlay
|
||||
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleConfiguration(request: RequestInfo | URL, init?: RequestInit) {
|
||||
if ((request as Request).method !== 'GET') {
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
|
||||
// Touch controller for all games
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
||||
const titleInfo = STATES.currentStream.titleInfo;
|
||||
if (titleInfo?.details.hasTouchSupport) {
|
||||
TouchController.disable();
|
||||
} else {
|
||||
TouchController.enable();
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept configurations
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
const text = await response.clone().text();
|
||||
if (!text.length) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = JSON.parse(text);
|
||||
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
|
||||
|
||||
overrides.inputConfiguration = overrides.inputConfiguration || {};
|
||||
overrides.inputConfiguration.enableVibration = true;
|
||||
|
||||
let overrideMkb: boolean | null = null;
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' || (STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId))) {
|
||||
overrideMkb = true;
|
||||
}
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'off') {
|
||||
overrideMkb = false;
|
||||
}
|
||||
|
||||
if (overrideMkb !== null) {
|
||||
overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
|
||||
enableMouseInput: overrideMkb,
|
||||
enableKeyboardInput: overrideMkb,
|
||||
});
|
||||
}
|
||||
|
||||
// Enable touch controller
|
||||
if (TouchController.isEnabled()) {
|
||||
overrides.inputConfiguration.enableTouchInput = true;
|
||||
overrides.inputConfiguration.maxTouchPoints = 10;
|
||||
}
|
||||
|
||||
// Enable mic
|
||||
if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING)) {
|
||||
overrides.audioConfiguration = overrides.audioConfiguration || {};
|
||||
overrides.audioConfiguration.enableMicrophone = true;
|
||||
}
|
||||
|
||||
obj.clientStreamingConfigOverrides = JSON.stringify(overrides);
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async handle(request: RequestInfo | URL, init?: RequestInit) {
|
||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
|
||||
// Server list
|
||||
if (url.endsWith('/v2/login/user')) {
|
||||
return XcloudInterceptor.#handleLogin(request, init);
|
||||
} else if (url.endsWith('/sessions/cloud/play')) { // Get session
|
||||
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 as Request).method === 'GET') {
|
||||
return patchIceCandidates(request as Request);
|
||||
}
|
||||
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function interceptHttpRequests() {
|
||||
let BLOCKED_URLS: string[] = [];
|
||||
if (getPref(PrefKey.BLOCK_TRACKING)) {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { SUPPORTED_LANGUAGES, t } from "@utils/translation";
|
||||
import { SUPPORTED_LANGUAGES, t} from "@utils/translation";
|
||||
import { SettingElement, SettingElementType } from "@utils/settings";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { StreamStat } from "@modules/stream/stream-stats";
|
||||
@ -17,6 +17,8 @@ export enum PrefKey {
|
||||
BETTER_XCLOUD_LOCALE = 'bx_locale',
|
||||
|
||||
SERVER_REGION = 'server_region',
|
||||
SERVER_BYPASS_RESTRICTION = 'server_bypass_restriction',
|
||||
|
||||
PREFER_IPV6_SERVER = 'prefer_ipv6_server',
|
||||
STREAM_TARGET_RESOLUTION = 'stream_target_resolution',
|
||||
STREAM_PREFERRED_LOCALE = 'stream_preferred_locale',
|
||||
@ -74,6 +76,7 @@ export enum PrefKey {
|
||||
UI_HIDE_SECTIONS = 'ui_hide_sections',
|
||||
|
||||
UI_HOME_CONTEXT_MENU_DISABLED = 'ui_home_context_menu_disabled',
|
||||
UI_GAME_CARD_SHOW_WAIT_TIME = 'ui_game_card_show_wait_time',
|
||||
|
||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||
VIDEO_PROCESSING = 'video_processing',
|
||||
@ -122,6 +125,12 @@ export class Preferences {
|
||||
label: t('region'),
|
||||
default: 'default',
|
||||
},
|
||||
[PrefKey.SERVER_BYPASS_RESTRICTION]: {
|
||||
label: t('bypass-region-restriction'),
|
||||
note: t('use-this-at-your-own-risk'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.STREAM_PREFERRED_LOCALE]: {
|
||||
label: t('preferred-game-language'),
|
||||
default: 'default',
|
||||
@ -573,6 +582,11 @@ export class Preferences {
|
||||
},
|
||||
},
|
||||
|
||||
[PrefKey.UI_GAME_CARD_SHOW_WAIT_TIME]: {
|
||||
label: t('show-wait-time-in-game-card'),
|
||||
default: false,
|
||||
},
|
||||
|
||||
[PrefKey.BLOCK_SOCIAL_FEATURES]: {
|
||||
label: t('disable-social-features'),
|
||||
default: false,
|
||||
@ -589,8 +603,8 @@ export class Preferences {
|
||||
[UserAgentProfile.DEFAULT]: t('default'),
|
||||
[UserAgentProfile.WINDOWS_EDGE]: 'Edge + Windows',
|
||||
[UserAgentProfile.MACOS_SAFARI]: 'Safari + macOS',
|
||||
[UserAgentProfile.SMARTTV_GENERIC]: 'Smart TV',
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: 'Samsung Smart TV',
|
||||
[UserAgentProfile.SMART_TV_GENERIC]: 'Smart TV',
|
||||
[UserAgentProfile.SMART_TV_TIZEN]: 'Samsung Smart TV',
|
||||
[UserAgentProfile.VR_OCULUS]: 'Meta Quest VR',
|
||||
[UserAgentProfile.CUSTOM]: t('custom'),
|
||||
},
|
||||
|
@ -51,6 +51,7 @@ const Texts = {
|
||||
"bottom-right": "Bottom-right",
|
||||
"brightness": "Brightness",
|
||||
"browser-unsupported-feature": "Your browser doesn't support this feature",
|
||||
"bypass-region-restriction": "Bypass region restriction",
|
||||
"can-stream-xbox-360-games": "Can stream Xbox 360 games",
|
||||
"cancel": "Cancel",
|
||||
"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
|
||||
@ -211,6 +212,7 @@ const Texts = {
|
||||
"show-stats-on-startup": "Show stats when starting the game",
|
||||
"show-touch-controller": "Show touch controller",
|
||||
"show-wait-time": "Show the estimated wait time",
|
||||
"show-wait-time-in-game-card": "Show wait time in game card",
|
||||
"simplify-stream-menu": "Simplify Stream's menu",
|
||||
"skip-splash-video": "Skip Xbox splash video",
|
||||
"slow": "Slow",
|
||||
@ -277,6 +279,7 @@ const Texts = {
|
||||
"unmuted": "Unmuted",
|
||||
"unsharp-masking": "Unsharp masking",
|
||||
"use-mouse-absolute-position": "Use mouse's absolute position",
|
||||
"use-this-at-your-own-risk": "Use this at your own risk",
|
||||
"user-agent-profile": "User-Agent profile",
|
||||
"vertical-scroll-sensitivity": "Vertical scroll sensitivity",
|
||||
"vertical-sensitivity": "Vertical sensitivity",
|
||||
|
@ -29,8 +29,8 @@ export class UserAgent {
|
||||
static #USER_AGENTS: PartialRecord<UserAgentProfile, string> = {
|
||||
[UserAgentProfile.WINDOWS_EDGE]: `Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${CHROMIUM_VERSION} Safari/537.36 Edg/${CHROMIUM_VERSION}`,
|
||||
[UserAgentProfile.MACOS_SAFARI]: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5.2 Safari/605.1.1',
|
||||
[UserAgentProfile.SMARTTV_GENERIC]: `${window.navigator.userAgent} SmartTV ${SMART_TV_UNIQUE_ID}`,
|
||||
[UserAgentProfile.SMARTTV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
|
||||
[UserAgentProfile.SMART_TV_GENERIC]: `${window.navigator.userAgent} SmartTV`,
|
||||
[UserAgentProfile.SMART_TV_TIZEN]: `Mozilla/5.0 (SMART-TV; LINUX; Tizen 7.0) AppleWebKit/537.36 (KHTML, like Gecko) ${CHROMIUM_VERSION}/7.0 TV Safari/537.36 ${SMART_TV_UNIQUE_ID}`,
|
||||
[UserAgentProfile.VR_OCULUS]: window.navigator.userAgent + ' OculusBrowser VR',
|
||||
}
|
||||
|
||||
|
79
src/utils/xcloud-api.ts
Normal file
79
src/utils/xcloud-api.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
|
||||
export class XcloudApi {
|
||||
private static instance: XcloudApi;
|
||||
|
||||
public static getInstance(): XcloudApi {
|
||||
if (!XcloudApi.instance) {
|
||||
XcloudApi.instance = new XcloudApi();
|
||||
}
|
||||
|
||||
return XcloudApi.instance;
|
||||
}
|
||||
|
||||
#CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
|
||||
#CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
|
||||
|
||||
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
|
||||
if (id in this.#CACHE_TITLES) {
|
||||
return this.#CACHE_TITLES[id];
|
||||
}
|
||||
|
||||
const baseUri = STATES.selectedRegion.baseUri;
|
||||
if (!baseUri || !STATES.gsToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let json;
|
||||
try {
|
||||
const response = await NATIVE_FETCH(`${baseUri}/v2/titles`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${STATES.gsToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
// format the data
|
||||
body: JSON.stringify({
|
||||
alternateIds: [id],
|
||||
alternateIdType: 'productId',
|
||||
}),
|
||||
});
|
||||
|
||||
json = (await response.json()).results[0];
|
||||
} catch (e) {
|
||||
json = {}
|
||||
}
|
||||
this.#CACHE_TITLES[id] = json;
|
||||
return json;
|
||||
}
|
||||
|
||||
async getWaitTime(id: string): Promise<XcloudWaitTimeInfo | null> {
|
||||
if (id in this.#CACHE_WAIT_TIME) {
|
||||
return this.#CACHE_WAIT_TIME[id];
|
||||
}
|
||||
|
||||
const baseUri = STATES.selectedRegion.baseUri;
|
||||
if (!baseUri || !STATES.gsToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let json;
|
||||
try {
|
||||
const response = await NATIVE_FETCH(`${baseUri}/v1/waittime/${id}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${STATES.gsToken}`,
|
||||
},
|
||||
});
|
||||
|
||||
json = await response.json();
|
||||
} catch (e) {
|
||||
json = {};
|
||||
}
|
||||
|
||||
this.#CACHE_WAIT_TIME[id] = json;
|
||||
return json;
|
||||
}
|
||||
}
|
223
src/utils/xcloud-interceptor.ts
Normal file
223
src/utils/xcloud-interceptor.ts
Normal file
@ -0,0 +1,223 @@
|
||||
import { LoadingScreen } from "@modules/loading-screen";
|
||||
import { RemotePlay } from "@modules/remote-play";
|
||||
import { StreamBadges } from "@modules/stream/stream-badges";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { NATIVE_FETCH, BX_FLAGS } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
import { patchIceCandidates } from "./network";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { getPreferredServerRegion } from "./region";
|
||||
|
||||
export
|
||||
class XcloudInterceptor {
|
||||
static async #handleLogin(request: RequestInfo | URL, init?: RequestInit) {
|
||||
if (getPref(PrefKey.SERVER_BYPASS_RESTRICTION)) {
|
||||
(request as Request).headers.set('X-Forwarded-For', '9.9.9.9');
|
||||
}
|
||||
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
if (response.status !== 200) {
|
||||
// Unsupported region
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_UNAVAILABLE);
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = await response.clone().json();
|
||||
|
||||
// Store xCloud token
|
||||
RemotePlay.XCLOUD_TOKEN = obj.gsToken;
|
||||
|
||||
// Get server list
|
||||
const serverEmojis = {
|
||||
AustraliaEast: '🇦🇺',
|
||||
AustraliaSouthEast: '🇦🇺',
|
||||
BrazilSouth: '🇧🇷',
|
||||
EastUS: '🇺🇸',
|
||||
EastUS2: '🇺🇸',
|
||||
JapanEast: '🇯🇵',
|
||||
KoreaCentral: '🇰🇷',
|
||||
MexicoCentral: '🇲🇽',
|
||||
NorthCentralUs: '🇺🇸',
|
||||
SouthCentralUS: '🇺🇸',
|
||||
UKSouth: '🇬🇧',
|
||||
WestEurope: '🇪🇺',
|
||||
WestUS: '🇺🇸',
|
||||
WestUS2: '🇺🇸',
|
||||
};
|
||||
|
||||
const serverRegex = /\/\/(\w+)\./;
|
||||
|
||||
for (let region of obj.offeringSettings.regions) {
|
||||
const regionName = region.name as keyof typeof serverEmojis;
|
||||
let shortName = region.name;
|
||||
|
||||
if (region.isDefault) {
|
||||
STATES.selectedRegion = Object.assign({}, region);
|
||||
}
|
||||
|
||||
let match = serverRegex.exec(region.baseUri);
|
||||
if (match) {
|
||||
shortName = match[1];
|
||||
if (serverEmojis[regionName]) {
|
||||
shortName = serverEmojis[regionName] + ' ' + shortName;
|
||||
}
|
||||
}
|
||||
|
||||
region.shortName = shortName.toUpperCase();
|
||||
STATES.serverRegions[region.name] = Object.assign({}, region);
|
||||
}
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.XCLOUD_SERVERS_READY);
|
||||
|
||||
const preferredRegion = getPreferredServerRegion();
|
||||
if (preferredRegion in STATES.serverRegions) {
|
||||
const tmp = Object.assign({}, STATES.serverRegions[preferredRegion]);
|
||||
tmp.isDefault = true;
|
||||
|
||||
obj.offeringSettings.regions = [tmp];
|
||||
STATES.selectedRegion = tmp;
|
||||
}
|
||||
|
||||
STATES.gsToken = obj.gsToken;
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handlePlay(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||
|
||||
const url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
let badgeRegion: string = parsedUrl.host.split('.', 1)[0];
|
||||
for (let regionName in STATES.serverRegions) {
|
||||
const region = STATES.serverRegions[regionName];
|
||||
if (parsedUrl.origin == region.baseUri) {
|
||||
badgeRegion = regionName;
|
||||
break;
|
||||
}
|
||||
}
|
||||
StreamBadges.getInstance().setRegion(badgeRegion);
|
||||
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
// Force stream's resolution
|
||||
if (PREF_STREAM_TARGET_RESOLUTION !== 'auto') {
|
||||
const osName = (PREF_STREAM_TARGET_RESOLUTION === '720p') ? 'android' : 'windows';
|
||||
body.settings.osName = osName;
|
||||
}
|
||||
|
||||
// Override "locale" value
|
||||
if (PREF_STREAM_PREFERRED_LOCALE !== 'default') {
|
||||
body.settings.locale = PREF_STREAM_PREFERRED_LOCALE;
|
||||
}
|
||||
|
||||
const newRequest = new Request(request, {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(newRequest);
|
||||
}
|
||||
|
||||
static async #handleWaitTime(request: RequestInfo | URL, init?: RequestInit) {
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
|
||||
if (getPref(PrefKey.UI_LOADING_SCREEN_WAIT_TIME)) {
|
||||
const json = await response.clone().json();
|
||||
if (json.estimatedAllocationTimeInSeconds > 0) {
|
||||
// Setup wait time overlay
|
||||
LoadingScreen.setupWaitTime(json.estimatedTotalWaitTimeInSeconds);
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleConfiguration(request: RequestInfo | URL, init?: RequestInit) {
|
||||
if ((request as Request).method !== 'GET') {
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
|
||||
// Touch controller for all games
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
|
||||
const titleInfo = STATES.currentStream.titleInfo;
|
||||
if (titleInfo?.details.hasTouchSupport) {
|
||||
TouchController.disable();
|
||||
} else {
|
||||
TouchController.enable();
|
||||
}
|
||||
}
|
||||
|
||||
// Intercept configurations
|
||||
const response = await NATIVE_FETCH(request, init);
|
||||
const text = await response.clone().text();
|
||||
if (!text.length) {
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = JSON.parse(text);
|
||||
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
|
||||
|
||||
overrides.inputConfiguration = overrides.inputConfiguration || {};
|
||||
overrides.inputConfiguration.enableVibration = true;
|
||||
|
||||
let overrideMkb: boolean | null = null;
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'on' || (STATES.currentStream.titleInfo && BX_FLAGS.ForceNativeMkbTitles?.includes(STATES.currentStream.titleInfo.details.productId))) {
|
||||
overrideMkb = true;
|
||||
}
|
||||
|
||||
if (getPref(PrefKey.NATIVE_MKB_ENABLED) === 'off') {
|
||||
overrideMkb = false;
|
||||
}
|
||||
|
||||
if (overrideMkb !== null) {
|
||||
overrides.inputConfiguration = Object.assign(overrides.inputConfiguration, {
|
||||
enableMouseInput: overrideMkb,
|
||||
enableKeyboardInput: overrideMkb,
|
||||
});
|
||||
}
|
||||
|
||||
// Enable touch controller
|
||||
if (TouchController.isEnabled()) {
|
||||
overrides.inputConfiguration.enableTouchInput = true;
|
||||
overrides.inputConfiguration.maxTouchPoints = 10;
|
||||
}
|
||||
|
||||
// Enable mic
|
||||
if (getPref(PrefKey.AUDIO_MIC_ON_PLAYING)) {
|
||||
overrides.audioConfiguration = overrides.audioConfiguration || {};
|
||||
overrides.audioConfiguration.enableMicrophone = true;
|
||||
}
|
||||
|
||||
obj.clientStreamingConfigOverrides = JSON.stringify(overrides);
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async handle(request: RequestInfo | URL, init?: RequestInit) {
|
||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
|
||||
// Server list
|
||||
if (url.endsWith('/v2/login/user')) {
|
||||
return XcloudInterceptor.#handleLogin(request, init);
|
||||
} else if (url.endsWith('/sessions/cloud/play')) { // Get session
|
||||
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 as Request).method === 'GET') {
|
||||
return patchIceCandidates(request as Request);
|
||||
}
|
||||
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
}
|
184
src/utils/xhome-interceptor.ts
Normal file
184
src/utils/xhome-interceptor.ts
Normal file
@ -0,0 +1,184 @@
|
||||
import { RemotePlay } from "@/modules/remote-play";
|
||||
import { TouchController } from "@/modules/touch-controller";
|
||||
import { BxEvent } from "./bx-event";
|
||||
import { InputType } from "./bx-exposed";
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { STATES } from "./global";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { patchIceCandidates } from "./network";
|
||||
|
||||
export class XhomeInterceptor {
|
||||
static #consoleAddrs: {[index: string]: number} = {};
|
||||
|
||||
static async #handleLogin(request: Request) {
|
||||
try {
|
||||
const clone = (request as Request).clone();
|
||||
|
||||
const obj = await clone.json();
|
||||
obj.offeringId = 'xhome';
|
||||
|
||||
request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(obj),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
alert(e);
|
||||
console.log(e);
|
||||
}
|
||||
|
||||
return NATIVE_FETCH(request);
|
||||
}
|
||||
|
||||
static async #handleConfiguration(request: Request | URL) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
const obj = await response.clone().json()
|
||||
console.log(obj);
|
||||
|
||||
const serverDetails = obj.serverDetails;
|
||||
if (serverDetails.ipAddress) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipAddress] = serverDetails.port;
|
||||
}
|
||||
|
||||
if (serverDetails.ipV4Address) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipV4Address] = serverDetails.ipV4Port;
|
||||
}
|
||||
|
||||
if (serverDetails.ipV6Address) {
|
||||
XhomeInterceptor.#consoleAddrs[serverDetails.ipV6Address] = serverDetails.ipV6Port;
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleInputConfigs(request: Request | URL, opts: {[index: string]: any}) {
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== 'all') {
|
||||
return response;
|
||||
}
|
||||
|
||||
const obj = await response.clone().json() as any;
|
||||
|
||||
const xboxTitleId = JSON.parse(opts.body).titleIds[0];
|
||||
STATES.currentStream.xboxTitleId = xboxTitleId;
|
||||
|
||||
const inputConfigs = obj[0];
|
||||
|
||||
let hasTouchSupport = inputConfigs.supportedTabs.length > 0;
|
||||
if (!hasTouchSupport) {
|
||||
const supportedInputTypes = inputConfigs.supportedInputTypes;
|
||||
hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) || supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY);
|
||||
}
|
||||
|
||||
if (hasTouchSupport) {
|
||||
TouchController.disable();
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.CUSTOM_TOUCH_LAYOUTS_LOADED, {
|
||||
data: null,
|
||||
});
|
||||
} else {
|
||||
TouchController.enable();
|
||||
TouchController.getCustomLayouts(xboxTitleId);
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
response.text = () => Promise.resolve(JSON.stringify(obj));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
static async #handleTitles(request: Request) {
|
||||
const clone = request.clone();
|
||||
|
||||
const headers: {[index: string]: any} = {};
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
headers.authorization = `Bearer ${RemotePlay.XCLOUD_TOKEN}`;
|
||||
|
||||
const index = request.url.indexOf('.xboxlive.com');
|
||||
request = new Request('https://wus.core.gssv-play-prod' + request.url.substring(index), {
|
||||
method: clone.method,
|
||||
body: await clone.text(),
|
||||
headers: headers,
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(request);
|
||||
}
|
||||
|
||||
static async #handlePlay(request: RequestInfo | URL) {
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
// body.settings.useIceConnection = true;
|
||||
|
||||
const newRequest = new Request(request, {
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
return NATIVE_FETCH(newRequest);
|
||||
}
|
||||
|
||||
static async handle(request: Request) {
|
||||
TouchController.disable();
|
||||
|
||||
const clone = request.clone();
|
||||
|
||||
const headers: {[index: string]: string} = {};
|
||||
for (const pair of (clone.headers as any).entries()) {
|
||||
headers[pair[0]] = pair[1];
|
||||
}
|
||||
// Add xHome token to headers
|
||||
headers.authorization = `Bearer ${RemotePlay.XHOME_TOKEN}`;
|
||||
|
||||
// Patch resolution
|
||||
const deviceInfo = RemotePlay.BASE_DEVICE_INFO;
|
||||
if (getPref(PrefKey.REMOTE_PLAY_RESOLUTION) === '720p') {
|
||||
deviceInfo.dev.os.name = 'android';
|
||||
}
|
||||
|
||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||
|
||||
const opts: {[index: string]: any} = {
|
||||
method: clone.method,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
if (clone.method === 'POST') {
|
||||
opts.body = await clone.text();
|
||||
}
|
||||
|
||||
let newUrl = request.url;
|
||||
if (!newUrl.includes('/servers/home')) {
|
||||
const index = request.url.indexOf('.xboxlive.com');
|
||||
newUrl = STATES.remotePlay.server + request.url.substring(index + 13);
|
||||
}
|
||||
|
||||
request = new Request(newUrl, opts);
|
||||
let url = (typeof request === 'string') ? request : request.url;
|
||||
|
||||
// Get console IP
|
||||
if (url.includes('/configuration')) {
|
||||
return XhomeInterceptor.#handleConfiguration(request);
|
||||
} else if (url.endsWith('/sessions/home/play')) {
|
||||
return XhomeInterceptor.#handlePlay(request);
|
||||
} else if (url.includes('inputconfigs')) {
|
||||
return XhomeInterceptor.#handleInputConfigs(request, opts);
|
||||
} else if (url.includes('/login/user')) {
|
||||
return XhomeInterceptor.#handleLogin(request);
|
||||
} else if (url.endsWith('/titles')) {
|
||||
return XhomeInterceptor.#handleTitles(request);
|
||||
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && (request as Request).method === 'GET') {
|
||||
return patchIceCandidates(request, XhomeInterceptor.#consoleAddrs);
|
||||
}
|
||||
|
||||
return await NATIVE_FETCH(request);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user