mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-06-08 16:47:19 +02:00
358 lines
12 KiB
TypeScript
358 lines
12 KiB
TypeScript
import { STATES, AppInterface } from "@utils/global";
|
|
import { CE, createButton, ButtonStyle, Icon } from "@utils/html";
|
|
import { Toast } from "@utils/toast";
|
|
import { BxEvent } from "@utils/bx-event";
|
|
import { getPref, PrefKey, setPref } from "@utils/preferences";
|
|
import { t } from "@utils/translation";
|
|
import { localRedirect } from "@modules/ui/ui";
|
|
import { BxLogger } from "@utils/bx-logger";
|
|
|
|
const LOG_TAG = 'RemotePlay';
|
|
|
|
enum RemotePlayConsoleState {
|
|
ON = 'On',
|
|
OFF = 'Off',
|
|
STANDBY = 'ConnectedStandby',
|
|
UNKNOWN = 'Unknown',
|
|
}
|
|
|
|
type RemotePlayRegion = {
|
|
name: string;
|
|
baseUri: string;
|
|
isDefault: boolean;
|
|
};
|
|
|
|
type RemotePlayConsole = {
|
|
deviceName: string;
|
|
serverId: string;
|
|
powerState: RemotePlayConsoleState;
|
|
consoleType: string;
|
|
// playPath: string;
|
|
// outOfHomeWarning: string;
|
|
// wirelessWarning: string;
|
|
// isDevKit: string;
|
|
};
|
|
|
|
export class RemotePlay {
|
|
static XCLOUD_TOKEN: string;
|
|
static XHOME_TOKEN: string;
|
|
static #CONSOLES: Array<RemotePlayConsole>;
|
|
static #REGIONS: Array<RemotePlayRegion>;
|
|
|
|
static readonly #STATE_LABELS: {[key in RemotePlayConsoleState]: string} = {
|
|
[RemotePlayConsoleState.ON]: t('powered-on'),
|
|
[RemotePlayConsoleState.OFF]: t('powered-off'),
|
|
[RemotePlayConsoleState.STANDBY]: t('standby'),
|
|
[RemotePlayConsoleState.UNKNOWN]: t('unknown'),
|
|
};
|
|
|
|
static readonly BASE_DEVICE_INFO = {
|
|
appInfo: {
|
|
env: {
|
|
clientAppId: window.location.host,
|
|
clientAppType: 'browser',
|
|
clientAppVersion: '21.1.98',
|
|
clientSdkVersion: '8.5.3',
|
|
httpEnvironment: 'prod',
|
|
sdkInstallId: '',
|
|
},
|
|
},
|
|
dev: {
|
|
displayInfo: {
|
|
dimensions: {
|
|
widthInPixels: 1920,
|
|
heightInPixels: 1080,
|
|
},
|
|
pixelDensity: {
|
|
dpiX: 1,
|
|
dpiY: 1,
|
|
},
|
|
},
|
|
hw: {
|
|
make: 'Microsoft',
|
|
model: 'unknown',
|
|
sdktype: 'web',
|
|
},
|
|
os: {
|
|
name: 'windows',
|
|
ver: '22631.2715',
|
|
platform: 'desktop',
|
|
},
|
|
browser: {
|
|
browserName: 'chrome',
|
|
browserVersion: '119.0',
|
|
},
|
|
},
|
|
};
|
|
|
|
static #$content: HTMLElement;
|
|
|
|
static #initialize() {
|
|
if (RemotePlay.#$content) {
|
|
return;
|
|
}
|
|
|
|
RemotePlay.#$content = CE('div', {}, t('getting-consoles-list'));
|
|
RemotePlay.#getXhomeToken(() => {
|
|
RemotePlay.#getConsolesList(() => {
|
|
BxLogger.info(LOG_TAG, 'Consoles', RemotePlay.#CONSOLES);
|
|
RemotePlay.#renderConsoles();
|
|
BxEvent.dispatch(window, BxEvent.REMOTE_PLAY_READY);
|
|
});
|
|
});
|
|
}
|
|
|
|
static #renderConsoles() {
|
|
const $fragment = CE('div', {'class': 'bx-remote-play-container'});
|
|
|
|
if (!RemotePlay.#CONSOLES || RemotePlay.#CONSOLES.length === 0) {
|
|
$fragment.appendChild(CE('span', {}, t('no-consoles-found')));
|
|
RemotePlay.#$content = CE('div', {}, $fragment);
|
|
return;
|
|
}
|
|
|
|
const $settingNote = CE('p', {});
|
|
|
|
const resolutions = [1080, 720];
|
|
const currentResolution = getPref(PrefKey.REMOTE_PLAY_RESOLUTION);
|
|
const $resolutionGroup = CE('div', {});
|
|
for (const resolution of resolutions) {
|
|
const value = `${resolution}p`;
|
|
const id = `bx_radio_xhome_resolution_${resolution}`;
|
|
|
|
const $radio = CE<HTMLInputElement>('input', {
|
|
'type': 'radio',
|
|
'value': value,
|
|
'id': id,
|
|
'name': 'bx_radio_xhome_resolution',
|
|
}, value);
|
|
|
|
$radio.addEventListener('change', e => {
|
|
const value = (e.target as HTMLInputElement).value;
|
|
|
|
$settingNote.textContent = value === '1080p' ? '✅ ' + t('can-stream-xbox-360-games') : '❌ ' + t('cant-stream-xbox-360-games');
|
|
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, value);
|
|
});
|
|
|
|
const $label = CE('label', {
|
|
'for': id,
|
|
'class': 'bx-remote-play-resolution',
|
|
}, $radio, `${resolution}p`);
|
|
|
|
$resolutionGroup.appendChild($label);
|
|
|
|
if (currentResolution === value) {
|
|
$radio.checked = true;
|
|
$radio.dispatchEvent(new Event('change'));
|
|
}
|
|
}
|
|
|
|
const $qualitySettings = CE('div', {'class': 'bx-remote-play-settings'},
|
|
CE('div', {},
|
|
CE('label', {}, t('target-resolution'), $settingNote),
|
|
$resolutionGroup,
|
|
)
|
|
);
|
|
|
|
$fragment.appendChild($qualitySettings);
|
|
|
|
// Render concoles list
|
|
for (let con of RemotePlay.#CONSOLES) {
|
|
const $child = CE('div', {'class': 'bx-remote-play-device-wrapper'},
|
|
CE('div', {'class': 'bx-remote-play-device-info'},
|
|
CE('div', {},
|
|
CE('span', {'class': 'bx-remote-play-device-name'}, con.deviceName),
|
|
CE('span', {'class': 'bx-remote-play-console-type'}, con.consoleType.replace('Xbox', ''))
|
|
),
|
|
CE('div', {'class': 'bx-remote-play-power-state'}, RemotePlay.#STATE_LABELS[con.powerState]),
|
|
),
|
|
|
|
// Connect button
|
|
createButton({
|
|
classes: ['bx-remote-play-connect-button'],
|
|
label: t('console-connect'),
|
|
style: ButtonStyle.PRIMARY | ButtonStyle.FOCUSABLE,
|
|
onClick: e => {
|
|
RemotePlay.play(con.serverId);
|
|
},
|
|
}),
|
|
);
|
|
|
|
$fragment.appendChild($child);
|
|
}
|
|
|
|
// Add Help button
|
|
$fragment.appendChild(createButton({
|
|
icon: Icon.QUESTION,
|
|
style: ButtonStyle.GHOST | ButtonStyle.FOCUSABLE,
|
|
url: 'https://better-xcloud.github.io/remote-play',
|
|
label: t('help'),
|
|
}));
|
|
|
|
RemotePlay.#$content = CE('div', {}, $fragment);
|
|
}
|
|
|
|
static #getXhomeToken(callback: any) {
|
|
if (RemotePlay.XHOME_TOKEN) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
let GSSV_TOKEN;
|
|
try {
|
|
GSSV_TOKEN = JSON.parse(localStorage.getItem('xboxcom_xbl_user_info')!).tokens['http://gssv.xboxlive.com/'].token;
|
|
} catch (e) {
|
|
for (let i = 0; i < localStorage.length; i++){
|
|
const key = localStorage.key(i)!;
|
|
if (!key.startsWith('Auth.User.')) {
|
|
continue;
|
|
}
|
|
|
|
const json = JSON.parse(localStorage.getItem(key)!);
|
|
for (const token of json.tokens) {
|
|
if (!token.relyingParty.includes('gssv.xboxlive.com')) {
|
|
continue;
|
|
}
|
|
|
|
GSSV_TOKEN = token.tokenData.token;
|
|
break;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
const request = new Request('https://xhome.gssv-play-prod.xboxlive.com/v2/login/user', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
offeringId: 'xhome',
|
|
token: GSSV_TOKEN,
|
|
}),
|
|
headers: {
|
|
'Content-Type': 'application/json; charset=utf-8',
|
|
},
|
|
});
|
|
fetch(request).then(resp => resp.json())
|
|
.then(json => {
|
|
RemotePlay.#REGIONS = json.offeringSettings.regions;
|
|
RemotePlay.XHOME_TOKEN = json.gsToken;
|
|
callback();
|
|
});
|
|
}
|
|
|
|
static async #getConsolesList(callback: any) {
|
|
if (RemotePlay.#CONSOLES) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
const options = {
|
|
method: 'GET',
|
|
headers: {
|
|
'Authorization': `Bearer ${RemotePlay.XHOME_TOKEN}`,
|
|
},
|
|
};
|
|
|
|
// Test servers one by one
|
|
for (const region of RemotePlay.#REGIONS) {
|
|
try {
|
|
const request = new Request(`${region.baseUri}/v6/servers/home?mr=50`, options);
|
|
const resp = await fetch(request);
|
|
|
|
const json = await resp.json();
|
|
RemotePlay.#CONSOLES = json.results;
|
|
|
|
// Store working server
|
|
STATES.remotePlay.server = region.baseUri;
|
|
|
|
callback();
|
|
} catch (e) {}
|
|
|
|
if (RemotePlay.#CONSOLES) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// None of the servers worked
|
|
if (!STATES.remotePlay.server) {
|
|
RemotePlay.#CONSOLES = [];
|
|
}
|
|
}
|
|
|
|
static play(serverId: string, resolution?: string) {
|
|
if (resolution) {
|
|
setPref(PrefKey.REMOTE_PLAY_RESOLUTION, resolution);
|
|
}
|
|
|
|
STATES.remotePlay.config = {
|
|
serverId: serverId,
|
|
};
|
|
window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config;
|
|
|
|
localRedirect('/launch/fortnite/BT5P2X999VH2#remote-play');
|
|
RemotePlay.detachPopup();
|
|
}
|
|
|
|
static preload() {
|
|
RemotePlay.#initialize();
|
|
}
|
|
|
|
static detachPopup() {
|
|
// Detach popup from body
|
|
const $popup = document.querySelector('.bx-remote-play-popup');
|
|
$popup && $popup.remove();
|
|
}
|
|
|
|
static togglePopup(force = null) {
|
|
if (!getPref(PrefKey.REMOTE_PLAY_ENABLED) || !RemotePlay.isReady()) {
|
|
Toast.show(t('getting-consoles-list'));
|
|
return;
|
|
}
|
|
|
|
RemotePlay.#initialize();
|
|
|
|
if (AppInterface && AppInterface.showRemotePlayDialog) {
|
|
AppInterface.showRemotePlayDialog(JSON.stringify(RemotePlay.#CONSOLES));
|
|
(document.activeElement as HTMLElement).blur();
|
|
return;
|
|
}
|
|
|
|
if (document.querySelector('.bx-remote-play-popup')) {
|
|
if (force === false) {
|
|
RemotePlay.#$content.classList.add('bx-gone');
|
|
} else {
|
|
RemotePlay.#$content.classList.toggle('bx-gone');
|
|
}
|
|
return;
|
|
}
|
|
|
|
const $header = document.querySelector('#gamepass-root header')!;
|
|
|
|
const group = $header.firstElementChild!.getAttribute('data-group')!;
|
|
RemotePlay.#$content.setAttribute('data-group', group);
|
|
RemotePlay.#$content.classList.add('bx-remote-play-popup');
|
|
RemotePlay.#$content.classList.remove('bx-gone');
|
|
|
|
$header.insertAdjacentElement('afterend', RemotePlay.#$content);
|
|
}
|
|
|
|
static detect() {
|
|
if (!getPref(PrefKey.REMOTE_PLAY_ENABLED)) {
|
|
return;
|
|
}
|
|
|
|
STATES.remotePlay.isPlaying = window.location.pathname.includes('/launch/') && window.location.hash.startsWith('#remote-play');
|
|
if (STATES.remotePlay?.isPlaying) {
|
|
window.BX_REMOTE_PLAY_CONFIG = STATES.remotePlay.config;
|
|
// Remove /launch/... from URL
|
|
window.history.replaceState({origin: 'better-xcloud'}, '', 'https://www.xbox.com/' + location.pathname.substring(1, 6) + '/play');
|
|
} else {
|
|
window.BX_REMOTE_PLAY_CONFIG = null;
|
|
}
|
|
}
|
|
|
|
static isReady() {
|
|
return RemotePlay.#CONSOLES !== null && RemotePlay.#CONSOLES.length > 0;
|
|
}
|
|
}
|