From 8de298d2456f92e9ee5ef0e4251fb59818f0adc7 Mon Sep 17 00:00:00 2001 From: Guoguo Date: Mon, 8 Dec 2025 19:17:35 +0800 Subject: [PATCH] feat: add video scale Patch from https://github.com/sipeed/NanoKVM-USB/pull/50 --- web/src/i18n/locales/en.ts | 1 + web/src/i18n/locales/zh.ts | 1 + web/src/jotai/screen.ts | 2 + web/src/lib/localstorage.ts | 13 +++++ web/src/pages/desktop/menu/screen/index.tsx | 2 + web/src/pages/desktop/menu/screen/scale.tsx | 58 ++++++++++++++++++++ web/src/pages/desktop/screen/h264-direct.tsx | 23 ++++++-- web/src/pages/desktop/screen/h264-webrtc.tsx | 23 ++++++-- web/src/pages/desktop/screen/mjpeg.tsx | 20 ++++++- 9 files changed, 128 insertions(+), 15 deletions(-) create mode 100644 web/src/pages/desktop/menu/screen/scale.tsx diff --git a/web/src/i18n/locales/en.ts b/web/src/i18n/locales/en.ts index b1fc6f5..04ea386 100644 --- a/web/src/i18n/locales/en.ts +++ b/web/src/i18n/locales/en.ts @@ -47,6 +47,7 @@ const en = { finishBtn: 'Finished' }, screen: { + scale: 'Scale', title: 'Screen', video: 'Video Mode', videoDirectTips: 'Enable HTTPS in "Settings > Device" to use this mode', diff --git a/web/src/i18n/locales/zh.ts b/web/src/i18n/locales/zh.ts index cbf0c06..e64d0da 100644 --- a/web/src/i18n/locales/zh.ts +++ b/web/src/i18n/locales/zh.ts @@ -46,6 +46,7 @@ const zh = { finishBtn: '完成' }, screen: { + scale: '缩放', title: '屏幕', video: '视频模式', videoDirectTips: '该模式需启用 HTTPS,请前往「设置 - 设备」中开启', diff --git a/web/src/jotai/screen.ts b/web/src/jotai/screen.ts index 16f75a2..a385c5b 100644 --- a/web/src/jotai/screen.ts +++ b/web/src/jotai/screen.ts @@ -10,5 +10,7 @@ export const isHdmiEnabledAtom = atom(true); // mjpeg: stream JPEG over HTTP export const videoModeAtom = atom(''); +export const videoScaleAtom = atom(1.0); + // browser screen resolution export const resolutionAtom = atom(null); diff --git a/web/src/lib/localstorage.ts b/web/src/lib/localstorage.ts index b6cfef0..fe6dc21 100644 --- a/web/src/lib/localstorage.ts +++ b/web/src/lib/localstorage.ts @@ -2,6 +2,7 @@ import { Resolution } from '@/types'; const LANGUAGE_KEY = 'nano-kvm-language'; const VIDEO_MODE_KEY = 'nano-kvm-vide-mode'; +const VIDEO_SCALE_KEY = 'nano-kvm-video-scale'; const WEB_RESOLUTION_KEY = 'nano-kvm-web-resolution'; const FPS_KEY = 'nano-kvm-fps'; const QUALITY_KEY = 'nano-kvm-quality'; @@ -65,6 +66,18 @@ export function setVideoMode(mode: string) { localStorage.setItem(VIDEO_MODE_KEY, mode); } +export function getVideoScale(): number | null { + const scale = localStorage.getItem(VIDEO_SCALE_KEY) + if (scale && Number(scale)) { + return Number(scale) + } + return null +} + +export function setVideoScale(scale: number): void { + localStorage.setItem(VIDEO_SCALE_KEY, String(scale)) +} + export function getResolution(): Resolution | null { const resolution = localStorage.getItem(WEB_RESOLUTION_KEY); if (resolution) { diff --git a/web/src/pages/desktop/menu/screen/index.tsx b/web/src/pages/desktop/menu/screen/index.tsx index 8297e67..203c7b6 100644 --- a/web/src/pages/desktop/menu/screen/index.tsx +++ b/web/src/pages/desktop/menu/screen/index.tsx @@ -15,6 +15,7 @@ import { Gop } from './gop.tsx'; import { Quality } from './quality'; import { Reset } from './reset.tsx'; import { Resolution } from './resolution'; +import { Scale } from './scale'; import { VideoMode } from './video-mode.tsx'; export const Screen = () => { @@ -70,6 +71,7 @@ export const Screen = () => { const content = (
+ diff --git a/web/src/pages/desktop/menu/screen/scale.tsx b/web/src/pages/desktop/menu/screen/scale.tsx new file mode 100644 index 0000000..c79731c --- /dev/null +++ b/web/src/pages/desktop/menu/screen/scale.tsx @@ -0,0 +1,58 @@ +import { ReactElement, useEffect } from 'react' +import { Popover, Slider } from 'antd' +import { useAtom } from 'jotai' +import { ScalingIcon } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +import { videoScaleAtom } from '@/jotai/screen.ts' +import * as storage from '@/lib/localstorage.ts' + +export const Scale = (): ReactElement => { + const { t } = useTranslation() + + const [videoScale, setVideoScale] = useAtom(videoScaleAtom) + + useEffect(() => { + const scale = storage.getVideoScale() + if (scale) { + setVideoScale(scale) + } + }, [setVideoScale]) + + async function updateScale(scale: number): Promise { + setVideoScale(scale) + storage.setVideoScale(scale) + } + + const content = ( +
+ x0.5, + 1: x1.0, + 1.5: x1.5, + 2: x2.0 + }} + range={false} + included={false} + min={0.5} + max={2} + step={0.1} + defaultValue={videoScale} + onChange={updateScale} + /> +
+ ) + + return ( + +
+
+ +
+ {t('screen.scale')} +
+
+ ) +} \ No newline at end of file diff --git a/web/src/pages/desktop/screen/h264-direct.tsx b/web/src/pages/desktop/screen/h264-direct.tsx index 4096909..971037e 100644 --- a/web/src/pages/desktop/screen/h264-direct.tsx +++ b/web/src/pages/desktop/screen/h264-direct.tsx @@ -1,21 +1,30 @@ import { useEffect, useRef } from 'react'; import clsx from 'clsx'; -import { useAtomValue } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { w3cwebsocket as W3cWebSocket } from 'websocket'; +import * as storage from '@/lib/localstorage.ts'; import { getBaseUrl } from '@/lib/service.ts'; import { mouseStyleAtom } from '@/jotai/mouse'; -import { resolutionAtom } from '@/jotai/screen.ts'; +import { resolutionAtom, videoScaleAtom } from '@/jotai/screen.ts'; import DirectWorker from './direct.worker.ts?worker'; export const H264Direct = () => { const resolution = useAtomValue(resolutionAtom); const mouseStyle = useAtomValue(mouseStyleAtom); + const [videoScale, setVideoScale] = useAtom(videoScaleAtom); const canvasRef = useRef(null); const workerRef = useRef(null); + useEffect(() => { + const scale = storage.getVideoScale() + if (scale) { + setVideoScale(scale) + } + }, [setVideoScale]) + useEffect(() => { if (!window.VideoDecoder) { console.log('Error: WebCodecs API not supported.'); @@ -65,11 +74,13 @@ export const H264Direct = () => { id="screen" ref={canvasRef} className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)} - style={ - resolution?.width + style={{ + transform: `scale(${videoScale})`, + transformOrigin: 'center', + ...(resolution?.width ? { width: resolution.width, height: resolution.height, objectFit: 'cover' } - : { maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' } - } + : { maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }), + }} >
); diff --git a/web/src/pages/desktop/screen/h264-webrtc.tsx b/web/src/pages/desktop/screen/h264-webrtc.tsx index 3d683d8..8c7e9c4 100644 --- a/web/src/pages/desktop/screen/h264-webrtc.tsx +++ b/web/src/pages/desktop/screen/h264-webrtc.tsx @@ -1,16 +1,18 @@ import { useEffect, useRef, useState } from 'react'; import { Spin } from 'antd'; import clsx from 'clsx'; -import { useAtomValue } from 'jotai'; +import { useAtom, useAtomValue } from 'jotai'; import { w3cwebsocket as W3cWebSocket } from 'websocket'; import { getBaseUrl } from '@/lib/service.ts'; import { mouseStyleAtom } from '@/jotai/mouse.ts'; -import { resolutionAtom } from '@/jotai/screen.ts'; +import { resolutionAtom, videoScaleAtom } from '@/jotai/screen.ts'; +import * as storage from '@/lib/localstorage.ts' export const H264Webrtc = () => { const resolution = useAtomValue(resolutionAtom); const mouseStyle = useAtomValue(mouseStyleAtom); + const [videoScale, setVideoScale] = useAtom(videoScaleAtom); const [isLoading, setIsLoading] = useState(true); @@ -170,6 +172,13 @@ export const H264Webrtc = () => { }; }, []); + useEffect(() => { + const scale = storage.getVideoScale() + if (scale) { + setVideoScale(scale) + } + }, [setVideoScale]) + return (
@@ -177,11 +186,13 @@ export const H264Webrtc = () => { id="screen" ref={videoRef} className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)} - style={ - resolution?.width + style={{ + transform: `scale(${videoScale})`, + transformOrigin: 'center', + ...(resolution?.width ? { width: resolution.width, height: resolution.height, objectFit: 'cover' } - : { maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' } - } + : { maxWidth: '100%', maxHeight: '100%', objectFit: 'scale-down' }) + }} muted autoPlay playsInline diff --git a/web/src/pages/desktop/screen/mjpeg.tsx b/web/src/pages/desktop/screen/mjpeg.tsx index 92a708c..30a464f 100644 --- a/web/src/pages/desktop/screen/mjpeg.tsx +++ b/web/src/pages/desktop/screen/mjpeg.tsx @@ -1,18 +1,21 @@ import { useEffect } from 'react'; import { Image } from 'antd'; import clsx from 'clsx'; -import { useAtomValue } from 'jotai'; +import { useAtomValue, useAtom } from 'jotai'; import MonitorXIcon from '@/assets/images/monitor-x.svg'; import { stopFrameDetect } from '@/api/stream.ts'; import { getFrameDetect } from '@/lib/localstorage.ts'; import { getBaseUrl } from '@/lib/service.ts'; import { mouseStyleAtom } from '@/jotai/mouse.ts'; -import { resolutionAtom } from '@/jotai/screen.ts'; +import { resolutionAtom, videoScaleAtom } from '@/jotai/screen.ts'; +import * as storage from '@/lib/localstorage.ts' + export const Mjpeg = () => { const resolution = useAtomValue(resolutionAtom); const mouseStyle = useAtomValue(mouseStyleAtom); + const [videoScale, setVideoScale] = useAtom(videoScaleAtom); useEffect(() => { // stop frame detect for a while @@ -22,8 +25,19 @@ export const Mjpeg = () => { } }, [resolution]); + useEffect(() => { + const scale = storage.getVideoScale() + if (scale) { + setVideoScale(scale) + } + }, [setVideoScale]) + return ( -
+