Spice protocol is a kind of remote control protocol commonly used with libvirt and Qemu.
More information on spice: https://www.spice-space.org/
Features:
- Display support (single display)
- Mouse and keyboard control (client mode)
- Audio playback
- Clipboard
- Audio recording
- USB Support
- File transfer
- Video streaming
To connect to a SPICE host, you need to implement two interfaces:
spice.Connector- Provides network connectivity to the SPICE serverspice.Driver- Handles display rendering, input events, and clipboard operations
The Connector interface establishes network connections to the SPICE server:
typeConnectorinterface {
SpiceConnect(compressbool) (net.Conn, error)
}The Driver interface handles display updates, user input, cursor, and clipboard:
typeDriverinterface {
// Display managementDisplayInit(image.Image)
DisplayRefresh()
// Input and channel setupSetEventsTarget(*ChInputs)
SetMainTarget(*ChMain)
SetCursor(img image.Image, x, yuint16)
// Clipboard operationsClipboardGrabbed(selectionSpiceClipboardSelection, clipboardTypes []SpiceClipboardFormat)
ClipboardFetch(selectionSpiceClipboardSelection, clTypeSpiceClipboardFormat) ([]byte, error)
ClipboardRelease(selectionSpiceClipboardSelection)
}package main
import (
"fmt""net""github.com/Shells-com/spice"
)
// SimpleConnector implements spice.Connector for direct TCP connectionstypeSimpleConnectorstruct {
HoststringPortint
}
func (c*SimpleConnector) SpiceConnect(compressbool) (net.Conn, error) {
returnnet.Dial("tcp", fmt.Sprintf("%s:%d", c.Host, c.Port))
}
funcmain() {
// Create connectorconnector:=&SimpleConnector{
Host: "localhost",
Port: 5900,
}
// Create driver (implement spice.Driver interface - see below)driver:=&MyDriver{}
// Connect to SPICE serverclient, err:=spice.New(connector, driver, "yourpassword")
iferr!=nil {
panic(err)
}
// Client is now connected and ready to usefmt.Println("Connected to SPICE server!")
// Access various featuresiffileTransfer:=client.GetFileTransfer(); fileTransfer!=nil {
// File transfer is available
}
}import (
"crypto/tls""fmt""net"
)
typeTLSConnectorstruct {
HoststringPortint
}
func (c*TLSConnector) SpiceConnect(compressbool) (net.Conn, error) {
config:=&tls.Config{
// Configure TLS settings as neededInsecureSkipVerify: false, // Set to true only for testing
}
addr:=fmt.Sprintf("%s:%d", c.Host, c.Port)
returntls.Dial("tcp", addr, config)
}import (
"net""github.com/gorilla/websocket"
)
typeWebSocketConnectorstruct {
URLstring
}
func (c*WebSocketConnector) SpiceConnect(compressbool) (net.Conn, error) {
conn, _, err:=websocket.DefaultDialer.Dial(c.URL, nil)
iferr!=nil {
returnnil, err
}
// Wrap websocket connection to implement net.Connreturn&wsConn{conn: conn}, nil
}
// wsConn wraps a websocket.Conn to implement net.Conn interfacetypewsConnstruct {
conn*websocket.Conn
}
func (w*wsConn) Read(p []byte) (nint, errerror) {
_, data, err:=w.conn.ReadMessage()
iferr!=nil {
return0, err
}
returncopy(p, data), nil
}
func (w*wsConn) Write(p []byte) (nint, errerror) {
err=w.conn.WriteMessage(websocket.BinaryMessage, p)
returnlen(p), err
}
// Implement other net.Conn methods...For headless operation or testing, you can implement a minimal driver:
package main
import (
"image""github.com/Shells-com/spice"
)
typeMinimalDriverstruct {
inputs*spice.ChInputsmain*spice.ChMain
}
func (d*MinimalDriver) DisplayInit(img image.Image) {
// Store or render the initial display image
}
func (d*MinimalDriver) DisplayRefresh() {
// Refresh the display
}
func (d*MinimalDriver) SetEventsTarget(inputs*spice.ChInputs) {
d.inputs=inputs
}
func (d*MinimalDriver) SetMainTarget(main*spice.ChMain) {
d.main=main
}
func (d*MinimalDriver) SetCursor(img image.Image, x, yuint16) {
// Update cursor position and image
}
func (d*MinimalDriver) ClipboardGrabbed(selection spice.SpiceClipboardSelection,
clipboardTypes []spice.SpiceClipboardFormat) {
// Handle clipboard grab event
}
func (d*MinimalDriver) ClipboardFetch(selection spice.SpiceClipboardSelection,
clType spice.SpiceClipboardFormat) ([]byte, error) {
// Return clipboard datareturnnil, nil
}
func (d*MinimalDriver) ClipboardRelease(selection spice.SpiceClipboardSelection) {
// Handle clipboard release
}For a complete GUI implementation, see the spicefyne package which provides a full-featured driver using the Fyne UI toolkit.
Once connected, you can send keyboard events through the inputs channel:
// After connection, the driver's SetEventsTarget will be called with the inputs channel// Store it in your driver implementation, then use it to send events// Send a key pressinputs.KeyDown(spice.KEY_A)
// Send a key releaseinputs.KeyUp(spice.KEY_A)
// Type a characterinputs.KeyPress(spice.KEY_H) // Press and release// Move mouse to absolute positioninputs.MouseMove(100, 200)
// Mouse button pressinputs.MouseDown(spice.MOUSE_BUTTON_LEFT)
// Mouse button releaseinputs.MouseUp(spice.MOUSE_BUTTON_LEFT)
// Mouse click (press and release)inputs.MouseClick(spice.MOUSE_BUTTON_LEFT)
// Mouse wheel scrollinputs.MouseScroll(0, -1) // Scroll upinputs.MouseScroll(0, 1) // Scroll downThe SPICE client automatically connects to the audio playback channel if available:
client, err:=spice.New(connector, driver, password)
iferr!=nil {
panic(err)
}
// Audio is automatically streamed to the system audio output// The client handles all audio decoding and playback internallyTo capture audio from the client and send it to the server:
// Get the audio recording interfacerecorder:=client.GetAudioRecorder()
ifrecorder!=nil {
// Start recording with specific audio formaterr:=recorder.Start(spice.AUDIO_FMT_S16, 2, 44100)
iferr!=nil {
log.Printf("Failed to start recording: %v", err)
}
// Send audio dataaudioData:= []int16{ /* PCM audio samples */ }
recorder.SendSamples(audioData)
// Stop recordingrecorder.Stop()
}Clipboard integration allows copy/paste between client and server:
// In your Driver implementation:func (d*MyDriver) ClipboardGrabbed(selection spice.SpiceClipboardSelection,
clipboardTypes []spice.SpiceClipboardFormat) {
// Server has grabbed the clipboard// You can now request clipboard data if needed
}
func (d*MyDriver) ClipboardFetch(selection spice.SpiceClipboardSelection,
clType spice.SpiceClipboardFormat) ([]byte, error) {
// Server is requesting clipboard data from the clientifclType==spice.SPICE_CLIPBOARD_FORMAT_TEXT {
return []byte("clipboard text content"), nil
}
returnnil, fmt.Errorf("unsupported clipboard format")
}
func (d*MyDriver) ClipboardRelease(selection spice.SpiceClipboardSelection) {
// Server has released the clipboard
}
// To grab clipboard from client side:// Use the main channel to announce clipboard grabmain.GrabClipboard(spice.SPICE_CLIPBOARD_SELECTION_CLIPBOARD,
[]spice.SpiceClipboardFormat{spice.SPICE_CLIPBOARD_FORMAT_TEXT})// Get the file transfer interfacefileTransfer:=client.GetFileTransfer()
iffileTransfer!=nil {
// Create a progress callbackprogressCb:=func(progress spice.FileTransferProgress) {
fmt.Printf("Transfer %s: %.1f%% (%d/%d bytes)\n", progress.FileName, progress.Percentage, progress.BytesSent, progress.TotalSize)
ifprogress.Error!=nil {
fmt.Printf("Error: %v\n", progress.Error)
}
}
// Send a file to the guesttransferID, err:=fileTransfer.SendFile("/path/to/file.txt", progressCb)
iferr!=nil {
log.Fatalf("Failed to start file transfer: %v", err)
}
fmt.Printf("File transfer started with ID: %d\n", transferID)
// To cancel a transfer (if needed):// fileTransfer.CancelTransfer(transferID)// Send multiple filesids, err:=fileTransfer.SendFiles([]string{
"/path/to/file1.txt",
"/path/to/file2.png",
}, progressCb)
iferr!=nil {
log.Printf("Some transfers failed: %v", err)
}
fmt.Printf("Started %d file transfers\n", len(ids))
}When you call spice.New(connector, driver, password), the following happens:
Main Channel Connection: The client establishes a connection to the main SPICE channel using your
Connector. This channel is responsible for:- Authentication using the provided password
- Retrieving the list of available channels from the server
- Coordinating overall session management
Capability Negotiation: The client and server exchange supported capabilities to determine which features are available (compression, audio formats, etc.)
Parallel Channel Setup: Once the main channel is established, the client connects to all available channels in parallel:
- Display Channel: Receives screen updates and rendering commands
- Inputs Channel: Sends keyboard and mouse events to the server
- Cursor Channel: Receives cursor shape and position updates
- Playback Channel: Streams audio from the server (if available)
- Record Channel: Sends audio to the server (if available)
- WebDAV Channel: Handles file transfer operations (if available)
Driver Initialization: As channels connect, your
Drivermethods are called:SetMainTarget(): Provides access to the main channelSetEventsTarget(): Provides access to the inputs channelDisplayInit(): Called when the initial screen image is readySetCursor(): Called with initial cursor information
Ready to Use: Once
spice.New()returns, all channels are connected and the client is ready for interaction.
- The
compressparameter inSpiceConnect()indicates whether the display channel should use compression. The client will request this when connecting to the display channel. - Connections may be established multiple times during the session (one per channel: main, display, inputs, cursor, audio, etc.)
- The client handles all protocol details, compression, and channel multiplexing automatically
- Your
Connectorshould return a freshnet.Connfor each call toSpiceConnect()
client, err:=spice.New(connector, driver, password)
iferr!=nil {
// Connection failed - could be:// - Network error (cannot reach server)// - Authentication error (wrong password)// - Protocol error (incompatible versions)log.Printf("Failed to connect: %v", err)
return
}Enable debug logging to see protocol details:
client, err:=spice.New(connector, driver, password)
iferr!=nil {
panic(err)
}
// Enable debug loggingclient.Debug=log.New(os.Stdout, "SPICE: ", log.LstdFlags)Not all SPICE servers support all features. Check availability before use:
// Check file transfer supportiffileTransfer:=client.GetFileTransfer(); fileTransfer!=nil {
// File transfer is available
} else {
log.Println("File transfer not supported by this server")
}
// Check audio recording supportifrecorder:=client.GetAudioRecorder(); recorder!=nil {
// Audio recording is available
} else {
log.Println("Audio recording not supported by this server")
}See the LICENSE file in the repository root.