Merge pull request #685 from imguoguo/add-scale

feat: add video scale
This commit is contained in:
wenjie
2025-12-23 15:19:02 +08:00
committed by GitHub
9 changed files with 128 additions and 15 deletions

View File

@@ -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',

View File

@@ -46,6 +46,7 @@ const zh = {
finishBtn: '完成'
},
screen: {
scale: '缩放',
title: '屏幕',
video: '视频模式',
videoDirectTips: '该模式需启用 HTTPS请前往「设置 - 设备」中开启',

View File

@@ -10,5 +10,7 @@ export const isHdmiEnabledAtom = atom(true);
// mjpeg: stream JPEG over HTTP
export const videoModeAtom = atom('');
export const videoScaleAtom = atom<number>(1.0);
// browser screen resolution
export const resolutionAtom = atom<Resolution | null>(null);

View File

@@ -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) {

View File

@@ -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 = (
<div className="flex flex-col space-y-1">
<VideoMode />
<Scale />
<Resolution />
<Quality quality={quality} setQuality={setQuality} />
<Fps fps={fps} setFps={setFps} />

View File

@@ -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<void> {
setVideoScale(scale)
storage.setVideoScale(scale)
}
const content = (
<div className="h-[150px] w-[60px] py-3">
<Slider
vertical
marks={{
0.5: <span>x0.5</span>,
1: <span>x1.0</span>,
1.5: <span>x1.5</span>,
2: <span>x2.0</span>
}}
range={false}
included={false}
min={0.5}
max={2}
step={0.1}
defaultValue={videoScale}
onChange={updateScale}
/>
</div>
)
return (
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [13, 0] }}>
<div className="flex h-[30px] cursor-pointer items-center space-x-1 rounded px-3 text-neutral-300 hover:bg-neutral-700/50">
<div className="flex h-[14px] w-[20px] items-end">
<ScalingIcon size={16} />
</div>
<span>{t('screen.scale')}</span>
</div>
</Popover>
)
}

View File

@@ -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<HTMLCanvasElement>(null);
const workerRef = useRef<Worker | null>(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' }),
}}
></canvas>
</div>
);

View File

@@ -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 (
<Spin size="large" tip="Loading" spinning={isLoading}>
<div className="flex h-screen w-screen items-start justify-center xl:items-center">
@@ -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

View File

@@ -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 (
<div className="flex h-screen w-screen items-start justify-center xl:items-center">
<div className="flex h-screen w-screen items-start justify-center xl:items-center"
style={{
transform: `scale(${videoScale})`,
transformOrigin: 'center'
}}>
<Image
id="screen"
className={clsx('block min-h-[480px] min-w-[640px] select-none', mouseStyle)}