Compare commits

...

20 Commits

Author SHA1 Message Date
741538ebcf Bump version to 5.8.4 2024-10-13 20:00:36 +07:00
6d2e04aff1 Refactor Game Bar actions 2024-10-13 19:15:29 +07:00
f2bc98229f Update version 2024-10-13 17:46:48 +07:00
49fb8e2818 Refactor "data-enabled" to "data-activated" 2024-10-13 17:32:38 +07:00
d012d96675 Add Game Bar action to toggle renderer's visibility 2024-10-13 17:05:27 +07:00
c129feaf2d Refactor WebGL2Player 2024-10-13 16:26:33 +07:00
4f7b23912d Refactor BxLogger 2024-10-13 16:06:01 +07:00
e4d73f9e36 Replace "#" with "private" 2024-10-13 10:51:50 +07:00
2eea9ce8f5 Bump version to 5.8.3 2024-10-12 18:41:41 +07:00
27abab8473 Change "FPS" unit to "fps" 2024-10-12 18:41:28 +07:00
0c34173815 Add "Limit video player's FPS" feature 2024-10-12 16:15:51 +07:00
0164423e45 Test WebGL2 shader 2024-10-12 11:14:55 +07:00
71dcaf4b07 Optimize Clarity boost shader 2024-10-11 17:11:32 +07:00
8f49c48e74 Bump version to 5.8.2 2024-10-11 07:11:37 +07:00
6fa1f73702 Optimize built scripts 2024-10-10 21:43:42 +07:00
728abced45 Add jitter stat 2024-10-10 21:35:36 +07:00
411e43ceb0 Disable inputPollingDurationStats 2024-10-10 20:55:57 +07:00
baa22dbefc Optimize Clarity Boost shader 2024-10-10 17:28:19 +07:00
97fb7a114f Set Sharpness's suggested value to 2 2024-10-09 09:02:52 +07:00
39b2f814b6 Fix stream badge always show "IPv6" even when connecting to IPv4 server #517 2024-10-09 06:30:09 +07:00
39 changed files with 1206 additions and 972 deletions

View File

@ -55,7 +55,7 @@ const postProcess = (str: string): string => {
// Minify SVG import code // Minify SVG import code
const svgMap = {} const svgMap = {}
str = str.replaceAll(/var ([\w_]+) = ("<svg.*?");\n\n/g, function(match, p1, p2) { str = str.replaceAll(/var ([\w_]+) = ("<svg.*?");\n\n/g, (match, p1, p2) => {
// Remove new lines in SVG // Remove new lines in SVG
p2 = p2.replaceAll(/\\n*\s*/g, ''); p2 = p2.replaceAll(/\\n*\s*/g, '');
@ -76,6 +76,17 @@ const postProcess = (str: string): string => {
// Remove blank lines // Remove blank lines
str = str.replaceAll(/\n([\s]*)\n/g, "\n"); str = str.replaceAll(/\n([\s]*)\n/g, "\n");
// Minify WebGL shaders & JS strings
// Replace "\n " with "\n"
str = str.replaceAll(/\\n+\s*/g, '\\n');
// Remove comment line
str = str.replaceAll(/\\n\/\/.*?(?=\\n)/g, '');
// Replace ${"time".toUpperCase()} with "TIME"
str = str.replaceAll(/\$\{"([^"]+)"\.toUpperCase\(\)\}/g, (match, p1) => {
return p1.toUpperCase();
});
assert(str.includes('/* ADDITIONAL CODE */')); assert(str.includes('/* ADDITIONAL CODE */'));
assert(str.includes('window.BX_EXPOSED = BxExposed')); assert(str.includes('window.BX_EXPOSED = BxExposed'));
assert(str.includes('window.BxEvent = BxEvent')); assert(str.includes('window.BxEvent = BxEvent'));

File diff suppressed because one or more lines are too long

View File

@ -1,5 +1,5 @@
// ==UserScript== // ==UserScript==
// @name Better xCloud // @name Better xCloud
// @namespace https://github.com/redphx // @namespace https://github.com/redphx
// @version 5.8.1 // @version 5.8.4
// ==/UserScript== // ==/UserScript==

File diff suppressed because one or more lines are too long

View File

@ -76,21 +76,21 @@
} }
/* Touch controller buttons */ /* Touch controller buttons */
div[data-enabled] { div[data-activated] {
button { button {
display: none; display: none;
} }
} }
/* Show enabled button */ /* Show default button */
div[data-enabled='true'] { div[data-activated='false'] {
button:first-of-type { button:first-of-type {
display: block; display: block;
} }
} }
/* Show enable button */ /* Show activated button */
div[data-enabled='false'] { div[data-activated='true'] {
button:last-of-type { button:last-of-type {
display: block; display: block;
} }

View File

@ -5,7 +5,7 @@
display: inline-block; display: inline-block;
min-width: 40px; min-width: 40px;
font-family: var(--bx-monospaced-font); font-family: var(--bx-monospaced-font);
font-size: 12px; font-size: 13px;
margin: 0 4px; margin: 0 4px;
} }

View File

@ -91,6 +91,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
&[data-stats*="[batt]"] > .bx-stat-batt, &[data-stats*="[batt]"] > .bx-stat-batt,
&[data-stats*="[fps]"] > .bx-stat-fps, &[data-stats*="[fps]"] > .bx-stat-fps,
&[data-stats*="[ping]"] > .bx-stat-ping, &[data-stats*="[ping]"] > .bx-stat-ping,
&[data-stats*="[jit]"] > .bx-stat-jit,
&[data-stats*="[btr]"] > .bx-stat-btr, &[data-stats*="[btr]"] > .bx-stat-btr,
&[data-stats*="[dt]"] > .bx-stat-dt, &[data-stats*="[dt]"] > .bx-stat-dt,
&[data-stats*="[pl]"] > .bx-stat-pl, &[data-stats*="[pl]"] > .bx-stat-pl,
@ -106,6 +107,7 @@ div[class^=StreamMenu-module__container] .bx-badges {
&[data-stats$="[batt]"] > .bx-stat-batt, &[data-stats$="[batt]"] > .bx-stat-batt,
&[data-stats$="[fps]"] > .bx-stat-fps, &[data-stats$="[fps]"] > .bx-stat-fps,
&[data-stats$="[ping]"] > .bx-stat-ping, &[data-stats$="[ping]"] > .bx-stat-ping,
&[data-stats$="[jit]"] > .bx-stat-jit,
&[data-stats$="[btr]"] > .bx-stat-btr, &[data-stats$="[btr]"] > .bx-stat-btr,
&[data-stats$="[dt]"] > .bx-stat-dt, &[data-stats$="[dt]"] > .bx-stat-dt,
&[data-stats$="[pl]"] > .bx-stat-pl, &[data-stats$="[pl]"] > .bx-stat-pl,

View File

@ -0,0 +1,8 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='#fff' stroke='none ' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
<clipPath id='A'>
<path d='M0 0h32v32H0z'/>
</clipPath>
<g clip-path='url(#A)'>
<path d='M6.123 3.549a1.07 1.07 0 0 0-.798-.359c-.585 0-1.067.482-1.067 1.067 0 .27.102.53.286.727l2.565 2.823C2.267 10.779.184 15.36.092 15.568c-.123.276-.123.591 0 .867.047.105 1.176 2.609 3.687 5.12 3.345 3.344 7.57 5.112 12.221 5.112a16.97 16.97 0 0 0 6.943-1.444l2.933 3.228c.202.228.493.359.798.359.585 0 1.067-.482 1.067-1.067a1.07 1.07 0 0 0-.286-.727L6.123 3.549zm6.31 10.112l5.556 6.114c-.612.322-1.294.49-1.986.49a4.29 4.29 0 0 1-4.267-4.266c0-.831.242-1.643.697-2.338zM16 24.533c-4.104 0-7.689-1.492-10.657-4.433A17.73 17.73 0 0 1 2.267 16c.625-1.172 2.621-4.452 6.313-6.584l2.4 2.633c-.878 1.125-1.356 2.512-1.356 3.939 0 3.511 2.89 6.4 6.4 6.4 1.221 0 2.416-.349 3.444-1.005l1.964 2.16a14.92 14.92 0 0 1-5.432.99zm.8-12.724a1.07 1.07 0 0 1-.867-1.048c0-.585.482-1.067 1.067-1.067a1.12 1.12 0 0 1 .2.019c2.784.54 4.896 2.863 5.169 5.686a1.07 1.07 0 0 1-.962 1.161c-.034.002-.067.002-.1 0a1.07 1.07 0 0 1-1.067-.968 4.29 4.29 0 0 0-3.44-3.783zm15.104 4.626c-.056.125-1.407 3.116-4.448 5.84a1.07 1.07 0 0 1-.724.283c-.585 0-1.067-.482-1.067-1.067a1.07 1.07 0 0 1 .368-.806A17.7 17.7 0 0 0 29.74 16a17.73 17.73 0 0 0-3.083-4.103C23.689 8.959 20.104 7.467 16 7.467a15.82 15.82 0 0 0-2.581.209 1.06 1.06 0 0 1-.186.016 1.07 1.07 0 0 1-1.067-1.066 1.07 1.07 0 0 1 .901-1.054A17.89 17.89 0 0 1 16 5.333c4.651 0 8.876 1.768 12.221 5.114 2.511 2.51 3.64 5.016 3.687 5.121.123.276.123.591 0 .867h-.004z' fill-rule='nonzero'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

8
src/assets/svg/eye.svg Normal file
View File

@ -0,0 +1,8 @@
<svg xmlns='http://www.w3.org/2000/svg' fill='#fff' stroke='none ' fill-rule='evenodd' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' viewBox='0 0 32 32'>
<clipPath id='A'>
<path d='M0 0h32v32H0z'/>
</clipPath>
<g clip-path='url(#A)'>
<path d='M31.908 15.568c-.047-.105-1.176-2.611-3.687-5.121C24.876 7.101 20.651 5.333 16 5.333S7.124 7.101 3.779 10.447c-2.511 2.51-3.646 5.02-3.687 5.121-.123.276-.123.591 0 .867.047.105 1.176 2.609 3.687 5.12 3.345 3.344 7.57 5.112 12.221 5.112s8.876-1.768 12.221-5.112c2.511-2.511 3.64-5.015 3.687-5.12.123-.276.123-.591 0-.867zM16 24.533c-4.104 0-7.689-1.492-10.657-4.433-1.218-1.211-2.254-2.592-3.076-4.1.822-1.508 1.858-2.889 3.076-4.1C8.311 8.959 11.896 7.467 16 7.467s7.689 1.492 10.657 4.433c1.221 1.211 2.259 2.592 3.083 4.1-.961 1.795-5.149 8.533-13.74 8.533zM16 9.6c-3.511 0-6.4 2.889-6.4 6.4s2.889 6.4 6.4 6.4 6.4-2.889 6.4-6.4A6.44 6.44 0 0 0 16 9.6zm0 10.667A4.29 4.29 0 0 1 11.733 16 4.29 4.29 0 0 1 16 11.733 4.29 4.29 0 0 1 20.267 16 4.29 4.29 0 0 1 16 20.267z' fill-rule='nonzero'/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -75,6 +75,7 @@ export enum PrefKey {
VIDEO_PLAYER_TYPE = 'video_player_type', VIDEO_PLAYER_TYPE = 'video_player_type',
VIDEO_PROCESSING = 'video_processing', VIDEO_PROCESSING = 'video_processing',
VIDEO_POWER_PREFERENCE = 'video_power_preference', VIDEO_POWER_PREFERENCE = 'video_power_preference',
VIDEO_MAX_FPS = 'video_max_fps',
VIDEO_SHARPNESS = 'video_sharpness', VIDEO_SHARPNESS = 'video_sharpness',
VIDEO_RATIO = 'video_ratio', VIDEO_RATIO = 'video_ratio',
VIDEO_BRIGHTNESS = 'video_brightness', VIDEO_BRIGHTNESS = 'video_brightness',

View File

@ -38,37 +38,37 @@ const enum ShortcutAction {
} }
export class ControllerShortcut { export class ControllerShortcut {
static readonly #STORAGE_KEY = 'better_xcloud_controller_shortcuts'; private static readonly STORAGE_KEY = 'better_xcloud_controller_shortcuts';
static #buttonsCache: {[key: string]: boolean[]} = {}; private static buttonsCache: {[key: string]: boolean[]} = {};
static #buttonsStatus: {[key: string]: boolean[]} = {}; private static buttonsStatus: {[key: string]: boolean[]} = {};
static #$selectProfile: HTMLSelectElement; private static $selectProfile: HTMLSelectElement;
static #$selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {}; private static $selectActions: Partial<{[key in GamepadKey]: HTMLSelectElement}> = {};
static #$container: HTMLElement; private static $container: HTMLElement;
static #ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null; private static ACTIONS: {[key: string]: (ShortcutAction | null)[]} | null = null;
static reset(index: number) { static reset(index: number) {
ControllerShortcut.#buttonsCache[index] = []; ControllerShortcut.buttonsCache[index] = [];
ControllerShortcut.#buttonsStatus[index] = []; ControllerShortcut.buttonsStatus[index] = [];
} }
static handle(gamepad: Gamepad): boolean { static handle(gamepad: Gamepad): boolean {
if (!ControllerShortcut.#ACTIONS) { if (!ControllerShortcut.ACTIONS) {
ControllerShortcut.#ACTIONS = ControllerShortcut.#getActionsFromStorage(); ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
} }
const gamepadIndex = gamepad.index; const gamepadIndex = gamepad.index;
const actions = ControllerShortcut.#ACTIONS![gamepad.id]; const actions = ControllerShortcut.ACTIONS![gamepad.id];
if (!actions) { if (!actions) {
return false; return false;
} }
// Move the buttons status from the previous frame to the cache // Move the buttons status from the previous frame to the cache
ControllerShortcut.#buttonsCache[gamepadIndex] = ControllerShortcut.#buttonsStatus[gamepadIndex].slice(0); ControllerShortcut.buttonsCache[gamepadIndex] = ControllerShortcut.buttonsStatus[gamepadIndex].slice(0);
// Clear the buttons status // Clear the buttons status
ControllerShortcut.#buttonsStatus[gamepadIndex] = []; ControllerShortcut.buttonsStatus[gamepadIndex] = [];
const pressed: boolean[] = []; const pressed: boolean[] = [];
let otherButtonPressed = false; let otherButtonPressed = false;
@ -80,17 +80,17 @@ export class ControllerShortcut {
pressed[index] = true; pressed[index] = true;
// If this is newly pressed button -> run action // If this is newly pressed button -> run action
if (actions[index] && !ControllerShortcut.#buttonsCache[gamepadIndex][index]) { if (actions[index] && !ControllerShortcut.buttonsCache[gamepadIndex][index]) {
setTimeout(() => ControllerShortcut.#runAction(actions[index]!), 0); setTimeout(() => ControllerShortcut.runAction(actions[index]!), 0);
} }
} }
}); });
ControllerShortcut.#buttonsStatus[gamepadIndex] = pressed; ControllerShortcut.buttonsStatus[gamepadIndex] = pressed;
return otherButtonPressed; return otherButtonPressed;
} }
static #runAction(action: ShortcutAction) { private static runAction(action: ShortcutAction) {
switch (action) { switch (action) {
case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW: case ShortcutAction.BETTER_XCLOUD_SETTINGS_SHOW:
SettingsNavigationDialog.getInstance().show(); SettingsNavigationDialog.getInstance().show();
@ -134,8 +134,8 @@ export class ControllerShortcut {
} }
} }
static #updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) { private static updateAction(profile: string, button: GamepadKey, action: ShortcutAction | null) {
const actions = ControllerShortcut.#ACTIONS!; const actions = ControllerShortcut.ACTIONS!;
if (!(profile in actions)) { if (!(profile in actions)) {
actions[profile] = []; actions[profile] = [];
} }
@ -147,9 +147,9 @@ export class ControllerShortcut {
actions[profile][button] = action; actions[profile][button] = action;
// Remove empty profiles // Remove empty profiles
for (const key in ControllerShortcut.#ACTIONS) { for (const key in ControllerShortcut.ACTIONS) {
let empty = true; let empty = true;
for (const value of ControllerShortcut.#ACTIONS[key]) { for (const value of ControllerShortcut.ACTIONS[key]) {
if (!!value) { if (!!value) {
empty = false; empty = false;
break; break;
@ -157,19 +157,19 @@ export class ControllerShortcut {
} }
if (empty) { if (empty) {
delete ControllerShortcut.#ACTIONS[key]; delete ControllerShortcut.ACTIONS[key];
} }
} }
// Save to storage // Save to storage
window.localStorage.setItem(ControllerShortcut.#STORAGE_KEY, JSON.stringify(ControllerShortcut.#ACTIONS)); window.localStorage.setItem(ControllerShortcut.STORAGE_KEY, JSON.stringify(ControllerShortcut.ACTIONS));
console.log(ControllerShortcut.#ACTIONS); console.log(ControllerShortcut.ACTIONS);
} }
static #updateProfileList(e?: GamepadEvent) { private static updateProfileList(e?: GamepadEvent) {
const $select = ControllerShortcut.#$selectProfile; const $select = ControllerShortcut.$selectProfile;
const $container = ControllerShortcut.#$container; const $container = ControllerShortcut.$container;
const $fragment = document.createDocumentFragment(); const $fragment = document.createDocumentFragment();
@ -205,16 +205,16 @@ export class ControllerShortcut {
} }
static #switchProfile(profile: string) { private static switchProfile(profile: string) {
let actions = ControllerShortcut.#ACTIONS![profile]; let actions = ControllerShortcut.ACTIONS![profile];
if (!actions) { if (!actions) {
actions = []; actions = [];
} }
// Reset selects' values // Reset selects' values
let button: any; let button: any;
for (button in ControllerShortcut.#$selectActions) { for (button in ControllerShortcut.$selectActions) {
const $select = ControllerShortcut.#$selectActions[button as GamepadKey]!; const $select = ControllerShortcut.$selectActions[button as GamepadKey]!;
$select.value = actions[button] || ''; $select.value = actions[button] || '';
BxEvent.dispatch($select, 'input', { BxEvent.dispatch($select, 'input', {
@ -224,15 +224,15 @@ export class ControllerShortcut {
} }
} }
static #getActionsFromStorage() { private static getActionsFromStorage() {
return JSON.parse(window.localStorage.getItem(ControllerShortcut.#STORAGE_KEY) || '{}'); return JSON.parse(window.localStorage.getItem(ControllerShortcut.STORAGE_KEY) || '{}');
} }
static renderSettings() { static renderSettings() {
const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY); const PREF_CONTROLLER_FRIENDLY_UI = getPref(PrefKey.UI_CONTROLLER_FRIENDLY);
// Read actions from localStorage // Read actions from localStorage
ControllerShortcut.#ACTIONS = ControllerShortcut.#getActionsFromStorage(); ControllerShortcut.ACTIONS = ControllerShortcut.getActionsFromStorage();
const buttons: Map<GamepadKey, PrompFont> = new Map(); const buttons: Map<GamepadKey, PrompFont> = new Map();
buttons.set(GamepadKey.Y, PrompFont.Y); buttons.set(GamepadKey.Y, PrompFont.Y);
@ -340,7 +340,7 @@ export class ControllerShortcut {
); );
$selectProfile.addEventListener('input', e => { $selectProfile.addEventListener('input', e => {
ControllerShortcut.#switchProfile($selectProfile.value); ControllerShortcut.switchProfile($selectProfile.value);
}); });
const onActionChanged = (e: Event) => { const onActionChanged = (e: Event) => {
@ -361,7 +361,7 @@ export class ControllerShortcut {
($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText; ($fakeSelect.firstElementChild as HTMLOptionElement).text = fakeText;
} }
!(e as any).ignoreOnChange && ControllerShortcut.#updateAction(profile, button as GamepadKey, action); !(e as any).ignoreOnChange && ControllerShortcut.updateAction(profile, button as GamepadKey, action);
}; };
@ -387,7 +387,7 @@ export class ControllerShortcut {
$select.dataset.button = button.toString(); $select.dataset.button = button.toString();
$select.addEventListener('input', onActionChanged); $select.addEventListener('input', onActionChanged);
ControllerShortcut.#$selectActions[button] = $select; ControllerShortcut.$selectActions[button] = $select;
if (PREF_CONTROLLER_FRIENDLY_UI) { if (PREF_CONTROLLER_FRIENDLY_UI) {
const $bxSelect = BxSelectElement.wrap($select); const $bxSelect = BxSelectElement.wrap($select);
@ -412,14 +412,14 @@ export class ControllerShortcut {
$container.appendChild($remap); $container.appendChild($remap);
ControllerShortcut.#$selectProfile = $selectProfile; ControllerShortcut.$selectProfile = $selectProfile;
ControllerShortcut.#$container = $container; ControllerShortcut.$container = $container;
// Detect when gamepad connected/disconnect // Detect when gamepad connected/disconnect
window.addEventListener('gamepadconnected', ControllerShortcut.#updateProfileList); window.addEventListener('gamepadconnected', ControllerShortcut.updateProfileList);
window.addEventListener('gamepaddisconnected', ControllerShortcut.#updateProfileList); window.addEventListener('gamepaddisconnected', ControllerShortcut.updateProfileList);
ControllerShortcut.#updateProfileList(); ControllerShortcut.updateProfileList();
return $container; return $container;
} }

View File

@ -1,6 +1,12 @@
import { BxEvent } from "@/utils/bx-event";
export abstract class BaseGameBarAction { export abstract class BaseGameBarAction {
constructor() {} constructor() {}
reset() {} reset() {}
onClick(e: Event) {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
};
abstract render(): HTMLElement; abstract render(): HTMLElement;
} }

View File

@ -13,29 +13,22 @@ export class MicrophoneAction extends BaseGameBarAction {
constructor() { constructor() {
super(); super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const enabled = MicrophoneShortcut.toggle(false);
this.$content.setAttribute('data-enabled', enabled.toString());
};
const $btnDefault = createButton({ const $btnDefault = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE, icon: BxIcon.MICROPHONE,
onClick: onClick, onClick: this.onClick.bind(this),
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
const $btnMuted = createButton({ const $btnMuted = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.MICROPHONE_MUTED, icon: BxIcon.MICROPHONE_MUTED,
onClick: onClick, onClick: this.onClick.bind(this),
}); });
this.$content = CE('div', {}, this.$content = CE('div', {},
$btnDefault,
$btnMuted, $btnMuted,
$btnDefault,
); );
this.reset(); this.reset();
@ -43,14 +36,19 @@ export class MicrophoneAction extends BaseGameBarAction {
window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => { window.addEventListener(BxEvent.MICROPHONE_STATE_CHANGED, e => {
const microphoneState = (e as any).microphoneState; const microphoneState = (e as any).microphoneState;
const enabled = microphoneState === MicrophoneState.ENABLED; const enabled = microphoneState === MicrophoneState.ENABLED;
this.$content.dataset.activated = enabled.toString();
this.$content.setAttribute('data-enabled', enabled.toString());
// Show the button in Game Bar if the mic is enabled // Show the button in Game Bar if the mic is enabled
this.$content.classList.remove('bx-gone'); this.$content.classList.remove('bx-gone');
}); });
} }
onClick(e: Event) {
super.onClick(e);
const enabled = MicrophoneShortcut.toggle(false);
this.$content.dataset.activated = enabled.toString();
}
render(): HTMLElement { render(): HTMLElement {
return this.$content; return this.$content;
} }
@ -58,6 +56,6 @@ export class MicrophoneAction extends BaseGameBarAction {
reset(): void { reset(): void {
this.visible = false; this.visible = false;
this.$content.classList.add('bx-gone'); this.$content.classList.add('bx-gone');
this.$content.setAttribute('data-enabled', 'false'); this.$content.dataset.activated = 'false';
} }
} }

View File

@ -0,0 +1,47 @@
import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html";
import { BaseGameBarAction } from "./action-base";
import { RendererShortcut } from "../shortcuts/shortcut-renderer";
export class RendererAction extends BaseGameBarAction {
$content: HTMLElement;
constructor() {
super();
const $btnDefault = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.EYE,
onClick: this.onClick.bind(this),
});
const $btnActivated = createButton({
style: ButtonStyle.GHOST,
icon: BxIcon.EYE_SLASH,
onClick: this.onClick.bind(this),
classes: ['bx-activated'],
});
this.$content = CE('div', {},
$btnDefault,
$btnActivated,
);
this.reset();
}
onClick(e: Event) {
super.onClick(e);
const isVisible = RendererShortcut.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString();
}
render(): HTMLElement {
return this.$content;
}
reset(): void {
this.$content.dataset.activated = 'false';
}
}

View File

@ -1,4 +1,3 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon"; import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle } from "@utils/html"; import { createButton, ButtonStyle } from "@utils/html";
import { BaseGameBarAction } from "./action-base"; import { BaseGameBarAction } from "./action-base";
@ -11,19 +10,19 @@ export class ScreenshotAction extends BaseGameBarAction {
constructor() { constructor() {
super(); super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
Screenshot.takeScreenshot();
};
this.$content = createButton({ this.$content = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.SCREENSHOT, icon: BxIcon.SCREENSHOT,
title: t('take-screenshot'), title: t('take-screenshot'),
onClick: onClick, onClick: this.onClick.bind(this),
}); });
} }
onClick(e: Event): void {
super.onClick(e);
Screenshot.takeScreenshot();
}
render(): HTMLElement { render(): HTMLElement {
return this.$content; return this.$content;
} }

View File

@ -11,21 +11,16 @@ export class SpeakerAction extends BaseGameBarAction {
constructor() { constructor() {
super(); super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
SoundShortcut.muteUnmute();
};
const $btnEnable = createButton({ const $btnEnable = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.AUDIO, icon: BxIcon.AUDIO,
onClick: onClick, onClick: this.onClick.bind(this),
}); });
const $btnMuted = createButton({ const $btnMuted = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.SPEAKER_MUTED, icon: BxIcon.SPEAKER_MUTED,
onClick: onClick, onClick: this.onClick.bind(this),
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
@ -40,15 +35,20 @@ export class SpeakerAction extends BaseGameBarAction {
const speakerState = (e as any).speakerState; const speakerState = (e as any).speakerState;
const enabled = speakerState === SpeakerState.ENABLED; const enabled = speakerState === SpeakerState.ENABLED;
this.$content.dataset.enabled = enabled.toString(); this.$content.dataset.activated = (!enabled).toString();
}); });
} }
onClick(e: Event) {
super.onClick(e);
SoundShortcut.muteUnmute();
}
render(): HTMLElement { render(): HTMLElement {
return this.$content; return this.$content;
} }
reset(): void { reset(): void {
this.$content.dataset.enabled = 'true'; this.$content.dataset.activated = 'false';
} }
} }

View File

@ -1,4 +1,3 @@
import { BxEvent } from "@utils/bx-event";
import { BxIcon } from "@utils/bx-icon"; import { BxIcon } from "@utils/bx-icon";
import { createButton, ButtonStyle, CE } from "@utils/html"; import { createButton, ButtonStyle, CE } from "@utils/html";
import { TouchController } from "@modules/touch-controller"; import { TouchController } from "@modules/touch-controller";
@ -11,28 +10,18 @@ export class TouchControlAction extends BaseGameBarAction {
constructor() { constructor() {
super(); super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
const $parent = (e as any).target.closest('div[data-enabled]');
let enabled = $parent.getAttribute('data-enabled', 'true') === 'true';
$parent.setAttribute('data-enabled', (!enabled).toString());
TouchController.toggleVisibility(enabled);
};
const $btnEnable = createButton({ const $btnEnable = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_ENABLE, icon: BxIcon.TOUCH_CONTROL_ENABLE,
title: t('show-touch-controller'), title: t('show-touch-controller'),
onClick: onClick, onClick: this.onClick.bind(this),
}); });
const $btnDisable = createButton({ const $btnDisable = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.TOUCH_CONTROL_DISABLE, icon: BxIcon.TOUCH_CONTROL_DISABLE,
title: t('hide-touch-controller'), title: t('hide-touch-controller'),
onClick: onClick, onClick: this.onClick.bind(this),
classes: ['bx-activated'], classes: ['bx-activated'],
}); });
@ -44,11 +33,17 @@ export class TouchControlAction extends BaseGameBarAction {
this.reset(); this.reset();
} }
onClick(e: Event) {
super.onClick(e);
const isVisible = TouchController.toggleVisibility();
this.$content.dataset.activated = (!isVisible).toString();
}
render(): HTMLElement { render(): HTMLElement {
return this.$content; return this.$content;
} }
reset(): void { reset(): void {
this.$content.setAttribute('data-enabled', 'true'); this.$content.dataset.activated = 'false';
} }
} }

View File

@ -1,4 +1,3 @@
import { BxEvent } from "@/utils/bx-event";
import { BxIcon } from "@/utils/bx-icon"; import { BxIcon } from "@/utils/bx-icon";
import { createButton, ButtonStyle } from "@/utils/html"; import { createButton, ButtonStyle } from "@/utils/html";
import { t } from "@/utils/translation"; import { t } from "@/utils/translation";
@ -11,19 +10,19 @@ export class TrueAchievementsAction extends BaseGameBarAction {
constructor() { constructor() {
super(); super();
const onClick = (e: Event) => {
BxEvent.dispatch(window, BxEvent.GAME_BAR_ACTION_ACTIVATED);
TrueAchievements.open(false);
};
this.$content = createButton({ this.$content = createButton({
style: ButtonStyle.GHOST, style: ButtonStyle.GHOST,
icon: BxIcon.TRUE_ACHIEVEMENTS, icon: BxIcon.TRUE_ACHIEVEMENTS,
title: t('true-achievements'), title: t('true-achievements'),
onClick: onClick, onClick: this.onClick.bind(this),
}); });
} }
onClick(e: Event) {
super.onClick(e);
TrueAchievements.open(false);
}
render(): HTMLElement { render(): HTMLElement {
return this.$content; return this.$content;
} }

View File

@ -10,6 +10,7 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage"; import { getPref, StreamTouchController } from "@/utils/settings-storages/global-settings-storage";
import { TrueAchievementsAction } from "./action-true-achievements"; import { TrueAchievementsAction } from "./action-true-achievements";
import { SpeakerAction } from "./action-speaker"; import { SpeakerAction } from "./action-speaker";
import { RendererAction } from "./action-renderer";
export class GameBar { export class GameBar {
@ -27,7 +28,7 @@ export class GameBar {
private $gameBar: HTMLElement; private $gameBar: HTMLElement;
private $container: HTMLElement; private $container: HTMLElement;
private timeout: number | null = null; private timeoutId: number | null = null;
private actions: BaseGameBarAction[] = []; private actions: BaseGameBarAction[] = [];
@ -45,6 +46,7 @@ export class GameBar {
new ScreenshotAction(), new ScreenshotAction(),
...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []), ...(STATES.userAgent.capabilities.touch && (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) !== StreamTouchController.OFF) ? [new TouchControlAction()] : []),
new SpeakerAction(), new SpeakerAction(),
new RendererAction(),
new MicrophoneAction(), new MicrophoneAction(),
new TrueAchievementsAction(), new TrueAchievementsAction(),
]; ];
@ -103,15 +105,15 @@ export class GameBar {
private beginHideTimeout() { private beginHideTimeout() {
this.clearHideTimeout(); this.clearHideTimeout();
this.timeout = window.setTimeout(() => { this.timeoutId = window.setTimeout(() => {
this.timeout = null; this.timeoutId = null;
this.hideBar(); this.hideBar();
}, GameBar.VISIBLE_DURATION); }, GameBar.VISIBLE_DURATION);
} }
private clearHideTimeout() { private clearHideTimeout() {
this.timeout && clearTimeout(this.timeout); this.timeoutId && clearTimeout(this.timeoutId);
this.timeout = null; this.timeoutId = null;
} }
enable() { enable() {

View File

@ -7,13 +7,13 @@ import { getPref } from "@/utils/settings-storages/global-settings-storage";
import { compressCss } from "@macros/build" with {type: "macro"}; import { compressCss } from "@macros/build" with {type: "macro"};
export class LoadingScreen { export class LoadingScreen {
static #$bgStyle: HTMLElement; private static $bgStyle: HTMLElement;
static #$waitTimeBox: HTMLElement; private static $waitTimeBox: HTMLElement;
static #waitTimeInterval?: number | null = null; private static waitTimeInterval?: number | null = null;
static #orgWebTitle: string; private static orgWebTitle: string;
static #secondsToString(seconds: number) { private static secondsToString(seconds: number) {
const m = Math.floor(seconds / 60); const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60); const s = Math.floor(seconds % 60);
@ -28,21 +28,21 @@ export class LoadingScreen {
return; return;
} }
if (!LoadingScreen.#$bgStyle) { if (!LoadingScreen.$bgStyle) {
const $bgStyle = CE('style'); const $bgStyle = CE('style');
document.documentElement.appendChild($bgStyle); document.documentElement.appendChild($bgStyle);
LoadingScreen.#$bgStyle = $bgStyle; LoadingScreen.$bgStyle = $bgStyle;
} }
LoadingScreen.#setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl); LoadingScreen.setBackground(titleInfo.product.heroImageUrl || titleInfo.product.titledHeroImageUrl || titleInfo.product.tileImageUrl);
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') { if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide') {
LoadingScreen.#hideRocket(); LoadingScreen.hideRocket();
} }
} }
static #hideRocket() { private static hideRocket() {
let $bgStyle = LoadingScreen.#$bgStyle; let $bgStyle = LoadingScreen.$bgStyle;
$bgStyle.textContent! += compressCss(` $bgStyle.textContent! += compressCss(`
#game-stream div[class*=RocketAnimation-module__container] > svg { #game-stream div[class*=RocketAnimation-module__container] > svg {
@ -55,9 +55,9 @@ export class LoadingScreen {
`); `);
} }
static #setBackground(imageUrl: string) { private static setBackground(imageUrl: string) {
// Setup style tag // Setup style tag
let $bgStyle = LoadingScreen.#$bgStyle; let $bgStyle = LoadingScreen.$bgStyle;
// Limit max width to reduce image size // Limit max width to reduce image size
imageUrl = imageUrl + '?w=1920'; imageUrl = imageUrl + '?w=1920';
@ -89,14 +89,14 @@ export class LoadingScreen {
static setupWaitTime(waitTime: number) { static setupWaitTime(waitTime: number) {
// Hide rocket when queing // Hide rocket when queing
if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') { if (getPref(PrefKey.UI_LOADING_SCREEN_ROCKET) === 'hide-queue') {
LoadingScreen.#hideRocket(); LoadingScreen.hideRocket();
} }
let secondsLeft = waitTime; let secondsLeft = waitTime;
let $countDown; let $countDown;
let $estimated; let $estimated;
LoadingScreen.#orgWebTitle = document.title; LoadingScreen.orgWebTitle = document.title;
const endDate = new Date(); const endDate = new Date();
const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60; const timeZoneOffsetSeconds = endDate.getTimezoneOffset() * 60;
@ -104,9 +104,9 @@ export class LoadingScreen {
let endDateStr = endDate.toISOString().slice(0, 19); let endDateStr = endDate.toISOString().slice(0, 19);
endDateStr = endDateStr.substring(0, 10) + ' ' + endDateStr.substring(11, 19); endDateStr = endDateStr.substring(0, 10) + ' ' + endDateStr.substring(11, 19);
endDateStr += ` (${LoadingScreen.#secondsToString(waitTime)})`; endDateStr += ` (${LoadingScreen.secondsToString(waitTime)})`;
let $waitTimeBox = LoadingScreen.#$waitTimeBox; let $waitTimeBox = LoadingScreen.$waitTimeBox;
if (!$waitTimeBox) { if (!$waitTimeBox) {
$waitTimeBox = CE('div', {'class': 'bx-wait-time-box'}, $waitTimeBox = CE('div', {'class': 'bx-wait-time-box'},
CE('label', {}, t('server')), CE('label', {}, t('server')),
@ -118,7 +118,7 @@ export class LoadingScreen {
); );
document.documentElement.appendChild($waitTimeBox); document.documentElement.appendChild($waitTimeBox);
LoadingScreen.#$waitTimeBox = $waitTimeBox; LoadingScreen.$waitTimeBox = $waitTimeBox;
} else { } else {
$waitTimeBox.classList.remove('bx-gone'); $waitTimeBox.classList.remove('bx-gone');
$estimated = $waitTimeBox.querySelector('.bx-wait-time-estimated')!; $estimated = $waitTimeBox.querySelector('.bx-wait-time-estimated')!;
@ -126,36 +126,36 @@ export class LoadingScreen {
} }
$estimated.textContent = endDateStr; $estimated.textContent = endDateStr;
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); $countDown.textContent = LoadingScreen.secondsToString(secondsLeft);
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`;
LoadingScreen.#waitTimeInterval = window.setInterval(() => { LoadingScreen.waitTimeInterval = window.setInterval(() => {
secondsLeft--; secondsLeft--;
$countDown.textContent = LoadingScreen.#secondsToString(secondsLeft); $countDown.textContent = LoadingScreen.secondsToString(secondsLeft);
document.title = `[${$countDown.textContent}] ${LoadingScreen.#orgWebTitle}`; document.title = `[${$countDown.textContent}] ${LoadingScreen.orgWebTitle}`;
if (secondsLeft <= 0) { if (secondsLeft <= 0) {
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval);
LoadingScreen.#waitTimeInterval = null; LoadingScreen.waitTimeInterval = null;
} }
}, 1000); }, 1000);
} }
static hide() { static hide() {
LoadingScreen.#orgWebTitle && (document.title = LoadingScreen.#orgWebTitle); LoadingScreen.orgWebTitle && (document.title = LoadingScreen.orgWebTitle);
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('bx-gone'); LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add('bx-gone');
if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.#$bgStyle) { if (getPref(PrefKey.UI_LOADING_SCREEN_GAME_ART) && LoadingScreen.$bgStyle) {
const $rocketBg = document.querySelector('#game-stream rect[width="800"]'); const $rocketBg = document.querySelector('#game-stream rect[width="800"]');
$rocketBg && $rocketBg.addEventListener('transitionend', e => { $rocketBg && $rocketBg.addEventListener('transitionend', e => {
LoadingScreen.#$bgStyle.textContent += compressCss(` LoadingScreen.$bgStyle.textContent += compressCss(`
#game-stream { #game-stream {
background: #000 !important; background: #000 !important;
} }
`); `);
}); });
LoadingScreen.#$bgStyle.textContent += compressCss(` LoadingScreen.$bgStyle.textContent += compressCss(`
#game-stream rect[width="800"] { #game-stream rect[width="800"] {
opacity: 1 !important; opacity: 1 !important;
} }
@ -166,10 +166,10 @@ export class LoadingScreen {
} }
static reset() { static reset() {
LoadingScreen.#$bgStyle && (LoadingScreen.#$bgStyle.textContent = ''); LoadingScreen.$bgStyle && (LoadingScreen.$bgStyle.textContent = '');
LoadingScreen.#$waitTimeBox && LoadingScreen.#$waitTimeBox.classList.add('bx-gone'); LoadingScreen.$waitTimeBox && LoadingScreen.$waitTimeBox.classList.add('bx-gone');
LoadingScreen.#waitTimeInterval && clearInterval(LoadingScreen.#waitTimeInterval); LoadingScreen.waitTimeInterval && clearInterval(LoadingScreen.waitTimeInterval);
LoadingScreen.#waitTimeInterval = null; LoadingScreen.waitTimeInterval = null;
} }
} }

View File

@ -23,37 +23,37 @@ export class PointerClient {
return PointerClient.instance; return PointerClient.instance;
} }
#socket: WebSocket | undefined | null; private socket: WebSocket | undefined | null;
#mkbHandler: MkbHandler | undefined; private mkbHandler: MkbHandler | undefined;
start(port: number, mkbHandler: MkbHandler) { start(port: number, mkbHandler: MkbHandler) {
if (!port) { if (!port) {
throw new Error('PointerServer port is 0'); throw new Error('PointerServer port is 0');
} }
this.#mkbHandler = mkbHandler; this.mkbHandler = mkbHandler;
// Create WebSocket connection. // Create WebSocket connection.
this.#socket = new WebSocket(`ws://localhost:${port}`); this.socket = new WebSocket(`ws://localhost:${port}`);
this.#socket.binaryType = 'arraybuffer'; this.socket.binaryType = 'arraybuffer';
// Connection opened // Connection opened
this.#socket.addEventListener('open', (event) => { this.socket.addEventListener('open', (event) => {
BxLogger.info(LOG_TAG, 'connected') BxLogger.info(LOG_TAG, 'connected')
}); });
// Error // Error
this.#socket.addEventListener('error', (event) => { this.socket.addEventListener('error', (event) => {
BxLogger.error(LOG_TAG, event); BxLogger.error(LOG_TAG, event);
Toast.show('Cannot setup mouse: ' + event); Toast.show('Cannot setup mouse: ' + event);
}); });
this.#socket.addEventListener('close', (event) => { this.socket.addEventListener('close', (event) => {
this.#socket = null; this.socket = null;
}); });
// Listen for messages // Listen for messages
this.#socket.addEventListener('message', (event) => { this.socket.addEventListener('message', (event) => {
const dataView = new DataView(event.data); const dataView = new DataView(event.data);
let messageType = dataView.getInt8(0); let messageType = dataView.getInt8(0);
@ -84,7 +84,7 @@ export class PointerClient {
offset += Int16Array.BYTES_PER_ELEMENT; offset += Int16Array.BYTES_PER_ELEMENT;
const y = dataView.getInt16(offset); const y = dataView.getInt16(offset);
this.#mkbHandler?.handleMouseMove({ this.mkbHandler?.handleMouseMove({
movementX: x, movementX: x,
movementY: y, movementY: y,
}); });
@ -94,7 +94,7 @@ export class PointerClient {
onPress(messageType: PointerAction, dataView: DataView, offset: number) { onPress(messageType: PointerAction, dataView: DataView, offset: number) {
const button = dataView.getUint8(offset); const button = dataView.getUint8(offset);
this.#mkbHandler?.handleMouseClick({ this.mkbHandler?.handleMouseClick({
pointerButton: button, pointerButton: button,
pressed: messageType === PointerAction.BUTTON_PRESS, pressed: messageType === PointerAction.BUTTON_PRESS,
}); });
@ -108,7 +108,7 @@ export class PointerClient {
offset += Int16Array.BYTES_PER_ELEMENT; offset += Int16Array.BYTES_PER_ELEMENT;
const hScroll = dataView.getInt16(offset); const hScroll = dataView.getInt16(offset);
this.#mkbHandler?.handleMouseWheel({ this.mkbHandler?.handleMouseWheel({
vertical: vScroll, vertical: vScroll,
horizontal: hScroll, horizontal: hScroll,
}); });
@ -118,13 +118,13 @@ export class PointerClient {
onPointerCaptureChanged(dataView: DataView, offset: number) { onPointerCaptureChanged(dataView: DataView, offset: number) {
const hasCapture = dataView.getInt8(offset) === 1; const hasCapture = dataView.getInt8(offset) === 1;
!hasCapture && this.#mkbHandler?.stop(); !hasCapture && this.mkbHandler?.stop();
} }
stop() { stop() {
try { try {
this.#socket?.close(); this.socket?.close();
} catch (e) {} } catch (e) {}
this.#socket = null; this.socket = null;
} }
} }

View File

@ -211,7 +211,8 @@ const PATCHES = {
// Block gamepad stats collecting // Block gamepad stats collecting
if (getPref(PrefKey.BLOCK_TRACKING)) { if (getPref(PrefKey.BLOCK_TRACKING)) {
codeBlock = codeBlock.replaceAll('this.inputPollingIntervalStats.addValue', ''); codeBlock = codeBlock.replace('this.inputPollingIntervalStats.addValue', '');
codeBlock = codeBlock.replace('this.inputPollingDurationStats.addValue', '');
} }
// Map the Share button on Xbox Series controller with the capturing screenshot feature // Map the Share button on Xbox Series controller with the capturing screenshot feature
@ -219,8 +220,8 @@ const PATCHES = {
if (match) { if (match) {
const gamepadVar = match[1]; const gamepadVar = match[1];
const newCode = renderString(codeControllerShortcuts, { const newCode = renderString(codeControllerShortcuts, {
gamepadVar, gamepadVar,
}); });
codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set'); codeBlock = codeBlock.replace('this.gamepadTimestamps.set', newCode + 'this.gamepadTimestamps.set');
} }

View File

@ -1,121 +1,100 @@
const int FILTER_UNSHARP_MASKING = 1; #version 300 es
const int FILTER_CAS = 2;
precision highp float; precision mediump float;
uniform sampler2D data; uniform sampler2D data;
uniform vec2 iResolution; uniform vec2 iResolution;
const int FILTER_UNSHARP_MASKING = 1;
// const int FILTER_CAS = 2;
// constrast = 0.8
const float CAS_CONTRAST_PEAK = 0.8 * -3.0 + 8.0;
// Luminosity factor
const vec3 LUMINOSITY_FACTOR = vec3(0.2126, 0.7152, 0.0722);
uniform int filterId; uniform int filterId;
uniform float sharpenFactor; uniform float sharpenFactor;
uniform float brightness; uniform float brightness;
uniform float contrast; uniform float contrast;
uniform float saturation; uniform float saturation;
vec3 textureAt(sampler2D tex, vec2 coord) { out vec4 fragColor;
return texture2D(tex, coord / iResolution.xy).rgb;
} vec3 clarityBoost(sampler2D tex, vec2 coord, vec3 e) {
vec2 texelSize = 1.0 / iResolution.xy;
vec3 clarityBoost(sampler2D tex, vec2 coord)
{
// Load a collection of samples in a 3x3 neighorhood, where e is the current pixel. // Load a collection of samples in a 3x3 neighorhood, where e is the current pixel.
// a b c // a b c
// d e f // d e f
// g h i // g h i
vec3 a = textureAt(tex, coord + vec2(-1, 1)); vec3 a = texture(tex, coord + texelSize * vec2(-1, 1)).rgb;
vec3 b = textureAt(tex, coord + vec2(0, 1)); vec3 b = texture(tex, coord + texelSize * vec2(0, 1)).rgb;
vec3 c = textureAt(tex, coord + vec2(1, 1)); vec3 c = texture(tex, coord + texelSize * vec2(1, 1)).rgb;
vec3 d = textureAt(tex, coord + vec2(-1, 0)); vec3 d = texture(tex, coord + texelSize * vec2(-1, 0)).rgb;
vec3 e = textureAt(tex, coord); vec3 f = texture(tex, coord + texelSize * vec2(1, 0)).rgb;
vec3 f = textureAt(tex, coord + vec2(1, 0));
vec3 g = textureAt(tex, coord + vec2(-1, -1)); vec3 g = texture(tex, coord + texelSize * vec2(-1, -1)).rgb;
vec3 h = textureAt(tex, coord + vec2(0, -1)); vec3 h = texture(tex, coord + texelSize * vec2(0, -1)).rgb;
vec3 i = textureAt(tex, coord + vec2(1, -1)); vec3 i = texture(tex, coord + texelSize * vec2(1, -1)).rgb;
if (filterId == FILTER_CAS) { // USM
// Soft min and max. if (filterId == FILTER_UNSHARP_MASKING) {
// a b c b vec3 gaussianBlur = (a + c + g + i) * 1.0 + (b + d + f + h) * 2.0 + e * 4.0;
// d e f * 0.5 + d e f * 0.5 gaussianBlur /= 16.0;
// g h i h
// These are 2.0x bigger (factored out the extra multiply).
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
vec3 minRgb2 = min(min(a, c), min(g, i));
minRgb += min(minRgb, minRgb2);
vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
vec3 maxRgb2 = max(max(a, c), max(g, i));
maxRgb += max(maxRgb, maxRgb2);
// Smooth minimum distance to signal limit divided by smooth max.
vec3 reciprocalMaxRgb = 1.0 / maxRgb;
vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);
// Shaping amount of sharpening.
amplifyRgb = inversesqrt(amplifyRgb);
float contrast = 0.8;
float peak = -3.0 * contrast + 8.0;
vec3 weightRgb = -(1.0 / (amplifyRgb * peak));
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
// 0 w 0
// Filter shape: w 1 w
// 0 w 0
vec3 window = (b + d) + (f + h);
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
outColor = mix(e, outColor, sharpenFactor / 2.0);
return outColor;
} else if (filterId == FILTER_UNSHARP_MASKING) {
vec3 gaussianBlur = (a * 1.0 + b * 2.0 + c * 1.0 +
d * 2.0 + e * 4.0 + f * 2.0 +
g * 1.0 + h * 2.0 + i * 1.0) / 16.0;
// Return edge detection // Return edge detection
return e + (e - gaussianBlur) * sharpenFactor / 3.0; return e + (e - gaussianBlur) * sharpenFactor / 3.0;
} }
return e; // CAS
} // Soft min and max.
// a b c b
// d e f * 0.5 + d e f * 0.5
// g h i h
// These are 2.0x bigger (factored out the extra multiply).
vec3 minRgb = min(min(min(d, e), min(f, b)), h);
minRgb += min(min(a, c), min(g, i));
vec3 adjustBrightness(vec3 color) { vec3 maxRgb = max(max(max(d, e), max(f, b)), h);
return (1.0 + brightness) * color; maxRgb += max(max(a, c), max(g, i));
}
vec3 adjustContrast(vec3 color) { // Smooth minimum distance to signal limit divided by smooth max.
return 0.5 + (1.0 + contrast) * (color - 0.5); vec3 reciprocalMaxRgb = 1.0 / maxRgb;
} vec3 amplifyRgb = clamp(min(minRgb, 2.0 - maxRgb) * reciprocalMaxRgb, 0.0, 1.0);
vec3 adjustSaturation(vec3 color) { // Shaping amount of sharpening.
const vec3 luminosityFactor = vec3(0.2126, 0.7152, 0.0722); amplifyRgb = inversesqrt(amplifyRgb);
vec3 grayscale = vec3(dot(color, luminosityFactor));
return mix(grayscale, color, 1.0 + saturation); vec3 weightRgb = -(1.0 / (amplifyRgb * CAS_CONTRAST_PEAK));
vec3 reciprocalWeightRgb = 1.0 / (4.0 * weightRgb + 1.0);
// 0 w 0
// Filter shape: w 1 w
// 0 w 0
vec3 window = b + d + f + h;
vec3 outColor = clamp((window * weightRgb + e) * reciprocalWeightRgb, 0.0, 1.0);
return mix(e, outColor, sharpenFactor / 2.0);
} }
void main() { void main() {
vec3 color; vec2 uv = gl_FragCoord.xy / iResolution.xy;
// Get current pixel
vec3 color = texture(data, uv).rgb;
if (sharpenFactor > 0.0) { // Clarity boost
color = clarityBoost(data, gl_FragCoord.xy); color = sharpenFactor > 0.0 ? clarityBoost(data, uv, color) : color;
} else {
color = textureAt(data, gl_FragCoord.xy);
}
if (saturation != 0.0) { // Saturation
color = adjustSaturation(color); color = saturation != 1.0 ? mix(vec3(dot(color, LUMINOSITY_FACTOR)), color, saturation) : color;
}
if (contrast != 0.0) { // Contrast
color = adjustContrast(color); color = contrast * (color - 0.5) + 0.5;
}
if (brightness != 0.0) { // Brightness
color = adjustBrightness(color); color = brightness * color;
}
gl_FragColor = vec4(color, 1.0); fragColor = vec4(color, 1.0);
} }

View File

@ -1,5 +1,7 @@
attribute vec2 position; #version 300 es
in vec4 position;
void main() { void main() {
gl_Position = vec4(position, 0, 1); gl_Position = position;
} }

View File

@ -5,19 +5,19 @@ import { PrefKey } from "@/enums/pref-keys";
import { getPref } from "@/utils/settings-storages/global-settings-storage"; import { getPref } from "@/utils/settings-storages/global-settings-storage";
const LOG_TAG = 'WebGL2Player';
export class WebGL2Player { export class WebGL2Player {
#$video: HTMLVideoElement; private readonly LOG_TAG = 'WebGL2Player';
#$canvas: HTMLCanvasElement;
#gl: WebGL2RenderingContext | null = null; private $video: HTMLVideoElement;
#resources: Array<any> = []; private $canvas: HTMLCanvasElement;
#program: WebGLProgram | null = null;
#stopped: boolean = false; private gl: WebGL2RenderingContext | null = null;
private resources: Array<any> = [];
private program: WebGLProgram | null = null;
#options = { private stopped: boolean = false;
private options = {
filterId: 1, filterId: 1,
sharpenFactor: 0, sharpenFactor: 0,
brightness: 0.0, brightness: 0.0,
@ -25,112 +25,125 @@ export class WebGL2Player {
saturation: 0.0, saturation: 0.0,
}; };
#animFrameId: number | null = null; private targetFps = 60;
private frameInterval = Math.ceil(1000 / this.targetFps);
private lastFrameTime = 0;
private animFrameId: number | null = null;
constructor($video: HTMLVideoElement) { constructor($video: HTMLVideoElement) {
BxLogger.info(LOG_TAG, 'Initialize'); BxLogger.info(this.LOG_TAG, 'Initialize');
this.#$video = $video; this.$video = $video;
const $canvas = document.createElement('canvas'); const $canvas = document.createElement('canvas');
$canvas.width = $video.videoWidth; $canvas.width = $video.videoWidth;
$canvas.height = $video.videoHeight; $canvas.height = $video.videoHeight;
this.#$canvas = $canvas; this.$canvas = $canvas;
this.#setupShaders(); this.setupShaders();
this.#setupRendering(); this.setupRendering();
$video.insertAdjacentElement('afterend', $canvas); $video.insertAdjacentElement('afterend', $canvas);
} }
setFilter(filterId: number, update = true) { setFilter(filterId: number, update = true) {
this.#options.filterId = filterId; this.options.filterId = filterId;
update && this.updateCanvas(); update && this.updateCanvas();
} }
setSharpness(sharpness: number, update = true) { setSharpness(sharpness: number, update = true) {
this.#options.sharpenFactor = sharpness; this.options.sharpenFactor = sharpness;
update && this.updateCanvas(); update && this.updateCanvas();
} }
setBrightness(brightness: number, update = true) { setBrightness(brightness: number, update = true) {
this.#options.brightness = (brightness - 100) / 100; this.options.brightness = 1 + (brightness - 100) / 100;
update && this.updateCanvas(); update && this.updateCanvas();
} }
setContrast(contrast: number, update = true) { setContrast(contrast: number, update = true) {
this.#options.contrast = (contrast - 100) / 100; this.options.contrast = 1 + (contrast - 100) / 100;
update && this.updateCanvas(); update && this.updateCanvas();
} }
setSaturation(saturation: number, update = true) { setSaturation(saturation: number, update = true) {
this.#options.saturation = (saturation - 100) / 100; this.options.saturation = 1 + (saturation - 100) / 100;
update && this.updateCanvas(); update && this.updateCanvas();
} }
setTargetFps(target: number) {
this.targetFps = target;
this.frameInterval = Math.ceil(1000 / target);
}
getCanvas() { getCanvas() {
return this.#$canvas; return this.$canvas;
} }
updateCanvas() { updateCanvas() {
const gl = this.#gl!; const gl = this.gl!;
const program = this.#program!; const program = this.program!;
gl.uniform2f(gl.getUniformLocation(program, 'iResolution'), this.#$canvas.width, this.#$canvas.height); gl.uniform2f(gl.getUniformLocation(program, 'iResolution'), this.$canvas.width, this.$canvas.height);
gl.uniform1i(gl.getUniformLocation(program, 'filterId'), this.#options.filterId); gl.uniform1i(gl.getUniformLocation(program, 'filterId'), this.options.filterId);
gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.#options.sharpenFactor); gl.uniform1f(gl.getUniformLocation(program, 'sharpenFactor'), this.options.sharpenFactor);
gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.#options.brightness); gl.uniform1f(gl.getUniformLocation(program, 'brightness'), this.options.brightness);
gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.#options.contrast); gl.uniform1f(gl.getUniformLocation(program, 'contrast'), this.options.contrast);
gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.#options.saturation); gl.uniform1f(gl.getUniformLocation(program, 'saturation'), this.options.saturation);
} }
drawFrame() { drawFrame() {
const gl = this.#gl!; // Limit FPS
const $video = this.#$video; if (this.targetFps < 60) {
const currentTime = performance.now();
const timeSinceLastFrame = currentTime - this.lastFrameTime;
if (timeSinceLastFrame < this.frameInterval) {
return;
}
this.lastFrameTime = currentTime;
}
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, $video); const gl = this.gl!;
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, this.$video);
gl.drawArrays(gl.TRIANGLES, 0, 6); gl.drawArrays(gl.TRIANGLES, 0, 6);
} }
#setupRendering() { private setupRendering() {
let animate: any; let animate: any;
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) { if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
const $video = this.#$video; const $video = this.$video;
animate = () => { animate = () => {
if (this.#stopped) { if (!this.stopped) {
return; this.drawFrame();
this.animFrameId = $video.requestVideoFrameCallback(animate);
} }
this.drawFrame();
this.#animFrameId = $video.requestVideoFrameCallback(animate);
} }
this.#animFrameId = $video.requestVideoFrameCallback(animate); this.animFrameId = $video.requestVideoFrameCallback(animate);
} else { } else {
animate = () => { animate = () => {
if (this.#stopped) { if (!this.stopped) {
return; this.drawFrame();
this.animFrameId = requestAnimationFrame(animate);
} }
this.drawFrame();
this.#animFrameId = requestAnimationFrame(animate);
} }
this.#animFrameId = requestAnimationFrame(animate); this.animFrameId = requestAnimationFrame(animate);
} }
} }
#setupShaders() { private setupShaders() {
BxLogger.info(LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE)); BxLogger.info(this.LOG_TAG, 'Setting up', getPref(PrefKey.VIDEO_POWER_PREFERENCE));
const gl = this.#$canvas.getContext('webgl', { const gl = this.$canvas.getContext('webgl2', {
isBx: true, isBx: true,
antialias: true, antialias: true,
alpha: false, alpha: false,
powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE), powerPreference: getPref(PrefKey.VIDEO_POWER_PREFERENCE),
}) as WebGL2RenderingContext; }) as WebGL2RenderingContext;
this.#gl = gl; this.gl = gl;
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth); gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferWidth);
@ -145,7 +158,7 @@ export class WebGL2Player {
// Create and link program // Create and link program
const program = gl.createProgram()!; const program = gl.createProgram()!;
this.#program = program; this.program = program;
gl.attachShader(program, vShader); gl.attachShader(program, vShader);
gl.attachShader(program, fShader); gl.attachShader(program, fShader);
@ -162,24 +175,17 @@ export class WebGL2Player {
// Vertices: A screen-filling quad made from two triangles // Vertices: A screen-filling quad made from two triangles
const buffer = gl.createBuffer(); const buffer = gl.createBuffer();
this.#resources.push(buffer); this.resources.push(buffer);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer); gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]), gl.STATIC_DRAW);
-1, -1,
1, -1,
-1, 1,
-1, 1,
1, -1,
1, 1,
]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.enableVertexAttribArray(0);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
// Texture to contain the video data // Texture to contain the video data
const texture = gl.createTexture(); const texture = gl.createTexture();
this.#resources.push(texture); this.resources.push(texture);
gl.bindTexture(gl.TEXTURE_2D, texture); gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true); gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
@ -197,38 +203,38 @@ export class WebGL2Player {
resume() { resume() {
this.stop(); this.stop();
this.#stopped = false; this.stopped = false;
BxLogger.info(LOG_TAG, 'Resume'); BxLogger.info(this.LOG_TAG, 'Resume');
this.#$canvas.classList.remove('bx-gone'); this.$canvas.classList.remove('bx-gone');
this.#setupRendering(); this.setupRendering();
} }
stop() { stop() {
BxLogger.info(LOG_TAG, 'Stop'); BxLogger.info(this.LOG_TAG, 'Stop');
this.#$canvas.classList.add('bx-gone'); this.$canvas.classList.add('bx-gone');
this.#stopped = true; this.stopped = true;
if (this.#animFrameId) { if (this.animFrameId) {
if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) { if ('requestVideoFrameCallback' in HTMLVideoElement.prototype) {
this.#$video.cancelVideoFrameCallback(this.#animFrameId); this.$video.cancelVideoFrameCallback(this.animFrameId);
} else { } else {
cancelAnimationFrame(this.#animFrameId); cancelAnimationFrame(this.animFrameId);
} }
this.#animFrameId = null; this.animFrameId = null;
} }
} }
destroy() { destroy() {
BxLogger.info(LOG_TAG, 'Destroy'); BxLogger.info(this.LOG_TAG, 'Destroy');
this.stop(); this.stop();
const gl = this.#gl; const gl = this.gl;
if (gl) { if (gl) {
gl.getExtension('WEBGL_lose_context')?.loseContext(); gl.getExtension('WEBGL_lose_context')?.loseContext();
for (const resource of this.#resources) { for (const resource of this.resources) {
if (resource instanceof WebGLProgram) { if (resource instanceof WebGLProgram) {
gl.useProgram(null); gl.useProgram(null);
gl.deleteProgram(resource); gl.deleteProgram(resource);
@ -241,14 +247,14 @@ export class WebGL2Player {
} }
} }
this.#gl = null; this.gl = null;
} }
if (this.#$canvas.isConnected) { if (this.$canvas.isConnected) {
this.#$canvas.parentElement?.removeChild(this.#$canvas); this.$canvas.parentElement?.removeChild(this.$canvas);
} }
this.#$canvas.width = 1; this.$canvas.width = 1;
this.#$canvas.height = 1; this.$canvas.height = 1;
} }
} }

View File

@ -0,0 +1,11 @@
export class RendererShortcut {
static toggleVisibility(): boolean {
const $mediaContainer = document.querySelector('#game-stream div[data-testid="media-container"]');
if (!$mediaContainer) {
return true;
}
$mediaContainer.classList.toggle('bx-gone');
return !$mediaContainer.classList.contains('bx-gone');
}
}

View File

@ -17,35 +17,35 @@ export type StreamPlayerOptions = Partial<{
}>; }>;
export class StreamPlayer { export class StreamPlayer {
#$video: HTMLVideoElement; private $video: HTMLVideoElement;
#playerType: StreamPlayerType = StreamPlayerType.VIDEO; private playerType: StreamPlayerType = StreamPlayerType.VIDEO;
#options: StreamPlayerOptions = {}; private options: StreamPlayerOptions = {};
#webGL2Player: WebGL2Player | null = null; private webGL2Player: WebGL2Player | null = null;
#$videoCss: HTMLStyleElement | null = null; private $videoCss: HTMLStyleElement | null = null;
#$usmMatrix: SVGFEConvolveMatrixElement | null = null; private $usmMatrix: SVGFEConvolveMatrixElement | null = null;
constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) { constructor($video: HTMLVideoElement, type: StreamPlayerType, options: StreamPlayerOptions) {
this.#setupVideoElements(); this.setupVideoElements();
this.#$video = $video; this.$video = $video;
this.#options = options || {}; this.options = options || {};
this.setPlayerType(type); this.setPlayerType(type);
} }
#setupVideoElements() { private setupVideoElements() {
this.#$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement; this.$videoCss = document.getElementById('bx-video-css') as HTMLStyleElement;
if (this.#$videoCss) { if (this.$videoCss) {
this.#$usmMatrix = this.#$videoCss.querySelector('#bx-filter-usm-matrix') as any; this.$usmMatrix = this.$videoCss.querySelector('#bx-filter-usm-matrix') as any;
return; return;
} }
const $fragment = document.createDocumentFragment(); const $fragment = document.createDocumentFragment();
this.#$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'}); this.$videoCss = CE<HTMLStyleElement>('style', {id: 'bx-video-css'});
$fragment.appendChild(this.#$videoCss); $fragment.appendChild(this.$videoCss);
// Setup SVG filters // Setup SVG filters
const $svg = CE('svg', { const $svg = CE('svg', {
@ -56,7 +56,7 @@ export class StreamPlayer {
CE('filter', { CE('filter', {
id: 'bx-filter-usm', id: 'bx-filter-usm',
xmlns: 'http://www.w3.org/2000/svg', xmlns: 'http://www.w3.org/2000/svg',
}, this.#$usmMatrix = CE('feConvolveMatrix', { }, this.$usmMatrix = CE('feConvolveMatrix', {
id: 'bx-filter-usm-matrix', id: 'bx-filter-usm-matrix',
order: '3', order: '3',
xmlns: 'http://www.w3.org/2000/svg', xmlns: 'http://www.w3.org/2000/svg',
@ -67,29 +67,29 @@ export class StreamPlayer {
document.documentElement.appendChild($fragment); document.documentElement.appendChild($fragment);
} }
#getVideoPlayerFilterStyle() { private getVideoPlayerFilterStyle() {
const filters = []; const filters = [];
const sharpness = this.#options.sharpness || 0; const sharpness = this.options.sharpness || 0;
if (this.#options.processing === StreamVideoProcessing.USM && sharpness != 0) { if (this.options.processing === StreamVideoProcessing.USM && sharpness != 0) {
const level = (7 - ((sharpness / 2) - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7 const level = (7 - ((sharpness / 2) - 1) * 0.5).toFixed(1); // 5, 5.5, 6, 6.5, 7
const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`; const matrix = `0 -1 0 -1 ${level} -1 0 -1 0`;
this.#$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix); this.$usmMatrix?.setAttributeNS(null, 'kernelMatrix', matrix);
filters.push(`url(#bx-filter-usm)`); filters.push(`url(#bx-filter-usm)`);
} }
const saturation = this.#options.saturation || 100; const saturation = this.options.saturation || 100;
if (saturation != 100) { if (saturation != 100) {
filters.push(`saturate(${saturation}%)`); filters.push(`saturate(${saturation}%)`);
} }
const contrast = this.#options.contrast || 100; const contrast = this.options.contrast || 100;
if (contrast != 100) { if (contrast != 100) {
filters.push(`contrast(${contrast}%)`); filters.push(`contrast(${contrast}%)`);
} }
const brightness = this.#options.brightness || 100; const brightness = this.options.brightness || 100;
if (brightness != 100) { if (brightness != 100) {
filters.push(`brightness(${brightness}%)`); filters.push(`brightness(${brightness}%)`);
} }
@ -97,14 +97,14 @@ export class StreamPlayer {
return filters.join(' '); return filters.join(' ');
} }
#resizePlayer() { private resizePlayer() {
const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO); const PREF_RATIO = getPref(PrefKey.VIDEO_RATIO);
const $video = this.#$video; const $video = this.$video;
const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport; const isNativeTouchGame = STATES.currentStream.titleInfo?.details.hasNativeTouchSupport;
let $webGL2Canvas; let $webGL2Canvas;
if (this.#playerType == StreamPlayerType.WEBGL2) { if (this.playerType == StreamPlayerType.WEBGL2) {
$webGL2Canvas = this.#webGL2Player?.getCanvas()!; $webGL2Canvas = this.webGL2Player?.getCanvas()!;
} }
let targetWidth; let targetWidth;
@ -166,67 +166,67 @@ export class StreamPlayer {
} }
// Update video dimensions // Update video dimensions
if (isNativeTouchGame && this.#playerType == StreamPlayerType.WEBGL2) { if (isNativeTouchGame && this.playerType == StreamPlayerType.WEBGL2) {
window.BX_EXPOSED.streamSession.updateDimensions(); window.BX_EXPOSED.streamSession.updateDimensions();
} }
} }
setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) { setPlayerType(type: StreamPlayerType, refreshPlayer: boolean = false) {
if (this.#playerType !== type) { if (this.playerType !== type) {
// Switch from Video -> WebGL2 // Switch from Video -> WebGL2
if (type === StreamPlayerType.WEBGL2) { if (type === StreamPlayerType.WEBGL2) {
// Initialize WebGL2 player // Initialize WebGL2 player
if (!this.#webGL2Player) { if (!this.webGL2Player) {
this.#webGL2Player = new WebGL2Player(this.#$video); this.webGL2Player = new WebGL2Player(this.$video);
} else { } else {
this.#webGL2Player.resume(); this.webGL2Player.resume();
} }
this.#$videoCss!.textContent = ''; this.$videoCss!.textContent = '';
this.#$video.classList.add('bx-pixel'); this.$video.classList.add('bx-pixel');
} else { } else {
// Cleanup WebGL2 Player // Cleanup WebGL2 Player
this.#webGL2Player?.stop(); this.webGL2Player?.stop();
this.#$video.classList.remove('bx-pixel'); this.$video.classList.remove('bx-pixel');
} }
} }
this.#playerType = type; this.playerType = type;
refreshPlayer && this.refreshPlayer(); refreshPlayer && this.refreshPlayer();
} }
setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) { setOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
this.#options = options; this.options = options;
refreshPlayer && this.refreshPlayer(); refreshPlayer && this.refreshPlayer();
} }
updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) { updateOptions(options: StreamPlayerOptions, refreshPlayer: boolean = false) {
this.#options = Object.assign(this.#options, options); this.options = Object.assign(this.options, options);
refreshPlayer && this.refreshPlayer(); refreshPlayer && this.refreshPlayer();
} }
getPlayerElement(playerType?: StreamPlayerType) { getPlayerElement(playerType?: StreamPlayerType) {
if (typeof playerType === 'undefined') { if (typeof playerType === 'undefined') {
playerType = this.#playerType; playerType = this.playerType;
} }
if (playerType === StreamPlayerType.WEBGL2) { if (playerType === StreamPlayerType.WEBGL2) {
return this.#webGL2Player?.getCanvas(); return this.webGL2Player?.getCanvas();
} }
return this.#$video; return this.$video;
} }
getWebGL2Player() { getWebGL2Player() {
return this.#webGL2Player; return this.webGL2Player;
} }
refreshPlayer() { refreshPlayer() {
if (this.#playerType === StreamPlayerType.WEBGL2) { if (this.playerType === StreamPlayerType.WEBGL2) {
const options = this.#options; const options = this.options;
const webGL2Player = this.#webGL2Player!; const webGL2Player = this.webGL2Player!;
if (options.processing === StreamVideoProcessing.USM) { if (options.processing === StreamVideoProcessing.USM) {
webGL2Player.setFilter(1); webGL2Player.setFilter(1);
@ -241,7 +241,7 @@ export class StreamPlayer {
webGL2Player.setContrast(options.contrast || 100); webGL2Player.setContrast(options.contrast || 100);
webGL2Player.setBrightness(options.brightness || 100); webGL2Player.setBrightness(options.brightness || 100);
} else { } else {
let filters = this.#getVideoPlayerFilterStyle(); let filters = this.getVideoPlayerFilterStyle();
let videoCss = ''; let videoCss = '';
if (filters) { if (filters) {
videoCss += `filter: ${filters} !important;`; videoCss += `filter: ${filters} !important;`;
@ -257,26 +257,26 @@ export class StreamPlayer {
css = `#game-stream video { ${videoCss} }`; css = `#game-stream video { ${videoCss} }`;
} }
this.#$videoCss!.textContent = css; this.$videoCss!.textContent = css;
} }
this.#resizePlayer(); this.resizePlayer();
} }
reloadPlayer() { reloadPlayer() {
this.#cleanUpWebGL2Player(); this.cleanUpWebGL2Player();
this.#playerType = StreamPlayerType.VIDEO; this.playerType = StreamPlayerType.VIDEO;
this.setPlayerType(StreamPlayerType.WEBGL2, false); this.setPlayerType(StreamPlayerType.WEBGL2, false);
} }
#cleanUpWebGL2Player() { private cleanUpWebGL2Player() {
// Clean up WebGL2 Player // Clean up WebGL2 Player
this.#webGL2Player?.destroy(); this.webGL2Player?.destroy();
this.#webGL2Player = null; this.webGL2Player = null;
} }
destroy() { destroy() {
this.#cleanUpWebGL2Player(); this.cleanUpWebGL2Player();
} }
} }

View File

@ -345,7 +345,7 @@ export class StreamBadges {
text += server.region; text += server.region;
} }
text += '@' + (server ? 'IPv6' : 'IPv4'); text += '@' + (server.ipv6 ? 'IPv6' : 'IPv4');
this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text); this.badges.server.$element = this.renderBadge(StreamBadge.SERVER, text);
} }
} }

View File

@ -7,9 +7,10 @@ import { getPref, setPref } from "@/utils/settings-storages/global-settings-stor
export function onChangeVideoPlayerType() { export function onChangeVideoPlayerType() {
const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE); const playerType = getPref(PrefKey.VIDEO_PLAYER_TYPE);
const $videoProcessing = document.getElementById('bx_setting_video_processing') as HTMLSelectElement; const $videoProcessing = document.getElementById(`bx_setting_${PrefKey.VIDEO_PROCESSING}`) as HTMLSelectElement;
const $videoSharpness = document.getElementById('bx_setting_video_sharpness') as HTMLElement; const $videoSharpness = document.getElementById(`bx_setting_${PrefKey.VIDEO_SHARPNESS}`) as HTMLElement;
const $videoPowerPreference = document.getElementById('bx_setting_video_power_preference') as HTMLElement; const $videoPowerPreference = document.getElementById(`bx_setting_${PrefKey.VIDEO_POWER_PREFERENCE}`) as HTMLElement;
const $videoMaxFps = document.getElementById(`bx_setting_${PrefKey.VIDEO_MAX_FPS}`) as HTMLElement;
if (!$videoProcessing) { if (!$videoProcessing) {
return; return;
@ -38,17 +39,27 @@ export function onChangeVideoPlayerType() {
// Hide Power Preference setting if renderer isn't WebGL2 // Hide Power Preference setting if renderer isn't WebGL2
$videoPowerPreference.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2); $videoPowerPreference.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
$videoMaxFps.closest('.bx-settings-row')!.classList.toggle('bx-gone', playerType !== StreamPlayerType.WEBGL2);
updateVideoPlayer(); updateVideoPlayer();
} }
export function limitVideoPlayerFps() {
const targetFps = getPref(PrefKey.VIDEO_MAX_FPS);
const streamPlayer = STATES.currentStream.streamPlayer;
streamPlayer?.getWebGL2Player()?.setTargetFps(targetFps);
}
export function updateVideoPlayer() { export function updateVideoPlayer() {
const streamPlayer = STATES.currentStream.streamPlayer; const streamPlayer = STATES.currentStream.streamPlayer;
if (!streamPlayer) { if (!streamPlayer) {
return; return;
} }
limitVideoPlayerFps();
const options = { const options = {
processing: getPref(PrefKey.VIDEO_PROCESSING), processing: getPref(PrefKey.VIDEO_PROCESSING),
sharpness: getPref(PrefKey.VIDEO_SHARPNESS), sharpness: getPref(PrefKey.VIDEO_SHARPNESS),
@ -60,6 +71,7 @@ export function updateVideoPlayer() {
streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE)); streamPlayer.setPlayerType(getPref(PrefKey.VIDEO_PLAYER_TYPE));
streamPlayer.updateOptions(options); streamPlayer.updateOptions(options);
streamPlayer.refreshPlayer(); streamPlayer.refreshPlayer();
} }
window.addEventListener('resize', updateVideoPlayer); window.addEventListener('resize', updateVideoPlayer);

View File

@ -37,6 +37,10 @@ export class StreamStats {
name: t('stat-ping'), name: t('stat-ping'),
$element: CE('span'), $element: CE('span'),
}, },
[StreamStat.JITTER]: {
name: t('jitter'),
$element: CE('span'),
},
[StreamStat.FPS]: { [StreamStat.FPS]: {
name: t('stat-fps'), name: t('stat-fps'),
$element: CE('span'), $element: CE('span'),
@ -179,10 +183,8 @@ export class StreamStats {
$element.textContent = value.toString(); $element.textContent = value.toString();
// Get stat's grade // Get stat's grade
if (PREF_STATS_CONDITIONAL_FORMATTING) { if (PREF_STATS_CONDITIONAL_FORMATTING && 'grades' in value) {
if (statKey === StreamStat.PING || statKey === StreamStat.DECODE_TIME) { grade = statsCollector.calculateGrade(value.current, value.grades);
grade = (value as any).calculateGrade();
}
} }
if ($element.dataset.grade !== grade) { if ($element.dataset.grade !== grade) {

View File

@ -85,16 +85,24 @@ export class TouchController {
document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.remove('bx-offscreen'); document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.remove('bx-offscreen');
} }
/*
static #hide() { static #hide() {
document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.add('bx-offscreen'); document.querySelector('#BabylonCanvasContainer-main')?.parentElement?.classList.add('bx-offscreen');
} }
*/
static toggleVisibility(status: boolean) { static toggleVisibility(): boolean {
if (!TouchController.#dataChannel) { if (!TouchController.#dataChannel) {
return; return false;
} }
status ? TouchController.#hide() : TouchController.#show(); const $container = document.querySelector('#BabylonCanvasContainer-main')?.parentElement;
if (!$container) {
return false;
}
$container.classList.toggle('bx-offscreen');
return !$container.classList.contains('bx-offscreen');
} }
static reset() { static reset() {

View File

@ -1,6 +1,6 @@
import { isFullVersion } from "@macros/build" with {type: "macro"}; import { isFullVersion } from "@macros/build" with {type: "macro"};
import { onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils"; import { limitVideoPlayerFps, onChangeVideoPlayerType, updateVideoPlayer } from "@/modules/stream/stream-settings-utils";
import { ButtonStyle, CE, createButton, createSvgIcon, removeChildElements, type BxButton } from "@/utils/html"; import { ButtonStyle, CE, createButton, createSvgIcon, removeChildElements, type BxButton } from "@/utils/html";
import { NavigationDialog, NavigationDirection } from "./navigation-dialog"; import { NavigationDialog, NavigationDirection } from "./navigation-dialog";
import { ControllerShortcut } from "@/modules/controller-shortcut"; import { ControllerShortcut } from "@/modules/controller-shortcut";
@ -407,6 +407,9 @@ export class SettingsNavigationDialog extends NavigationDialog {
items: [{ items: [{
pref: PrefKey.VIDEO_PLAYER_TYPE, pref: PrefKey.VIDEO_PLAYER_TYPE,
onChange: onChangeVideoPlayerType, onChange: onChangeVideoPlayerType,
}, {
pref: PrefKey.VIDEO_MAX_FPS,
onChange: limitVideoPlayerFps,
}, { }, {
pref: PrefKey.VIDEO_POWER_PREFERENCE, pref: PrefKey.VIDEO_POWER_PREFERENCE,
onChange: () => { onChange: () => {

View File

@ -7,6 +7,8 @@ import iconCopy from "@assets/svg/copy.svg" with { type: "text" };
import iconCreateShortcut from "@assets/svg/create-shortcut.svg" with { type: "text" }; import iconCreateShortcut from "@assets/svg/create-shortcut.svg" with { type: "text" };
import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" }; import iconCursorText from "@assets/svg/cursor-text.svg" with { type: "text" };
import iconDisplay from "@assets/svg/display.svg" with { type: "text" }; import iconDisplay from "@assets/svg/display.svg" with { type: "text" };
import iconEye from "@assets/svg/eye.svg" with { type: "text" };
import iconEyeSlash from "@assets/svg/eye-slash.svg" with { type: "text" };
import iconHome from "@assets/svg/home.svg" with { type: "text" }; import iconHome from "@assets/svg/home.svg" with { type: "text" };
import iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" }; import iconNativeMkb from "@assets/svg/native-mkb.svg" with { type: "text" };
import iconNew from "@assets/svg/new.svg" with { type: "text" }; import iconNew from "@assets/svg/new.svg" with { type: "text" };
@ -48,6 +50,8 @@ export const BxIcon = {
CONTROLLER: iconController, CONTROLLER: iconController,
CREATE_SHORTCUT: iconCreateShortcut, CREATE_SHORTCUT: iconCreateShortcut,
DISPLAY: iconDisplay, DISPLAY: iconDisplay,
EYE: iconEye,
EYE_SLASH: iconEyeSlash,
HOME: iconHome, HOME: iconHome,
NATIVE_MKB: iconNativeMkb, NATIVE_MKB: iconNativeMkb,
NEW: iconNew, NEW: iconNew,

View File

@ -5,22 +5,12 @@ const enum TextColor {
} }
export class BxLogger { export class BxLogger {
static #PREFIX = '[BxC]'; static info = (tag: string, ...args: any[]) => BxLogger.log(TextColor.INFO, tag, ...args);
static warning = (tag: string, ...args: any[]) => BxLogger.log(TextColor.WARNING, tag, ...args);
static error = (tag: string, ...args: any[]) => BxLogger.log(TextColor.ERROR, tag, ...args);
static info(tag: string, ...args: any[]) { private static log(color: string, tag: string, ...args: any) {
BxLogger.#log(TextColor.INFO, tag, ...args); console.log(`%c[BxC]`, `color:${color};font-weight:bold;`, tag, '//', ...args);
}
static warning(tag: string, ...args: any[]) {
BxLogger.#log(TextColor.WARNING, tag, ...args);
}
static error(tag: string, ...args: any[]) {
BxLogger.#log(TextColor.ERROR, tag, ...args);
}
static #log(color: string, tag: string, ...args: any) {
console.log(`%c${BxLogger.#PREFIX}`, `color:${color};font-weight:bold;`, tag, '//', ...args);
} }
} }

View File

@ -616,6 +616,21 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
highest: 'low-power', highest: 'low-power',
}, },
}, },
[PrefKey.VIDEO_MAX_FPS]: {
label: t('max-fps'),
type: SettingElementType.NUMBER_STEPPER,
default: 60,
min: 10,
max: 60,
steps: 10,
params: {
exactTicks: 10,
customTextValue: (value: any) => {
value = parseInt(value);
return value === 60 ? t('unlimited') : value + 'fps';
},
},
},
[PrefKey.VIDEO_SHARPNESS]: { [PrefKey.VIDEO_SHARPNESS]: {
label: t('sharpness'), label: t('sharpness'),
type: SettingElementType.NUMBER_STEPPER, type: SettingElementType.NUMBER_STEPPER,
@ -631,7 +646,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
}, },
suggest: { suggest: {
lowest: 0, lowest: 0,
highest: 4, highest: 2,
}, },
}, },
[PrefKey.VIDEO_RATIO]: { [PrefKey.VIDEO_RATIO]: {
@ -714,6 +729,7 @@ export class GlobalSettingsStorage extends BaseSettingsStorage {
[StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`, [StreamStat.PLAYTIME]: `${StreamStat.PLAYTIME.toUpperCase()}: ${t('playtime')}`,
[StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`, [StreamStat.BATTERY]: `${StreamStat.BATTERY.toUpperCase()}: ${t('battery')}`,
[StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`, [StreamStat.PING]: `${StreamStat.PING.toUpperCase()}: ${t('stat-ping')}`,
[StreamStat.JITTER]: `${StreamStat.JITTER.toUpperCase()}: ${t('jitter')}`,
[StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`, [StreamStat.FPS]: `${StreamStat.FPS.toUpperCase()}: ${t('stat-fps')}`,
[StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`, [StreamStat.BITRATE]: `${StreamStat.BITRATE.toUpperCase()}: ${t('stat-bitrate')}`,
[StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`, [StreamStat.DECODE_TIME]: `${StreamStat.DECODE_TIME.toUpperCase()}: ${t('stat-decode-time')}`,

View File

@ -4,6 +4,7 @@ import { humanFileSize, secondsToHm } from "./html";
export enum StreamStat { export enum StreamStat {
PING = 'ping', PING = 'ping',
JITTER = 'jit',
FPS = 'fps', FPS = 'fps',
BITRATE = 'btr', BITRATE = 'btr',
DECODE_TIME = 'dt', DECODE_TIME = 'dt',
@ -21,7 +22,13 @@ export type StreamStatGrade = '' | 'bad' | 'ok' | 'good';
type CurrentStats = { type CurrentStats = {
[StreamStat.PING]: { [StreamStat.PING]: {
current: number; current: number;
calculateGrade: () => StreamStatGrade; grades: [number, number, number];
toString: () => string;
};
[StreamStat.JITTER]: {
current: number;
grades: [number, number, number];
toString: () => string; toString: () => string;
}; };
@ -50,7 +57,7 @@ type CurrentStats = {
[StreamStat.DECODE_TIME]: { [StreamStat.DECODE_TIME]: {
current: number; current: number;
total: number; total: number;
calculateGrade: () => StreamStatGrade; grades: [number, number, number];
toString: () => string; toString: () => string;
}; };
@ -96,17 +103,27 @@ export class StreamStatsCollector {
// Collect in background - 60 seconds // Collect in background - 60 seconds
static readonly INTERVAL_BACKGROUND = 60 * 1000; static readonly INTERVAL_BACKGROUND = 60 * 1000;
public calculateGrade(value: number, grades: [number, number, number]): StreamStatGrade {
return (value > grades[2]) ? 'bad' : (value > grades[1]) ? 'ok' : (value > grades[0]) ? 'good' : '';
}
private currentStats: CurrentStats = { private currentStats: CurrentStats = {
[StreamStat.PING]: { [StreamStat.PING]: {
current: -1, current: -1,
calculateGrade() { grades: [40, 75, 100],
return (this.current >= 100) ? 'bad' : (this.current > 75) ? 'ok' : (this.current > 40) ? 'good' : '';
},
toString() { toString() {
return this.current === -1 ? '???' : this.current.toString(); return this.current === -1 ? '???' : this.current.toString();
}, },
}, },
[StreamStat.JITTER]: {
current: 0,
grades: [30, 40, 60],
toString() {
return `${this.current.toFixed(2)}ms`;
},
},
[StreamStat.FPS]: { [StreamStat.FPS]: {
current: 0, current: 0,
toString() { toString() {
@ -142,9 +159,7 @@ export class StreamStatsCollector {
[StreamStat.DECODE_TIME]: { [StreamStat.DECODE_TIME]: {
current: 0, current: 0,
total: 0, total: 0,
calculateGrade() { grades: [6, 9, 12],
return (this.current > 12) ? 'bad' : (this.current > 9) ? 'ok' : (this.current > 6) ? 'good' : '';
},
toString() { toString() {
return isNaN(this.current) ? '??ms' : `${this.current.toFixed(2)}ms`; return isNaN(this.current) ? '??ms' : `${this.current.toFixed(2)}ms`;
}, },
@ -200,12 +215,15 @@ export class StreamStatsCollector {
}, },
}; };
private lastVideoStat?: RTCBasicStat | null; private lastVideoStat?: RTCInboundRtpStreamStats | null;
async collect() { async collect() {
const stats = await STATES.currentStream.peerConnection?.getStats(); const stats = await STATES.currentStream.peerConnection?.getStats();
if (!stats) {
return;
}
stats?.forEach(stat => { stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') { if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
// FPS // FPS
const fps = this.currentStats[StreamStat.FPS]; const fps = this.currentStats[StreamStat.FPS];
@ -229,15 +247,23 @@ export class StreamStatsCollector {
const lastStat = this.lastVideoStat; const lastStat = this.lastVideoStat;
// Jitter
const jit = this.currentStats[StreamStat.JITTER];
const bufferDelayDiff = (stat as RTCInboundRtpStreamStats).jitterBufferDelay! - lastStat.jitterBufferDelay!;
const emittedCountDiff = (stat as RTCInboundRtpStreamStats).jitterBufferEmittedCount! - lastStat.jitterBufferEmittedCount!;
if (emittedCountDiff > 0) {
jit.current = bufferDelayDiff / emittedCountDiff * 1000;
}
// Bitrate // Bitrate
const btr = this.currentStats[StreamStat.BITRATE]; const btr = this.currentStats[StreamStat.BITRATE];
const timeDiff = stat.timestamp - lastStat.timestamp; const timeDiff = stat.timestamp - lastStat.timestamp;
btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived) / timeDiff / 1000; btr.current = 8 * (stat.bytesReceived - lastStat.bytesReceived!) / timeDiff / 1000;
// Decode time // Decode time
const dt = this.currentStats[StreamStat.DECODE_TIME]; const dt = this.currentStats[StreamStat.DECODE_TIME];
dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime; dt.total = stat.totalDecodeTime - lastStat.totalDecodeTime!;
const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded; const framesDecodedDiff = stat.framesDecoded - lastStat.framesDecoded!;
dt.current = dt.total / framesDecodedDiff * 1000; dt.current = dt.total / framesDecodedDiff * 1000;
this.lastVideoStat = stat; this.lastVideoStat = stat;

View File

@ -6,14 +6,14 @@ type ToastOptions = {
} }
export class Toast { export class Toast {
static #$wrapper: HTMLElement; private static $wrapper: HTMLElement;
static #$msg: HTMLElement; private static $msg: HTMLElement;
static #$status: HTMLElement; private static $status: HTMLElement;
static #stack: Array<[string, string, ToastOptions]> = []; private static stack: Array<[string, string, ToastOptions]> = [];
static #isShowing = false; private static isShowing = false;
static #timeout?: number | null; private static timeout?: number | null;
static #DURATION = 3000; private static DURATION = 3000;
static show(msg: string, status?: string, options: Partial<ToastOptions> = {}) { static show(msg: string, status?: string, options: Partial<ToastOptions> = {}) {
options = options || {}; options = options || {};
@ -21,69 +21,70 @@ export class Toast {
const args = Array.from(arguments) as [string, string, ToastOptions]; const args = Array.from(arguments) as [string, string, ToastOptions];
if (options.instant) { if (options.instant) {
// Clear stack // Clear stack
Toast.#stack = [args]; Toast.stack = [args];
Toast.#showNext(); Toast.showNext();
} else { } else {
Toast.#stack.push(args); Toast.stack.push(args);
!Toast.#isShowing && Toast.#showNext(); !Toast.isShowing && Toast.showNext();
} }
} }
static #showNext() { private static showNext() {
if (!Toast.#stack.length) { if (!Toast.stack.length) {
Toast.#isShowing = false; Toast.isShowing = false;
return; return;
} }
Toast.#isShowing = true; Toast.isShowing = true;
Toast.#timeout && clearTimeout(Toast.#timeout); Toast.timeout && clearTimeout(Toast.timeout);
Toast.#timeout = window.setTimeout(Toast.#hide, Toast.#DURATION); Toast.timeout = window.setTimeout(Toast.hide, Toast.DURATION);
// Get values from item // Get values from item
const [msg, status, options] = Toast.#stack.shift()!; const [msg, status, options] = Toast.stack.shift()!;
if (options && options.html) { if (options && options.html) {
Toast.#$msg.innerHTML = msg; Toast.$msg.innerHTML = msg;
} else { } else {
Toast.#$msg.textContent = msg; Toast.$msg.textContent = msg;
} }
if (status) { if (status) {
Toast.#$status.classList.remove('bx-gone'); Toast.$status.classList.remove('bx-gone');
Toast.#$status.textContent = status; Toast.$status.textContent = status;
} else { } else {
Toast.#$status.classList.add('bx-gone'); Toast.$status.classList.add('bx-gone');
} }
const classList = Toast.#$wrapper.classList; const classList = Toast.$wrapper.classList;
classList.remove('bx-offscreen', 'bx-hide'); classList.remove('bx-offscreen', 'bx-hide');
classList.add('bx-show'); classList.add('bx-show');
} }
static #hide() { private static hide() {
Toast.#timeout = null; Toast.timeout = null;
const classList = Toast.#$wrapper.classList; const classList = Toast.$wrapper.classList;
classList.remove('bx-show'); classList.remove('bx-show');
classList.add('bx-hide'); classList.add('bx-hide');
} }
static setup() { static setup() {
Toast.#$wrapper = CE('div', {'class': 'bx-toast bx-offscreen'}, Toast.$wrapper = CE('div', {'class': 'bx-toast bx-offscreen'},
Toast.#$msg = CE('span', {'class': 'bx-toast-msg'}), Toast.$msg = CE('span', {'class': 'bx-toast-msg'}),
Toast.#$status = CE('span', {'class': 'bx-toast-status'})); Toast.$status = CE('span', {'class': 'bx-toast-status'}),
);
Toast.#$wrapper.addEventListener('transitionend', e => { Toast.$wrapper.addEventListener('transitionend', e => {
const classList = Toast.#$wrapper.classList; const classList = Toast.$wrapper.classList;
if (classList.contains('bx-hide')) { if (classList.contains('bx-hide')) {
classList.remove('bx-offscreen', 'bx-hide'); classList.remove('bx-offscreen', 'bx-hide');
classList.add('bx-offscreen'); classList.add('bx-offscreen');
Toast.#showNext(); Toast.showNext();
} }
}); });
document.documentElement.appendChild(Toast.#$wrapper); document.documentElement.appendChild(Toast.$wrapper);
} }
} }

View File

@ -131,6 +131,7 @@ const Texts = {
"increase": "Increase", "increase": "Increase",
"install-android": "Better xCloud app for Android", "install-android": "Better xCloud app for Android",
"japan": "Japan", "japan": "Japan",
"jitter": "Jitter",
"keyboard-shortcuts": "Keyboard shortcuts", "keyboard-shortcuts": "Keyboard shortcuts",
"korea": "Korea", "korea": "Korea",
"language": "Language", "language": "Language",
@ -142,6 +143,7 @@ const Texts = {
"local-co-op": "Local co-op", "local-co-op": "Local co-op",
"lowest-quality": "Lowest quality", "lowest-quality": "Lowest quality",
"map-mouse-to": "Map mouse to", "map-mouse-to": "Map mouse to",
"max-fps": "Max FPS",
"may-not-work-properly": "May not work properly!", "may-not-work-properly": "May not work properly!",
"menu": "Menu", "menu": "Menu",
"microphone": "Microphone", "microphone": "Microphone",

View File

@ -3,7 +3,6 @@ import { STATES } from "./global";
export class XcloudApi { export class XcloudApi {
private static instance: XcloudApi; private static instance: XcloudApi;
public static getInstance(): XcloudApi { public static getInstance(): XcloudApi {
if (!XcloudApi.instance) { if (!XcloudApi.instance) {
XcloudApi.instance = new XcloudApi(); XcloudApi.instance = new XcloudApi();
@ -12,12 +11,12 @@ export class XcloudApi {
return XcloudApi.instance; return XcloudApi.instance;
} }
#CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {}; private CACHE_TITLES: {[key: string]: XcloudTitleInfo} = {};
#CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {}; private CACHE_WAIT_TIME: {[key: string]: XcloudWaitTimeInfo} = {};
async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> { async getTitleInfo(id: string): Promise<XcloudTitleInfo | null> {
if (id in this.#CACHE_TITLES) { if (id in this.CACHE_TITLES) {
return this.#CACHE_TITLES[id]; return this.CACHE_TITLES[id];
} }
const baseUri = STATES.selectedRegion.baseUri; const baseUri = STATES.selectedRegion.baseUri;
@ -45,13 +44,13 @@ export class XcloudApi {
} catch (e) { } catch (e) {
json = {} json = {}
} }
this.#CACHE_TITLES[id] = json; this.CACHE_TITLES[id] = json;
return json; return json;
} }
async getWaitTime(id: string): Promise<XcloudWaitTimeInfo | null> { async getWaitTime(id: string): Promise<XcloudWaitTimeInfo | null> {
if (id in this.#CACHE_WAIT_TIME) { if (id in this.CACHE_WAIT_TIME) {
return this.#CACHE_WAIT_TIME[id]; return this.CACHE_WAIT_TIME[id];
} }
const baseUri = STATES.selectedRegion.baseUri; const baseUri = STATES.selectedRegion.baseUri;
@ -73,7 +72,7 @@ export class XcloudApi {
json = {}; json = {};
} }
this.#CACHE_WAIT_TIME[id] = json; this.CACHE_WAIT_TIME[id] = json;
return json; return json;
} }
} }