Add WebGL2 renderer

This commit is contained in:
redphx
2024-06-21 17:45:43 +07:00
parent 6150c2ea70
commit f169c17e18
40 changed files with 955 additions and 220 deletions

View File

@@ -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);

View File

@@ -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',
}

View File

@@ -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';
}
}
}

View File

@@ -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";

View File

@@ -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',

View File

@@ -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";

View File

@@ -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 = '⇁',
}

View File

@@ -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) {

View File

@@ -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',

View File

@@ -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();

View File

@@ -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() {

View File

@@ -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,