Initial GuruConnect implementation - Phase 1 MVP

- Agent: DXGI/GDI screen capture, mouse/keyboard input, WebSocket transport
- Server: Axum relay, session management, REST API
- Dashboard: React viewer components with TypeScript
- Protocol: Protobuf definitions for all message types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AZ Computer Guru
2025-12-21 17:18:05 -07:00
commit 33893ea73b
38 changed files with 7724 additions and 0 deletions

25
dashboard/package.json Normal file
View File

@@ -0,0 +1,25 @@
{
"name": "@guruconnect/dashboard",
"version": "0.1.0",
"description": "GuruConnect Remote Desktop Viewer Components",
"author": "AZ Computer Guru",
"license": "Proprietary",
"main": "src/components/index.ts",
"types": "src/components/index.ts",
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"typescript": "^5.0.0"
},
"dependencies": {
"fzstd": "^0.1.1"
}
}

View File

@@ -0,0 +1,215 @@
/**
* RemoteViewer Component
*
* Canvas-based remote desktop viewer that connects to a GuruConnect
* agent via the relay server. Handles frame rendering and input capture.
*/
import React, { useRef, useEffect, useCallback, useState } from 'react';
import { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';
import type { VideoFrame, ConnectionStatus, MouseEventType } from '../types/protocol';
interface RemoteViewerProps {
serverUrl: string;
sessionId: string;
className?: string;
onStatusChange?: (status: ConnectionStatus) => void;
autoConnect?: boolean;
showStatusBar?: boolean;
}
export const RemoteViewer: React.FC<RemoteViewerProps> = ({
serverUrl,
sessionId,
className = '',
onStatusChange,
autoConnect = true,
showStatusBar = true,
}) => {
const canvasRef = useRef<HTMLCanvasElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const ctxRef = useRef<CanvasRenderingContext2D | null>(null);
// Display dimensions from received frames
const [displaySize, setDisplaySize] = useState({ width: 1920, height: 1080 });
// Frame buffer for rendering
const frameBufferRef = useRef<ImageData | null>(null);
// Handle incoming video frames
const handleFrame = useCallback((frame: VideoFrame) => {
if (!frame.raw || !canvasRef.current) return;
const { width, height, data, compressed, isKeyframe } = frame.raw;
// Update display size if changed
if (width !== displaySize.width || height !== displaySize.height) {
setDisplaySize({ width, height });
}
// Get or create context
if (!ctxRef.current) {
ctxRef.current = canvasRef.current.getContext('2d', {
alpha: false,
desynchronized: true,
});
}
const ctx = ctxRef.current;
if (!ctx) return;
// For MVP, we assume raw BGRA frames
// In production, handle compressed frames with fzstd
let frameData = data;
// Create or reuse ImageData
if (!frameBufferRef.current ||
frameBufferRef.current.width !== width ||
frameBufferRef.current.height !== height) {
frameBufferRef.current = ctx.createImageData(width, height);
}
const imageData = frameBufferRef.current;
// Convert BGRA to RGBA for canvas
const pixels = imageData.data;
const len = Math.min(frameData.length, pixels.length);
for (let i = 0; i < len; i += 4) {
pixels[i] = frameData[i + 2]; // R <- B
pixels[i + 1] = frameData[i + 1]; // G <- G
pixels[i + 2] = frameData[i]; // B <- R
pixels[i + 3] = 255; // A (opaque)
}
// Draw to canvas
ctx.putImageData(imageData, 0, 0);
}, [displaySize]);
// Set up session
const { status, connect, disconnect, sendMouseEvent, sendKeyEvent } = useRemoteSession({
serverUrl,
sessionId,
onFrame: handleFrame,
onStatusChange,
});
// Auto-connect on mount
useEffect(() => {
if (autoConnect) {
connect();
}
return () => {
disconnect();
};
}, [autoConnect, connect, disconnect]);
// Update canvas size when display size changes
useEffect(() => {
if (canvasRef.current) {
canvasRef.current.width = displaySize.width;
canvasRef.current.height = displaySize.height;
// Reset context reference
ctxRef.current = null;
frameBufferRef.current = null;
}
}, [displaySize]);
// Get canvas rect for coordinate translation
const getCanvasRect = useCallback(() => {
return canvasRef.current?.getBoundingClientRect() ?? new DOMRect();
}, []);
// Mouse event handlers
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 0);
sendMouseEvent(event);
}, [getCanvasRect, displaySize, sendMouseEvent]);
const handleMouseDown = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
e.preventDefault();
const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 1);
sendMouseEvent(event);
}, [getCanvasRect, displaySize, sendMouseEvent]);
const handleMouseUp = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
const event = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 2);
sendMouseEvent(event);
}, [getCanvasRect, displaySize, sendMouseEvent]);
const handleWheel = useCallback((e: React.WheelEvent<HTMLCanvasElement>) => {
e.preventDefault();
const baseEvent = createMouseEvent(e, getCanvasRect(), displaySize.width, displaySize.height, 3);
sendMouseEvent({
...baseEvent,
wheelDeltaX: Math.round(e.deltaX),
wheelDeltaY: Math.round(e.deltaY),
});
}, [getCanvasRect, displaySize, sendMouseEvent]);
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
e.preventDefault(); // Prevent browser context menu
}, []);
// Keyboard event handlers
const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
e.preventDefault();
const event = createKeyEvent(e, true);
sendKeyEvent(event);
}, [sendKeyEvent]);
const handleKeyUp = useCallback((e: React.KeyboardEvent<HTMLCanvasElement>) => {
e.preventDefault();
const event = createKeyEvent(e, false);
sendKeyEvent(event);
}, [sendKeyEvent]);
return (
<div ref={containerRef} className={`remote-viewer ${className}`}>
<canvas
ref={canvasRef}
tabIndex={0}
onMouseMove={handleMouseMove}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
onWheel={handleWheel}
onContextMenu={handleContextMenu}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
style={{
width: '100%',
height: 'auto',
aspectRatio: `${displaySize.width} / ${displaySize.height}`,
cursor: 'none', // Hide cursor, remote cursor is shown in frame
outline: 'none',
backgroundColor: '#1a1a1a',
}}
/>
{showStatusBar && (
<div className="remote-viewer-status" style={{
display: 'flex',
justifyContent: 'space-between',
padding: '4px 8px',
backgroundColor: '#333',
color: '#fff',
fontSize: '12px',
fontFamily: 'monospace',
}}>
<span>
{status.connected ? (
<span style={{ color: '#4ade80' }}>Connected</span>
) : (
<span style={{ color: '#f87171' }}>Disconnected</span>
)}
</span>
<span>{displaySize.width}x{displaySize.height}</span>
{status.fps !== undefined && <span>{status.fps} FPS</span>}
{status.latencyMs !== undefined && <span>{status.latencyMs}ms</span>}
</div>
)}
</div>
);
};
export default RemoteViewer;

View File

@@ -0,0 +1,187 @@
/**
* Session Controls Component
*
* Toolbar for controlling the remote session (quality, displays, special keys)
*/
import React, { useState } from 'react';
import type { QualitySettings, Display } from '../types/protocol';
interface SessionControlsProps {
displays?: Display[];
currentDisplay?: number;
onDisplayChange?: (displayId: number) => void;
quality?: QualitySettings;
onQualityChange?: (settings: QualitySettings) => void;
onSpecialKey?: (key: 'ctrl-alt-del' | 'lock-screen' | 'print-screen') => void;
onDisconnect?: () => void;
}
export const SessionControls: React.FC<SessionControlsProps> = ({
displays = [],
currentDisplay = 0,
onDisplayChange,
quality,
onQualityChange,
onSpecialKey,
onDisconnect,
}) => {
const [showQuality, setShowQuality] = useState(false);
const handleQualityPreset = (preset: 'auto' | 'low' | 'balanced' | 'high') => {
onQualityChange?.({
preset,
codec: 'auto',
});
};
return (
<div className="session-controls" style={{
display: 'flex',
gap: '8px',
padding: '8px',
backgroundColor: '#222',
borderBottom: '1px solid #444',
}}>
{/* Display selector */}
{displays.length > 1 && (
<select
value={currentDisplay}
onChange={(e) => onDisplayChange?.(Number(e.target.value))}
style={{
padding: '4px 8px',
backgroundColor: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
}}
>
{displays.map((d) => (
<option key={d.id} value={d.id}>
{d.name || `Display ${d.id + 1}`}
{d.isPrimary ? ' (Primary)' : ''}
</option>
))}
</select>
)}
{/* Quality dropdown */}
<div style={{ position: 'relative' }}>
<button
onClick={() => setShowQuality(!showQuality)}
style={{
padding: '4px 12px',
backgroundColor: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Quality: {quality?.preset || 'auto'}
</button>
{showQuality && (
<div style={{
position: 'absolute',
top: '100%',
left: 0,
marginTop: '4px',
backgroundColor: '#333',
border: '1px solid #555',
borderRadius: '4px',
zIndex: 100,
}}>
{(['auto', 'low', 'balanced', 'high'] as const).map((preset) => (
<button
key={preset}
onClick={() => {
handleQualityPreset(preset);
setShowQuality(false);
}}
style={{
display: 'block',
width: '100%',
padding: '8px 16px',
backgroundColor: quality?.preset === preset ? '#444' : 'transparent',
color: '#fff',
border: 'none',
textAlign: 'left',
cursor: 'pointer',
}}
>
{preset.charAt(0).toUpperCase() + preset.slice(1)}
</button>
))}
</div>
)}
</div>
{/* Special keys */}
<button
onClick={() => onSpecialKey?.('ctrl-alt-del')}
title="Send Ctrl+Alt+Delete"
style={{
padding: '4px 12px',
backgroundColor: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Ctrl+Alt+Del
</button>
<button
onClick={() => onSpecialKey?.('lock-screen')}
title="Lock Screen (Win+L)"
style={{
padding: '4px 12px',
backgroundColor: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Lock
</button>
<button
onClick={() => onSpecialKey?.('print-screen')}
title="Print Screen"
style={{
padding: '4px 12px',
backgroundColor: '#333',
color: '#fff',
border: '1px solid #555',
borderRadius: '4px',
cursor: 'pointer',
}}
>
PrtSc
</button>
{/* Spacer */}
<div style={{ flex: 1 }} />
{/* Disconnect */}
<button
onClick={onDisconnect}
style={{
padding: '4px 12px',
backgroundColor: '#dc2626',
color: '#fff',
border: 'none',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Disconnect
</button>
</div>
);
};
export default SessionControls;

View File

@@ -0,0 +1,22 @@
/**
* GuruConnect Dashboard Components
*
* Export all components for use in GuruRMM dashboard
*/
export { RemoteViewer } from './RemoteViewer';
export { SessionControls } from './SessionControls';
// Re-export types
export type {
ConnectionStatus,
Display,
DisplayInfo,
QualitySettings,
VideoFrame,
MouseEvent as ProtoMouseEvent,
KeyEvent as ProtoKeyEvent,
} from '../types/protocol';
// Re-export hooks
export { useRemoteSession, createMouseEvent, createKeyEvent } from '../hooks/useRemoteSession';

View File

@@ -0,0 +1,239 @@
/**
* React hook for managing remote desktop session connection
*/
import { useState, useEffect, useCallback, useRef } from 'react';
import type { ConnectionStatus, VideoFrame, MouseEvent as ProtoMouseEvent, KeyEvent as ProtoKeyEvent, MouseEventType, KeyEventType, Modifiers } from '../types/protocol';
import { encodeMouseEvent, encodeKeyEvent, decodeVideoFrame } from '../lib/protobuf';
interface UseRemoteSessionOptions {
serverUrl: string;
sessionId: string;
onFrame?: (frame: VideoFrame) => void;
onStatusChange?: (status: ConnectionStatus) => void;
}
interface UseRemoteSessionReturn {
status: ConnectionStatus;
connect: () => void;
disconnect: () => void;
sendMouseEvent: (event: ProtoMouseEvent) => void;
sendKeyEvent: (event: ProtoKeyEvent) => void;
}
export function useRemoteSession(options: UseRemoteSessionOptions): UseRemoteSessionReturn {
const { serverUrl, sessionId, onFrame, onStatusChange } = options;
const [status, setStatus] = useState<ConnectionStatus>({
connected: false,
});
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<number | null>(null);
const frameCountRef = useRef(0);
const lastFpsUpdateRef = useRef(Date.now());
// Update status and notify
const updateStatus = useCallback((newStatus: Partial<ConnectionStatus>) => {
setStatus(prev => {
const updated = { ...prev, ...newStatus };
onStatusChange?.(updated);
return updated;
});
}, [onStatusChange]);
// Calculate FPS
const updateFps = useCallback(() => {
const now = Date.now();
const elapsed = now - lastFpsUpdateRef.current;
if (elapsed >= 1000) {
const fps = Math.round((frameCountRef.current * 1000) / elapsed);
updateStatus({ fps });
frameCountRef.current = 0;
lastFpsUpdateRef.current = now;
}
}, [updateStatus]);
// Handle incoming WebSocket messages
const handleMessage = useCallback((event: MessageEvent) => {
if (event.data instanceof Blob) {
event.data.arrayBuffer().then(buffer => {
const data = new Uint8Array(buffer);
const frame = decodeVideoFrame(data);
if (frame) {
frameCountRef.current++;
updateFps();
onFrame?.(frame);
}
});
} else if (event.data instanceof ArrayBuffer) {
const data = new Uint8Array(event.data);
const frame = decodeVideoFrame(data);
if (frame) {
frameCountRef.current++;
updateFps();
onFrame?.(frame);
}
}
}, [onFrame, updateFps]);
// Connect to server
const connect = useCallback(() => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
return;
}
// Clear any pending reconnect
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
const wsUrl = `${serverUrl}/ws/viewer?session_id=${encodeURIComponent(sessionId)}`;
const ws = new WebSocket(wsUrl);
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
updateStatus({
connected: true,
sessionId,
});
};
ws.onmessage = handleMessage;
ws.onclose = (event) => {
updateStatus({
connected: false,
latencyMs: undefined,
fps: undefined,
});
// Auto-reconnect after 2 seconds
if (!event.wasClean) {
reconnectTimeoutRef.current = window.setTimeout(() => {
connect();
}, 2000);
}
};
ws.onerror = () => {
updateStatus({ connected: false });
};
wsRef.current = ws;
}, [serverUrl, sessionId, handleMessage, updateStatus]);
// Disconnect from server
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
window.clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
if (wsRef.current) {
wsRef.current.close(1000, 'User disconnected');
wsRef.current = null;
}
updateStatus({
connected: false,
sessionId: undefined,
latencyMs: undefined,
fps: undefined,
});
}, [updateStatus]);
// Send mouse event
const sendMouseEvent = useCallback((event: ProtoMouseEvent) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const data = encodeMouseEvent(event);
wsRef.current.send(data);
}
}, []);
// Send key event
const sendKeyEvent = useCallback((event: ProtoKeyEvent) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
const data = encodeKeyEvent(event);
wsRef.current.send(data);
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
disconnect();
};
}, [disconnect]);
return {
status,
connect,
disconnect,
sendMouseEvent,
sendKeyEvent,
};
}
/**
* Helper to create mouse event from DOM mouse event
*/
export function createMouseEvent(
domEvent: React.MouseEvent<HTMLElement>,
canvasRect: DOMRect,
displayWidth: number,
displayHeight: number,
eventType: MouseEventType
): ProtoMouseEvent {
// Calculate position relative to canvas and scale to display coordinates
const scaleX = displayWidth / canvasRect.width;
const scaleY = displayHeight / canvasRect.height;
const x = Math.round((domEvent.clientX - canvasRect.left) * scaleX);
const y = Math.round((domEvent.clientY - canvasRect.top) * scaleY);
return {
x,
y,
buttons: {
left: (domEvent.buttons & 1) !== 0,
right: (domEvent.buttons & 2) !== 0,
middle: (domEvent.buttons & 4) !== 0,
x1: (domEvent.buttons & 8) !== 0,
x2: (domEvent.buttons & 16) !== 0,
},
wheelDeltaX: 0,
wheelDeltaY: 0,
eventType,
};
}
/**
* Helper to create key event from DOM keyboard event
*/
export function createKeyEvent(
domEvent: React.KeyboardEvent<HTMLElement>,
down: boolean
): ProtoKeyEvent {
const modifiers: Modifiers = {
ctrl: domEvent.ctrlKey,
alt: domEvent.altKey,
shift: domEvent.shiftKey,
meta: domEvent.metaKey,
capsLock: domEvent.getModifierState('CapsLock'),
numLock: domEvent.getModifierState('NumLock'),
};
// Use key code for special keys, unicode for regular characters
const isCharacter = domEvent.key.length === 1;
return {
down,
keyType: isCharacter ? 2 : 0, // KEY_UNICODE or KEY_VK
vkCode: domEvent.keyCode,
scanCode: 0, // Not available in browser
unicode: isCharacter ? domEvent.key : undefined,
modifiers,
};
}

View File

@@ -0,0 +1,162 @@
/**
* Minimal protobuf encoder/decoder for GuruConnect messages
*
* For MVP, we use a simplified binary format. In production,
* this would use a proper protobuf library like protobufjs.
*/
import type { MouseEvent, KeyEvent, MouseEventType, KeyEventType, VideoFrame, RawFrame } from '../types/protocol';
// Message type identifiers (matching proto field numbers)
const MSG_VIDEO_FRAME = 10;
const MSG_MOUSE_EVENT = 20;
const MSG_KEY_EVENT = 21;
/**
* Encode a mouse event to binary format
*/
export function encodeMouseEvent(event: MouseEvent): Uint8Array {
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
// Message type
view.setUint8(0, MSG_MOUSE_EVENT);
// Event type
view.setUint8(1, event.eventType);
// Coordinates (scaled to 16-bit for efficiency)
view.setInt16(2, event.x, true);
view.setInt16(4, event.y, true);
// Buttons bitmask
let buttons = 0;
if (event.buttons.left) buttons |= 1;
if (event.buttons.right) buttons |= 2;
if (event.buttons.middle) buttons |= 4;
if (event.buttons.x1) buttons |= 8;
if (event.buttons.x2) buttons |= 16;
view.setUint8(6, buttons);
// Wheel deltas
view.setInt16(7, event.wheelDeltaX, true);
view.setInt16(9, event.wheelDeltaY, true);
return new Uint8Array(buffer, 0, 11);
}
/**
* Encode a key event to binary format
*/
export function encodeKeyEvent(event: KeyEvent): Uint8Array {
const buffer = new ArrayBuffer(32);
const view = new DataView(buffer);
// Message type
view.setUint8(0, MSG_KEY_EVENT);
// Key down/up
view.setUint8(1, event.down ? 1 : 0);
// Key type
view.setUint8(2, event.keyType);
// Virtual key code
view.setUint16(3, event.vkCode, true);
// Scan code
view.setUint16(5, event.scanCode, true);
// Modifiers bitmask
let mods = 0;
if (event.modifiers.ctrl) mods |= 1;
if (event.modifiers.alt) mods |= 2;
if (event.modifiers.shift) mods |= 4;
if (event.modifiers.meta) mods |= 8;
if (event.modifiers.capsLock) mods |= 16;
if (event.modifiers.numLock) mods |= 32;
view.setUint8(7, mods);
// Unicode character (if present)
if (event.unicode && event.unicode.length > 0) {
const charCode = event.unicode.charCodeAt(0);
view.setUint16(8, charCode, true);
return new Uint8Array(buffer, 0, 10);
}
return new Uint8Array(buffer, 0, 8);
}
/**
* Decode a video frame from binary format
*/
export function decodeVideoFrame(data: Uint8Array): VideoFrame | null {
if (data.length < 2) return null;
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
const msgType = view.getUint8(0);
if (msgType !== MSG_VIDEO_FRAME) return null;
const encoding = view.getUint8(1);
const displayId = view.getUint8(2);
const sequence = view.getUint32(3, true);
const timestamp = Number(view.getBigInt64(7, true));
// Frame dimensions
const width = view.getUint16(15, true);
const height = view.getUint16(17, true);
// Compressed flag
const compressed = view.getUint8(19) === 1;
// Is keyframe
const isKeyframe = view.getUint8(20) === 1;
// Frame data starts at offset 21
const frameData = data.slice(21);
const encodingStr = ['raw', 'vp9', 'h264', 'h265'][encoding] as 'raw' | 'vp9' | 'h264' | 'h265';
if (encodingStr === 'raw') {
return {
timestamp,
displayId,
sequence,
encoding: 'raw',
raw: {
width,
height,
data: frameData,
compressed,
dirtyRects: [], // TODO: Parse dirty rects
isKeyframe,
},
};
}
return {
timestamp,
displayId,
sequence,
encoding: encodingStr,
encoded: {
data: frameData,
keyframe: isKeyframe,
pts: timestamp,
dts: timestamp,
},
};
}
/**
* Simple zstd decompression placeholder
* In production, use a proper zstd library like fzstd
*/
export async function decompressZstd(data: Uint8Array): Promise<Uint8Array> {
// For MVP, assume uncompressed frames or use fzstd library
// This is a placeholder - actual implementation would use:
// import { decompress } from 'fzstd';
// return decompress(data);
return data;
}

View File

@@ -0,0 +1,135 @@
/**
* TypeScript types matching guruconnect.proto definitions
* These are used for WebSocket message handling in the viewer
*/
export enum SessionType {
SCREEN_CONTROL = 0,
VIEW_ONLY = 1,
BACKSTAGE = 2,
FILE_TRANSFER = 3,
}
export interface SessionRequest {
agentId: string;
sessionToken: string;
sessionType: SessionType;
clientVersion: string;
}
export interface SessionResponse {
success: boolean;
sessionId: string;
error?: string;
displayInfo?: DisplayInfo;
}
export interface DisplayInfo {
displays: Display[];
primaryDisplay: number;
}
export interface Display {
id: number;
name: string;
x: number;
y: number;
width: number;
height: number;
isPrimary: boolean;
}
export interface DirtyRect {
x: number;
y: number;
width: number;
height: number;
}
export interface RawFrame {
width: number;
height: number;
data: Uint8Array;
compressed: boolean;
dirtyRects: DirtyRect[];
isKeyframe: boolean;
}
export interface EncodedFrame {
data: Uint8Array;
keyframe: boolean;
pts: number;
dts: number;
}
export interface VideoFrame {
timestamp: number;
displayId: number;
sequence: number;
encoding: 'raw' | 'vp9' | 'h264' | 'h265';
raw?: RawFrame;
encoded?: EncodedFrame;
}
export enum MouseEventType {
MOUSE_MOVE = 0,
MOUSE_DOWN = 1,
MOUSE_UP = 2,
MOUSE_WHEEL = 3,
}
export interface MouseButtons {
left: boolean;
right: boolean;
middle: boolean;
x1: boolean;
x2: boolean;
}
export interface MouseEvent {
x: number;
y: number;
buttons: MouseButtons;
wheelDeltaX: number;
wheelDeltaY: number;
eventType: MouseEventType;
}
export enum KeyEventType {
KEY_VK = 0,
KEY_SCAN = 1,
KEY_UNICODE = 2,
}
export interface Modifiers {
ctrl: boolean;
alt: boolean;
shift: boolean;
meta: boolean;
capsLock: boolean;
numLock: boolean;
}
export interface KeyEvent {
down: boolean;
keyType: KeyEventType;
vkCode: number;
scanCode: number;
unicode?: string;
modifiers: Modifiers;
}
export interface QualitySettings {
preset: 'auto' | 'low' | 'balanced' | 'high';
customFps?: number;
customBitrate?: number;
codec: 'auto' | 'raw' | 'vp9' | 'h264' | 'h265';
}
export interface ConnectionStatus {
connected: boolean;
sessionId?: string;
latencyMs?: number;
fps?: number;
bitrateKbps?: number;
}

21
dashboard/tsconfig.json Normal file
View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"],
"exclude": ["node_modules"]
}