mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
Fix direct H.264 issues and optimize UI
feat: support custom mouse wheel speed fix: prevent direct H.264 stream buffer overflow and replay style: optimize virtual keyboard layout style: optimize menu bar and settings styling
This commit is contained in:
@@ -34,7 +34,8 @@
|
||||
"react-simple-keyboard": "^3.8.19",
|
||||
"semver": "^7.6.3",
|
||||
"vaul": "^0.9.9",
|
||||
"websocket": "^1.0.35"
|
||||
"websocket": "^1.0.35",
|
||||
"yocto-queue": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^4.3.1",
|
||||
|
||||
9
web/pnpm-lock.yaml
generated
9
web/pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
||||
websocket:
|
||||
specifier: ^1.0.35
|
||||
version: 1.0.35
|
||||
yocto-queue:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1
|
||||
devDependencies:
|
||||
'@ianvs/prettier-plugin-sort-imports':
|
||||
specifier: ^4.3.1
|
||||
@@ -2916,6 +2919,10 @@ packages:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
yocto-queue@1.2.1:
|
||||
resolution: {integrity: sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
yoctocolors-cjs@2.1.2:
|
||||
resolution: {integrity: sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5899,4 +5906,6 @@ snapshots:
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
yocto-queue@1.2.1: {}
|
||||
|
||||
yoctocolors-cjs@2.1.2: {}
|
||||
|
||||
@@ -78,8 +78,8 @@ export function disableHdmi() {
|
||||
}
|
||||
|
||||
// set HDMI state
|
||||
export function setHdmiState(state: string) {
|
||||
if (state === 'enabled') {
|
||||
export function setHdmiState(enabled: boolean) {
|
||||
if (enabled) {
|
||||
return enableHdmi();
|
||||
}
|
||||
return disableHdmi();
|
||||
|
||||
@@ -64,9 +64,7 @@ const en = {
|
||||
frameDetect: 'Frame Detect',
|
||||
frameDetectTip:
|
||||
"Calculate the difference between frames. Stop transmitting video stream when no changes are detected on the remote host's screen.",
|
||||
resetHdmi: 'Reset HDMI',
|
||||
enableHdmi: 'Enable HDMI',
|
||||
disableHdmi: 'Disable HDMI',
|
||||
resetHdmi: 'Reset HDMI'
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Keyboard',
|
||||
@@ -75,8 +73,7 @@ const en = {
|
||||
placeholder: 'Please input',
|
||||
submit: 'Submit',
|
||||
virtual: 'Keyboard',
|
||||
ctrlaltdel: 'Ctrl+Alt+Del',
|
||||
layout: 'Keyboard Layout'
|
||||
ctrlaltdel: 'Ctrl+Alt+Del'
|
||||
},
|
||||
mouse: {
|
||||
title: 'Mouse',
|
||||
@@ -90,6 +87,9 @@ const en = {
|
||||
mode: 'Mouse mode',
|
||||
absolute: 'Absolute mode',
|
||||
relative: 'Relative mode',
|
||||
speed: 'Wheel speed',
|
||||
fast: 'Fast',
|
||||
slow: 'Slow',
|
||||
requestPointer: 'Using relative mode. Please click desktop to get mouse pointer.',
|
||||
resetHid: 'Reset HID',
|
||||
hidOnly: {
|
||||
@@ -213,8 +213,7 @@ const en = {
|
||||
menuBar: 'Menu Bar',
|
||||
menuBarDesc: 'Display icons in the menu bar',
|
||||
webTitle: 'Web Title',
|
||||
webTitleDesc: 'Customize the web page title',
|
||||
keyboardLayout: 'Keyboard Layout'
|
||||
webTitleDesc: 'Customize the web page title'
|
||||
},
|
||||
device: {
|
||||
title: 'Device',
|
||||
@@ -262,6 +261,9 @@ const en = {
|
||||
description: 'Enable mDNS discovery service',
|
||||
tip: "Turning it off if it's not needed"
|
||||
},
|
||||
hdmi: {
|
||||
description: 'Enable HDMI/monitor output'
|
||||
},
|
||||
hidOnly: 'HID-Only Mode',
|
||||
disk: 'Virtual Disk',
|
||||
diskDesc: 'Mount virtual U-disk on the remote host',
|
||||
|
||||
@@ -85,6 +85,9 @@ const zh = {
|
||||
mode: '鼠标模式',
|
||||
absolute: '绝对模式',
|
||||
relative: '相对模式',
|
||||
speed: '滚轮速度',
|
||||
fast: '快',
|
||||
slow: '慢',
|
||||
requestPointer: '正在使用鼠标相对模式,请点击桌面获取鼠标指针。',
|
||||
resetHid: '重置 HID',
|
||||
hidOnly: {
|
||||
@@ -238,6 +241,9 @@ const zh = {
|
||||
description: '启用 mDNS 发现服务',
|
||||
tip: '如果您未使用此功能,建议将其关闭'
|
||||
},
|
||||
hdmi: {
|
||||
description: '启用 HDMI/显示器 输出功能'
|
||||
},
|
||||
hidOnly: 'HID-Only 模式',
|
||||
disk: '虚拟U盘',
|
||||
diskDesc: '在远程主机中挂载虚拟U盘',
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
import { atom } from 'jotai';
|
||||
|
||||
// hid mode: normal or hid-only
|
||||
export const hdmiEnabledAtom = atom<'enabled' | 'disabled'>('enabled');
|
||||
@@ -6,5 +6,8 @@ export const mouseStyleAtom = atom('cursor-default');
|
||||
// mouse mode: absolute or relative
|
||||
export const mouseModeAtom = atom('absolute');
|
||||
|
||||
// mouse scroll interval (unit: ms)
|
||||
export const scrollIntervalAtom = atom(0);
|
||||
|
||||
// hid mode: normal or hid-only
|
||||
export const hidModeAtom = atom<'normal' | 'hid-only'>('normal');
|
||||
|
||||
@@ -2,6 +2,8 @@ import { atom } from 'jotai';
|
||||
|
||||
import { Resolution } from '@/types';
|
||||
|
||||
export const isHdmiEnabledAtom = atom(true);
|
||||
|
||||
// video mode
|
||||
// direct: stream H.264 over HTTP
|
||||
// h264: stream H.264 over WebRTC
|
||||
|
||||
@@ -9,8 +9,10 @@ const GOP_KEY = 'nano-kvm-gop';
|
||||
const FRAME_DETECT_KEY = 'nano-kvm-frame-detect';
|
||||
const MOUSE_STYLE_KEY = 'nano-kvm-mouse-style';
|
||||
const MOUSE_MODE_KEY = 'nano-kvm-mouse-mode';
|
||||
const MOUSE_SCROLL_INTERVAL_KEY = 'nanokvm-kvm-mouse-scroll-interval';
|
||||
const SKIP_UPDATE_KEY = 'nano-kvm-check-update';
|
||||
const KEYBOARD_LAYOUT_KEY = 'nano-kvm-keyboard-layout';
|
||||
const KEYBOARD_SYSTEM_KEY = 'nano-kvm-keyboard-system';
|
||||
const KEYBOARD_LANGUAGE_KEY = 'nano-kvm-keyboard-language';
|
||||
const SKIP_MODIFY_PASSWORD_KEY = 'nano-kvm-skip-modify-password';
|
||||
const MENU_DISABLED_ITEMS_KEY = 'nano-kvm-menu-disabled-items';
|
||||
const POWER_CONFIRM_KEY = 'nano-kvm-power-confirm';
|
||||
@@ -129,6 +131,15 @@ export function setMouseMode(mouse: string) {
|
||||
localStorage.setItem(MOUSE_MODE_KEY, mouse);
|
||||
}
|
||||
|
||||
export function getMouseScrollInterval() {
|
||||
const interval = localStorage.getItem(MOUSE_SCROLL_INTERVAL_KEY);
|
||||
return interval ? Number(interval) : null;
|
||||
}
|
||||
|
||||
export function setMouseScrollInterval(interval: number): void {
|
||||
localStorage.setItem(MOUSE_SCROLL_INTERVAL_KEY, String(interval));
|
||||
}
|
||||
|
||||
export function getSkipUpdate() {
|
||||
const skip = getWithExpiry(SKIP_UPDATE_KEY);
|
||||
return skip === 'true';
|
||||
@@ -139,17 +150,20 @@ export function setSkipUpdate(skip: boolean) {
|
||||
setWithExpiry(SKIP_UPDATE_KEY, String(skip), expiry);
|
||||
}
|
||||
|
||||
export function setKeyboardLayout(layout: string) {
|
||||
localStorage.setItem(KEYBOARD_LAYOUT_KEY, layout);
|
||||
export function setKeyboardSystem(system: string) {
|
||||
localStorage.setItem(KEYBOARD_SYSTEM_KEY, system);
|
||||
}
|
||||
|
||||
export function getKeyboardLayout(): string {
|
||||
// Return 'azerty' or 'qwerty' (or 'qwerty' if not defined)
|
||||
const layout = localStorage.getItem(KEYBOARD_LAYOUT_KEY);
|
||||
if (layout === 'azerty' || layout === 'mac' || layout === 'rus') {
|
||||
return layout;
|
||||
}
|
||||
return 'default';
|
||||
export function getKeyboardSystem() {
|
||||
return localStorage.getItem(KEYBOARD_SYSTEM_KEY);
|
||||
}
|
||||
|
||||
export function setKeyboardLanguage(language: string) {
|
||||
localStorage.setItem(KEYBOARD_LANGUAGE_KEY, language);
|
||||
}
|
||||
|
||||
export function getKeyboardLanguage() {
|
||||
return localStorage.getItem(KEYBOARD_LANGUAGE_KEY);
|
||||
}
|
||||
|
||||
export function setSkipModifyPassword(skip: boolean) {
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import { AppleOutlined, WindowsOutlined } from '@ant-design/icons';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { XIcon } from 'lucide-react';
|
||||
import Keyboard, { KeyboardButtonTheme } from 'react-simple-keyboard';
|
||||
import { Drawer } from 'vaul';
|
||||
|
||||
import 'react-simple-keyboard/build/css/index.css';
|
||||
import '@/assets/styles/keyboard.css';
|
||||
|
||||
import { ConfigProvider, Segmented, Select, theme } from 'antd';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
|
||||
import { getKeyboardLayout } from '@/lib/localstorage.ts';
|
||||
import * as storage from '@/lib/localstorage.ts';
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { isKeyboardOpenAtom } from '@/jotai/keyboard.ts';
|
||||
|
||||
@@ -24,50 +26,60 @@ import {
|
||||
specialKeyMap
|
||||
} from './virtual-keys.ts';
|
||||
|
||||
// Helper function to map keys per active layout
|
||||
function getKeyCode(key: string, layout: string) {
|
||||
// AZERTY: map < > key (next to left-shift) to OEM_102 usage (100)
|
||||
if (layout === 'azerty' && key === 'Backquote_azerty') {
|
||||
return KeyboardCodes.get('Backquote_azerty');
|
||||
}
|
||||
|
||||
// AZERTY: swap A↔Q and Z↔W on French physical positions
|
||||
if (layout === 'azerty' && key.endsWith('_azerty')) {
|
||||
const base = key.replace('_azerty', '');
|
||||
if (base === 'KeyA') return KeyboardCodes.get('KeyQ');
|
||||
if (base === 'KeyQ') return KeyboardCodes.get('KeyA');
|
||||
if (base === 'KeyZ') return KeyboardCodes.get('KeyW');
|
||||
if (base === 'KeyW') return KeyboardCodes.get('KeyZ');
|
||||
// all other labels use their own code
|
||||
return KeyboardCodes.get(base);
|
||||
}
|
||||
|
||||
// default (QWERTY / Mac / Rus)
|
||||
return KeyboardCodes.get(key);
|
||||
}
|
||||
|
||||
export const VirtualKeyboard = () => {
|
||||
const isBigScreen = useMediaQuery({ minWidth: 850 });
|
||||
|
||||
const [isKeyboardOpen, setIsKeyboardOpen] = useAtom(isKeyboardOpenAtom);
|
||||
const [layout, setLayout] = useState(getKeyboardLayout() || 'default');
|
||||
|
||||
const [keyboardLayout, setKeyboardLayout] = useState('default');
|
||||
const [keyboardSystem, setKeyboardSystem] = useState('win');
|
||||
const [keyboardLanguage, setKeyboardLanguage] = useState('en');
|
||||
const [activeModifierKeys, setActiveModifierKeys] = useState<string[]>([]);
|
||||
|
||||
const keyboardRef = useRef<any>(null);
|
||||
|
||||
// Update the layout when it changes in settings
|
||||
useEffect(() => {
|
||||
// Function to check if the layout has changed
|
||||
const checkLayout = () => {
|
||||
const currentLayout = getKeyboardLayout() || 'default';
|
||||
if (currentLayout !== layout) {
|
||||
setLayout(currentLayout);
|
||||
}
|
||||
};
|
||||
const systems = [
|
||||
{ value: 'win', icon: <WindowsOutlined /> },
|
||||
{ value: 'mac', icon: <AppleOutlined /> }
|
||||
];
|
||||
|
||||
// Check for layout changes every time the keyboard is opened
|
||||
if (isKeyboardOpen) {
|
||||
checkLayout();
|
||||
const languages = [
|
||||
{ value: 'en', label: 'English' },
|
||||
{ value: 'fr', label: 'French' },
|
||||
{ value: 'ru', label: 'Russian' }
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
const system = storage.getKeyboardSystem();
|
||||
if (system && ['win', 'mac'].includes(system)) {
|
||||
setKeyboardSystem(system);
|
||||
}
|
||||
}, [isKeyboardOpen, layout]);
|
||||
|
||||
const language = storage.getKeyboardLanguage();
|
||||
if (language && languages.some((lng) => lng.value === language)) {
|
||||
setKeyboardLanguage(language);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const layoutMap = new Map([
|
||||
['en', 'default'],
|
||||
['ru', 'rus'],
|
||||
['fr', 'azerty']
|
||||
]);
|
||||
|
||||
if (keyboardLanguage === 'en' && keyboardSystem === 'mac') {
|
||||
setKeyboardLayout('mac');
|
||||
return;
|
||||
}
|
||||
|
||||
if (layoutMap.has(keyboardLanguage)) {
|
||||
setKeyboardLayout(layoutMap.get(keyboardLanguage)!);
|
||||
return;
|
||||
}
|
||||
|
||||
setKeyboardLayout('default');
|
||||
}, [keyboardSystem, keyboardLanguage]);
|
||||
|
||||
function onKeyPress(key: string) {
|
||||
if (modifierKeys.includes(key)) {
|
||||
@@ -92,9 +104,7 @@ export const VirtualKeyboard = () => {
|
||||
}
|
||||
|
||||
function sendKeydown(key: string) {
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
const code = getKeyCode(specialKey ? specialKey : key, layout);
|
||||
|
||||
const code = getKeyCode(key);
|
||||
if (!code) {
|
||||
console.log('unknown code: ', key);
|
||||
return;
|
||||
@@ -105,6 +115,26 @@ export const VirtualKeyboard = () => {
|
||||
client.send([1, code, ...modifiers]);
|
||||
}
|
||||
|
||||
function getKeyCode(key: string) {
|
||||
// AZERTY: swap A↔Q and Z↔W on French physical positions
|
||||
if (keyboardLanguage === 'fr' && key.endsWith('_azerty')) {
|
||||
const base = key.replace('_azerty', '');
|
||||
if (base === 'KeyA') return KeyboardCodes.get('KeyQ');
|
||||
if (base === 'KeyQ') return KeyboardCodes.get('KeyA');
|
||||
if (base === 'KeyZ') return KeyboardCodes.get('KeyW');
|
||||
if (base === 'KeyW') return KeyboardCodes.get('KeyZ');
|
||||
// all other labels use their own code
|
||||
return KeyboardCodes.get(base);
|
||||
}
|
||||
|
||||
const specialKey = specialKeyMap.get(key);
|
||||
if (specialKey) {
|
||||
return KeyboardCodes.get(specialKey);
|
||||
}
|
||||
|
||||
return KeyboardCodes.get(key);
|
||||
}
|
||||
|
||||
function sendKeyup() {
|
||||
sendModifierKeyUp();
|
||||
client.send([1, 0, 0, 0, 0, 0]);
|
||||
@@ -148,6 +178,16 @@ export const VirtualKeyboard = () => {
|
||||
setActiveModifierKeys([]);
|
||||
}
|
||||
|
||||
function selectSystem(system: string) {
|
||||
setKeyboardSystem(system);
|
||||
storage.setKeyboardSystem(system);
|
||||
}
|
||||
|
||||
function selectLanguage(language: string) {
|
||||
setKeyboardLanguage(language);
|
||||
storage.setKeyboardLanguage(language);
|
||||
}
|
||||
|
||||
function getButtonTheme(): KeyboardButtonTheme[] {
|
||||
const theme = [{ class: 'hg-double', buttons: doubleKeys.join(' ') }];
|
||||
|
||||
@@ -170,15 +210,30 @@ export const VirtualKeyboard = () => {
|
||||
>
|
||||
{/* header */}
|
||||
<div className="flex items-center justify-between px-3 py-1">
|
||||
<div className="keyboard-header text-sm font-medium px-2">
|
||||
{layout === 'default'
|
||||
? 'QWERTY (Win)'
|
||||
: layout === 'mac'
|
||||
? 'QWERTY (Mac)'
|
||||
: layout === 'azerty'
|
||||
? 'AZERTY (Win)'
|
||||
: 'Russian'}
|
||||
</div>
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
algorithm: theme.defaultAlgorithm
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center space-x-5">
|
||||
<Select
|
||||
size="small"
|
||||
style={{ minWidth: 90 }}
|
||||
defaultValue={keyboardLanguage}
|
||||
options={languages}
|
||||
onChange={selectLanguage}
|
||||
/>
|
||||
|
||||
{keyboardLanguage === 'en' && (
|
||||
<Segmented
|
||||
size="small"
|
||||
options={systems}
|
||||
value={keyboardSystem}
|
||||
onChange={selectSystem}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
|
||||
<div className="flex w-[100px] items-center justify-end">
|
||||
<div
|
||||
@@ -199,7 +254,7 @@ export const VirtualKeyboard = () => {
|
||||
keyboardRef={(r) => (keyboardRef.current = r)}
|
||||
onKeyPress={onKeyPress}
|
||||
onKeyReleased={onKeyReleased}
|
||||
layoutName={layout}
|
||||
layoutName={keyboardLayout}
|
||||
{...keyboardOptions}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// TODO: refactor it
|
||||
|
||||
// main keys
|
||||
export const keyboardOptions = {
|
||||
theme: 'simple-keyboard hg-theme-default',
|
||||
@@ -119,7 +121,7 @@ export const keyboardOptions = {
|
||||
RusBracketLeft: 'Х',
|
||||
RusBracketRight: 'Ъ',
|
||||
RusBackslash: '/<br>\\',
|
||||
|
||||
|
||||
RusA: 'Ф',
|
||||
RusS: 'Ы',
|
||||
RusD: 'В',
|
||||
@@ -145,59 +147,59 @@ export const keyboardOptions = {
|
||||
|
||||
// AZERTY specific display keys
|
||||
// Row 1
|
||||
'Backquote_azerty': '<<br/>>',
|
||||
'Digit1_azerty': '&<br/>1',
|
||||
'Digit2_azerty': 'é<br/>2',
|
||||
'Digit3_azerty': '"<br/>#',
|
||||
'Digit4_azerty': '\'<br/>{',
|
||||
'Digit5_azerty': '(<br/>[',
|
||||
'Digit6_azerty': '-<br/>|',
|
||||
'Digit7_azerty': 'è<br/>`',
|
||||
'Digit8_azerty': '_<br/>\\',
|
||||
'Digit9_azerty': 'ç<br/>^',
|
||||
'Digit0_azerty': 'à<br/>@',
|
||||
'Minus_azerty': ')<br/>]',
|
||||
'Equal_azerty': '=<br/>}',
|
||||
Backquote_azerty: '<<br/>>',
|
||||
Digit1_azerty: '&<br/>1',
|
||||
Digit2_azerty: 'é<br/>2',
|
||||
Digit3_azerty: '"<br/>#',
|
||||
Digit4_azerty: "'<br/>{",
|
||||
Digit5_azerty: '(<br/>[',
|
||||
Digit6_azerty: '-<br/>|',
|
||||
Digit7_azerty: 'è<br/>`',
|
||||
Digit8_azerty: '_<br/>\\',
|
||||
Digit9_azerty: 'ç<br/>^',
|
||||
Digit0_azerty: 'à<br/>@',
|
||||
Minus_azerty: ')<br/>]',
|
||||
Equal_azerty: '=<br/>}',
|
||||
|
||||
// Row 2
|
||||
'KeyA_azerty': 'A',
|
||||
'KeyZ_azerty': 'Z',
|
||||
'KeyE_azerty': 'E<br/>€',
|
||||
'KeyR_azerty': 'R',
|
||||
'KeyT_azerty': 'T',
|
||||
'KeyY_azerty': 'Y',
|
||||
'KeyU_azerty': 'U',
|
||||
'KeyI_azerty': 'I',
|
||||
'KeyO_azerty': 'O',
|
||||
'KeyP_azerty': 'P',
|
||||
'BracketLeft_azerty': '¨<br/>^',
|
||||
'BracketRight_azerty': '£<br/>$',
|
||||
'Backslash_azerty': 'µ<br/>*',
|
||||
KeyA_azerty: 'A',
|
||||
KeyZ_azerty: 'Z',
|
||||
KeyE_azerty: 'E<br/>€',
|
||||
KeyR_azerty: 'R',
|
||||
KeyT_azerty: 'T',
|
||||
KeyY_azerty: 'Y',
|
||||
KeyU_azerty: 'U',
|
||||
KeyI_azerty: 'I',
|
||||
KeyO_azerty: 'O',
|
||||
KeyP_azerty: 'P',
|
||||
BracketLeft_azerty: '¨<br/>^',
|
||||
BracketRight_azerty: '£<br/>$',
|
||||
Backslash_azerty: 'µ<br/>*',
|
||||
|
||||
// Row 3
|
||||
'KeyQ_azerty': 'Q',
|
||||
'KeyS_azerty': 'S',
|
||||
'KeyD_azerty': 'D',
|
||||
'KeyF_azerty': 'F',
|
||||
'KeyG_azerty': 'G',
|
||||
'KeyH_azerty': 'H',
|
||||
'KeyJ_azerty': 'J',
|
||||
'KeyK_azerty': 'K',
|
||||
'KeyL_azerty': 'L',
|
||||
'Semicolon_azerty': 'M',
|
||||
'Quote_azerty': '%<br/>ù',
|
||||
KeyQ_azerty: 'Q',
|
||||
KeyS_azerty: 'S',
|
||||
KeyD_azerty: 'D',
|
||||
KeyF_azerty: 'F',
|
||||
KeyG_azerty: 'G',
|
||||
KeyH_azerty: 'H',
|
||||
KeyJ_azerty: 'J',
|
||||
KeyK_azerty: 'K',
|
||||
KeyL_azerty: 'L',
|
||||
Semicolon_azerty: 'M',
|
||||
Quote_azerty: '%<br/>ù',
|
||||
|
||||
// Row 4
|
||||
'KeyW_azerty': 'W',
|
||||
'KeyX_azerty': 'X',
|
||||
'KeyC_azerty': 'C',
|
||||
'KeyV_azerty': 'V',
|
||||
'KeyB_azerty': 'B',
|
||||
'KeyN_azerty': 'N',
|
||||
'KeyM_azerty': '?<br/>,',
|
||||
'Comma_azerty': '.<br/>;',
|
||||
'Period_azerty': '/<br/>:',
|
||||
'Slash_azerty': '§<br/>!',
|
||||
KeyW_azerty: 'W',
|
||||
KeyX_azerty: 'X',
|
||||
KeyC_azerty: 'C',
|
||||
KeyV_azerty: 'V',
|
||||
KeyB_azerty: 'B',
|
||||
KeyN_azerty: 'N',
|
||||
KeyM_azerty: '?<br/>,',
|
||||
Comma_azerty: '.<br/>;',
|
||||
Period_azerty: '/<br/>:',
|
||||
Slash_azerty: '§<br/>!'
|
||||
},
|
||||
// Enable layout-specific display
|
||||
mergeDisplay: true,
|
||||
@@ -205,7 +207,7 @@ export const keyboardOptions = {
|
||||
default: 'default',
|
||||
shift: 'shift',
|
||||
azerty: 'azerty'
|
||||
},
|
||||
}
|
||||
// ...remaining options...
|
||||
};
|
||||
|
||||
@@ -313,4 +315,4 @@ export const doubleKeys = [
|
||||
'Comma',
|
||||
'Period',
|
||||
'Slash'
|
||||
];
|
||||
];
|
||||
|
||||
@@ -5,18 +5,21 @@ import { MouseIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as ls from '@/lib/localstorage';
|
||||
import { mouseModeAtom, mouseStyleAtom } from '@/jotai/mouse';
|
||||
import { mouseModeAtom, mouseStyleAtom, scrollIntervalAtom } from '@/jotai/mouse';
|
||||
import { MenuItem } from '@/components/menu-item.tsx';
|
||||
|
||||
import { Cursor } from './cursor.tsx';
|
||||
import { HidMode } from './hid-mode.tsx';
|
||||
import { MouseMode } from './mouse-mode.tsx';
|
||||
import { ResetHid } from './reset-hid.tsx';
|
||||
import { Speed } from './speed.tsx';
|
||||
|
||||
export const Mouse = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const setMouseStyle = useSetAtom(mouseStyleAtom);
|
||||
const setMouseMode = useSetAtom(mouseModeAtom);
|
||||
const setScrollInterval = useSetAtom(scrollIntervalAtom);
|
||||
|
||||
useEffect(() => {
|
||||
const mouseStyle = ls.getMouseStyle();
|
||||
@@ -28,12 +31,18 @@ export const Mouse = () => {
|
||||
if (mouseMode) {
|
||||
setMouseMode(mouseMode);
|
||||
}
|
||||
|
||||
const interval = ls.getMouseScrollInterval();
|
||||
if (interval) {
|
||||
setScrollInterval(interval);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const content = (
|
||||
<div className="flex flex-col space-y-1">
|
||||
<Cursor />
|
||||
<MouseMode />
|
||||
<Speed />
|
||||
<Divider style={{ margin: '10px 0' }} />
|
||||
|
||||
<HidMode />
|
||||
|
||||
66
web/src/pages/desktop/menu/mouse/speed.tsx
Normal file
66
web/src/pages/desktop/menu/mouse/speed.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Popover, Slider } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { GaugeIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as storage from '@/lib/localstorage.ts';
|
||||
import { scrollIntervalAtom } from '@/jotai/mouse.ts';
|
||||
|
||||
const MAX_INTERVAL = 300;
|
||||
|
||||
export const Speed = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [scrollInterval, setScrollInterval] = useAtom(scrollIntervalAtom);
|
||||
|
||||
const [scrollSpeed, setScrollSpeed] = useState(100);
|
||||
|
||||
useEffect(() => {
|
||||
const speed = interval2Speed(scrollInterval);
|
||||
setScrollSpeed(speed);
|
||||
}, [scrollInterval]);
|
||||
|
||||
function update(speed: number): void {
|
||||
const interval = speed2Interval(speed);
|
||||
setScrollInterval(interval);
|
||||
storage.setMouseScrollInterval(interval);
|
||||
}
|
||||
|
||||
function interval2Speed(interval: number) {
|
||||
if (interval === MAX_INTERVAL) {
|
||||
return 0;
|
||||
}
|
||||
return ((MAX_INTERVAL - interval) * 100) / MAX_INTERVAL;
|
||||
}
|
||||
|
||||
function speed2Interval(speed: number) {
|
||||
return MAX_INTERVAL - speed * (MAX_INTERVAL / 100);
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div className="h-[150px] w-[60px] py-3">
|
||||
<Slider
|
||||
vertical
|
||||
marks={{
|
||||
0: <span>{t('mouse.slow')}</span>,
|
||||
100: <span>{t('mouse.fast')}</span>
|
||||
}}
|
||||
range={false}
|
||||
included={false}
|
||||
step={10}
|
||||
defaultValue={scrollSpeed}
|
||||
onChange={update}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [14, 0] }}>
|
||||
<div className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70">
|
||||
<GaugeIcon size={18} />
|
||||
<span>{t('mouse.speed')}</span>
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom } from 'jotai';
|
||||
import { PenIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
import { hdmiEnabledAtom } from '@/jotai/hdmi.ts';
|
||||
|
||||
|
||||
export const HdmiState = () => {
|
||||
const { t } = useTranslation();
|
||||
const [hdmiMode, setHdmiMode] = useAtom(hdmiEnabledAtom);
|
||||
const [isPcie, setIsPcie] = useState(true);
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [_, setErrMsg] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
getHdmiState();
|
||||
api.getHardware().then((rsp) => {
|
||||
if (rsp.code === 0) {
|
||||
setIsPcie(rsp.data?.version === 'PCIE');
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
function getHdmiState() {
|
||||
setIsLoading(true);
|
||||
|
||||
api
|
||||
.getHdmiState()
|
||||
.then((rsp) => {
|
||||
if (rsp.code !== 0) {
|
||||
setErrMsg(rsp.msg);
|
||||
return;
|
||||
}
|
||||
|
||||
setHdmiMode(rsp.data.state);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function updateHDMIState() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const mode = hdmiMode === 'enabled' ? 'disabled' : 'enabled';
|
||||
|
||||
api
|
||||
.setHdmiState(mode)
|
||||
.then((rsp) => {
|
||||
setIsLoading(false);
|
||||
if (rsp.code !== 0) {
|
||||
console.log(rsp.msg);
|
||||
} else {
|
||||
setHdmiMode(mode);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
setIsLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPcie ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'flex h-[30px] cursor-pointer select-none items-center space-x-2 rounded px-3 hover:bg-neutral-700/70',
|
||||
'text-neutral-300'
|
||||
)}
|
||||
onClick={updateHDMIState}
|
||||
>
|
||||
<PenIcon size={18} />
|
||||
<span>{hdmiMode === 'enabled' ? t('screen.disableHdmi') : t('screen.enableHdmi')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -16,7 +16,6 @@ import { Quality } from './quality';
|
||||
import { Reset } from './reset.tsx';
|
||||
import { Resolution } from './resolution';
|
||||
import { VideoMode } from './video-mode.tsx';
|
||||
import { HdmiState } from './hdmi-state.tsx';
|
||||
|
||||
export const Screen = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -77,7 +76,6 @@ export const Screen = () => {
|
||||
<Fps fps={fps} setFps={setFps} />
|
||||
{videoMode === 'mjpeg' ? <FrameDetect /> : <Gop gop={gop} setGop={setGop} />}
|
||||
<Reset />
|
||||
<HdmiState />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const Reset = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPcie ? (
|
||||
{isPcie && (
|
||||
<div
|
||||
className="flex h-[30px] cursor-pointer items-center space-x-2 rounded px-3 text-neutral-300 hover:bg-neutral-700/70"
|
||||
onClick={reset}
|
||||
@@ -43,8 +43,6 @@ export const Reset = () => {
|
||||
/>
|
||||
<span className="select-none text-sm">{t('screen.resetHdmi')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Language } from './language.tsx';
|
||||
import { MenuBar } from './menu-bar.tsx';
|
||||
import { WebTitle } from './web-title.tsx';
|
||||
import { KeyboardLayout } from './keyboard-layout.tsx';
|
||||
|
||||
export const Appearance = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,7 +16,6 @@ export const Appearance = () => {
|
||||
<div className="flex flex-col space-y-6">
|
||||
<Language />
|
||||
<WebTitle />
|
||||
<KeyboardLayout />
|
||||
</div>
|
||||
<Divider />
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { KeyboardIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as ls from '@/lib/localstorage.ts';
|
||||
|
||||
export const KeyboardLayout = () => {
|
||||
const { t } = useTranslation();
|
||||
const [layout, setLayout] = useState<string>('default');
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const savedLayout = ls.getKeyboardLayout();
|
||||
if (savedLayout) {
|
||||
setLayout(savedLayout);
|
||||
}
|
||||
setIsLoading(false);
|
||||
}, []);
|
||||
|
||||
const options = [
|
||||
{ value: 'default', label: 'QWERTY (US)' },
|
||||
{ value: 'azerty', label: 'AZERTY (FR)' },
|
||||
{ value: 'mac', label: 'QWERTY (Mac)' },
|
||||
{ value: 'rus', label: 'Russian' }
|
||||
];
|
||||
|
||||
function changeLayout(value: string) {
|
||||
if (layout === value) return;
|
||||
setLayout(value);
|
||||
ls.setKeyboardLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-1">
|
||||
<KeyboardIcon size={16} />
|
||||
<span>{t('settings.appearance.keyboardLayout', { defaultValue: 'Keyboard Layout' })}</span>
|
||||
</div>
|
||||
|
||||
<Select
|
||||
value={layout}
|
||||
style={{ width: 180 }}
|
||||
options={options}
|
||||
loading={isLoading}
|
||||
onChange={changeLayout}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
77
web/src/pages/desktop/menu/settings/device/hdmi.tsx
Normal file
77
web/src/pages/desktop/menu/settings/device/hdmi.tsx
Normal file
@@ -0,0 +1,77 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Switch } from 'antd';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import * as api from '@/api/vm.ts';
|
||||
import { isHdmiEnabledAtom } from '@/jotai/screen.ts';
|
||||
|
||||
export const Hdmi = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [isHdmiEnabled, setIsHdmiEnabled] = useAtom(isHdmiEnabledAtom);
|
||||
|
||||
const [isPcie, setIsPcie] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
getHardware();
|
||||
getHdmiState();
|
||||
}, []);
|
||||
|
||||
async function getHardware() {
|
||||
const rsp = await api.getHardware();
|
||||
if (rsp.code !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsPcie(rsp.data?.version === 'PCIE');
|
||||
}
|
||||
|
||||
async function getHdmiState() {
|
||||
setIsLoading(true);
|
||||
|
||||
const rsp = await api.getHdmiState();
|
||||
if (rsp.code === 0) {
|
||||
setIsHdmiEnabled(rsp.data.enabled);
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
async function setHdmiState() {
|
||||
if (isLoading) return;
|
||||
setIsLoading(true);
|
||||
|
||||
const enabled = !isHdmiEnabled;
|
||||
|
||||
const rsp = await api.setHdmiState(enabled);
|
||||
if (rsp.code !== 0) {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setIsHdmiEnabled(enabled);
|
||||
setIsLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isPcie && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col">
|
||||
<span>HDMI</span>
|
||||
|
||||
<span className="text-xs text-neutral-500">
|
||||
{t('settings.device.hdmi.description')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Switch checked={isHdmiEnabled} loading={isLoading} onChange={setHdmiState} />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import * as api from '@/api/hid.ts';
|
||||
import { hidModeAtom } from '@/jotai/mouse.ts';
|
||||
|
||||
import { Advanced } from './advanced';
|
||||
import { Hdmi } from './hdmi.tsx';
|
||||
import { HidMode } from './hid-mode.tsx';
|
||||
import { Mdns } from './mdns.tsx';
|
||||
import { MouseJiggler } from './mouse-jiggler.tsx';
|
||||
@@ -39,6 +40,7 @@ export const Device = () => {
|
||||
<Tls />
|
||||
<Ssh />
|
||||
<Mdns />
|
||||
<Hdmi />
|
||||
|
||||
{hidMode === 'normal' ? <VirtualDevices /> : <HidMode />}
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useAtomValue } from 'jotai';
|
||||
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { scrollIntervalAtom } from '@/jotai/mouse.ts';
|
||||
import { resolutionAtom } from '@/jotai/screen.ts';
|
||||
|
||||
import { MouseButton, MouseEvent } from './constants';
|
||||
|
||||
export const Absolute = () => {
|
||||
const resolution = useAtomValue(resolutionAtom);
|
||||
const scrollInterval = useAtomValue(scrollIntervalAtom);
|
||||
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
|
||||
// listen mouse events
|
||||
useEffect(() => {
|
||||
@@ -74,6 +78,12 @@ export const Absolute = () => {
|
||||
const delta = Math.floor(event.deltaY);
|
||||
if (delta === 0) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastScrollTimeRef.current < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
|
||||
const data = [2, MouseEvent.Scroll, 0, 0, delta];
|
||||
client.send(data);
|
||||
}
|
||||
@@ -82,10 +92,11 @@ export const Absolute = () => {
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
canvas.removeEventListener('click', disableEvent);
|
||||
canvas.removeEventListener('contextmenu', disableEvent);
|
||||
};
|
||||
}, [resolution]);
|
||||
}, [resolution, scrollInterval]);
|
||||
|
||||
// disable default events
|
||||
function disableEvent(event: any) {
|
||||
|
||||
@@ -4,19 +4,21 @@ import { useAtomValue } from 'jotai';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { client } from '@/lib/websocket.ts';
|
||||
import { scrollIntervalAtom } from '@/jotai/mouse.ts';
|
||||
import { resolutionAtom } from '@/jotai/screen.ts';
|
||||
|
||||
import { MouseButton, MouseEvent } from './constants';
|
||||
|
||||
export const Relative = () => {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const resolution = useAtomValue(resolutionAtom);
|
||||
const scrollInterval = useAtomValue(scrollIntervalAtom);
|
||||
|
||||
const isLockedRef = useRef(false);
|
||||
const buttonRef = useRef<MouseButton>(MouseButton.None);
|
||||
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
const lastScrollTimeRef = useRef(0);
|
||||
|
||||
// listen mouse events
|
||||
useEffect(() => {
|
||||
@@ -113,6 +115,12 @@ export const Relative = () => {
|
||||
const delta = Math.floor(event.deltaY);
|
||||
if (delta === 0) return;
|
||||
|
||||
const currentTime = Date.now();
|
||||
if (currentTime - lastScrollTimeRef.current < scrollInterval) {
|
||||
return;
|
||||
}
|
||||
lastScrollTimeRef.current = currentTime;
|
||||
|
||||
const data = [2, MouseEvent.Scroll, 0, 0, delta];
|
||||
client.send(data);
|
||||
}
|
||||
@@ -124,9 +132,10 @@ export const Relative = () => {
|
||||
canvas.removeEventListener('mousemove', handleMouseMove);
|
||||
canvas.removeEventListener('mousedown', handleMouseDown);
|
||||
canvas.removeEventListener('mouseup', handleMouseUp);
|
||||
canvas.removeEventListener('wheel', handleWheel);
|
||||
canvas.removeEventListener('contextmenu', disableEvent);
|
||||
};
|
||||
}, [resolution]);
|
||||
}, [resolution, scrollInterval]);
|
||||
|
||||
// disable default events
|
||||
function disableEvent(event: any) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useEffect, useRef } from 'react';
|
||||
import clsx from 'clsx';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { w3cwebsocket as W3cWebSocket } from 'websocket';
|
||||
import Queue from 'yocto-queue';
|
||||
|
||||
import { getBaseUrl } from '@/lib/service.ts';
|
||||
import { mouseStyleAtom } from '@/jotai/mouse';
|
||||
@@ -11,11 +12,13 @@ export const H264Direct = () => {
|
||||
const resolution = useAtomValue(resolutionAtom);
|
||||
const mouseStyle = useAtomValue(mouseStyleAtom);
|
||||
|
||||
const canvasRef = useRef<any>(null);
|
||||
const decoderRef = useRef<any>(null);
|
||||
const frameQueueRef = useRef<VideoFrame[]>([]);
|
||||
const canvasRef = useRef<any>();
|
||||
const renderingRef = useRef(false);
|
||||
const decoderRef = useRef<VideoDecoder | null>(null);
|
||||
|
||||
const frameQueue = new Queue<VideoFrame>();
|
||||
|
||||
// init websocket
|
||||
useEffect(() => {
|
||||
if (!window.VideoDecoder) {
|
||||
console.log('Error: WebCodecs API not supported.');
|
||||
@@ -42,11 +45,9 @@ export const H264Direct = () => {
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
console.log(err);
|
||||
ws.onerror = () => {
|
||||
resetDecoder();
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
resetDecoder();
|
||||
};
|
||||
@@ -59,6 +60,7 @@ export const H264Direct = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// init video decoder
|
||||
function initializeDecoder() {
|
||||
if (!window.VideoDecoder) {
|
||||
return;
|
||||
@@ -69,7 +71,11 @@ export const H264Direct = () => {
|
||||
|
||||
const init = {
|
||||
output: (frame: VideoFrame) => {
|
||||
frameQueueRef.current.push(frame);
|
||||
frameQueue.enqueue(frame);
|
||||
if (frameQueue.size >= 10) {
|
||||
frameQueue.dequeue()?.close();
|
||||
}
|
||||
|
||||
if (!renderingRef.current) {
|
||||
requestAnimationFrame(processFrameQueue);
|
||||
}
|
||||
@@ -94,6 +100,7 @@ export const H264Direct = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// decode video chunk
|
||||
function decode(message: any) {
|
||||
const byteString = atob(message.data);
|
||||
const byteArray = new Uint8Array(byteString.length);
|
||||
@@ -108,7 +115,7 @@ export const H264Direct = () => {
|
||||
});
|
||||
|
||||
try {
|
||||
decoderRef.current.decode(chunk);
|
||||
decoderRef.current?.decode(chunk);
|
||||
} catch (err: any) {
|
||||
if (err.name === 'TypeError' || err.message.includes('configured')) {
|
||||
resetDecoder();
|
||||
@@ -116,22 +123,18 @@ export const H264Direct = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const processFrameQueue = () => {
|
||||
function processFrameQueue() {
|
||||
renderingRef.current = true;
|
||||
const frame = frameQueueRef.current.shift();
|
||||
|
||||
const frame = frameQueue.dequeue();
|
||||
if (frame) {
|
||||
renderFrame(frame);
|
||||
}
|
||||
|
||||
if (frameQueueRef.current.length === 0) {
|
||||
renderingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
requestAnimationFrame(processFrameQueue);
|
||||
};
|
||||
}
|
||||
|
||||
const renderFrame = (frame: any) => {
|
||||
function renderFrame(frame: VideoFrame) {
|
||||
const canvas = canvasRef.current;
|
||||
const ctx = canvas?.getContext('2d');
|
||||
if (!canvas || !ctx) {
|
||||
@@ -146,9 +149,10 @@ export const H264Direct = () => {
|
||||
|
||||
ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
|
||||
frame.close();
|
||||
};
|
||||
}
|
||||
|
||||
const resetDecoder = () => {
|
||||
// reset video decoder
|
||||
function resetDecoder() {
|
||||
if (decoderRef.current && decoderRef.current.state !== 'closed') {
|
||||
try {
|
||||
decoderRef.current.close();
|
||||
@@ -160,9 +164,8 @@ export const H264Direct = () => {
|
||||
decoderRef.current = null;
|
||||
renderingRef.current = false;
|
||||
|
||||
frameQueueRef.current.forEach((frame) => frame.close());
|
||||
frameQueueRef.current = [];
|
||||
};
|
||||
Array.from(frameQueue.drain()).forEach((frame) => frame.close());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-screen w-screen items-start justify-center xl:items-center">
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { Select } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { getKeyboardLayout, setKeyboardLayout } from '@/lib/localstorage.ts';
|
||||
|
||||
export const KeyboardLayoutSetting = () => {
|
||||
const { t } = useTranslation();
|
||||
const [layout, setLayout] = useState(getKeyboardLayout() || 'default');
|
||||
|
||||
function handleChange(value: string) {
|
||||
setLayout(value);
|
||||
setKeyboardLayout(value);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<label className="text-sm font-medium">{t('settings.keyboard.layout')}</label>
|
||||
<Select
|
||||
value={layout}
|
||||
onChange={handleChange}
|
||||
options={[
|
||||
{ value: 'default', label: 'QWERTY (Win)' },
|
||||
{ value: 'mac', label: 'QWERTY (Mac)' },
|
||||
{ value: 'azerty', label: 'AZERTY (Win)' }
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user