mirror of
https://github.com/redphx/better-xcloud.git
synced 2025-11-19 07:14:04 +01:00
Add WebGL2 renderer
This commit is contained in:
@@ -60,7 +60,8 @@ export const BxExposed = {
|
||||
}
|
||||
|
||||
// Pre-check supported input types
|
||||
titleInfo.details.hasTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH) ||
|
||||
titleInfo.details.hasNativeTouchSupport = supportedInputTypes.includes(InputType.NATIVE_TOUCH);
|
||||
titleInfo.details.hasTouchSupport = titleInfo.details.hasNativeTouchSupport ||
|
||||
supportedInputTypes.includes(InputType.CUSTOM_TOUCH_OVERLAY) ||
|
||||
supportedInputTypes.includes(InputType.GENERIC_TOUCH);
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
export enum GamePassCloudGallery {
|
||||
ALL = '29a81209-df6f-41fd-a528-2ae6b91f719c',
|
||||
NATIVE_MKB = '8fa264dd-124f-4af3-97e8-596fcdf4b486',
|
||||
TOUCH = '9c86f07a-f3e8-45ad-82a0-a1f759597059',
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { getPref, PrefKey } from "@utils/preferences";
|
||||
import { STATES } from "@utils/global";
|
||||
import { BxLogger } from "@utils/bx-logger";
|
||||
import { patchSdpBitrate } from "./sdp";
|
||||
import { StreamPlayer, type StreamPlayerOptions } from "@/modules/stream-player";
|
||||
|
||||
export function patchVideoApi() {
|
||||
const PREF_SKIP_SPLASH_VIDEO = getPref(PrefKey.SKIP_SPLASH_VIDEO);
|
||||
@@ -16,12 +17,22 @@ export function patchVideoApi() {
|
||||
return;
|
||||
}
|
||||
|
||||
const playerOptions = {
|
||||
processing: getPref(PrefKey.VIDEO_PROCESSING),
|
||||
sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
|
||||
saturation: getPref(PrefKey.VIDEO_SATURATION),
|
||||
contrast: getPref(PrefKey.VIDEO_CONTRAST),
|
||||
brightness: getPref(PrefKey.VIDEO_BRIGHTNESS),
|
||||
} satisfies StreamPlayerOptions;
|
||||
STATES.currentStream.streamPlayer = new StreamPlayer(this, getPref(PrefKey.VIDEO_PLAYER_TYPE), playerOptions);
|
||||
|
||||
BxEvent.dispatch(window, BxEvent.STREAM_PLAYING, {
|
||||
$video: this,
|
||||
});
|
||||
}
|
||||
|
||||
const nativePlay = HTMLMediaElement.prototype.play;
|
||||
(HTMLMediaElement.prototype as any).nativePlay = nativePlay;
|
||||
HTMLMediaElement.prototype.play = function() {
|
||||
if (this.className && this.className.startsWith('XboxSplashVideo')) {
|
||||
if (PREF_SKIP_SPLASH_VIDEO) {
|
||||
@@ -97,21 +108,23 @@ export function patchRtcPeerConnection() {
|
||||
return dataChannel;
|
||||
}
|
||||
|
||||
const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
|
||||
RTCPeerConnection.prototype.setLocalDescription = function(description?: RTCLocalSessionDescriptionInit): Promise<void> {
|
||||
// set maximum bitrate
|
||||
try {
|
||||
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
|
||||
if (maxVideoBitrate > 0) {
|
||||
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
|
||||
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
|
||||
if (maxVideoBitrate > 0) {
|
||||
const nativeSetLocalDescription = RTCPeerConnection.prototype.setLocalDescription;
|
||||
RTCPeerConnection.prototype.setLocalDescription = function(description?: RTCLocalSessionDescriptionInit): Promise<void> {
|
||||
// set maximum bitrate
|
||||
try {
|
||||
if (description) {
|
||||
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
|
||||
}
|
||||
} catch (e) {
|
||||
BxLogger.error('setLocalDescription', e);
|
||||
}
|
||||
} catch (e) {
|
||||
BxLogger.error('setLocalDescription', e);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return nativeSetLocalDescription.apply(this, arguments);
|
||||
};
|
||||
// @ts-ignore
|
||||
return nativeSetLocalDescription.apply(this, arguments);
|
||||
};
|
||||
}
|
||||
|
||||
const OrgRTCPeerConnection = window.RTCPeerConnection;
|
||||
// @ts-ignore
|
||||
@@ -132,6 +145,10 @@ export function patchAudioContext() {
|
||||
|
||||
// @ts-ignore
|
||||
window.AudioContext = function(options?: AudioContextOptions | undefined): AudioContext {
|
||||
if (options && options.latencyHint) {
|
||||
options.latencyHint = 0;
|
||||
}
|
||||
|
||||
const ctx = new OrgAudioContext(options);
|
||||
BxLogger.info('patchAudioContext', ctx, options);
|
||||
|
||||
@@ -160,7 +177,12 @@ export function patchMeControl() {
|
||||
};
|
||||
|
||||
const MSA = {
|
||||
MeControl: {},
|
||||
MeControl: {
|
||||
API: {
|
||||
setDisplayMode: () => {},
|
||||
setMobileState: () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
const MeControl = {};
|
||||
|
||||
@@ -207,12 +229,13 @@ export function patchCanvasContext() {
|
||||
HTMLCanvasElement.prototype.getContext = function(contextType: string, contextAttributes?: any) {
|
||||
if (contextType.includes('webgl')) {
|
||||
contextAttributes = contextAttributes || {};
|
||||
if (!contextAttributes.isBx) {
|
||||
contextAttributes.antialias = false;
|
||||
|
||||
contextAttributes.antialias = false;
|
||||
|
||||
// Use low-power profile for touch controller
|
||||
if (contextAttributes.powerPreference === 'high-performance') {
|
||||
contextAttributes.powerPreference = 'low-power';
|
||||
// Use low-power profile for touch controller
|
||||
if (contextAttributes.powerPreference === 'high-performance') {
|
||||
contextAttributes.powerPreference = 'low-power';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ 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 "./gamepass-gallery";
|
||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||
import { InputType } from "./bx-exposed";
|
||||
import { FeatureGates } from "./feature-gates";
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { CE } from "@utils/html";
|
||||
import { SUPPORTED_LANGUAGES, t } from "@utils/translation";
|
||||
import { SettingElement, SettingElementType } from "@utils/settings";
|
||||
import { UserAgent, UserAgentProfile } from "@utils/user-agent";
|
||||
import { UserAgent } from "@utils/user-agent";
|
||||
import { StreamStat } from "@modules/stream/stream-stats";
|
||||
import type { PreferenceSetting, PreferenceSettings } from "@/types/preferences";
|
||||
import { AppInterface, STATES } from "@utils/global";
|
||||
import { StreamPlayerType, StreamVideoProcessing } from "@enums/stream-player";
|
||||
import { UserAgentProfile } from "@/enums/user-agent";
|
||||
|
||||
export enum PrefKey {
|
||||
LAST_UPDATE_CHECK = 'version_last_check',
|
||||
@@ -70,7 +72,9 @@ export enum PrefKey {
|
||||
|
||||
UI_HOME_CONTEXT_MENU_DISABLED = 'ui_home_context_menu_disabled',
|
||||
|
||||
VIDEO_CLARITY = 'video_clarity',
|
||||
VIDEO_PLAYER_TYPE = 'video_player_type',
|
||||
VIDEO_PROCESSING = 'video_processing',
|
||||
VIDEO_SHARPNESS = 'video_sharpness',
|
||||
VIDEO_RATIO = 'video_ratio',
|
||||
VIDEO_BRIGHTNESS = 'video_brightness',
|
||||
VIDEO_CONTRAST = 'video_contrast',
|
||||
@@ -570,19 +574,35 @@ export class Preferences {
|
||||
[UserAgentProfile.CUSTOM]: t('custom'),
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_CLARITY]: {
|
||||
label: t('clarity'),
|
||||
[PrefKey.VIDEO_PLAYER_TYPE]: {
|
||||
label: t('renderer'),
|
||||
default: 'default',
|
||||
options: {
|
||||
[StreamPlayerType.VIDEO]: t('default'),
|
||||
[StreamPlayerType.WEBGL2]: t('webgl2'),
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_PROCESSING]: {
|
||||
label: t('clarity-boost'),
|
||||
default: StreamVideoProcessing.USM,
|
||||
options: {
|
||||
[StreamVideoProcessing.USM]: t('unsharp-masking'),
|
||||
[StreamVideoProcessing.CAS]: t('amd-fidelity-cas'),
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_SHARPNESS]: {
|
||||
label: t('sharpness'),
|
||||
type: SettingElementType.NUMBER_STEPPER,
|
||||
default: 0,
|
||||
min: 0,
|
||||
max: 5,
|
||||
max: 10,
|
||||
params: {
|
||||
hideSlider: true,
|
||||
},
|
||||
},
|
||||
[PrefKey.VIDEO_RATIO]: {
|
||||
label: t('ratio'),
|
||||
note: t('stretch-note'),
|
||||
label: t('aspect-ratio'),
|
||||
note: t('aspect-ratio-note'),
|
||||
default: '16:9',
|
||||
options: {
|
||||
'16:9': '16:9',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { STATES } from "@utils/global";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
import { TouchController } from "@modules/touch-controller";
|
||||
import { GamePassCloudGallery } from "./gamepass-gallery";
|
||||
import { GamePassCloudGallery } from "../enums/game-pass-gallery";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
import { BX_FLAGS } from "./bx-flags";
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
export enum PrompFont {
|
||||
A = '⇓',
|
||||
B = '⇒',
|
||||
X = '⇐',
|
||||
Y = '⇑',
|
||||
|
||||
LB = '↘',
|
||||
RB = '↙',
|
||||
LT = '↖',
|
||||
RT = '↗',
|
||||
|
||||
SELECT = '⇺',
|
||||
START = '⇻',
|
||||
HOME = '',
|
||||
|
||||
UP = '≻',
|
||||
DOWN = '≽',
|
||||
LEFT = '≺',
|
||||
RIGHT = '≼',
|
||||
|
||||
L3 = '↺',
|
||||
LS_UP = '↾',
|
||||
LS_DOWN = '⇂',
|
||||
LS_LEFT = '↼',
|
||||
LS_RIGHT = '⇀',
|
||||
|
||||
R3 = '↻',
|
||||
RS_UP = '↿',
|
||||
RS_DOWN = '⇃',
|
||||
RS_LEFT = '↽',
|
||||
RS_RIGHT = '⇁',
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { StreamPlayerType } from "@enums/stream-player";
|
||||
import { AppInterface, STATES } from "./global";
|
||||
import { CE } from "./html";
|
||||
import { getPref, PrefKey } from "./preferences";
|
||||
|
||||
|
||||
export class Screenshot {
|
||||
@@ -31,23 +33,39 @@ export class Screenshot {
|
||||
Screenshot.#canvasContext.filter = filters;
|
||||
}
|
||||
|
||||
private static onAnimationEnd(e: Event) {
|
||||
(e.target as any).classList.remove('bx-taking-screenshot');
|
||||
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 $video = currentStream.$video;
|
||||
const streamPlayer = currentStream.streamPlayer;
|
||||
const $canvas = Screenshot.#$canvas;
|
||||
if (!$video || !$canvas) {
|
||||
if (!streamPlayer || !$canvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
$video.parentElement?.addEventListener('animationend', this.onAnimationEnd);
|
||||
$video.parentElement?.classList.add('bx-taking-screenshot');
|
||||
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);
|
||||
$player.parentElement!.classList.add('bx-taking-screenshot');
|
||||
|
||||
const canvasContext = Screenshot.#canvasContext;
|
||||
canvasContext.drawImage($video, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
if ($player instanceof HTMLCanvasElement) {
|
||||
streamPlayer.getWebGL2Player().drawFrame();
|
||||
}
|
||||
canvasContext.drawImage($player, 0, 0, $canvas.width, $canvas.height);
|
||||
|
||||
// Get data URL and pass to parent app
|
||||
if (AppInterface) {
|
||||
|
||||
@@ -155,7 +155,7 @@ export class SettingElement {
|
||||
return textContent;
|
||||
};
|
||||
|
||||
const $wrapper = CE('div', {'class': 'bx-number-stepper'},
|
||||
const $wrapper = CE('div', {'class': 'bx-number-stepper', id: `bx_setting_${key}`},
|
||||
$decBtn = CE('button', {
|
||||
'data-type': 'dec',
|
||||
type: 'button',
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { NATIVE_FETCH } from "./bx-flags";
|
||||
import { BxLogger } from "./bx-logger";
|
||||
|
||||
export const SUPPORTED_LANGUAGES = {
|
||||
'en-US': 'English (United States)',
|
||||
@@ -26,8 +27,11 @@ const Texts = {
|
||||
"activated": "Activated",
|
||||
"active": "Active",
|
||||
"advanced": "Advanced",
|
||||
"amd-fidelity-cas": "AMD FidelityFX CAS",
|
||||
"android-app-settings": "Android app settings",
|
||||
"apply": "Apply",
|
||||
"aspect-ratio": "Aspect ratio",
|
||||
"aspect-ratio-note": "Don't use with native touch games",
|
||||
"audio": "Audio",
|
||||
"auto": "Auto",
|
||||
"back-to-home": "Back to home",
|
||||
@@ -48,7 +52,7 @@ const Texts = {
|
||||
"can-stream-xbox-360-games": "Can stream Xbox 360 games",
|
||||
"cancel": "Cancel",
|
||||
"cant-stream-xbox-360-games": "Can't stream Xbox 360 games",
|
||||
"clarity": "Clarity",
|
||||
"clarity-boost": "Clarity boost",
|
||||
"clarity-boost-warning": "These settings don't work when the Clarity Boost mode is ON",
|
||||
"clear": "Clear",
|
||||
"close": "Close",
|
||||
@@ -152,14 +156,14 @@ const Texts = {
|
||||
,
|
||||
(e: any) => `${e.key}: Funktion an-/ausschalten`,
|
||||
,
|
||||
,
|
||||
(e: any) => `Pulsa ${e.key} para alternar esta función`,
|
||||
(e: any) => `Appuyez sur ${e.key} pour activer cette fonctionnalité`,
|
||||
(e: any) => `Premi ${e.key} per attivare questa funzionalità`,
|
||||
(e: any) => `${e.key} でこの機能を切替`,
|
||||
(e: any) => `${e.key} 키를 눌러 이 기능을 켜고 끄세요`,
|
||||
(e: any) => `Naciśnij ${e.key} aby przełączyć tę funkcję`,
|
||||
,
|
||||
,
|
||||
,
|
||||
,
|
||||
(e: any) => `Нажмите ${e.key} для переключения этой функции`,
|
||||
,
|
||||
(e: any) => `Etkinleştirmek için ${e.key} tuşuna basın`,
|
||||
(e: any) => `Натисніть ${e.key} щоб перемкнути цю функцію`,
|
||||
@@ -168,12 +172,12 @@ const Texts = {
|
||||
],
|
||||
"press-to-bind": "Press a key or do a mouse click to bind...",
|
||||
"prompt-preset-name": "Preset's name:",
|
||||
"ratio": "Ratio",
|
||||
"reduce-animations": "Reduce UI animations",
|
||||
"region": "Region",
|
||||
"reload-stream": "Reload stream",
|
||||
"remote-play": "Remote Play",
|
||||
"rename": "Rename",
|
||||
"renderer": "Renderer",
|
||||
"right-click-to-unbind": "Right-click on a key to unbind it",
|
||||
"right-stick": "Right stick",
|
||||
"rocket-always-hide": "Always hide",
|
||||
@@ -191,6 +195,7 @@ const Texts = {
|
||||
"settings": "Settings",
|
||||
"settings-reload": "Reload page to reflect changes",
|
||||
"settings-reloading": "Reloading...",
|
||||
"sharpness": "Sharpness",
|
||||
"shortcut-keys": "Shortcut keys",
|
||||
"show": "Show",
|
||||
"show-game-art": "Show game art",
|
||||
@@ -218,7 +223,6 @@ const Texts = {
|
||||
"stream-settings": "Stream settings",
|
||||
"stream-stats": "Stream stats",
|
||||
"stretch": "Stretch",
|
||||
"stretch-note": "Don't use with native touch games",
|
||||
"support-better-xcloud": "Support Better xCloud",
|
||||
"swap-buttons": "Swap buttons",
|
||||
"take-screenshot": "Take screenshot",
|
||||
@@ -263,6 +267,7 @@ const Texts = {
|
||||
"unknown": "Unknown",
|
||||
"unlimited": "Unlimited",
|
||||
"unmuted": "Unmuted",
|
||||
"unsharp-masking": "Unsharp masking",
|
||||
"use-mouse-absolute-position": "Use mouse's absolute position",
|
||||
"user-agent-profile": "User-Agent profile",
|
||||
"vertical-scroll-sensitivity": "Vertical scroll sensitivity",
|
||||
@@ -278,6 +283,7 @@ const Texts = {
|
||||
"volume": "Volume",
|
||||
"wait-time-countdown": "Countdown",
|
||||
"wait-time-estimated": "Estimated finish time",
|
||||
"webgl2": "WebGL2",
|
||||
};
|
||||
|
||||
export class Translations {
|
||||
@@ -390,5 +396,9 @@ export class Translations {
|
||||
}
|
||||
|
||||
export const t = Translations.get;
|
||||
export const ut = (text: string): string => {
|
||||
BxLogger.warning('Untranslated text', text);
|
||||
return text;
|
||||
}
|
||||
|
||||
Translations.init();
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
import { UserAgentProfile } from "@enums/user-agent";
|
||||
|
||||
type UserAgentConfig = {
|
||||
profile: UserAgentProfile,
|
||||
custom?: string,
|
||||
};
|
||||
|
||||
export enum UserAgentProfile {
|
||||
WINDOWS_EDGE = 'windows-edge',
|
||||
MACOS_SAFARI = 'macos-safari',
|
||||
SMARTTV_GENERIC = 'smarttv-generic',
|
||||
SMARTTV_TIZEN = 'smarttv-tizen',
|
||||
VR_OCULUS = 'vr-oculus',
|
||||
ANDROID_KIWI_V123 = 'android-kiwi-v123',
|
||||
DEFAULT = 'default',
|
||||
CUSTOM = 'custom',
|
||||
}
|
||||
|
||||
let CHROMIUM_VERSION = '123.0.0.0';
|
||||
if (!!(window as any).chrome || window.navigator.userAgent.includes('Chrome')) {
|
||||
// Get Chromium version in the original User-Agent value
|
||||
@@ -27,6 +18,10 @@ export class UserAgent {
|
||||
static readonly STORAGE_KEY = 'better_xcloud_user_agent';
|
||||
static #config: UserAgentConfig;
|
||||
|
||||
static #isMobile: boolean | null = null;
|
||||
static #isSafari: boolean | null = null;
|
||||
static #isSafariMobile: boolean | null = null;
|
||||
|
||||
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',
|
||||
@@ -79,20 +74,40 @@ export class UserAgent {
|
||||
}
|
||||
}
|
||||
|
||||
static isSafari(mobile=false): boolean {
|
||||
static isSafari(): boolean {
|
||||
if (this.#isSafari !== null) {
|
||||
return this.#isSafari;
|
||||
}
|
||||
|
||||
const userAgent = UserAgent.getDefault().toLowerCase();
|
||||
let result = userAgent.includes('safari') && !userAgent.includes('chrom');
|
||||
|
||||
if (result && mobile) {
|
||||
result = userAgent.includes('mobile');
|
||||
this.#isSafari = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
static isSafariMobile(): boolean {
|
||||
if (this.#isSafariMobile !== null) {
|
||||
return this.#isSafariMobile;
|
||||
}
|
||||
|
||||
const userAgent = UserAgent.getDefault().toLowerCase();
|
||||
const result = this.isSafari() && userAgent.includes('mobile');
|
||||
|
||||
this.#isSafariMobile = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
static isMobile(): boolean {
|
||||
if (this.#isMobile !== null) {
|
||||
return this.#isMobile;
|
||||
}
|
||||
|
||||
const userAgent = UserAgent.getDefault().toLowerCase();
|
||||
return /iphone|ipad|android/.test(userAgent);
|
||||
const result = /iphone|ipad|android/.test(userAgent);
|
||||
|
||||
this.#isMobile = result;
|
||||
return result;
|
||||
}
|
||||
|
||||
static spoof() {
|
||||
|
||||
@@ -47,7 +47,7 @@ export function disablePwa() {
|
||||
}
|
||||
|
||||
// Check if it's Safari on mobile
|
||||
if (!!AppInterface || UserAgent.isSafari(true)) {
|
||||
if (!!AppInterface || UserAgent.isSafariMobile()) {
|
||||
// Disable the PWA prompt
|
||||
Object.defineProperty(window.navigator, 'standalone', {
|
||||
value: true,
|
||||
|
||||
Reference in New Issue
Block a user