spice-html5-react
v0.1.1
Published
React + TypeScript SPICE protocol client component library
Downloads
172
Maintainers
Readme
spice-html5-react
A React + TypeScript client library for the SPICE remote desktop protocol. Connects to SPICE servers over WebSocket (via websockify) and renders the remote display in a browser.
Features
- Drop-in
<SpiceDisplay>React component with full connection lifecycle management - Low-level
useSpicehook for custom UI and fine-grained control - Keyboard, mouse, and scroll input forwarding
- Clipboard sharing between host and guest
- File transfer via drag-and-drop
- Audio playback (Opus codec)
- Dynamic guest display resizing
- SPICE ticket (RSA) authentication
- SSR-safe (guards against
document/WebSocketin server environments) - Ships as ESM, CJS, and TypeScript declarations
Requirements
- React 18+
- A running SPICE server (e.g. QEMU with
-spiceflag) - A WebSocket proxy such as websockify bridging WebSocket traffic to the SPICE server's TCP port
Installation
npm install spice-html5-reactPeer dependencies: react and react-dom (>= 18.0.0).
Quick Start
<SpiceDisplay> Component
The simplest way to embed a SPICE session:
import { useRef } from 'react';
import { SpiceDisplay } from 'spice-html5-react';
import type { SpiceDisplayHandle } from 'spice-html5-react';
function RemoteDesktop() {
const displayRef = useRef<SpiceDisplayHandle>(null);
return (
<SpiceDisplay
ref={displayRef}
connection={{
host: "localhost",
port: 5959,
password: "secret",
}}
display={{ fitContainer: true }}
features={{
enableKeyboard: true,
enableMouse: true,
enableClipboard: true,
enableFileTransfer: true,
}}
onConnect={() => console.log("connected")}
onDisconnect={() => console.log("disconnected")}
onError={(e) => console.error(e)}
onResize={(w, h) => console.log(`${w}x${h}`)}
style={{ width: "100%", height: 600 }}
/>
);
}The imperative handle exposes sendCtrlAltDel(), disconnect(), reconnect(), and getConnection().
useSpice Hook
For full control over the connection lifecycle and input events:
import { useEffect, useRef } from 'react';
import { useSpice } from 'spice-html5-react';
function CustomDisplay() {
const containerRef = useRef<HTMLDivElement>(null);
const { status, resolution, controls } = useSpice({
canvasRef: containerRef,
callbacks: {
onConnect: () => console.log("connected"),
onError: (e) => console.error(e),
},
});
useEffect(() => {
controls.connect({ uri: "ws://localhost:5959", password: "secret" });
return () => controls.disconnect();
}, [controls]);
return (
<div>
<p>Status: {status}</p>
{resolution && <p>Resolution: {resolution.width}x{resolution.height}</p>}
<div ref={containerRef} style={{ width: "100%", height: 600 }} />
<button onClick={() => controls.sendCtrlAltDel()}>Ctrl+Alt+Del</button>
</div>
);
}The hook returns reactive status, error, resolution, and surfaces state, plus a controls object with methods for input, clipboard, resolution changes, and connection management.
SPICE Server Setup
- Start a SPICE-enabled VM (example using QEMU):
qemu-system-x86_64 \
-spice port=5900,disable-ticketing=on \
-drive file=disk.qcow2,format=qcow2- Start websockify to proxy WebSocket connections to the SPICE TCP port:
websockify 5959 localhost:5900- Point the client at
ws://localhost:5959.
Development
# Install dependencies
npm install
# Dev server
npm run dev
# Type-check (no emit)
npm run typecheck
# Build library (ESM + CJS + .d.ts)
npm run build
# Run tests
npm run testExample App
A full working example lives in the example/ directory. It demonstrates how to integrate <SpiceDisplay> into a React application with a connection form, status indicator, error handling, and imperative controls.
What It Shows
- Embedding
<SpiceDisplay>with grouped props (connection,display,features) - Using a
refto access the imperative handle (sendCtrlAltDel,disconnect,reconnect) - Handling lifecycle callbacks (
onConnect,onDisconnect,onError,onResize) - Conditionally mounting/unmounting the display based on user actions
- Resilient error handling that keeps the component mounted on transient errors
Running the Example
The example links to the parent library via "file:..", so you need to build the library first:
# From the repository root
npm install
npm run build
# Then start the example
cd example
npm install
npm run devThe Vite dev server starts at http://localhost:3000. Enter the host and port matching your websockify proxy (defaults to localhost:5959), optionally a password, and click Connect.
Example Structure
example/
index.html # HTML entry point
package.json # Dependencies (links to parent library via file:..)
tsconfig.json # TypeScript config
vite.config.ts # Vite dev server config
src/
main.tsx # React root mount
App.tsx # Main app — connection form, SpiceDisplay, controlsApp.tsx is the main file to look at. It wires up connection form state, mounts <SpiceDisplay> on connect, and exposes Disconnect, Reconnect, and Ctrl+Alt+Del buttons through the imperative ref.
API Reference
<SpiceDisplay> Props
| Prop | Type | Description |
|------|------|-------------|
| connection | SpiceConnectionProps | Host, port, password, scheme, path |
| display | SpiceDisplayOptions | scale, resize, fitContainer |
| features | SpiceFeatureToggles | Toggle keyboard, mouse, clipboard, file transfer, audio |
| audio | SpiceAudioProps | volume (0-1), muted |
| onConnect | () => void | Fired when the SPICE connection is established |
| onDisconnect | () => void | Fired when the connection is closed |
| onError | (error: Error) => void | Fired on connection errors |
| onResize | (width, height) => void | Fired when the display resolution changes |
| onClipboard | (text: string) => void | Fired when clipboard text is received from the guest |
| onFileTransferProgress | (taskId, bytes, total, filename) => void | File transfer progress |
| onFileTransferComplete | (taskId, filename, error?) => void | File transfer completion |
| className | string | CSS class for the outer container |
| style | CSSProperties | Inline styles for the outer container |
SpiceDisplayHandle (imperative ref)
| Method | Description |
|--------|-------------|
| sendCtrlAltDel() | Send Ctrl+Alt+Delete to the guest |
| disconnect() | Close the SPICE connection |
| reconnect() | Reconnect using current props |
| getConnection() | Get the underlying SpiceMainConn instance |
useSpice Return Value
| Field | Type | Description |
|-------|------|-------------|
| status | "disconnected" \| "connecting" \| "connected" \| "error" | Current connection status |
| error | Error \| null | Last connection error |
| resolution | { width, height } \| null | Current display resolution |
| surfaces | number | Number of active display surfaces |
| controls | SpiceControls | Methods: connect, disconnect, sendKeyDown, sendKeyUp, sendMouseMove, sendMouseButton, sendClipboard, setResolution, sendCtrlAltDel, getConnection |
Project Structure
src/
index.ts # Package entry point / barrel export
components/
SpiceDisplay.tsx # Drop-in React component
hooks/
useSpice.ts # Low-level React hook
protocol/ # SPICE protocol implementation
spiceconn.ts # Base WebSocket connection
main.ts # Main channel, agent, clipboard
display.ts # Display rendering (canvas 2D)
inputs.ts # Keyboard and mouse input
cursor.ts # Cursor rendering
playback.ts # Audio playback (Opus/WebM)
port.ts # Port channel
enums.ts # Protocol constants
spicemsg.ts # Message serialization
spicetype.ts # SPICE data types
wire.ts # Binary framing / wire reader
ticket.ts # RSA ticket authentication
quic.ts, lz.ts # Image decompression
bitmap.ts, png.ts # Image format conversion
webm.ts # WebM container for VP8
resize.ts # Guest screen resizing
filexfer.ts # File transfer (drag-and-drop)
simulatecursor.ts # Software cursor
utils/
debug.ts # Debug flags and utilities
atKeynames.ts # AT keyboard scancode names
codeToScancode.ts # KeyboardEvent.code to scancode map