Controller & device vibration (#184)

* Test vibration

* Modify vibration patterns

* Try another vibration patterns

* Update vibration patterns

* Add setting to toggle device vibration

* Disable device vibration based on setting

* Bug fixes

* Test PWM

* Rename GamepadVibration to VibrationManager

* Add setting to toggle controller vibration

* Add vibration intensity setting

* Move Controller settings to the top

* Fix device vibration intensity

* No longer parse "delayMs" and "repeat"

* Fix device vibration intensity

* Disable vibration features on unsupported browsers

* Add "step" property to Vibration intensity slider

* Disable PWA prompt & Stream Gate dialog

* Update translations
This commit is contained in:
redphx 2023-12-12 17:37:04 +07:00 committed by GitHub
parent 584509a53d
commit 4ab265e370
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -54,7 +54,7 @@ function createElement(elmName, props = {}) {
const argType = typeof arg;
if (argType === 'string' || argType === 'number') {
$elm.textContent = arg;
$elm.appendChild(document.createTextNode(arg));
} else if (arg) {
$elm.appendChild(arg);
}
@ -486,6 +486,13 @@ const Translations = {
"uk-UA": "Частота опитувань контролера",
"vi-VN": "Tần suất cập nhật của bộ điều khiển",
},
"controller-vibration": {
"de-DE": "Vibration des Controllers",
"en-US": "Controller vibration",
"ja-JP": "コントローラーの振動",
"tr-TR": "Oyun kumandası titreşimi",
"vi-VN": "Rung bộ điều khiển",
},
"custom": {
"de-DE": "Benutzerdefiniert",
"en-US": "Custom",
@ -534,6 +541,22 @@ const Translations = {
"vi-VN": "Thiết bị này không hỗ trợ cảm ứng",
"zh-CN": "您的设备不支持触摸",
},
"device-vibration": {
"de-DE": "Vibration des Geräts",
"en-US": "Device vibration",
"ja-JP": "デバイスの振動",
"tr-TR": "Cihaz titreşimi",
"uk-UA": "Вібрація пристрою",
"vi-VN": "Rung thiết bị",
},
"device-vibration-not-using-gamepad": {
"de-DE": "Aktiviert, wenn kein Gamepad verwendet wird",
"en-US": "On when not using gamepad",
"ja-JP": "ゲームパッド未使用時にオン",
"tr-TR": "Oyun kumandası bağlanmadan titreşim",
"uk-UA": "Увімкнена, коли не використовується геймпад",
"vi-VN": "Bật khi không dùng tay cầm",
},
"disable": {
"de-DE": "Deaktiviert",
"en-US": "Disable",
@ -647,6 +670,7 @@ const Translations = {
"de-DE": "Maus- und Tastaturunterstützung aktivieren",
"en-US": "Enable Mouse & Keyboard support",
"es-ES": "Habilitar soporte para ratón y teclado",
"it-IT": "Abilitare il supporto di mouse e tastiera",
"ja-JP": "マウス&キーボードのサポートを有効化",
"pl-PL": "Włącz obsługę myszy i klawiatury",
"pt-BR": "Habilitar suporte ao Mouse & Teclado",
@ -675,6 +699,7 @@ const Translations = {
"de-DE": "\"Remote Play\" Funktion aktivieren",
"en-US": "Enable the \"Remote Play\" feature",
"es-ES": "Activar la función \"Reproducción remota\"",
"it-IT": "Abilitare la funzione \"Riproduzione remota\"",
"ja-JP": "リモートプレイ機能を有効化",
"pl-PL": "Włącz funkcję \"Gra zdalna\"",
"pt-BR": "Ativar o recurso \"Reprodução Remota\"",
@ -703,6 +728,7 @@ const Translations = {
"de-DE": "Schnell",
"en-US": "Fast",
"es-ES": "Rápido",
"it-IT": "Veloce",
"ja-JP": "高速",
"pl-PL": "Szybko",
"pt-BR": "Rápido",
@ -792,6 +818,7 @@ const Translations = {
"de-DE": "Layout",
"en-US": "Layout",
"es-ES": "Diseño",
"it-IT": "Layout",
"ja-JP": "レイアウト",
"pl-PL": "Układ",
"pt-BR": "Layout",
@ -820,6 +847,7 @@ const Translations = {
"de-DE": "Max. Bitrate",
"en-US": "Max bitrate",
"es-ES": "Tasa de bits máxima",
"it-IT": "Bitrate massimo",
"ja-JP": "最大ビットレート",
"pl-PL": "Maksymalny bitrate",
"pt-BR": "Taxa máxima dos bits",
@ -832,6 +860,7 @@ const Translations = {
"de-DE": "Funktioniert evtl. nicht fehlerfrei!",
"en-US": "May not work properly!",
"es-ES": "¡Puede que no funcione correctamente!",
"it-IT": "Potrebbe non funzionare correttamente!",
"ja-JP": "正常に動作しない場合があります!",
"pl-PL": "Może nie działać poprawnie!",
"pt-BR": "Pode não funcionar corretamente!",
@ -890,6 +919,7 @@ const Translations = {
"de-DE": "Maus & Tastatur",
"en-US": "Mouse & Keyboard",
"es-ES": "Ratón y teclado",
"it-IT": "Mouse e tastiera",
"ja-JP": "マウス&キーボード",
"pl-PL": "Mysz i klawiatura",
"pt-BR": "Mouse e Teclado",
@ -976,6 +1006,7 @@ const Translations = {
"de-DE": "Unterstützt nur einige Spiele",
"en-US": "Only supports some games",
"es-ES": "Sólo soporta algunos juegos",
"it-IT": "Supporta solo alcuni giochi",
"ja-JP": "一部のゲームのみサポート",
"pl-PL": "Wspiera tylko niektóre gry",
"pt-BR": "Suporta apenas alguns jogos",
@ -1398,6 +1429,7 @@ const Translations = {
"de-DE": "Langsam",
"en-US": "Slow",
"es-ES": "Lento",
"it-IT": "Lento",
"ja-JP": "低速",
"pl-PL": "Wolno",
"pt-BR": "Lento",
@ -1426,6 +1458,7 @@ const Translations = {
"de-DE": "Smart TV",
"en-US": "Smart TV",
"es-ES": "Smart TV",
"it-IT": "Smart TV",
"ja-JP": "スマートTV",
"pl-PL": "Smart TV",
"pt-BR": "Smart TV",
@ -1622,6 +1655,15 @@ const Translations = {
"vi-VN": "Kéo giãn",
"zh-CN": "拉伸",
},
"swap-buttons": {
"de-DE": "Tasten tauschen",
"en-US": "Swap buttons",
"ja-JP": "ボタン入れ替え",
"pt-BR": "Trocar botões",
"tr-TR": "Düğme düzenini ters çevir",
"uk-UA": "Поміняти кнопки місцями",
"vi-VN": "Hoán đổi nút",
},
"target-resolution": {
"de-DE": "Festgelegte Auflösung",
"en-US": "Target resolution",
@ -1864,6 +1906,7 @@ const Translations = {
"de-DE": "Unbegrenzt",
"en-US": "Unlimited",
"es-ES": "Ilimitado",
"it-IT": "Illimitato",
"ja-JP": "無制限",
"pl-PL": "Bez ograniczeń",
"pt-BR": "Ilimitado",
@ -1914,6 +1957,13 @@ const Translations = {
"vi-VN": "User-Agent",
"zh-CN": "浏览器UA伪装",
},
"vibration-intensity": {
"de-DE": "Vibrationsstärke",
"en-US": "Vibration intensity",
"ja-JP": "振動の強さ",
"tr-TR": "Titreşim gücü",
"vi-VN": "Cường độ rung",
},
"video": {
"de-DE": "Video",
"en-US": "Video",
@ -2910,6 +2960,147 @@ class GamepadHandler {
}
}
class VibrationManager {
static #playDeviceVibration(data) {
// console.log(+new Date, data);
const intensity = Math.min(100, data.leftMotorPercent + data.rightMotorPercent / 2) * window.BX_VIBRATION_INTENSITY;
if (intensity === 0 || intensity === 100) {
// Stop vibration
window.navigator.vibrate(intensity ? data.durationMs : 0);
return;
}
const pulseDuration = 200;
const onDuration = Math.floor(pulseDuration * intensity / 100);
const offDuration = pulseDuration - onDuration;
const repeats = Math.ceil(data.durationMs / pulseDuration);
const pulses = Array(repeats).fill([onDuration, offDuration]).flat();
// console.log(pulses);
window.navigator.vibrate(pulses);
}
static supportControllerVibration() {
return Gamepad.prototype.hasOwnProperty('vibrationActuator');
}
static supportDeviceVibration() {
return !!window.navigator.vibrate;
}
static updateGlobalVars() {
window.BX_ENABLE_CONTROLLER_VIBRATION = VibrationManager.supportControllerVibration() ? PREFS.get(Preferences.CONTROLLER_ENABLE_VIBRATION) : false;
window.BX_VIBRATION_INTENSITY = PREFS.get(Preferences.CONTROLLER_VIBRATION_INTENSITY) / 100;
if (!VibrationManager.supportDeviceVibration()) {
window.BX_ENABLE_DEVICE_VIBRATION = false;
return;
}
// Stop vibration
window.navigator.vibrate(0);
const value = PREFS.get(Preferences.CONTROLLER_DEVICE_VIBRATION);
let enabled;
if (value === 'on') {
enabled = true;
} else if (value === 'auto') {
enabled = true;
const gamepads = window.navigator.getGamepads();
for (const gamepad of gamepads) {
if (gamepad) {
enabled = false;
break;
}
}
} else {
enabled = false;
}
window.BX_ENABLE_DEVICE_VIBRATION = enabled;
}
static initialSetup() {
window.addEventListener('gamepadconnected', VibrationManager.updateGlobalVars);
window.addEventListener('gamepaddisconnected', VibrationManager.updateGlobalVars);
VibrationManager.updateGlobalVars();
const orgCreateDataChannel = RTCPeerConnection.prototype.createDataChannel;
RTCPeerConnection.prototype.createDataChannel = function() {
const dataChannel = orgCreateDataChannel.apply(this, arguments);
if (dataChannel.label !== 'input') {
return dataChannel;
}
const VIBRATION_DATA_MAP = {
'gamepadIndex': 8,
'leftMotorPercent': 8,
'rightMotorPercent': 8,
'leftTriggerMotorPercent': 8,
'rightTriggerMotorPercent': 8,
'durationMs': 16,
// 'delayMs': 16,
// 'repeat': 8,
};
dataChannel.addEventListener('message', e => {
if (!window.BX_ENABLE_DEVICE_VIBRATION) {
return;
}
if (typeof e !== 'object' || !(e.data instanceof ArrayBuffer)) {
return;
}
const dataView = new DataView(e.data);
let offset = 0;
let messageType;
if (dataView.byteLength === 13) { // version >= 8
messageType = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
messageType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
if (!(messageType & 128)) { // Vibration
return;
}
const vibrationType = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
if (vibrationType !== 0) { // FourMotorRumble
return;
}
const data = {};
for (const key in VIBRATION_DATA_MAP) {
if (VIBRATION_DATA_MAP[key] === 16) {
data[key] = dataView.getUint16(offset, true);
offset += Uint16Array.BYTES_PER_ELEMENT;
} else {
data[key] = dataView.getUint8(offset);
offset += Uint8Array.BYTES_PER_ELEMENT;
}
}
VibrationManager.#playDeviceVibration(data);
});
return dataChannel;
};
}
}
class MouseCursorHider {
static #timeout;
static #cursorVisible = true;
@ -3553,6 +3744,9 @@ class Preferences {
static get STREAM_DISABLE_FEEDBACK_DIALOG() { return 'stream_disable_feedback_dialog'; }
static get CONTROLLER_ENABLE_SHORTCUTS() { return 'controller_enable_shortcuts'; }
static get CONTROLLER_ENABLE_VIBRATION() { return 'controller_enable_vibration'; }
static get CONTROLLER_DEVICE_VIBRATION() { return 'controller_device_vibration'; }
static get CONTROLLER_VIBRATION_INTENSITY() { return 'controller_vibration_intensity'; }
static get MKB_ENABLED() { return 'mkb_enabled'; }
static get MKB_ABSOLUTE_MOUSE() { return 'mkb_absolute_mouse'; }
@ -3786,6 +3980,26 @@ class Preferences {
'default': false,
},
[Preferences.CONTROLLER_ENABLE_VIBRATION]: {
'default': true,
},
[Preferences.CONTROLLER_DEVICE_VIBRATION]: {
'default': 'off',
'options': {
'on': __('on'),
'auto': __('device-vibration-not-using-gamepad'),
'off': __('off'),
},
},
[Preferences.CONTROLLER_VIBRATION_INTENSITY]: {
'default': 100,
'min': 0,
'max': 100,
'steps': 10,
},
[Preferences.MKB_ENABLED]: {
'default': false,
},
@ -4164,7 +4378,7 @@ class Preferences {
);
if (!options.disabled && !options.hideSlider) {
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value});
$range = CE('input', {'type': 'range', 'min': MIN, 'max': MAX, 'value': value, 'step': STEPS});
$range.addEventListener('input', e => {
value = parseInt(e.target.value);
@ -4373,6 +4587,48 @@ class Patcher {
return funcStr;
},
// Control controller vibration
playVibration: function(funcStr) {
const text = '}playVibration(e){';
if (!funcStr.includes(text)) {
return false;
}
const newCode = `
if (!window.BX_ENABLE_CONTROLLER_VIBRATION) {
return void(0);
}
if (window.BX_VIBRATION_INTENSITY && window.BX_VIBRATION_INTENSITY < 1) {
e.leftMotorPercent = e.leftMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightMotorPercent = e.rightMotorPercent * window.BX_VIBRATION_INTENSITY;
e.leftTriggerMotorPercent = e.leftTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
e.rightTriggerMotorPercent = e.rightTriggerMotorPercent * window.BX_VIBRATION_INTENSITY;
}
`;
VibrationManager.updateGlobalVars();
funcStr = funcStr.replaceAll(text, text + newCode);
return funcStr;
},
// Override website's settings
overrideSettings: function(funcStr) {
const index = funcStr.indexOf(',EnableStreamGate:');
if (index === -1) {
return false;
}
// Find the next "},"
const endIndex = funcStr.indexOf('},', index);
const newCode = `
EnableStreamGate: false,
PwaPrompt: false,
`;
funcStr = funcStr.substring(0, endIndex) + ',' + newCode + funcStr.substring(endIndex);
return funcStr;
},
// Enable Mouse and Keyboard support
enableMouseAndKeyboard: PREFS.get(Preferences.MKB_ENABLED) && function(funcStr) {
if (!funcStr.includes('EnableMouseAndKeyboard:')) {
@ -4399,7 +4655,8 @@ class Patcher {
['enableXcloudLogger'],
[
'enableMouseAndKeyboard',
// 'enableMouseAndKeyboard',
'overrideSettings',
'remotePlayDirectConnectUrl',
'disableTrackEvent',
'enableConsoleLogging',
@ -4409,6 +4666,7 @@ class Patcher {
// Only when playing
['remotePlayConnectMode'],
['playVibration'],
['enableConsoleLogging'],
];
@ -5091,7 +5349,7 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
bottom: 20px;
z-index: var(--bx-stream-settings-z-index);
padding: 8px;
width: 220px;
width: 320px;
background: #1a1b1e;
color: #fff;
border-radius: 8px 0 0 8px;
@ -5164,6 +5422,12 @@ div[class*=StreamMenu-module__menuContainer] > div[class*=Menu-module] {
font-family: var(--bx-monospaced-font);
}
.bx-quick-settings-bar-note {
font-size: 12px;
font-weight: lighter;
font-style: italic;
}
.bx-toast {
position: fixed;
left: 50%;
@ -5896,6 +6160,7 @@ function interceptHttpRequests() {
// Enable touch controller
if (TouchController.isEnabled()) {
overrides.inputConfiguration = overrides.inputConfiguration || {};
overrides.enableVibration = true;
overrides.inputConfiguration.enableTouchInput = true;
overrides.inputConfiguration.maxTouchPoints = 10;
}
@ -6593,30 +6858,54 @@ function setupVideoSettingsBar() {
let $stretchInp;
const $wrapper = CE('div', {'class': 'bx-quick-settings-bar'},
CE('h2', {}, __('controller')),
CE('div', {},
CE('label', {}, __('controller-vibration')),
VibrationManager.supportControllerVibration() && PREFS.toElement(Preferences.CONTROLLER_ENABLE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportControllerVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
CE('div', {},
CE('label', {}, __('device-vibration')),
VibrationManager.supportDeviceVibration() && PREFS.toElement(Preferences.CONTROLLER_DEVICE_VIBRATION, VibrationManager.updateGlobalVars),
!VibrationManager.supportDeviceVibration() && CE('div', {'class': 'bx-quick-settings-bar-note'}, __('browser-unsupported-feature')),
),
(VibrationManager.supportControllerVibration() || VibrationManager.supportDeviceVibration()) &&
CE('div', {},
CE('label', {}, __('vibration-intensity')),
PREFS.toNumberStepper(Preferences.CONTROLLER_VIBRATION_INTENSITY, VibrationManager.updateGlobalVars, {suffix: '%', ticks: 50}),
),
CE('h2', {}, __('audio')),
CE('div', {},
CE('label', {}, __('volume')),
PREFS.toNumberStepper(Preferences.AUDIO_VOLUME, (e, value) => {
STREAM_AUDIO_GAIN_NODE && (STREAM_AUDIO_GAIN_NODE.gain.value = (value / 100).toFixed(2));
}, {suffix: '%', ticks: 100, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)})),
}, {suffix: '%', ticks: 100, disabled: !PREFS.get(Preferences.AUDIO_ENABLE_VOLUME_CONTROL)}),
),
CE('h2', {}, __('video')),
CE('div', {'class': 'bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'class': 'bx-quick-settings-bar-note bx-clarity-boost-warning'}, `⚠️ ${__('clarity-boost-warning')}`),
CE('div', {'data-type': 'video'},
CE('label', {'for': 'bx-quick-setting-stretch'}, __('ratio')),
PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange)),
CE('label', {}, __('ratio')),
PREFS.toElement(Preferences.VIDEO_RATIO, onVideoChange),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('clarity')),
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true})), // disable this feature in Safari
PREFS.toNumberStepper(Preferences.VIDEO_CLARITY, onVideoChange, {disabled: isSafari, hideSlider: true}), // disable this feature in Safari
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('saturation')),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25})),
PREFS.toNumberStepper(Preferences.VIDEO_SATURATION, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('contrast')),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25})),
PREFS.toNumberStepper(Preferences.VIDEO_CONTRAST, onVideoChange, {suffix: '%', ticks: 25}),
),
CE('div', {'data-type': 'video'},
CE('label', {}, __('brightness')),
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}))
PREFS.toNumberStepper(Preferences.VIDEO_BRIGHTNESS, onVideoChange, {suffix: '%', ticks: 25}),
),
);
document.documentElement.appendChild($wrapper);
@ -6951,6 +7240,8 @@ if (PREFS.get(Preferences.STREAM_TOUCH_CONTROLLER) === 'all') {
TouchController.setup();
}
VibrationManager.initialSetup();
const OrgRTCPeerConnection = window.RTCPeerConnection;
window.RTCPeerConnection = function() {
const peer = new OrgRTCPeerConnection();