{t('screen.video')}
diff --git a/web/src/pages/desktop/menu/settings/appearance/index.tsx b/web/src/pages/desktop/menu/settings/appearance/index.tsx
index 6ef4cdf..1c24d20 100644
--- a/web/src/pages/desktop/menu/settings/appearance/index.tsx
+++ b/web/src/pages/desktop/menu/settings/appearance/index.tsx
@@ -13,13 +13,13 @@ export const Appearance = () => {
{t('settings.appearance.title')}
-
+
+
+
+
-
-
-
>
);
};
diff --git a/web/src/pages/desktop/menu/settings/device/index.tsx b/web/src/pages/desktop/menu/settings/device/index.tsx
index e9f6392..00d1c31 100644
--- a/web/src/pages/desktop/menu/settings/device/index.tsx
+++ b/web/src/pages/desktop/menu/settings/device/index.tsx
@@ -13,6 +13,7 @@ import { MouseJiggler } from './mouse-jiggler.tsx';
import { Oled } from './oled.tsx';
import { Reboot } from './reboot.tsx';
import { Ssh } from './ssh.tsx';
+import { Tls } from './tls.tsx';
import { VirtualDevices } from './virtual-devices.tsx';
import { Wifi } from './wifi.tsx';
@@ -35,11 +36,17 @@ export const Device = () => {
-
-
+
+
{hidMode === 'normal' ?
:
}
+
+
+
+
diff --git a/web/src/pages/desktop/menu/settings/device/tls.tsx b/web/src/pages/desktop/menu/settings/device/tls.tsx
new file mode 100644
index 0000000..8fa477d
--- /dev/null
+++ b/web/src/pages/desktop/menu/settings/device/tls.tsx
@@ -0,0 +1,69 @@
+import { useEffect, useState } from 'react';
+import { Switch, Tooltip } from 'antd';
+import { CircleAlertIcon } from 'lucide-react';
+import { useTranslation } from 'react-i18next';
+
+import * as api from '@/api/vm.ts';
+
+export const Tls = () => {
+ const { t } = useTranslation();
+
+ const [isEnabled, setIsEnabled] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+
+ useEffect(() => {
+ setIsEnabled(window.location.protocol === 'https:');
+ }, []);
+
+ async function update() {
+ if (isLoading) return;
+ setIsLoading(true);
+
+ const enable = !isEnabled;
+
+ const seconds = enable ? 30 : 10;
+ setTimeout(() => {
+ reload(enable);
+ }, seconds * 1000);
+
+ try {
+ const rsp = await api.setTLS(enable);
+ if (rsp.code === 0) {
+ setIsEnabled(enable);
+ }
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ function reload(enable: boolean) {
+ if (!enable) {
+ const target = window.location.href.replace(/^https:/, 'http:');
+ window.open(target, '_blank');
+ }
+
+ window.location.reload();
+ }
+
+ return (
+
+
+
+ HTTPS
+
+
+
+
+
+
{t('settings.device.tls.description')}
+
+
+
+
+ );
+};
diff --git a/web/src/pages/desktop/screen/h264-direct.tsx b/web/src/pages/desktop/screen/h264-direct.tsx
new file mode 100644
index 0000000..6412b67
--- /dev/null
+++ b/web/src/pages/desktop/screen/h264-direct.tsx
@@ -0,0 +1,181 @@
+import { useEffect, useRef } from 'react';
+import clsx from 'clsx';
+import { useAtomValue } from 'jotai';
+import { w3cwebsocket as W3cWebSocket } from 'websocket';
+
+import { getBaseUrl } from '@/lib/service.ts';
+import { mouseStyleAtom } from '@/jotai/mouse';
+import { resolutionAtom } from '@/jotai/screen.ts';
+
+export const H264Direct = () => {
+ const resolution = useAtomValue(resolutionAtom);
+ const mouseStyle = useAtomValue(mouseStyleAtom);
+
+ const canvasRef = useRef
(null);
+ const decoderRef = useRef(null);
+ const frameQueueRef = useRef([]);
+ const renderingRef = useRef(false);
+
+ useEffect(() => {
+ if (!window.VideoDecoder) {
+ console.log('Error: WebCodecs API not supported.');
+ return;
+ }
+
+ const url = `${getBaseUrl('ws')}/api/stream/h264/direct`;
+ const ws = new W3cWebSocket(url);
+ ws.binaryType = 'arraybuffer';
+
+ ws.onmessage = (event) => {
+ try {
+ const message = JSON.parse(event.data as string);
+
+ if (!decoderRef.current && message.isKeyFrame) {
+ initializeDecoder();
+ }
+
+ if (decoderRef.current?.state === 'configured') {
+ decode(message);
+ }
+ } catch (error) {
+ console.error('Error processing WebSocket message:', error);
+ }
+ };
+
+ ws.onerror = (err) => {
+ console.log(err);
+ resetDecoder();
+ };
+
+ ws.onclose = () => {
+ resetDecoder();
+ };
+
+ return () => {
+ if (ws.readyState === 1) {
+ ws.close();
+ }
+ resetDecoder();
+ };
+ }, []);
+
+ function initializeDecoder() {
+ if (!window.VideoDecoder) {
+ return;
+ }
+ if (decoderRef.current && decoderRef.current.state !== 'unconfigured') {
+ return;
+ }
+
+ const init = {
+ output: (frame: VideoFrame) => {
+ frameQueueRef.current.push(frame);
+ if (!renderingRef.current) {
+ requestAnimationFrame(processFrameQueue);
+ }
+ },
+ error: () => {
+ resetDecoder();
+ }
+ };
+
+ const config = {
+ codec: 'avc1.42E01F',
+ optimizeForLatency: true
+ };
+
+ try {
+ const decoder = new VideoDecoder(init);
+ decoder.configure(config);
+ decoderRef.current = decoder;
+ } catch (err) {
+ console.log(err);
+ decoderRef.current = null;
+ }
+ }
+
+ function decode(message: any) {
+ const byteString = atob(message.data);
+ const byteArray = new Uint8Array(byteString.length);
+ for (let i = 0; i < byteString.length; i++) {
+ byteArray[i] = byteString.charCodeAt(i);
+ }
+
+ const chunk = new EncodedVideoChunk({
+ type: message.isKeyFrame ? 'key' : 'delta',
+ timestamp: message.timestamp,
+ data: byteArray
+ });
+
+ try {
+ decoderRef.current.decode(chunk);
+ } catch (err: any) {
+ if (err.name === 'TypeError' || err.message.includes('configured')) {
+ resetDecoder();
+ }
+ }
+ }
+
+ const processFrameQueue = () => {
+ renderingRef.current = true;
+ const frame = frameQueueRef.current.shift();
+ if (frame) {
+ renderFrame(frame);
+ }
+
+ if (frameQueueRef.current.length === 0) {
+ renderingRef.current = false;
+ return;
+ }
+
+ requestAnimationFrame(processFrameQueue);
+ };
+
+ const renderFrame = (frame: any) => {
+ const canvas = canvasRef.current;
+ const ctx = canvas?.getContext('2d');
+ if (!canvas || !ctx) {
+ frame.close();
+ return;
+ }
+
+ if (canvas.width !== frame.displayWidth || canvas.height !== frame.displayHeight) {
+ canvas.width = frame.displayWidth;
+ canvas.height = frame.displayHeight;
+ }
+
+ ctx.drawImage(frame, 0, 0, canvas.width, canvas.height);
+ frame.close();
+ };
+
+ const resetDecoder = () => {
+ if (decoderRef.current && decoderRef.current.state !== 'closed') {
+ try {
+ decoderRef.current.close();
+ } catch (err) {
+ console.log(err);
+ }
+ }
+
+ decoderRef.current = null;
+ renderingRef.current = false;
+
+ frameQueueRef.current.forEach((frame) => frame.close());
+ frameQueueRef.current = [];
+ };
+
+ return (
+
+
+
+ );
+};
diff --git a/web/src/pages/desktop/screen/h264.tsx b/web/src/pages/desktop/screen/h264-webrtc.tsx
similarity index 95%
rename from web/src/pages/desktop/screen/h264.tsx
rename to web/src/pages/desktop/screen/h264-webrtc.tsx
index aefec85..68221fc 100644
--- a/web/src/pages/desktop/screen/h264.tsx
+++ b/web/src/pages/desktop/screen/h264-webrtc.tsx
@@ -8,7 +8,7 @@ import { getBaseUrl } from '@/lib/service.ts';
import { mouseStyleAtom } from '@/jotai/mouse.ts';
import { resolutionAtom } from '@/jotai/screen.ts';
-export const H264 = () => {
+export const H264Webrtc = () => {
const resolution = useAtomValue(resolutionAtom);
const mouseStyle = useAtomValue(mouseStyleAtom);
@@ -88,7 +88,7 @@ export const H264 = () => {
setTimeout(() => {
setIsLoading(false);
- }, 15 * 1000);
+ }, 5 * 1000);
return () => {
ws.close();
@@ -101,7 +101,7 @@ export const H264 = () => {
return (
-
+