- Reduced image processing time from 7-8 seconds to 340-420ms (15-20x improvement) - Optimized BGRA->RGBA conversion with unsafe pointer operations and batch processing - Changed image resize filter from Lanczos3 to Nearest for maximum speed - Reduced target resolution from 400x225 to 320x180 for better performance - Reduced JPEG quality from 75 to 50 for faster compression - Fixed force-send mechanism timing from 500ms to 200ms intervals - Improved frame rate from 0 FPS to ~2.5 FPS - Cleaned up extensive debug logging and performance instrumentation - Removed unused imports and variables to reduce compiler warnings
238 lines
6.7 KiB
TypeScript
238 lines
6.7 KiB
TypeScript
import {
|
|
Component,
|
|
createEffect,
|
|
createSignal,
|
|
JSX,
|
|
onCleanup,
|
|
onMount,
|
|
splitProps,
|
|
} from 'solid-js';
|
|
import { ScreenViewWebSocket } from './screen-view-websocket';
|
|
|
|
type ScreenViewProps = {
|
|
displayId: number;
|
|
useWebSocket?: boolean;
|
|
} & JSX.HTMLAttributes<HTMLDivElement>;
|
|
|
|
export const ScreenView: Component<ScreenViewProps> = (props) => {
|
|
const [localProps, rootProps] = splitProps(props, ['displayId', 'useWebSocket']);
|
|
|
|
// Use WebSocket by default for better performance
|
|
if (localProps.useWebSocket !== false) {
|
|
return <ScreenViewWebSocket displayId={localProps.displayId} {...rootProps} />;
|
|
}
|
|
|
|
// Fallback to HTTP polling (legacy mode)
|
|
let canvas: HTMLCanvasElement;
|
|
let root: HTMLDivElement;
|
|
const [ctx, setCtx] = createSignal<CanvasRenderingContext2D | null>(null);
|
|
|
|
// Cache temporary canvas for scaling
|
|
let tempCanvas: HTMLCanvasElement | null = null;
|
|
let tempCtx: CanvasRenderingContext2D | null = null;
|
|
const [drawInfo, setDrawInfo] = createSignal({
|
|
drawX: 0,
|
|
drawY: 0,
|
|
drawWidth: 0,
|
|
drawHeight: 0,
|
|
});
|
|
const [imageData, setImageData] = createSignal<{
|
|
buffer: Uint8ClampedArray;
|
|
width: number;
|
|
height: number;
|
|
} | null>(null);
|
|
const [hidden, setHidden] = createSignal(false);
|
|
const [isLoading, setIsLoading] = createSignal(false);
|
|
let isMounted = true;
|
|
|
|
// Fetch screenshot data from backend with frame-based rendering
|
|
const fetchScreenshot = async () => {
|
|
if (isLoading()) {
|
|
return; // Skip if already loading - frame-based approach
|
|
}
|
|
|
|
try {
|
|
setIsLoading(true);
|
|
|
|
const timestamp = Date.now();
|
|
const response = await fetch(`ambient-light://displays/${localProps.displayId}?width=400&height=225&t=${timestamp}`);
|
|
|
|
if (!response.ok) {
|
|
console.error('Screenshot fetch failed:', response.status);
|
|
return;
|
|
}
|
|
|
|
const width = parseInt(response.headers.get('X-Image-Width') || '400');
|
|
const height = parseInt(response.headers.get('X-Image-Height') || '225');
|
|
const arrayBuffer = await response.arrayBuffer();
|
|
const buffer = new Uint8ClampedArray(arrayBuffer);
|
|
const expectedSize = width * height * 4;
|
|
|
|
// Validate buffer size
|
|
if (buffer.length !== expectedSize) {
|
|
console.error('Invalid buffer size:', buffer.length, 'expected:', expectedSize);
|
|
return;
|
|
}
|
|
|
|
setImageData({
|
|
buffer,
|
|
width,
|
|
height
|
|
});
|
|
|
|
// Draw immediately after data is set
|
|
setTimeout(() => {
|
|
draw(false);
|
|
}, 0);
|
|
|
|
// Frame-based rendering: wait for current frame to complete before scheduling next
|
|
const shouldContinue = !hidden() && isMounted;
|
|
if (shouldContinue) {
|
|
setTimeout(() => {
|
|
if (isMounted) {
|
|
fetchScreenshot(); // Start next frame only after current one completes
|
|
}
|
|
}, 500); // Reduced frequency to 500ms for better performance
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error fetching screenshot:', error);
|
|
// On error, wait longer before retry
|
|
const shouldContinueOnError = !hidden() && isMounted;
|
|
if (shouldContinueOnError) {
|
|
setTimeout(() => {
|
|
if (isMounted) {
|
|
fetchScreenshot();
|
|
}
|
|
}, 2000);
|
|
}
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
};
|
|
|
|
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,
|
|
});
|
|
|
|
draw(true);
|
|
};
|
|
|
|
const draw = (cached: boolean = false) => {
|
|
const { drawX, drawY, drawWidth, drawHeight } = drawInfo();
|
|
let _ctx = ctx();
|
|
let raw = imageData();
|
|
|
|
if (_ctx && raw) {
|
|
_ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
// Apply transparency effect for cached images if needed
|
|
let buffer = raw.buffer;
|
|
if (cached) {
|
|
buffer = new Uint8ClampedArray(raw.buffer);
|
|
for (let i = 3; i < buffer.length; i += 4) {
|
|
buffer[i] = Math.floor(buffer[i] * 0.7);
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Create ImageData and draw directly
|
|
const img = new ImageData(buffer, raw.width, raw.height);
|
|
|
|
// If the image size matches the draw size, use putImageData directly
|
|
if (raw.width === drawWidth && raw.height === drawHeight) {
|
|
_ctx.putImageData(img, drawX, drawY);
|
|
} else {
|
|
// Otherwise, use cached temporary canvas for scaling
|
|
if (!tempCanvas || tempCanvas.width !== raw.width || tempCanvas.height !== raw.height) {
|
|
tempCanvas = document.createElement('canvas');
|
|
tempCanvas.width = raw.width;
|
|
tempCanvas.height = raw.height;
|
|
tempCtx = tempCanvas.getContext('2d');
|
|
}
|
|
|
|
if (tempCtx) {
|
|
tempCtx.putImageData(img, 0, 0);
|
|
_ctx.drawImage(tempCanvas, drawX, drawY, drawWidth, drawHeight);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error in draw():', error);
|
|
}
|
|
}
|
|
};
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
// Start screenshot fetching after context is ready
|
|
setTimeout(() => {
|
|
fetchScreenshot(); // Initial fetch - will self-schedule subsequent frames
|
|
}, 100); // Small delay to ensure context is ready
|
|
|
|
onCleanup(() => {
|
|
isMounted = false; // Stop scheduling new frames
|
|
resizeObserver?.unobserve(root);
|
|
});
|
|
});
|
|
|
|
|
|
|
|
// Note: Removed window focus/blur logic as it was causing screenshot loop to stop
|
|
// when user interacted with dev tools or other windows
|
|
|
|
return (
|
|
<div
|
|
ref={root!}
|
|
{...rootProps}
|
|
class={'overflow-hidden h-full w-full ' + rootProps.class}
|
|
>
|
|
<canvas
|
|
ref={canvas!}
|
|
style={{
|
|
display: 'block',
|
|
width: '100%',
|
|
height: '100%',
|
|
'background-color': '#f0f0f0'
|
|
}}
|
|
/>
|
|
{rootProps.children}
|
|
</div>
|
|
);
|
|
};
|