mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-08-01 10:56:42 +02:00
Optimize + refactor code
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
|
||||
const enum TextColor {
|
||||
INFO = '#008746',
|
||||
WARNING = '#c1a404',
|
||||
@@ -10,7 +12,7 @@ export class BxLogger {
|
||||
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
|
||||
|
||||
private static log(color: string, tag: string, ...args: any) {
|
||||
console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
|
||||
BX_FLAGS.Debug && console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -56,6 +56,8 @@ function createElement<T=HTMLElement>(elmName: string, props: CreateElementOptio
|
||||
let $elm;
|
||||
const hasNs = 'xmlns' in props;
|
||||
|
||||
// console.trace('createElement', elmName, props);
|
||||
|
||||
if (hasNs) {
|
||||
$elm = document.createElementNS(props.xmlns, elmName);
|
||||
delete props.xmlns;
|
||||
@@ -111,11 +113,11 @@ const ButtonStyleIndices = Object.keys(ButtonStyleClass).map(i => parseInt(i));
|
||||
export function createButton<T=HTMLButtonElement>(options: BxButton): T {
|
||||
let $btn;
|
||||
if (options.url) {
|
||||
$btn = CE('a', {'class': 'bx-button'}) as HTMLAnchorElement;
|
||||
$btn = CE<HTMLAnchorElement>('a', {'class': 'bx-button'});
|
||||
$btn.href = options.url;
|
||||
$btn.target = '_blank';
|
||||
} else {
|
||||
$btn = CE('button', {'class': 'bx-button', type: 'button'}) as HTMLButtonElement;
|
||||
$btn = CE<HTMLButtonElement>('button', {'class': 'bx-button', type: 'button'});
|
||||
}
|
||||
|
||||
const style = (options.style || 0) as number;
|
||||
|
@@ -1,165 +0,0 @@
|
||||
import { MkbPreset } from "@modules/mkb/mkb-preset";
|
||||
import { t } from "@utils/translation";
|
||||
import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { setPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
export class LocalDb {
|
||||
static #instance: LocalDb;
|
||||
static get INSTANCE() {
|
||||
if (!LocalDb.#instance) {
|
||||
LocalDb.#instance = new LocalDb();
|
||||
}
|
||||
|
||||
return LocalDb.#instance;
|
||||
}
|
||||
|
||||
static readonly DB_NAME = 'BetterXcloud';
|
||||
static readonly DB_VERSION = 1;
|
||||
static readonly TABLE_PRESETS = 'mkb_presets';
|
||||
|
||||
#DB: any;
|
||||
|
||||
#open() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.#DB) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
|
||||
request.onupgradeneeded = (e: IDBVersionChangeEvent) => {
|
||||
const db = (e.target! as any).result;
|
||||
|
||||
switch (e.oldVersion) {
|
||||
case 0: {
|
||||
const presets = db.createObjectStore(LocalDb.TABLE_PRESETS, {keyPath: 'id', autoIncrement: true});
|
||||
presets.createIndex('name_idx', 'name');
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = e => {
|
||||
console.log(e);
|
||||
alert((e.target as any).error.message);
|
||||
reject && reject();
|
||||
};
|
||||
|
||||
request.onsuccess = e => {
|
||||
this.#DB = (e.target as any).result;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#table(name: string, type: string): Promise<IDBObjectStore> {
|
||||
const transaction = this.#DB.transaction(name, type || 'readonly');
|
||||
const table = transaction.objectStore(name);
|
||||
|
||||
return new Promise(resolve => resolve(table));
|
||||
}
|
||||
|
||||
// Convert IndexDB method to Promise
|
||||
#call(method: any) {
|
||||
const table = arguments[1];
|
||||
return new Promise(resolve => {
|
||||
const request = method.call(table, ...Array.from(arguments).slice(2));
|
||||
request.onsuccess = (e: Event) => {
|
||||
resolve([table, (e.target as any).result]);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#count(table: IDBObjectStore): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.count, ...arguments);
|
||||
}
|
||||
|
||||
#add(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.add, ...arguments);
|
||||
}
|
||||
|
||||
#put(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.put, ...arguments);
|
||||
}
|
||||
|
||||
#delete(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.delete, ...arguments);
|
||||
}
|
||||
|
||||
#get(table: IDBObjectStore, id: number): Promise<any> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.get, ...arguments);
|
||||
}
|
||||
|
||||
#getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
|
||||
// @ts-ignore
|
||||
return this.#call(table.getAll, ...arguments);
|
||||
}
|
||||
|
||||
newPreset(name: string, data: any) {
|
||||
return this.#open()
|
||||
.then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
|
||||
.then(table => this.#add(table, {name, data}))
|
||||
.then(([table, id]) => new Promise<number>(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
updatePreset(preset: MkbStoredPreset) {
|
||||
return this.#open()
|
||||
.then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
|
||||
.then(table => this.#put(table, preset))
|
||||
.then(([table, id]) => new Promise(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
deletePreset(id: number) {
|
||||
return this.#open()
|
||||
.then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
|
||||
.then(table => this.#delete(table, id))
|
||||
.then(([table, id]) => new Promise(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
getPreset(id: number): Promise<MkbStoredPreset> {
|
||||
return this.#open()
|
||||
.then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
|
||||
.then(table => this.#get(table, id))
|
||||
.then(([table, preset]) => new Promise(resolve => resolve(preset)));
|
||||
}
|
||||
|
||||
getPresets(): Promise<MkbStoredPresets> {
|
||||
return this.#open()
|
||||
.then(() => this.#table(LocalDb.TABLE_PRESETS, 'readwrite'))
|
||||
.then(table => this.#count(table))
|
||||
.then(([table, count]) => {
|
||||
if (count > 0) {
|
||||
return new Promise(resolve => {
|
||||
this.#getAll(table)
|
||||
.then(([table, items]) => {
|
||||
const presets: MkbStoredPresets = {};
|
||||
items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
|
||||
resolve(presets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create "Default" preset when the table is empty
|
||||
const preset: MkbStoredPreset = {
|
||||
name: t('default'),
|
||||
data: MkbPreset.DEFAULT_PRESET,
|
||||
}
|
||||
|
||||
return new Promise<MkbStoredPresets>(resolve => {
|
||||
this.#add(table, preset)
|
||||
.then(([table, id]) => {
|
||||
preset.id = id;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
|
||||
|
||||
resolve({[id]: preset});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
79
src/utils/local-db/local-db.ts
Normal file
79
src/utils/local-db/local-db.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
export abstract class LocalDb {
|
||||
static readonly DB_NAME = 'BetterXcloud';
|
||||
static readonly DB_VERSION = 1;
|
||||
|
||||
private db: any;
|
||||
|
||||
protected open() {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (this.db) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const request = window.indexedDB.open(LocalDb.DB_NAME, LocalDb.DB_VERSION);
|
||||
request.onupgradeneeded = this.onUpgradeNeeded;
|
||||
|
||||
request.onerror = e => {
|
||||
console.log(e);
|
||||
alert((e.target as any).error.message);
|
||||
reject && reject();
|
||||
};
|
||||
|
||||
request.onsuccess = e => {
|
||||
this.db = (e.target as any).result;
|
||||
resolve();
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract onUpgradeNeeded(e: IDBVersionChangeEvent): void;
|
||||
|
||||
protected table(name: string, type: string): Promise<IDBObjectStore> {
|
||||
const transaction = this.db.transaction(name, type || 'readonly');
|
||||
const table = transaction.objectStore(name);
|
||||
|
||||
return new Promise(resolve => resolve(table));
|
||||
}
|
||||
|
||||
// Convert IndexDB method to Promise
|
||||
protected call(method: any) {
|
||||
const table = arguments[1];
|
||||
return new Promise(resolve => {
|
||||
const request = method.call(table, ...Array.from(arguments).slice(2));
|
||||
request.onsuccess = (e: Event) => {
|
||||
resolve([table, (e.target as any).result]);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected count(table: IDBObjectStore): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.count, ...arguments);
|
||||
}
|
||||
|
||||
protected add(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.add, ...arguments);
|
||||
}
|
||||
|
||||
protected put(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.put, ...arguments);
|
||||
}
|
||||
|
||||
protected delete(table: IDBObjectStore, data: any): Promise<[IDBObjectStore, number]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.delete, ...arguments);
|
||||
}
|
||||
|
||||
protected get(table: IDBObjectStore, id: number): Promise<any> {
|
||||
// @ts-ignore
|
||||
return this.call(table.get, ...arguments);
|
||||
}
|
||||
|
||||
protected getAll(table: IDBObjectStore): Promise<[IDBObjectStore, any]> {
|
||||
// @ts-ignore
|
||||
return this.call(table.getAll, ...arguments);
|
||||
}
|
||||
}
|
96
src/utils/local-db/mkb-presets-db.ts
Normal file
96
src/utils/local-db/mkb-presets-db.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { MkbPreset } from "@/modules/mkb/mkb-preset";
|
||||
import type { MkbStoredPreset, MkbStoredPresets } from "@/types/mkb";
|
||||
import { setPref } from "../settings-storages/global-settings-storage";
|
||||
import { t } from "../translation";
|
||||
import { LocalDb } from "./local-db";
|
||||
import { BxLogger } from "../bx-logger";
|
||||
|
||||
export class MkbPresetsDb extends LocalDb {
|
||||
private static instance: MkbPresetsDb;
|
||||
public static getInstance = () => MkbPresetsDb.instance ?? (MkbPresetsDb.instance = new MkbPresetsDb());
|
||||
private readonly LOG_TAG = 'MkbPresetsDb';
|
||||
|
||||
private readonly TABLE_PRESETS = 'mkb_presets';
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
}
|
||||
|
||||
protected onUpgradeNeeded(e: IDBVersionChangeEvent): void {
|
||||
const db = (e.target! as any).result;
|
||||
switch (e.oldVersion) {
|
||||
case 0: {
|
||||
const presets = db.createObjectStore(this.TABLE_PRESETS, {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
presets.createIndex('name_idx', 'name');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private presetsTable() {
|
||||
return this.open()
|
||||
.then(() => this.table(this.TABLE_PRESETS, 'readwrite'))
|
||||
}
|
||||
|
||||
newPreset(name: string, data: any) {
|
||||
return this.presetsTable()
|
||||
.then(table => this.add(table, {name, data}))
|
||||
.then(([table, id]) => new Promise<number>(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
updatePreset(preset: MkbStoredPreset) {
|
||||
return this.presetsTable()
|
||||
.then(table => this.put(table, preset))
|
||||
.then(([table, id]) => new Promise(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
deletePreset(id: number) {
|
||||
return this.presetsTable()
|
||||
.then(table => this.delete(table, id))
|
||||
.then(([table, id]) => new Promise(resolve => resolve(id)));
|
||||
}
|
||||
|
||||
getPreset(id: number): Promise<MkbStoredPreset> {
|
||||
return this.presetsTable()
|
||||
.then(table => this.get(table, id))
|
||||
.then(([table, preset]) => new Promise(resolve => resolve(preset)));
|
||||
}
|
||||
|
||||
getPresets(): Promise<MkbStoredPresets> {
|
||||
return this.presetsTable()
|
||||
.then(table => this.count(table))
|
||||
.then(([table, count]) => {
|
||||
if (count > 0) {
|
||||
return new Promise(resolve => {
|
||||
this.getAll(table)
|
||||
.then(([table, items]) => {
|
||||
const presets: MkbStoredPresets = {};
|
||||
items.forEach((item: MkbStoredPreset) => (presets[item.id!] = item));
|
||||
resolve(presets);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Create "Default" preset when the table is empty
|
||||
const preset: MkbStoredPreset = {
|
||||
name: t('default'),
|
||||
data: MkbPreset.DEFAULT_PRESET,
|
||||
}
|
||||
|
||||
return new Promise<MkbStoredPresets>(resolve => {
|
||||
this.add(table, preset)
|
||||
.then(([table, id]) => {
|
||||
preset.id = id;
|
||||
setPref(PrefKey.MKB_DEFAULT_PRESET_ID, id);
|
||||
|
||||
resolve({[id]: preset});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,6 +1,5 @@
|
||||
import { isFullVersion } from "@macros/build" with {type: "macro"};
|
||||
|
||||
import { BxEvent } from "@utils/bx-event";
|
||||
import { BX_FLAGS, NATIVE_FETCH } from "@utils/bx-flags";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { STATES } from "@utils/global";
|
||||
@@ -29,9 +28,7 @@ function clearDbLogs(dbName: string, table: string) {
|
||||
const objectStore = db.transaction(table, 'readwrite').objectStore(table);
|
||||
const objectStoreRequest = objectStore.clear();
|
||||
|
||||
objectStoreRequest.onsuccess = function() {
|
||||
console.log(`[Better xCloud] Cleared ${dbName}.${table}`);
|
||||
};
|
||||
objectStoreRequest.onsuccess = () => BxLogger.info('clearDbLogs', `Cleared ${dbName}.${table}`);
|
||||
} catch (ex) {}
|
||||
}
|
||||
}
|
||||
@@ -134,6 +131,7 @@ export function interceptHttpRequests() {
|
||||
'https://browser.events.data.microsoft.com',
|
||||
'https://dc.services.visualstudio.com',
|
||||
'https://2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
|
||||
'https://mscom.demdex.net',
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -172,29 +170,42 @@ export function interceptHttpRequests() {
|
||||
};
|
||||
|
||||
let gamepassAllGames: string[] = [];
|
||||
const IGNORED_DOMAINS = [
|
||||
'accounts.xboxlive.com',
|
||||
'chat.xboxlive.com',
|
||||
'notificationinbox.xboxlive.com',
|
||||
'peoplehub.xboxlive.com',
|
||||
'rta.xboxlive.com',
|
||||
'userpresence.xboxlive.com',
|
||||
'xblmessaging.xboxlive.com',
|
||||
'consent.config.office.com',
|
||||
|
||||
'arc.msn.com',
|
||||
'browser.events.data.microsoft.com',
|
||||
'dc.services.visualstudio.com',
|
||||
'2c06dea3f26c40c69b8456d319791fd0@o427368.ingest.sentry.io',
|
||||
];
|
||||
|
||||
(window as any).BX_FETCH = window.fetch = async (request: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
||||
let url = (typeof request === 'string') ? request : (request as Request).url;
|
||||
|
||||
// Check blocked URLs
|
||||
for (let blocked of BLOCKED_URLS) {
|
||||
if (!url.startsWith(blocked)) {
|
||||
continue;
|
||||
if (url.startsWith(blocked)) {
|
||||
return new Response('{"acc":1,"webResult":{}}', {
|
||||
status: 200,
|
||||
statusText: '200 OK',
|
||||
});
|
||||
}
|
||||
|
||||
return new Response('{"acc":1,"webResult":{}}', {
|
||||
status: 200,
|
||||
statusText: '200 OK',
|
||||
});
|
||||
}
|
||||
|
||||
if (url.endsWith('/play')) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
||||
// Ignore URLs
|
||||
const domain = (new URL(url)).hostname;
|
||||
if (IGNORED_DOMAINS.includes(domain)) {
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
|
||||
if (url.endsWith('/configuration')) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||
}
|
||||
// BxLogger.info('fetch', url);
|
||||
|
||||
// Override experimentals
|
||||
if (url.startsWith('https://emerald.xboxservices.com/xboxcomfd/experimentation')) {
|
||||
@@ -212,6 +223,7 @@ export function interceptHttpRequests() {
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return NATIVE_FETCH(request, init);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -60,7 +60,7 @@ export class RootDialogObserver {
|
||||
}
|
||||
} else if ($root.querySelector('div[class*=GuideDialog]')) {
|
||||
// Guide menu
|
||||
GuideMenu.observe($addedElm);
|
||||
GuideMenu.getInstance().observe($addedElm);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
105
src/utils/screenshot-manager.ts
Normal file
105
src/utils/screenshot-manager.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { StreamPlayerType } from "@enums/stream-player";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { CE } from "./html";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
|
||||
|
||||
export class ScreenshotManager {
|
||||
private static instance: ScreenshotManager;
|
||||
public static getInstance = () => ScreenshotManager.instance ?? (ScreenshotManager.instance = new ScreenshotManager());
|
||||
private readonly LOG_TAG = 'ScreenshotManager';
|
||||
|
||||
private $download: HTMLAnchorElement;
|
||||
private $canvas: HTMLCanvasElement;
|
||||
private canvasContext: CanvasRenderingContext2D;
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
|
||||
this.$download = CE<HTMLAnchorElement>('a');
|
||||
|
||||
this.$canvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
|
||||
this.canvasContext = this.$canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
willReadFrequently: false,
|
||||
})!;
|
||||
}
|
||||
|
||||
updateCanvasSize(width: number, height: number) {
|
||||
this.$canvas.width = width;
|
||||
this.$canvas.height = height;
|
||||
}
|
||||
|
||||
updateCanvasFilters(filters: string) {
|
||||
this.canvasContext.filter = filters;
|
||||
}
|
||||
|
||||
private onAnimationEnd(e: Event) {
|
||||
(e.target as HTMLElement).classList.remove('bx-taking-screenshot');
|
||||
}
|
||||
|
||||
takeScreenshot(callback?: any) {
|
||||
const currentStream = STATES.currentStream;
|
||||
const streamPlayer = currentStream.streamPlayer;
|
||||
const $canvas = this.$canvas;
|
||||
if (!streamPlayer || !$canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let $player;
|
||||
if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
|
||||
$player = streamPlayer.getPlayerElement();
|
||||
} else {
|
||||
$player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
|
||||
}
|
||||
|
||||
if (!$player || !$player.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
$player.parentElement!.addEventListener('animationend', this.onAnimationEnd, { once: true });
|
||||
$player.parentElement!.classList.add('bx-taking-screenshot');
|
||||
|
||||
const canvasContext = this.canvasContext;
|
||||
|
||||
if ($player instanceof HTMLCanvasElement) {
|
||||
streamPlayer.getWebGL2Player().drawFrame(true);
|
||||
}
|
||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
// Get data URL and pass to parent app
|
||||
if (AppInterface) {
|
||||
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
||||
AppInterface.saveScreenshot(currentStream.titleSlug, data);
|
||||
|
||||
// Free screenshot from memory
|
||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
$canvas.toBlob(blob => {
|
||||
if (!blob) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Download screenshot
|
||||
const now = +new Date;
|
||||
const $download = this.$download;
|
||||
$download.download = `${currentStream.titleSlug}-${now}.png`;
|
||||
$download.href = URL.createObjectURL(blob);
|
||||
$download.click();
|
||||
|
||||
// Free screenshot from memory
|
||||
URL.revokeObjectURL($download.href);
|
||||
$download.href = '';
|
||||
$download.download = '';
|
||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
callback && callback();
|
||||
}, 'image/png');
|
||||
}
|
||||
}
|
@@ -1,99 +0,0 @@
|
||||
import { StreamPlayerType } from "@enums/stream-player";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { CE } from "./html";
|
||||
import { PrefKey } from "@/enums/pref-keys";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
|
||||
|
||||
export class Screenshot {
|
||||
static #$canvas: HTMLCanvasElement;
|
||||
static #canvasContext: CanvasRenderingContext2D;
|
||||
|
||||
static setup() {
|
||||
if (Screenshot.#$canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
Screenshot.#$canvas = CE<HTMLCanvasElement>('canvas', {'class': 'bx-gone'});
|
||||
|
||||
Screenshot.#canvasContext = Screenshot.#$canvas.getContext('2d', {
|
||||
alpha: false,
|
||||
willReadFrequently: false,
|
||||
})!;
|
||||
}
|
||||
|
||||
static updateCanvasSize(width: number, height: number) {
|
||||
const $canvas = Screenshot.#$canvas;
|
||||
if ($canvas) {
|
||||
$canvas.width = width;
|
||||
$canvas.height = height;
|
||||
}
|
||||
}
|
||||
|
||||
static updateCanvasFilters(filters: string) {
|
||||
Screenshot.#canvasContext && (Screenshot.#canvasContext.filter = filters);
|
||||
}
|
||||
|
||||
static #onAnimationEnd(e: Event) {
|
||||
const $target = e.target as HTMLElement;
|
||||
$target.classList.remove('bx-taking-screenshot');
|
||||
}
|
||||
|
||||
static takeScreenshot(callback?: any) {
|
||||
const currentStream = STATES.currentStream;
|
||||
const streamPlayer = currentStream.streamPlayer;
|
||||
const $canvas = Screenshot.#$canvas;
|
||||
if (!streamPlayer || !$canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
let $player;
|
||||
if (getPref(PrefKey.SCREENSHOT_APPLY_FILTERS)) {
|
||||
$player = streamPlayer.getPlayerElement();
|
||||
} else {
|
||||
$player = streamPlayer.getPlayerElement(StreamPlayerType.VIDEO);
|
||||
}
|
||||
|
||||
if (!$player || !$player.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
$player.parentElement!.addEventListener('animationend', this.#onAnimationEnd, { once: true });
|
||||
$player.parentElement!.classList.add('bx-taking-screenshot');
|
||||
|
||||
const canvasContext = Screenshot.#canvasContext;
|
||||
|
||||
if ($player instanceof HTMLCanvasElement) {
|
||||
streamPlayer.getWebGL2Player().drawFrame(true);
|
||||
}
|
||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
// Get data URL and pass to parent app
|
||||
if (AppInterface) {
|
||||
const data = $canvas.toDataURL('image/png').split(';base64,')[1];
|
||||
AppInterface.saveScreenshot(currentStream.titleSlug, data);
|
||||
|
||||
// Free screenshot from memory
|
||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
callback && callback();
|
||||
return;
|
||||
}
|
||||
|
||||
$canvas && $canvas.toBlob(blob => {
|
||||
// Download screenshot
|
||||
const now = +new Date;
|
||||
const $anchor = CE<HTMLAnchorElement>('a', {
|
||||
'download': `${currentStream.titleSlug}-${now}.png`,
|
||||
'href': URL.createObjectURL(blob!),
|
||||
});
|
||||
$anchor.click();
|
||||
|
||||
// Free screenshot from memory
|
||||
URL.revokeObjectURL($anchor.href);
|
||||
canvasContext.clearRect(0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
callback && callback();
|
||||
}, 'image/png');
|
||||
}
|
||||
}
|
@@ -339,10 +339,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
requiredVariants: 'full',
|
||||
label: t('enable-local-co-op-support'),
|
||||
default: false,
|
||||
note: CE<HTMLAnchorElement>('a', {
|
||||
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
||||
target: '_blank',
|
||||
}, t('enable-local-co-op-support-note')),
|
||||
note: () => CE<HTMLAnchorElement>('a', {
|
||||
href: 'https://github.com/redphx/better-xcloud/discussions/275',
|
||||
target: '_blank',
|
||||
}, t('enable-local-co-op-support-note')),
|
||||
},
|
||||
|
||||
/*
|
||||
@@ -409,10 +409,10 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
|
||||
url = 'https://better-xcloud.github.io/mouse-and-keyboard/#disclaimer';
|
||||
}
|
||||
|
||||
setting.unsupportedNote = CE('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + note);
|
||||
setting.unsupportedNote = () => CE<HTMLAnchorElement>('a', {
|
||||
href: url,
|
||||
target: '_blank',
|
||||
}, '⚠️ ' + note);
|
||||
},
|
||||
},
|
||||
|
||||
|
@@ -3,6 +3,7 @@ import { BxEvent } from "./bx-event";
|
||||
import { STATES } from "./global";
|
||||
import { humanFileSize, secondsToHm } from "./html";
|
||||
import { getPref } from "./settings-storages/global-settings-storage";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
|
||||
export enum StreamStat {
|
||||
PING = 'ping',
|
||||
@@ -95,6 +96,7 @@ type CurrentStats = {
|
||||
export class StreamStatsCollector {
|
||||
private static instance: StreamStatsCollector;
|
||||
public static getInstance = () => StreamStatsCollector.instance ?? (StreamStatsCollector.instance = new StreamStatsCollector());
|
||||
private readonly LOG_TAG = 'StreamStatsCollector';
|
||||
|
||||
// Collect in background - 60 seconds
|
||||
static readonly INTERVAL_BACKGROUND = 60 * 1000;
|
||||
@@ -214,6 +216,10 @@ export class StreamStatsCollector {
|
||||
|
||||
private lastVideoStat?: RTCInboundRtpStreamStats | null;
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
}
|
||||
|
||||
async collect() {
|
||||
const stats = await STATES.currentStream.peerConnection?.getStats();
|
||||
if (!stats) {
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
|
||||
type ToastOptions = {
|
||||
instant?: boolean;
|
||||
@@ -6,85 +7,100 @@ type ToastOptions = {
|
||||
}
|
||||
|
||||
export class Toast {
|
||||
private static $wrapper: HTMLElement;
|
||||
private static $msg: HTMLElement;
|
||||
private static $status: HTMLElement;
|
||||
private static stack: Array<[string, string, ToastOptions]> = [];
|
||||
private static isShowing = false;
|
||||
private static instance: Toast;
|
||||
public static getInstance = () => Toast.instance ?? (Toast.instance = new Toast());
|
||||
private readonly LOG_TAG = 'Toast';
|
||||
|
||||
private static timeout?: number | null;
|
||||
private static DURATION = 3000;
|
||||
private $wrapper: HTMLElement;
|
||||
private $msg: HTMLElement;
|
||||
private $status: HTMLElement;
|
||||
|
||||
static show(msg: string, status?: string, options: Partial<ToastOptions> = {}) {
|
||||
private stack: Array<[string, string, ToastOptions]> = [];
|
||||
private isShowing = false;
|
||||
|
||||
private timeoutId?: number | null;
|
||||
private DURATION = 3000;
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
|
||||
this.$wrapper = CE('div', {class: 'bx-toast bx-offscreen'},
|
||||
this.$msg = CE('span', {class: 'bx-toast-msg'}),
|
||||
this.$status = CE('span', {class: 'bx-toast-status'}),
|
||||
);
|
||||
|
||||
this.$wrapper.addEventListener('transitionend', e => {
|
||||
const classList = this.$wrapper.classList;
|
||||
if (classList.contains('bx-hide')) {
|
||||
classList.remove('bx-offscreen', 'bx-hide');
|
||||
classList.add('bx-offscreen');
|
||||
|
||||
this.showNext();
|
||||
}
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(this.$wrapper);
|
||||
}
|
||||
|
||||
private show(msg: string, status?: string, options: Partial<ToastOptions> = {}) {
|
||||
options = options || {};
|
||||
|
||||
const args = Array.from(arguments) as [string, string, ToastOptions];
|
||||
if (options.instant) {
|
||||
// Clear stack
|
||||
Toast.stack = [args];
|
||||
Toast.showNext();
|
||||
this.stack = [args];
|
||||
this.showNext();
|
||||
} else {
|
||||
Toast.stack.push(args);
|
||||
!Toast.isShowing && Toast.showNext();
|
||||
this.stack.push(args);
|
||||
!this.isShowing && this.showNext();
|
||||
}
|
||||
}
|
||||
|
||||
private static showNext() {
|
||||
if (!Toast.stack.length) {
|
||||
Toast.isShowing = false;
|
||||
private showNext() {
|
||||
if (!this.stack.length) {
|
||||
this.isShowing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.isShowing = true;
|
||||
this.isShowing = true;
|
||||
|
||||
Toast.timeout && clearTimeout(Toast.timeout);
|
||||
Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION);
|
||||
this.timeoutId && clearTimeout(this.timeoutId);
|
||||
this.timeoutId = window.setTimeout(this.hide.bind(this), this.DURATION);
|
||||
|
||||
// Get values from item
|
||||
const [msg, status, options] = Toast.stack.shift()!;
|
||||
const [msg, status, options] = this.stack.shift()!;
|
||||
|
||||
if (options && options.html) {
|
||||
Toast.$msg.innerHTML = msg;
|
||||
this.$msg.innerHTML = msg;
|
||||
} else {
|
||||
Toast.$msg.textContent = msg;
|
||||
this.$msg.textContent = msg;
|
||||
}
|
||||
|
||||
if (status) {
|
||||
Toast.$status.classList.remove('bx-gone');
|
||||
Toast.$status.textContent = status;
|
||||
this.$status.classList.remove('bx-gone');
|
||||
this.$status.textContent = status;
|
||||
} else {
|
||||
Toast.$status.classList.add('bx-gone');
|
||||
this.$status.classList.add('bx-gone');
|
||||
}
|
||||
|
||||
const classList = Toast.$wrapper.classList;
|
||||
const classList = this.$wrapper.classList;
|
||||
classList.remove('bx-offscreen', 'bx-hide');
|
||||
classList.add('bx-show');
|
||||
}
|
||||
|
||||
private static hide() {
|
||||
Toast.timeout = null;
|
||||
private hide() {
|
||||
this.timeoutId = null;
|
||||
|
||||
const classList = Toast.$wrapper.classList;
|
||||
const classList = this.$wrapper.classList;
|
||||
classList.remove('bx-show');
|
||||
classList.add('bx-hide');
|
||||
}
|
||||
|
||||
static setup() {
|
||||
Toast.$wrapper = CE('div', {'class': 'bx-toast bx-offscreen'},
|
||||
Toast.$msg = CE('span', {'class': 'bx-toast-msg'}),
|
||||
Toast.$status = CE('span', {'class': 'bx-toast-status'}),
|
||||
);
|
||||
static show(msg: string, status?: string, options: Partial<ToastOptions> = {}) {
|
||||
Toast.getInstance().show(msg, status, options);
|
||||
}
|
||||
|
||||
Toast.$wrapper.addEventListener('transitionend', e => {
|
||||
const classList = Toast.$wrapper.classList;
|
||||
if (classList.contains('bx-hide')) {
|
||||
classList.remove('bx-offscreen', 'bx-hide');
|
||||
classList.add('bx-offscreen');
|
||||
|
||||
Toast.showNext();
|
||||
}
|
||||
});
|
||||
|
||||
document.documentElement.appendChild(Toast.$wrapper);
|
||||
static showNext() {
|
||||
Toast.getInstance().showNext();
|
||||
}
|
||||
}
|
||||
|
@@ -1,42 +1,55 @@
|
||||
import { BxIcon } from "./bx-icon";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { AppInterface, SCRIPT_VARIANT, STATES } from "./global";
|
||||
import { ButtonStyle, CE, clearDataSet, createButton, getReactProps } from "./html";
|
||||
import { t } from "./translation";
|
||||
|
||||
export class TrueAchievements {
|
||||
private static $link = createButton({
|
||||
label: t('true-achievements'),
|
||||
url: '#',
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
||||
onClick: TrueAchievements.onClick,
|
||||
}) as HTMLAnchorElement;
|
||||
private static instance: TrueAchievements;
|
||||
public static getInstance = () => TrueAchievements.instance ?? (TrueAchievements.instance = new TrueAchievements());
|
||||
private readonly LOG_TAG = 'TrueAchievements';
|
||||
|
||||
static $button = createButton({
|
||||
label: t('true-achievements'),
|
||||
title: t('true-achievements'),
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: TrueAchievements.onClick,
|
||||
}) as HTMLAnchorElement;
|
||||
private $link: HTMLElement;
|
||||
private $button: HTMLElement;
|
||||
private $hiddenLink: HTMLAnchorElement;
|
||||
|
||||
private static onClick(e: Event) {
|
||||
constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
|
||||
this.$link = createButton<HTMLAnchorElement>({
|
||||
label: t('true-achievements'),
|
||||
url: '#',
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE | ButtonStyle.GHOST | ButtonStyle.FULL_WIDTH | ButtonStyle.NORMAL_LINK,
|
||||
onClick: this.onClick.bind(this),
|
||||
});
|
||||
|
||||
this.$button = createButton<HTMLAnchorElement>({
|
||||
label: t('true-achievements'),
|
||||
title: t('true-achievements'),
|
||||
icon: BxIcon.TRUE_ACHIEVEMENTS,
|
||||
style: ButtonStyle.FOCUSABLE,
|
||||
onClick: this.onClick.bind(this),
|
||||
});
|
||||
|
||||
this.$hiddenLink = CE<HTMLAnchorElement>('a', {
|
||||
target: '_blank',
|
||||
});
|
||||
}
|
||||
|
||||
private onClick(e: Event) {
|
||||
e.preventDefault();
|
||||
|
||||
const dataset = TrueAchievements.$link.dataset;
|
||||
TrueAchievements.open(true, dataset.xboxTitleId, dataset.id);
|
||||
|
||||
// Close all xCloud's dialogs
|
||||
window.BX_EXPOSED.dialogRoutes?.closeAll();
|
||||
|
||||
const dataset = this.$link.dataset;
|
||||
this.open(true, dataset.xboxTitleId, dataset.id);
|
||||
}
|
||||
|
||||
private static $hiddenLink = CE<HTMLAnchorElement>('a', {
|
||||
target: '_blank',
|
||||
});
|
||||
|
||||
private static updateIds(xboxTitleId?: string, id?: string) {
|
||||
const $link = TrueAchievements.$link;
|
||||
const $button = TrueAchievements.$button;
|
||||
private updateIds(xboxTitleId?: string, id?: string) {
|
||||
const $link = this.$link;
|
||||
const $button = this.$button;
|
||||
|
||||
clearDataSet($link);
|
||||
clearDataSet($button);
|
||||
@@ -52,7 +65,7 @@ export class TrueAchievements {
|
||||
}
|
||||
}
|
||||
|
||||
static injectAchievementsProgress($elm: HTMLElement) {
|
||||
injectAchievementsProgress($elm: HTMLElement) {
|
||||
// Only do this in Full version
|
||||
if (SCRIPT_VARIANT !== 'full') {
|
||||
return;
|
||||
@@ -68,7 +81,7 @@ export class TrueAchievements {
|
||||
// Get xboxTitleId of the game
|
||||
let xboxTitleId: string | number | undefined;
|
||||
try {
|
||||
const $container = $parent.closest('div[class*=AchievementsPreview-module__container]') as HTMLElement;
|
||||
const $container = $parent.closest<HTMLElement>('div[class*=AchievementsPreview-module__container]');
|
||||
if ($container) {
|
||||
const props = getReactProps($container);
|
||||
xboxTitleId = props.children.props.data.data.xboxTitleId;
|
||||
@@ -76,24 +89,24 @@ export class TrueAchievements {
|
||||
} catch (e) {}
|
||||
|
||||
if (!xboxTitleId) {
|
||||
xboxTitleId = TrueAchievements.getStreamXboxTitleId();
|
||||
xboxTitleId = this.getStreamXboxTitleId();
|
||||
}
|
||||
|
||||
if (typeof xboxTitleId !== 'undefined') {
|
||||
xboxTitleId = xboxTitleId.toString();
|
||||
}
|
||||
TrueAchievements.updateIds(xboxTitleId);
|
||||
this.updateIds(xboxTitleId);
|
||||
|
||||
if (document.documentElement.dataset.xdsPlatform === 'tv') {
|
||||
$div.appendChild(TrueAchievements.$link);
|
||||
$div.appendChild(this.$link);
|
||||
} else {
|
||||
$div.appendChild(TrueAchievements.$button);
|
||||
$div.appendChild(this.$button);
|
||||
}
|
||||
|
||||
$parent.appendChild($div);
|
||||
}
|
||||
|
||||
static injectAchievementDetailPage($parent: HTMLElement) {
|
||||
injectAchievementDetailPage($parent: HTMLElement) {
|
||||
// Only do this in Full version
|
||||
if (SCRIPT_VARIANT !== 'full') {
|
||||
return;
|
||||
@@ -109,7 +122,7 @@ export class TrueAchievements {
|
||||
const achievementList: XboxAchievement[] = props.children.props.data.data;
|
||||
|
||||
// Get current achievement name
|
||||
const $header = $parent.querySelector('div[class*=AchievementDetailHeader]') as HTMLElement;
|
||||
const $header = $parent.querySelector<HTMLElement>('div[class*=AchievementDetailHeader]')!;
|
||||
const achievementName = getReactProps($header).children[0].props.achievementName;
|
||||
|
||||
// Find achievement based on name
|
||||
@@ -125,19 +138,19 @@ export class TrueAchievements {
|
||||
|
||||
// Found achievement -> add TrueAchievements button
|
||||
if (id) {
|
||||
TrueAchievements.updateIds(xboxTitleId, id);
|
||||
$parent.appendChild(TrueAchievements.$link);
|
||||
this.updateIds(xboxTitleId, id);
|
||||
$parent.appendChild(this.$link);
|
||||
}
|
||||
} catch (e) {};
|
||||
}
|
||||
|
||||
private static getStreamXboxTitleId() : number | undefined {
|
||||
private getStreamXboxTitleId() : number | undefined {
|
||||
return STATES.currentStream.xboxTitleId || STATES.currentStream.titleInfo?.details.xboxTitleId;
|
||||
}
|
||||
|
||||
static open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
|
||||
open(override: boolean, xboxTitleId?: number | string, id?: number | string) {
|
||||
if (!xboxTitleId || xboxTitleId === 'undefined') {
|
||||
xboxTitleId = TrueAchievements.getStreamXboxTitleId();
|
||||
xboxTitleId = this.getStreamXboxTitleId();
|
||||
}
|
||||
|
||||
if (AppInterface && AppInterface.openTrueAchievementsLink) {
|
||||
@@ -154,7 +167,7 @@ export class TrueAchievements {
|
||||
}
|
||||
}
|
||||
|
||||
TrueAchievements.$hiddenLink.href = url;
|
||||
TrueAchievements.$hiddenLink.click();
|
||||
this.$hiddenLink.href = url;
|
||||
this.$hiddenLink.click();
|
||||
}
|
||||
}
|
||||
|
@@ -1,13 +1,19 @@
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { STATES } from "./global";
|
||||
|
||||
export class XcloudApi {
|
||||
private static instance: XcloudApi;
|
||||
public static getInstance = () => XcloudApi.instance ?? (XcloudApi.instance = new XcloudApi());
|
||||
private readonly LOG_TAG = 'XcloudApi';
|
||||
|
||||
private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
|
||||
private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
|
||||
|
||||
private constructor() {
|
||||
BxLogger.info(this.LOG_TAG, 'constructor()');
|
||||
}
|
||||
|
||||
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
|
||||
if (id in this.CACHE_TITLES) {
|
||||
return this.CACHE_TITLES[id];
|
||||
|
@@ -92,6 +92,8 @@ export class XcloudInterceptor {
|
||||
}
|
||||
|
||||
private static async handlePlay(request: RequestInfo | URL, init?: RequestInit) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
||||
|
||||
const PREF_STREAM_TARGET_RESOLUTION = getPref(PrefKey.STREAM_TARGET_RESOLUTION);
|
||||
const PREF_STREAM_PREFERRED_LOCALE = getPref(PrefKey.STREAM_PREFERRED_LOCALE);
|
||||
|
||||
@@ -165,6 +167,8 @@ export class XcloudInterceptor {
|
||||
return response;
|
||||
}
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||
|
||||
const obj = JSON.parse(text);
|
||||
let overrides = JSON.parse(obj.clientStreamingConfigOverrides || '{}') || {};
|
||||
|
||||
|
@@ -54,8 +54,7 @@ export class XhomeInterceptor {
|
||||
|
||||
private static async handleLogin(request: Request) {
|
||||
try {
|
||||
const clone = (request as Request).clone();
|
||||
|
||||
const clone = request.clone();
|
||||
const obj = await clone.json();
|
||||
obj.offeringId = 'xhome';
|
||||
|
||||
@@ -75,30 +74,30 @@ export class XhomeInterceptor {
|
||||
}
|
||||
|
||||
private static async handleConfiguration(request: Request | URL) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_STARTING);
|
||||
|
||||
const response = await NATIVE_FETCH(request);
|
||||
|
||||
const obj = await response.clone().json()
|
||||
console.log(obj);
|
||||
|
||||
const processPorts = (port: number): number[] => {
|
||||
const ports = new Set<number>();
|
||||
port && ports.add(port);
|
||||
ports.add(9002);
|
||||
|
||||
return Array.from(ports);
|
||||
};
|
||||
const obj = await response.clone().json();
|
||||
|
||||
const serverDetails = obj.serverDetails;
|
||||
if (serverDetails.ipAddress) {
|
||||
XhomeInterceptor.consoleAddrs[serverDetails.ipAddress] = processPorts(serverDetails.port);
|
||||
}
|
||||
const pairs = [
|
||||
['ipAddress', 'port'],
|
||||
['ipV4Address', 'ipV4Port'],
|
||||
['ipV6Address', 'ipV6Port'],
|
||||
];
|
||||
|
||||
if (serverDetails.ipV4Address) {
|
||||
XhomeInterceptor.consoleAddrs[serverDetails.ipV4Address] = processPorts(serverDetails.ipV4Port);
|
||||
}
|
||||
|
||||
if (serverDetails.ipV6Address) {
|
||||
XhomeInterceptor.consoleAddrs[serverDetails.ipV6Address] = processPorts(serverDetails.ipV6Port);
|
||||
XhomeInterceptor.consoleAddrs = {};
|
||||
for (const pair in pairs) {
|
||||
const [keyAddr, keyPort] = pair;
|
||||
if (serverDetails[keyAddr]) {
|
||||
const port = serverDetails[keyPort];
|
||||
// Add port 9002 to the list of ports
|
||||
const ports = new Set<number>();
|
||||
port && ports.add(port);
|
||||
ports.add(9002);
|
||||
// Save it
|
||||
XhomeInterceptor.consoleAddrs[serverDetails[keyAddr]] = Array.from(ports);
|
||||
}
|
||||
}
|
||||
|
||||
response.json = () => Promise.resolve(obj);
|
||||
@@ -164,6 +163,8 @@ export class XhomeInterceptor {
|
||||
}
|
||||
|
||||
private static async handlePlay(request: RequestInfo | URL) {
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_LOADING);
|
||||
|
||||
const clone = (request as Request).clone();
|
||||
const body = await clone.json();
|
||||
|
||||
@@ -196,23 +197,25 @@ export class XhomeInterceptor {
|
||||
|
||||
headers['x-ms-device-info'] = JSON.stringify(deviceInfo);
|
||||
|
||||
const opts: {[index: string]: any} = {
|
||||
const opts: Record<string, any> = {
|
||||
method: clone.method,
|
||||
headers: headers,
|
||||
};
|
||||
|
||||
// Copy body
|
||||
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);
|
||||
// Replace xCloud domain with xHome domain
|
||||
let url = request.url;
|
||||
if (!url.includes('/servers/home')) {
|
||||
const parsed = new URL(url);
|
||||
url = STATES.remotePlay.server + parsed.pathname;
|
||||
}
|
||||
|
||||
request = new Request(newUrl, opts);
|
||||
let url = (typeof request === 'string') ? request : request.url;
|
||||
// Create new Request instance
|
||||
request = new Request(url, opts);
|
||||
|
||||
// Get console IP
|
||||
if (url.includes('/configuration')) {
|
||||
@@ -225,7 +228,7 @@ export class XhomeInterceptor {
|
||||
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') {
|
||||
} else if (url && url.endsWith('/ice') && url.includes('/sessions/') && request.method === 'GET') {
|
||||
return patchIceCandidates(request, XhomeInterceptor.consoleAddrs);
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user