Files
desktop/src/components/led-strip-configuration/screen-view-websocket.tsx
Ivan Li 90cace679b Implement synchronized LED strip highlighting with theme colors and clean up debug logs
- Add three-way synchronized highlighting between LED strip components
- Implement hover and selection state synchronization across display borders, sorter, and control panels
- Replace hardcoded colors with DaisyUI theme colors (primary, warning, base-content)
- Use background highlighting for sorter to prevent interface jittering
- Reduce LED strip width from 24px to 20px for better visual appearance
- Clean up console.log statements and debug output for production readiness
- Maintain layout stability by avoiding size changes in highlighting effects
2025-07-05 14:32:31 +08:00

256 lines
6.6 KiB
TypeScript

import {
Component,
createEffect,
createSignal,
JSX,
onCleanup,
onMount,
splitProps,
} from 'solid-js';
import { invoke } from '@tauri-apps/api/core';
type ScreenViewWebSocketProps = {
displayId: number;
width?: number;
height?: number;
quality?: number;
} & JSX.HTMLAttributes<HTMLDivElement>;
export const ScreenViewWebSocket: Component<ScreenViewWebSocketProps> = (props) => {
const [localProps, rootProps] = splitProps(props, ['displayId', 'width', 'height', 'quality']);
let canvas: HTMLCanvasElement;
let root: HTMLDivElement;
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
const [drawInfo, setDrawInfo] = createSignal({
drawX: 0,
drawY: 0,
drawWidth: 0,
drawHeight: 0,
});
const [connectionStatus, setConnectionStatus] = createSignal<'connecting' | 'connected' | 'disconnected' | 'error'>('disconnected');
const [frameCount, setFrameCount] = createSignal(0);
const [lastFrameTime, setLastFrameTime] = createSignal(0);
const [fps, setFps] = createSignal(0);
let websocket: WebSocket | null = null;
let reconnectTimeout: number | null = null;
let isMounted = true;
// Performance monitoring
let frameTimestamps: number[] = [];
const connectWebSocket = () => {
if (!isMounted) {
return;
}
const wsUrl = `ws://127.0.0.1:8765`;
setConnectionStatus('connecting');
websocket = new WebSocket(wsUrl);
websocket.binaryType = 'arraybuffer';
websocket.onopen = () => {
setConnectionStatus('connected');
// Send initial configuration
const config = {
display_id: localProps.displayId,
width: localProps.width || 320,
height: localProps.height || 180,
quality: localProps.quality || 50
};
websocket?.send(JSON.stringify(config));
};
websocket.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
handleJpegFrame(new Uint8Array(event.data));
}
};
websocket.onclose = (event) => {
setConnectionStatus('disconnected');
websocket = null;
// Auto-reconnect after 2 seconds if component is still mounted
if (isMounted && !reconnectTimeout) {
reconnectTimeout = window.setTimeout(() => {
reconnectTimeout = null;
connectWebSocket();
}, 2000);
}
};
websocket.onerror = (error) => {
setConnectionStatus('error');
};
};
const handleJpegFrame = async (jpegData: Uint8Array) => {
const _ctx = ctx();
if (!_ctx) return;
try {
// Update performance metrics
const now = performance.now();
frameTimestamps.push(now);
// Keep only last 30 frames for FPS calculation
if (frameTimestamps.length > 30) {
frameTimestamps = frameTimestamps.slice(-30);
}
// Calculate FPS
if (frameTimestamps.length >= 2) {
const timeSpan = frameTimestamps[frameTimestamps.length - 1] - frameTimestamps[0];
if (timeSpan > 0) {
const currentFps = Math.round((frameTimestamps.length - 1) * 1000 / timeSpan);
setFps(Math.max(0, currentFps)); // Ensure FPS is never negative
}
}
setFrameCount(prev => prev + 1);
setLastFrameTime(now);
// Create blob from JPEG data
const blob = new Blob([jpegData], { type: 'image/jpeg' });
const imageUrl = URL.createObjectURL(blob);
// Create image element
const img = new Image();
img.onload = () => {
const { drawX, drawY, drawWidth, drawHeight } = drawInfo();
// Clear canvas
_ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw image
_ctx.drawImage(img, drawX, drawY, drawWidth, drawHeight);
// Clean up
URL.revokeObjectURL(imageUrl);
};
img.onerror = () => {
console.error('Failed to load JPEG image');
URL.revokeObjectURL(imageUrl);
};
img.src = imageUrl;
} catch (error) {
console.error('Error handling JPEG frame:', error);
}
};
const resetSize = () => {
// Set canvas size first
canvas.width = root.clientWidth;
canvas.height = root.clientHeight;
// Use a default aspect ratio if canvas dimensions are invalid
const aspectRatio = (canvas.width > 0 && canvas.height > 0)
? canvas.width / canvas.height
: 16 / 9; // Default 16:9 aspect ratio
const drawWidth = Math.round(
Math.min(root.clientWidth, root.clientHeight * aspectRatio),
);
const drawHeight = Math.round(
Math.min(root.clientHeight, root.clientWidth / aspectRatio),
);
const drawX = Math.round((root.clientWidth - drawWidth) / 2);
const drawY = Math.round((root.clientHeight - drawHeight) / 2);
setDrawInfo({
drawX,
drawY,
drawWidth,
drawHeight,
});
};
const disconnect = () => {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (websocket) {
websocket.close();
websocket = null;
}
};
// Initialize canvas and resize observer
onMount(() => {
const context = canvas.getContext('2d');
setCtx(context);
// Initial size setup
resetSize();
const resizeObserver = new ResizeObserver(() => {
resetSize();
});
resizeObserver.observe(root);
// Connect WebSocket
connectWebSocket();
onCleanup(() => {
isMounted = false;
disconnect();
resizeObserver?.unobserve(root);
});
});
// Status indicator
const getStatusColor = () => {
switch (connectionStatus()) {
case 'connected': return '#10b981'; // green
case 'connecting': return '#f59e0b'; // yellow
case 'error': return '#ef4444'; // red
default: return '#6b7280'; // gray
}
};
return (
<div
ref={root!}
{...rootProps}
class={'overflow-hidden h-full w-full relative ' + (rootProps.class || '')}
>
<canvas
ref={canvas!}
style={{
display: 'block',
width: '100%',
height: '100%',
'background-color': '#f0f0f0'
}}
/>
{/* Status indicator */}
<div class="absolute top-2 right-2 flex items-center gap-2 bg-black bg-opacity-50 text-white px-2 py-1 rounded text-xs">
<div
class="w-2 h-2 rounded-full"
style={{ 'background-color': getStatusColor() }}
/>
<span>{connectionStatus()}</span>
{connectionStatus() === 'connected' && (
<span>| {fps()} FPS | {frameCount()} frames</span>
)}
</div>
{rootProps.children}
</div>
);
};