Support emulated MKB in Android app

commit ad365d4ee854971122f0e8cb9157ed44b3aac0d8
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 17:19:57 2024 +0700

    Fix not able to reconnect to WebSocket server when switching game

commit ca9369318d4cbb831650e8ca631e7997dc7706cb
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 17:19:23 2024 +0700

    Stop emulated MKB when losing pointer capture

commit 8cca1a0554c46b8f61455e79d5b16f1dff9a8014
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 17:17:42 2024 +0700

    Allow fine-tuning maximum video bitrate

commit 763d414d560d9d2aa6710fd60e3f80bf43a534d6
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 08:13:56 2024 +0700

    Update mouse settings

commit d65c5ab4e4a33ed8ad13acf0a15c4bb5ace870eb
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 08:10:49 2024 +0700

    Increase MKB dialog's bg opacity

commit 3e72f2ad2700737c8148ef47629528954a606578
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 08:02:57 2024 +0700

    Show/hide MKB dialog properly

commit e7786f36508e3aa843604d9886861930bada5d60
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 07:47:21 2024 +0700

    Fix connecting to WebSocket server when it's not ready

commit 512d8c227a057e5c0399bf128bc1c52a88fcf853
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Wed May 29 07:18:06 2024 +0700

    Fix arrow keys not working in Android app

commit 0ce90f47f37d057d5a4fab0003e2bec8960d1eee
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 17:36:56 2024 +0700

    Set mouse's default sensitivities to 50

commit 16eb48660dd44497e16ca22343a880d9a2e53a30
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 17:33:37 2024 +0700

    Allow emulated MKB feature in Android app

commit c3d0e64f8502e19cd4f167fea4cdbdfc2e14b65e
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 17:32:49 2024 +0700

    Remove stick decay settings

commit d289d2a0dea61a440c1bc6b9392920b8e6ab6298
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 17:21:39 2024 +0700

    Remove stick decaying feature

commit 76bd001d98bac53f757f4ae793b2850aad055007
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 17:21:14 2024 +0700

    Update data structure

commit c5d3c87da9e6624ebefb288f6d7c8d06dc00916b
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Tue May 28 08:14:27 2024 +0700

    Fix not toggling the MKB feature correctly

commit 9615535cf0e4d4372e201aefb6f1231ddbc22536
Author: redphx <96280+redphx@users.noreply.github.com>
Date:   Mon May 27 20:51:57 2024 +0700

    Handle mouse data from the app
This commit is contained in:
redphx 2024-05-29 17:28:39 +07:00
parent 228c2ad008
commit 0f48cb891f
16 changed files with 449 additions and 229 deletions

View File

@ -25,7 +25,7 @@
top: 50%;
transform: translateX(-50%) translateY(-50%);
margin: auto;
background: #000000e5;
background: #000000b3;
z-index: var(--bx-mkb-pointer-lock-msg-z-index);
color: #fff;
text-align: center;

View File

@ -282,6 +282,9 @@ function main() {
if (getPref(PrefKey.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.setup();
}
// Start PointerProviderServer
(getPref(PrefKey.MKB_ENABLED)) && AppInterface && AppInterface.startPointerServer();
}
main();

View File

@ -82,6 +82,18 @@ export class GameBar {
document.documentElement.appendChild($gameBar);
this.$gameBar = $gameBar;
this.$container = $container;
// Enable/disable Game Bar when playing/pausing
getPref(PrefKey.GAME_BAR_POSITION) !== 'off' && window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, ((e: Event) => {
if (!STATES.isPlaying) {
this.disable();
return;
}
// Toggle Game bar
const mode = (e as any).mode;
mode !== 'None' ? this.disable() : this.enable();
}).bind(this));
}
private beginHideTimeout() {

View File

@ -99,7 +99,4 @@ export enum MkbPresetKey {
MOUSE_SENSITIVITY_Y = 'sensitivity_y',
MOUSE_DEADZONE_COUNTERWEIGHT = 'deadzone_counterweight',
MOUSE_STICK_DECAY_STRENGTH = 'stick_decay_strength',
MOUSE_STICK_DECAY_MIN = 'stick_decay_min',
}

View File

@ -20,7 +20,7 @@ export class KeyHelper {
let name;
if (e instanceof KeyboardEvent) {
code = e.code;
code = e.code || e.key;
} else if (e instanceof WheelEvent) {
if (e.deltaY < 0) {
code = WheelCode.SCROLL_UP;
@ -28,7 +28,7 @@ export class KeyHelper {
code = WheelCode.SCROLL_DOWN;
} else if (e.deltaX < 0) {
code = WheelCode.SCROLL_LEFT;
} else {
} else if (e.deltaX > 0) {
code = WheelCode.SCROLL_RIGHT;
}
} else if (e instanceof MouseEvent) {

View File

@ -9,13 +9,152 @@ import { LocalDb } from "@utils/local-db";
import { KeyHelper } from "./key-helper";
import type { MkbStoredPreset } from "@/types/mkb";
import { showStreamSettings } from "@modules/stream/stream-ui";
import { STATES } from "@utils/global";
import { AppInterface, STATES } from "@utils/global";
import { UserAgent } from "@utils/user-agent";
import { BxLogger } from "@utils/bx-logger";
import { BxIcon } from "@utils/bx-icon";
import { PointerClient } from "./pointer-client";
const LOG_TAG = 'MkbHandler';
abstract class MouseDataProvider {
protected mkbHandler: MkbHandler;
constructor(handler: MkbHandler) {
this.mkbHandler = handler;
}
abstract init(): void;
abstract start(): void;
abstract stop(): void;
abstract destroy(): void;
abstract toggle(enabled: boolean): void;
}
class WebSocketMouseDataProvider extends MouseDataProvider {
#pointerClient: PointerClient | undefined
#connected = false
init(): void {
this.#pointerClient = PointerClient.getInstance();
this.#connected = false;
try {
this.#pointerClient.start(this.mkbHandler);
this.#connected = true;
} catch (e) {
Toast.show('Cannot enable Mouse & Keyboard feature');
}
}
start(): void {
this.#connected && AppInterface.requestPointerCapture();
}
stop(): void {
this.#connected && AppInterface.releasePointerCapture();
}
destroy(): void {
this.#connected && this.#pointerClient?.stop();
}
toggle(enabled: boolean): void {
if (!this.#connected) {
enabled = false;
}
enabled ? this.mkbHandler.start() : this.mkbHandler.stop();
this.mkbHandler.waitForMouseData(!enabled);
}
}
class PointerLockMouseDataProvider extends MouseDataProvider {
init(): void {
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
document.addEventListener('pointerlockerror', this.#onPointerLockError);
}
start(): void {
if (!document.pointerLockElement) {
document.body.requestPointerLock();
}
window.addEventListener('mousemove', this.#onMouseMoveEvent);
window.addEventListener('mousedown', this.#onMouseEvent);
window.addEventListener('mouseup', this.#onMouseEvent);
window.addEventListener('wheel', this.#onWheelEvent);
window.addEventListener('contextmenu', this.#disableContextMenu);
}
stop(): void {
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
window.removeEventListener('mousedown', this.#onMouseEvent);
window.removeEventListener('mouseup', this.#onMouseEvent);
window.removeEventListener('wheel', this.#onWheelEvent);
window.removeEventListener('contextmenu', this.#disableContextMenu);
}
destroy(): void {
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
}
toggle(enabled: boolean): void {
enabled ? document.pointerLockElement && this.mkbHandler.start() : this.mkbHandler.stop();
if (enabled) {
!document.pointerLockElement && this.mkbHandler.waitForMouseData(true);
} else {
this.mkbHandler.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock();
}
}
#onPointerLockChange = () => {
if (this.mkbHandler.isEnabled() && !document.pointerLockElement) {
this.mkbHandler.stop();
}
}
#onPointerLockError = (e: Event) => {
console.log(e);
this.stop();
}
#onMouseMoveEvent = (e: MouseEvent) => {
this.mkbHandler.handleMouseMove({
movementX: e.movementX,
movementY: e.movementY,
});
}
#onMouseEvent = (e: MouseEvent) => {
e.preventDefault();
const isMouseDown = e.type === 'mousedown';
const key = KeyHelper.getKeyFromEvent(e);
const data: MkbMouseClick = {
key: key,
pressed: isMouseDown
};
this.mkbHandler.handleMouseClick(data);
}
#onWheelEvent = (e: WheelEvent) => {
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
if (this.mkbHandler.handleMouseWheel({key})) {
e.preventDefault();
}
}
#disableContextMenu = (e: Event) => e.preventDefault();
}
/*
This class uses some code from Yuzu emulator to handle mouse's movements
Source: https://github.com/yuzu-emu/yuzu-mainline/blob/master/src/input_common/drivers/mouse.cpp
@ -33,7 +172,6 @@ export class MkbHandler {
#CURRENT_PRESET_DATA = MkbPreset.convert(MkbPreset.DEFAULT_PRESET);
static readonly DEFAULT_PANNING_SENSITIVITY = 0.0010;
static readonly DEFAULT_STICK_SENSITIVITY = 0.0006;
static readonly DEFAULT_DEADZONE_COUNTERWEIGHT = 0.01;
static readonly MAXIMUM_STICK_RANGE = 1.1;
@ -55,13 +193,13 @@ export class MkbHandler {
#nativeGetGamepads = window.navigator.getGamepads.bind(window.navigator);
#enabled = false;
#mouseDataProvider: MouseDataProvider | undefined;
#isPolling = false;
#prevWheelCode = null;
#wheelStoppedTimeout?: number | null;
#detectMouseStoppedTimeout?: number | null;
#allowStickDecaying = false;
#$message?: HTMLElement;
@ -85,6 +223,8 @@ export class MkbHandler {
};
}
isEnabled = () => this.#enabled;
#patchedGetGamepads = () => {
const gamepads = this.#nativeGetGamepads() || [];
(gamepads as any)[this.#VIRTUAL_GAMEPAD.index] = this.#VIRTUAL_GAMEPAD;
@ -102,6 +242,7 @@ export class MkbHandler {
virtualGamepad.timestamp = performance.now();
}
/*
#getStickAxes(stick: GamepadStick) {
const virtualGamepad = this.#getVirtualGamepad();
return {
@ -109,11 +250,10 @@ export class MkbHandler {
y: virtualGamepad.axes[stick * 2 + 1],
};
}
*/
#vectorLength = (x: number, y: number): number => Math.sqrt(x ** 2 + y ** 2);
#disableContextMenu = (e: Event) => e.preventDefault();
#resetGamepad = () => {
const gamepad = this.#getVirtualGamepad();
@ -172,6 +312,10 @@ export class MkbHandler {
e.preventDefault();
this.toggle();
return;
} else if (e.code === 'Escape') {
e.preventDefault();
this.#enabled && this.stop();
return;
}
if (!this.#isPolling) {
@ -179,7 +323,7 @@ export class MkbHandler {
}
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code]!;
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[e.code || e.key]!;
if (typeof buttonIndex === 'undefined') {
return;
}
@ -193,89 +337,29 @@ export class MkbHandler {
this.#pressButton(buttonIndex, isKeyDown);
}
#onMouseEvent = (e: MouseEvent) => {
const isMouseDown = e.type === 'mousedown';
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
if (typeof buttonIndex === 'undefined') {
return;
}
e.preventDefault();
this.#pressButton(buttonIndex, isMouseDown);
}
#onWheelEvent = (e: WheelEvent) => {
const key = KeyHelper.getKeyFromEvent(e);
if (!key) {
return;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[key.code]!;
if (typeof buttonIndex === 'undefined') {
return;
}
e.preventDefault();
if (this.#prevWheelCode === null || this.#prevWheelCode === key.code) {
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
this.#pressButton(buttonIndex, true);
}
this.#wheelStoppedTimeout = window.setTimeout(() => {
this.#prevWheelCode = null;
this.#pressButton(buttonIndex, false);
}, 20);
}
#decayStick = () => {
if (!this.#allowStickDecaying) {
return;
}
#onMouseStopped = () => {
// Reset stick position
this.#detectMouseStoppedTimeout = null;
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
this.#updateStick(analog, 0, 0);
}
handleMouseClick = (data: MkbMouseClick) => {
if (!data || !data.key) {
return;
}
const analog = mouseMapTo === MouseMapTo.LS ? GamepadStick.LEFT : GamepadStick.RIGHT;
let { x, y } = this.#getStickAxes(analog);
const length = this.#vectorLength(x, y);
const clampedLength = Math.min(1.0, length);
const decayStrength = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH];
const decay = 1 - clampedLength * clampedLength * decayStrength;
const minDecay = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN];
const clampedDecay = Math.min(1 - minDecay, decay);
x *= clampedDecay;
y *= clampedDecay;
const deadzoneCounterweight = 20 * MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
if (Math.abs(x) <= deadzoneCounterweight && Math.abs(y) <= deadzoneCounterweight) {
x = 0;
y = 0;
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[data.key.code]!;
if (typeof buttonIndex === 'undefined') {
return;
}
if (this.#allowStickDecaying) {
this.#updateStick(analog, x, y);
(x !== 0 || y !== 0) && requestAnimationFrame(this.#decayStick);
}
this.#pressButton(buttonIndex, data.pressed);
}
#onMouseStopped = () => {
this.#allowStickDecaying = true;
requestAnimationFrame(this.#decayStick);
}
#onMouseMoveEvent = (e: MouseEvent) => {
handleMouseMove = (data: MkbMouseMove) => {
// TODO: optimize this
const mouseMapTo = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_MAP_TO];
if (mouseMapTo === MouseMapTo.OFF) {
@ -283,17 +367,13 @@ export class MkbHandler {
return;
}
this.#allowStickDecaying = false;
this.#detectMouseStoppedTimeout && clearTimeout(this.#detectMouseStoppedTimeout);
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 10);
const deltaX = e.movementX;
const deltaY = e.movementY;
this.#detectMouseStoppedTimeout = window.setTimeout(this.#onMouseStopped.bind(this), 50);
const deadzoneCounterweight = this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT];
let x = deltaX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = deltaY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let x = data.movementX * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_X];
let y = data.movementY * this.#CURRENT_PRESET_DATA.mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y];
let length = this.#vectorLength(x, y);
if (length !== 0 && length < deadzoneCounterweight) {
@ -308,18 +388,33 @@ export class MkbHandler {
this.#updateStick(analog, x, y);
}
handleMouseWheel = (data: MkbMouseWheel): boolean => {
if (!data || !data.key) {
return false;
}
const buttonIndex = this.#CURRENT_PRESET_DATA.mapping[data.key.code]!;
if (typeof buttonIndex === 'undefined') {
return false;
}
if (this.#prevWheelCode === null || this.#prevWheelCode === data.key.code) {
this.#wheelStoppedTimeout && clearTimeout(this.#wheelStoppedTimeout);
this.#pressButton(buttonIndex, true);
}
this.#wheelStoppedTimeout = window.setTimeout(() => {
this.#prevWheelCode = null;
this.#pressButton(buttonIndex, false);
}, 20);
return true;
}
toggle = () => {
this.#enabled = !this.#enabled;
this.#enabled ? document.pointerLockElement && this.start() : this.stop();
Toast.show(t('mouse-and-keyboard'), t(this.#enabled ? 'enabled' : 'disabled'), {instant: true});
if (this.#enabled) {
!document.pointerLockElement && this.#waitForPointerLock(true);
} else {
this.#waitForPointerLock(false);
document.pointerLockElement && document.exitPointerLock();
}
this.#mouseDataProvider?.toggle(this.#enabled);
}
#getCurrentPreset = (): Promise<MkbStoredPreset> => {
@ -338,47 +433,35 @@ export class MkbHandler {
});
}
#onPointerLockChange = () => {
if (this.#enabled && !document.pointerLockElement) {
this.stop();
this.#waitForPointerLock(true);
}
}
#onPointerLockError = (e: Event) => {
console.log(e);
this.stop();
}
#onActivatePointerLock = () => {
if (!document.pointerLockElement) {
document.body.requestPointerLock();
}
this.#waitForPointerLock(false);
this.start();
}
#waitForPointerLock = (wait: boolean) => {
waitForMouseData = (wait: boolean) => {
this.#$message && this.#$message.classList.toggle('bx-gone', !wait);
}
#onStreamMenuShown = () => {
this.#enabled && this.#waitForPointerLock(false);
}
#onPollingModeChanged = (e: Event) => {
if (!this.#$message) {
return;
}
#onStreamMenuHidden = () => {
this.#enabled && this.#waitForPointerLock(true);
const mode = (e as any).mode;
if (mode === 'None') {
this.#$message.classList.remove('bx-offscreen');
} else {
this.#$message.classList.add('bx-offscreen');
}
}
init = () => {
this.refreshPresetData();
this.#enabled = true;
window.addEventListener('keydown', this.#onKeyboardEvent);
if (AppInterface) {
this.#mouseDataProvider = new WebSocketMouseDataProvider(this);
} else {
this.#mouseDataProvider = new PointerLockMouseDataProvider(this);
}
this.#mouseDataProvider.init();
document.addEventListener('pointerlockchange', this.#onPointerLockChange);
document.addEventListener('pointerlockerror', this.#onPointerLockError);
window.addEventListener('keydown', this.#onKeyboardEvent);
this.#$message = CE('div', {'class': 'bx-mkb-pointer-lock-msg bx-gone'},
createButton({
@ -397,13 +480,12 @@ export class MkbHandler {
),
);
this.#$message.addEventListener('click', this.#onActivatePointerLock);
this.#$message.addEventListener('click', this.start.bind(this));
document.documentElement.appendChild(this.#$message);
window.addEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.addEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
window.addEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
this.#waitForPointerLock(true);
this.waitForMouseData(true);
}
destroy = () => {
@ -411,31 +493,31 @@ export class MkbHandler {
this.#enabled = false;
this.stop();
this.#waitForPointerLock(false);
this.waitForMouseData(false);
document.pointerLockElement && document.exitPointerLock();
window.removeEventListener('keydown', this.#onKeyboardEvent);
document.removeEventListener('pointerlockchange', this.#onPointerLockChange);
document.removeEventListener('pointerlockerror', this.#onPointerLockError);
this.#mouseDataProvider?.destroy();
window.removeEventListener(BxEvent.STREAM_MENU_SHOWN, this.#onStreamMenuShown);
window.removeEventListener(BxEvent.STREAM_MENU_HIDDEN, this.#onStreamMenuHidden);
window.removeEventListener(BxEvent.XCLOUD_POLLING_MODE_CHANGED, this.#onPollingModeChanged);
}
start = () => {
if (!this.#enabled) {
this.#enabled = true;
Toast.show(t('mouse-and-keyboard'), t('enabled'), {instant: true});
}
this.#isPolling = true;
window.navigator.getGamepads = this.#patchedGetGamepads;
this.#resetGamepad();
window.navigator.getGamepads = this.#patchedGetGamepads;
this.waitForMouseData(false);
window.addEventListener('keyup', this.#onKeyboardEvent);
window.addEventListener('mousemove', this.#onMouseMoveEvent);
window.addEventListener('mousedown', this.#onMouseEvent);
window.addEventListener('mouseup', this.#onMouseEvent);
window.addEventListener('wheel', this.#onWheelEvent);
window.addEventListener('contextmenu', this.#disableContextMenu);
this.#mouseDataProvider?.start();
// Dispatch "gamepadconnected" event
const virtualGamepad = this.#getVirtualGamepad();
@ -451,6 +533,8 @@ export class MkbHandler {
this.#isPolling = false;
// Dispatch "gamepaddisconnected" event
this.#resetGamepad();
const virtualGamepad = this.#getVirtualGamepad();
virtualGamepad.connected = false;
virtualGamepad.timestamp = performance.now();
@ -461,19 +545,14 @@ export class MkbHandler {
window.navigator.getGamepads = this.#nativeGetGamepads;
this.#resetGamepad();
window.removeEventListener('keyup', this.#onKeyboardEvent);
window.removeEventListener('mousemove', this.#onMouseMoveEvent);
window.removeEventListener('mousedown', this.#onMouseEvent);
window.removeEventListener('mouseup', this.#onMouseEvent);
window.removeEventListener('wheel', this.#onWheelEvent);
window.removeEventListener('contextmenu', this.#disableContextMenu);
this.waitForMouseData(true);
this.#mouseDataProvider?.stop();
}
static setupEvents() {
getPref(PrefKey.MKB_ENABLED) && !UserAgent.isMobile() && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
getPref(PrefKey.MKB_ENABLED) && (AppInterface || !UserAgent.isMobile()) && window.addEventListener(BxEvent.STREAM_PLAYING, () => {
// Enable MKB
if (!STATES.currentStream.titleInfo?.details.hasMkbSupport) {
BxLogger.info(LOG_TAG, 'Emulate MKB');

View File

@ -24,11 +24,11 @@ export class MkbPreset {
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 200,
max: 300,
params: {
suffix: '%',
exactTicks: 20,
exactTicks: 50,
},
},
@ -37,11 +37,11 @@ export class MkbPreset {
type: SettingElementType.NUMBER_STEPPER,
default: 50,
min: 1,
max: 200,
max: 300,
params: {
suffix: '%',
exactTicks: 20,
exactTicks: 50,
},
},
@ -50,38 +50,13 @@ export class MkbPreset {
type: SettingElementType.NUMBER_STEPPER,
default: 20,
min: 1,
max: 100,
max: 50,
params: {
suffix: '%',
exactTicks: 10,
},
},
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: {
label: t('stick-decay-strength'),
type: SettingElementType.NUMBER_STEPPER,
default: 100,
min: 10,
max: 100,
params: {
suffix: '%',
exactTicks: 10,
},
},
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: {
label: t('stick-decay-minimum'),
type: SettingElementType.NUMBER_STEPPER,
default: 10,
min: 1,
max: 10,
params: {
suffix: '%',
},
},
};
static DEFAULT_PRESET: MkbPresetData = {
@ -124,11 +99,9 @@ export class MkbPreset {
'mouse': {
[MkbPresetKey.MOUSE_MAP_TO]: MouseMapTo[MouseMapTo.RS],
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 50,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 50,
[MkbPresetKey.MOUSE_SENSITIVITY_X]: 100,
[MkbPresetKey.MOUSE_SENSITIVITY_Y]: 100,
[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT]: 20,
[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH]: 100,
[MkbPresetKey.MOUSE_STICK_DECAY_MIN]: 10,
},
};
@ -149,8 +122,6 @@ export class MkbPreset {
mouse[MkbPresetKey.MOUSE_SENSITIVITY_X] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_SENSITIVITY_Y] *= MkbHandler.DEFAULT_PANNING_SENSITIVITY;
mouse[MkbPresetKey.MOUSE_DEADZONE_COUNTERWEIGHT] *= MkbHandler.DEFAULT_DEADZONE_COUNTERWEIGHT;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_STRENGTH] *= 0.01;
mouse[MkbPresetKey.MOUSE_STICK_DECAY_MIN] *= 0.01;
const mouseMapTo = MouseMapTo[mouse[MkbPresetKey.MOUSE_MAP_TO]!];
if (typeof mouseMapTo !== 'undefined') {

View File

@ -0,0 +1,152 @@
import { BxLogger } from "@/utils/bx-logger";
import type { MkbHandler } from "./mkb-handler";
import { KeyHelper } from "./key-helper";
import { WheelCode } from "./definitions";
import { Toast } from "@/utils/toast";
const LOG_TAG = 'PointerClient';
enum PointerAction {
MOVE = 1,
BUTTON_PRESS = 2,
BUTTON_RELEASE = 3,
SCROLL = 4,
POINTER_CAPTURE_CHANGED = 5,
}
const FixedMouseIndex = {
1: 0,
2: 2,
4: 1,
}
export class PointerClient {
static #PORT = 9269;
private static instance: PointerClient;
public static getInstance(): PointerClient {
if (!PointerClient.instance) {
PointerClient.instance = new PointerClient();
}
return PointerClient.instance;
}
#socket: WebSocket | undefined | null;
#mkbHandler: MkbHandler | undefined;
start(mkbHandler: MkbHandler) {
this.#mkbHandler = mkbHandler;
// Create WebSocket connection.
this.#socket = new WebSocket(`ws://localhost:${PointerClient.#PORT}`);
this.#socket.binaryType = 'arraybuffer';
// Connection opened
this.#socket.addEventListener('open', (event) => {
BxLogger.info(LOG_TAG, 'connected')
});
// Error
this.#socket.addEventListener('error', (event) => {
BxLogger.error(LOG_TAG, event);
Toast.show('Cannot setup mouse');
});
this.#socket.addEventListener('close', (event) => {
this.#socket = null;
});
// Listen for messages
this.#socket.addEventListener('message', (event) => {
const dataView = new DataView(event.data);
let messageType = dataView.getInt8(0);
let offset = Int8Array.BYTES_PER_ELEMENT;
switch (messageType) {
case PointerAction.MOVE:
this.onMove(dataView, offset);
break;
case PointerAction.BUTTON_PRESS:
case PointerAction.BUTTON_RELEASE:
this.onPress(messageType, dataView, offset);
break;
case PointerAction.SCROLL:
this.onScroll(dataView, offset);
break;
case PointerAction.POINTER_CAPTURE_CHANGED:
this.onPointerCaptureChanged(dataView, offset);
}
});
}
onMove(dataView: DataView, offset: number) {
// [X, Y]
const x = dataView.getInt16(offset);
offset += Int16Array.BYTES_PER_ELEMENT;
const y = dataView.getInt16(offset);
this.#mkbHandler?.handleMouseMove({
movementX: x,
movementY: y,
});
// BxLogger.info(LOG_TAG, 'move', x, y);
}
onPress(messageType: PointerAction, dataView: DataView, offset: number) {
const buttonIndex = dataView.getInt8(offset);
const fixedIndex = FixedMouseIndex[buttonIndex as keyof typeof FixedMouseIndex];
const keyCode = 'Mouse' + fixedIndex;
this.#mkbHandler?.handleMouseClick({
key: {
code: keyCode,
name: KeyHelper.codeToKeyName(keyCode),
},
pressed: messageType === PointerAction.BUTTON_PRESS,
});
// BxLogger.info(LOG_TAG, 'press', buttonIndex);
}
onScroll(dataView: DataView, offset: number) {
// [V_SCROLL, H_SCROLL]
const vScroll = dataView.getInt8(offset);
offset += Int8Array.BYTES_PER_ELEMENT;
const hScroll = dataView.getInt8(offset);
let code = '';
if (vScroll < 0) {
code = WheelCode.SCROLL_UP;
} else if (vScroll > 0) {
code = WheelCode.SCROLL_DOWN;
} else if (hScroll < 0) {
code = WheelCode.SCROLL_LEFT;
} else if (hScroll > 0) {
code = WheelCode.SCROLL_RIGHT;
}
code && this.#mkbHandler?.handleMouseWheel({
key: {
code: code,
name: KeyHelper.codeToKeyName(code),
},
});
// BxLogger.info(LOG_TAG, 'scroll', vScroll, hScroll);
}
onPointerCaptureChanged(dataView: DataView, offset: number) {
const hasCapture = dataView.getInt8(offset) === 1;
!hasCapture && this.#mkbHandler?.stop();
}
stop() {
try {
this.#socket?.close();
} catch (e) {}
}
}

View File

@ -407,7 +407,7 @@ e.guideUI = null;
}
const newCode = `
window.BX_EXPOSED.onPollingModeChanged && window.BX_EXPOSED.onPollingModeChanged(e);
BxEvent.dispatch(window, BxEvent.XCLOUD_POLLING_MODE_CHANGED, {mode: e});
`;
str = str.replace(text, text + newCode);
return str;

View File

@ -105,12 +105,6 @@ export function injectStreamMenuButtons() {
if (!($node as HTMLElement).className || !($node as HTMLElement).className.startsWith) {
return;
}
if (($node as HTMLElement).className.startsWith('StreamMenu')) {
if (!document.querySelector('div[class^=PureInStreamConfirmationModal]')) {
BxEvent.dispatch(window, BxEvent.STREAM_MENU_HIDDEN);
}
}
});
item.addedNodes.forEach(async $node => {
@ -139,8 +133,6 @@ export function injectStreamMenuButtons() {
// Render badges
if ($elm.className?.startsWith('StreamMenu-module__container')) {
BxEvent.dispatch(window, BxEvent.STREAM_MENU_SHOWN);
const $btnCloseHud = document.querySelector('button[class*=StreamMenu-module__backButton]');
if (!$btnCloseHud) {
return;

20
src/types/index.d.ts vendored
View File

@ -74,3 +74,23 @@ type XcloudTitleInfo = {
declare module '*.js';
declare module '*.svg';
declare module '*.styl';
type MkbMouseMove = {
movementX: number;
movementY: number;
}
type MkbMouseClick = {
key: {
code: string;
name: string;
} | null;
pressed: boolean;
}
type MkbMouseWheel = {
key: {
code: string;
name: string;
} | null;
}

View File

@ -6,7 +6,7 @@ export type PreferenceSetting = {
note?: string | HTMLElement;
type?: SettingElementType;
ready?: (setting: PreferenceSetting) => void;
migrate?: (savedPrefs: any, value: any) => {};
migrate?: (this: Preferences, savedPrefs: any, value: any) => void;
min?: number;
max?: number;
steps?: number;

View File

@ -13,9 +13,6 @@ export enum BxEvent {
STREAM_STOPPED = 'bx-stream-stopped',
STREAM_ERROR_PAGE = 'bx-stream-error-page',
STREAM_MENU_SHOWN = 'bx-stream-menu-shown',
STREAM_MENU_HIDDEN = 'bx-stream-menu-hidden',
STREAM_WEBRTC_CONNECTED = 'bx-stream-webrtc-connected',
STREAM_WEBRTC_DISCONNECTED = 'bx-stream-webrtc-disconnected',
@ -41,6 +38,8 @@ export enum BxEvent {
// xCloud Dialog events
XCLOUD_DIALOG_SHOWN = 'bx-xcloud-dialog-shown',
XCLOUD_DIALOG_DISMISSED = 'bx-xcloud-dialog-dismissed',
XCLOUD_POLLING_MODE_CHANGED = 'bx-xcloud-polling-mode-changed',
}
export enum XcloudEvent {

View File

@ -16,23 +16,6 @@ export enum InputType {
}
export const BxExposed = {
// Enable/disable Game Bar when playing/pausing
onPollingModeChanged: (mode: 'All' | 'None') => {
if (getPref(PrefKey.GAME_BAR_POSITION) === 'off') {
return;
}
const gameBar = GameBar.getInstance();
if (!STATES.isPlaying) {
gameBar.disable();
return;
}
// Toggle Game bar
mode !== 'None' ? gameBar.disable() : gameBar.enable();
},
getTitleInfo: () => STATES.currentStream.titleInfo,
modifyTitleInfo: (titleInfo: XcloudTitleInfo): XcloudTitleInfo => {

View File

@ -103,7 +103,7 @@ export function patchRtcPeerConnection() {
try {
const maxVideoBitrate = getPref(PrefKey.BITRATE_VIDEO_MAX);
if (maxVideoBitrate > 0) {
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, maxVideoBitrate * 1000);
arguments[0].sdp = patchSdpBitrate(arguments[0].sdp, Math.round(maxVideoBitrate / 1000));
}
} catch (e) {
BxLogger.error('setLocalDescription', e);

View File

@ -4,7 +4,7 @@ import { SettingElement, SettingElementType } from "@utils/settings";
import { UserAgentProfile } from "@utils/user-agent";
import { StreamStat } from "@modules/stream/stream-stats";
import type { PreferenceSetting, PreferenceSettings } from "@/types/preferences";
import { STATES } from "@utils/global";
import { AppInterface, STATES } from "@utils/global";
export enum PrefKey {
LAST_UPDATE_CHECK = 'version_last_check',
@ -325,21 +325,33 @@ export class Preferences {
note: '⚠️ ' + t('unexpected-behavior'),
default: 0,
min: 0,
max: 14,
steps: 1,
max: 14 * 1024 * 1000,
steps: 100 * 1024,
params: {
suffix: ' Mb/s',
exactTicks: 5,
exactTicks: 5 * 1024 * 1000,
customTextValue: (value: any) => {
value = parseInt(value);
if (value === 0) {
return t('unlimited');
} else {
return (value / (1024 * 1000)).toFixed(1) + ' Mb/s';
}
return null;
},
},
migrate: function(savedPrefs: any, value: any) {
try {
value = parseInt(value);
if (value < 100) {
value *= 1024 * 1000;
}
this.set(PrefKey.BITRATE_VIDEO_MAX, value);
savedPrefs[PrefKey.BITRATE_VIDEO_MAX] = value;
} catch (e) {}
},
},
[PrefKey.GAME_BAR_POSITION]: {
@ -405,7 +417,7 @@ export class Preferences {
default: false,
unsupported: ((): string | boolean => {
const userAgent = ((window.navigator as any).orgUserAgent || window.navigator.userAgent || '').toLowerCase();
return userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false;
return !AppInterface && userAgent.match(/(android|iphone|ipad)/) ? t('browser-unsupported-feature') : false;
})(),
ready: (setting: PreferenceSetting) => {
let note;