mirror of
https://github.com/sipeed/NanoKVM.git
synced 2026-09-11 00:22:56 -05:00
@@ -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',
|
||||
|
||||
@@ -46,6 +46,7 @@ const zh = {
|
||||
finishBtn: '完成'
|
||||
},
|
||||
screen: {
|
||||
scale: '缩放',
|
||||
title: '屏幕',
|
||||
video: '视频模式',
|
||||
videoDirectTips: '该模式需启用 HTTPS,请前往「设置 - 设备」中开启',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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} />
|
||||
|
||||
58
web/src/pages/desktop/menu/screen/scale.tsx
Normal file
58
web/src/pages/desktop/menu/screen/scale.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user