Modular ES6 JavaScript library for building multi-user web applications.
Supported from MouseMux v2.2.8+ | Current SDK: v2.2.36
The Web SDK enables real-time multi-user interaction in web applications. Each physical mouse connected to the system generates independent input events, allowing multiple users to interact simultaneously with the same application.
Key capabilities:
// 1. Create SDK instance
const sdk = new MouseMuxSDK({
appName: 'My App',
appVersion: '1.0.0'
});
// 2. Register all 10 required handlers
sdk.registerHandlers({
onPointerMotion: (data) => {
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) drawCursor(pos.x, pos.y, data.hwid);
},
onPointerButton: (data) => {
if (data.button & MouseMuxSDK.BUTTON_LEFT_DOWN) handleClick(data);
},
onPointerWheel: () => {},
onPointerPen: () => {},
onKeyboard: () => {},
onUserList: (data) => updateUserCount(data.users.length),
onUserCreate: () => {},
onUserDispose: () => {},
onConnected: () => console.log('Connected!'),
onDisconnected: () => console.log('Disconnected')
});
// 3. Connect
sdk.connect('ws://localhost:41001');
| Requirement | Details |
|---|---|
| MouseMux V2 | Must be installed and running |
| SDK mode | Enabled in MouseMux configuration |
| Browser | Modern browser with WebSocket and ES6 support |
| Electron | Optional -- preload bridge for accurate coordinate conversion |
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>My MouseMux App</title>
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div id="app-container"><!-- Your UI here --></div>
<!-- SDK Path Configuration -->
<script src="sdk-path.js"></script>
<!-- Load SDK modules in order -->
<script src="../websdk/sdk/core/events.js"></script>
<script src="../websdk/sdk/core/connection.js"></script>
<script src="../websdk/sdk/core/messages.js"></script>
<script src="../websdk/sdk/state/handlers.js"></script>
<script src="../websdk/sdk/state/devices.js"></script>
<script src="../websdk/sdk/state/users.js"></script>
<script src="../websdk/sdk/helpers/coords.js"></script>
<script src="../websdk/sdk/helpers/hit-detection.js"></script>
<script src="../websdk/sdk/helpers/cursors.js"></script>
<script src="../websdk/sdk/helpers/click-bindings.js"></script>
<script src="../websdk/sdk/input/simplified.js"></script>
<script src="../websdk/sdk/input/native.js"></script>
<script src="../websdk/sdk/index.js"></script>
<!-- Optional: UI and i18n -->
<script src="../websdk/sdk/i18n.js"></script>
<script src="../websdk/sdk/ui.js"></script>
<!-- Your app modules -->
<script src="js/app-config.js"></script>
<script src="js/app-state.js"></script>
<script src="js/app-ui.js"></script>
<script src="js/app-sdk.js"></script>
<script src="js/app-init.js"></script>
</body>
</html>
websdk/sdk/
+-- core/ # Core functionality
| +-- events.js # Event system (on/off/emit)
| +-- connection.js # WebSocket lifecycle
| +-- messages.js # Message parsing and routing
|
+-- state/ # State management
| +-- handlers.js # Handler registration
| +-- devices.js # Device state and colors
| +-- users.js # User management
|
+-- helpers/ # UI helpers
| +-- coords.js # Coordinate conversion
| +-- forwarder.js # Coordinate forwarding (screen mirror/sharing)
| +-- hit-detection.js # Hit detection wrappers
| +-- cursors.js # Remote cursor visualization
| +-- click-bindings.js # Declarative click bindings
|
+-- input/ # Input handling
| +-- simplified.js # Simplified event handlers
| +-- native.js # Debug/offline mode
|
+-- i18n.js # Internationalization
+-- ui.js # UI components
+-- index.js # Module assembler
events.js (no deps)
|
handlers.js (no deps)
|
connection.js --> events, handlers
|
devices.js --> users (optional)
|
users.js --> events, connection
|
messages.js --> events, handlers, devices, users, connection
|
coords.js (standalone)
|
forwarder.js --> coords, users (via sdk instance)
|
hit-detection.js --> coords
|
cursors.js --> events, devices, users
|
click-bindings.js --> events, hit-detection
|
simplified.js --> events, devices
|
native.js --> messages
|
index.js (assembles all)
All 10 required handlers must be registered via registerHandlers() before connecting:
| Handler | Data Fields | Description |
|---|---|---|
onPointerMotion |
hwid, userId, x, y, user, deviceType, inputSource |
Pointer moved |
onPointerButton |
hwid, userId, x, y, button, user, deviceType, inputSource |
Button pressed/released |
onPointerWheel |
hwid, userId, x, y, delta, horizontal, user, deviceType, inputSource |
Scroll wheel |
onPointerPen |
hwid, userId, x, y, pressure, tilt_x, tilt_y, rotation, user, deviceType, inputSource |
Pen/stylus input |
onKeyboard |
hwid, userId, vkey, message, scan, flags, user, deviceType, inputSource |
Keyboard input |
onUserList |
users[] |
Full user list received |
onUserCreate |
hwid_ms, hwid_kb, name |
New user connected |
onUserDispose |
hwid_ms, hwid_kb |
User disconnected |
onConnected |
url |
Connected to server |
onDisconnected |
-- | Disconnected from server |
Input event fields:
| Field | Description |
|---|---|
hwid |
Device hardware ID (different for mouse vs keyboard) |
userId |
User ID -- same for mouse and keyboard from the same user. Use this to identify users. |
user |
Full user object, or null if user list not requested |
deviceType |
'mouse', 'keyboard', or 'pen' |
inputSource |
'server' for MouseMux input, 'native' for browser input |
For handlers you don't need, use empty functions: onPointerWheel: () => {}
| Handler | Description |
|---|---|
onServerShutdown |
Server shutting down (data: {reason}) |
onServerTimeoutWarning |
Timeout warning (data: {minutes}; return true to suppress default alert) |
onServerTimeoutStopped |
Session ended due to timeout (data: {reason}) |
onDeviceNew |
SDK saw a new HWID for the first time (data: {hwid, data}) |
onUserMap |
Keyboard mapped to existing user (data: {action, userId, hwid_ms, hwid_kb, name}) |
onUserUpdate |
User state changed (data: {action, userId, ...}) |
onUserChanged |
Any user state change -- create/map/dispose/update |
onScreenList |
Screen configuration received (data: {virtual, screens[]}) |
onScreenChange |
Screen configuration changed |
onWindowInfo |
Tracked window moved/resized/changed (data: {hwnd, pid, title, ...}) |
onWindowDestroyed |
Tracked window was destroyed (data: {hwnd}) |
onServerMode |
Server mode changed (data: {mode[]}) |
// Async connection with error handling
try {
await sdk.connectAsync('ws://localhost:41001', { timeout: 5000 });
console.log('Connected!');
} catch (err) {
console.error('Connection failed:', err.message);
sdk.enableNativeInput(document.body);
}
The connection handshake has two parts:
1. Server Info (Server -> App)
When the WebSocket opens, the server sends server.info.notify.M2A containing server metadata and a machine fingerprint:
{
type: 'server.info.notify.M2A',
version: '3.0.1',
protocol_version: '2.2.45',
mode: 4,
buildDate: 'Mar 15 2026',
buildTime: '11:31:56',
systemId: 'A1B2C3D4-E5F60718-9A0B1C2D-3E4F5061-000000000000000000000000-000'
}
2. Client Login (App -> Server)
The SDK automatically sends a login request:
{
type: 'client.login.request.A2M',
appName: 'Your App',
appVersion: '1.0.0',
appBuildDate: '2026-03-15',
sdkVersion: '1.0.0',
sdkBuildDate: '2026-03-15'
}
Listen for the server info event:
sdk.on('server.info.notify.M2A', (data) => {
console.log('Server:', data.version, '| System:', data.systemId);
});
The server sends periodic keepalive pings. The SDK handles pong responses automatically -- no app code required.
// Try auto-connect silently, show dialog only on failure
const autoConnected = await sdk.tryAutoConnect();
if (!autoConnected) {
const result = await MouseMuxUI.showConnectionDialog({ ... });
}
// Or probe first
try {
const info = await MouseMuxSDK.probe('ws://localhost:41001');
console.log('Server running: v' + info.version);
sdk.connect('ws://localhost:41001');
} catch (err) {
console.log('Server not available');
}
// Graceful shutdown - waits for server to clean up virtual users and captures
await sdk.disconnectAsync('user');
The SDK sends button events (transitions), not cumulative state. Each button has paired DOWN and UP constants:
| Constant | Value | Event |
|---|---|---|
BUTTON_LEFT_DOWN |
0x01 |
Left button pressed |
BUTTON_LEFT_UP |
0x02 |
Left button released |
BUTTON_RIGHT_DOWN |
0x04 |
Right button pressed |
BUTTON_RIGHT_UP |
0x08 |
Right button released |
BUTTON_MIDDLE_DOWN |
0x10 |
Middle button pressed |
BUTTON_MIDDLE_UP |
0x20 |
Middle button released |
BUTTON_X1_DOWN |
0x40 |
X1/Back pressed |
BUTTON_X1_UP |
0x80 |
X1/Back released |
BUTTON_X2_DOWN |
0x100 |
X2/Forward pressed |
BUTTON_X2_UP |
0x200 |
X2/Forward released |
All constants are accessed via MouseMuxSDK.BUTTON_*.
const { BUTTON_LEFT_DOWN, BUTTON_LEFT_UP, BUTTON_RIGHT_DOWN } = MouseMuxSDK;
onPointerButton: (data) => {
if (data.button & BUTTON_LEFT_DOWN) handleClick(data.x, data.y, data.userId);
if (data.button & BUTTON_LEFT_UP) handleRelease(data.hwid);
if (data.button & BUTTON_RIGHT_DOWN) showContextMenu(data);
}
Since events are transitions, track cumulative state yourself:
const { BUTTON_LEFT_DOWN, BUTTON_LEFT_UP } = MouseMuxSDK;
onPointerButton: (data) => {
const device = sdk.getDevice(data.hwid);
if (data.button & BUTTON_LEFT_DOWN) device.buttonDown = true;
if (data.button & BUTTON_LEFT_UP) device.buttonDown = false;
}
MouseMux sends coordinates in physical screen pixels. The SDK provides helpers to convert to element-relative coordinates:
// Convert screen coords to element coords
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) {
drawAt(pos.x, pos.y);
}
// Check if point is in element
if (sdk.coords.isInElement(data.x, data.y, canvas)) {
// Handle canvas interaction
}
// Find clicked element by selector
const btn = sdk.findClickedElement(data.x, data.y, '.btn');
if (btn) btn.click();
Important: Always use sdk.coords.screenToElement() -- never use raw x/y coordinates directly. The SDK handles multi-monitor layouts, DPI scaling, and Electron zoom factors.
| Path | Accuracy | How it works |
|---|---|---|
| Electron (recommended) | Accurate | Main process pushes transform cache on window move/resize. Zero IPC per event. Handles multi-monitor + mixed DPI + zoom. |
| Browser (fallback) | Approximate | Uses window.devicePixelRatio and window.screen*. Works for single monitor, same-DPI setups. |
| Range | Hex | Description |
|---|---|---|
| 0 - 4095 | 0x0000-0x0FFF |
Physical hardware devices |
| 12288 - 13311 | 0x3000-0x33FF |
SDK server-assigned devices |
| 13312 - 14335 | 0x3400-0x37FF |
SDK client-side virtual users |
| 16383 | 0x3FFF |
Native input (debug mode) |
| 24576 - 26623 | 0x6000-0x67FF |
RustDesk remote devices |
const device = sdk.getDevice(hwid);
// {
// hwid: 197992480,
// buttonDown: true,
// lastX: 500, lastY: 300,
// color: '#ef4444',
// user: { name: 'John', ... },
// customData: {}
// }
// Store per-device app data
sdk.setDeviceData(hwid, 'brushColor', '#ff0000');
sdk.setDeviceData(hwid, 'brushSize', 10);
// Retrieve
const color = sdk.getDeviceData(hwid, 'brushColor');
const all = sdk.getAllDeviceData(hwid);
// Auto-assigned from palette
const color = sdk.getDeviceColor(hwid); // '#ef4444'
// Custom palette
sdk.setColorPalette([
'#3b82f6', '#ef4444', '#10b981', '#f59e0b',
'#8b5cf6', '#ec4899', '#06b6d4', '#84cc16'
]);
const devices = sdk.getAllDevices(); // Map of all devices
const lastHwid = sdk.getLastActiveDevice(); // Most recently active HWID
const isLocal = sdk.isMyDevice(hwid); // Is this one of my devices?
const myHwids = sdk.getMyDevices(); // Set of local HWIDs
Users are physical persons with one or more devices (mouse + keyboard):
// Get user for a device
const user = sdk.getUserForDevice(hwid);
// {
// id: number,
// name: string, // Color-based name: "Red", "Blue", etc.
// flag: number,
// flags: ['active', 'root'],
// devices: [{ hwid, type, subtype, path, buttons, udev }],
// cursor: { x, y, type, size, hidden, theme },
// color: { r, g, b }
// }
// Get user ID from any device HWID (mouse or keyboard)
const userId = sdk.getUserId(hwid);
// Check if two devices belong to the same user
if (sdk.isSameUser(mouseHwid, keyboardHwid)) { /* same person */ }
// Get all HWIDs for a user
const hwids = sdk.getDevicesForUser(userId);
// Friendly device name
const name = sdk.getDeviceName(hwid); // "John's Mouse"
// Request user list from server (most apps need this)
sdk.requestUserList();
| Action | Handler | When |
|---|---|---|
create |
onUserCreate |
Mouse arrives, new user created |
map |
onUserMap (optional) |
Keyboard mapped to existing user |
dispose |
onUserDispose |
User removed |
All changes also trigger onUserChanged(data) where data.action indicates the change type.
| Value | Description |
|---|---|
mouse |
Standard mouse |
pen_external |
External pen/stylus |
pen_internal |
Built-in pen (tablet) |
touch |
Touch input |
touchpad |
Touchpad |
| Flag | Description |
|---|---|
active |
User is active |
root |
Admin user |
locked |
User locked |
captured |
Device captured (SDK) |
cursor_hidden |
Cursor hidden |
cursor_move_locked |
Movement locked |
cursor_type_locked |
Type locked |
macro_recording |
Recording macro |
macro_playing |
Playing macro |
Create programmatic users without a server connection. Each virtual user gets a deterministic cursor color and name tag.
sdk.enableVirtualInput();
// Create virtual users
const alice = sdk.createVirtualUser('Alice');
// { hwid_ms: 0x3400, hwid_kb: 0x3401, name: 'Alice', color: '#e6194b' }
const bob = sdk.createVirtualUser('Bob');
// { hwid_ms: 0x3402, hwid_kb: 0x3403, name: 'Bob', color: '#3cb44b' }
// Inject input events
sdk.injectVirtualPointerMotion(alice.hwid_ms, 500, 300);
sdk.injectVirtualPointerButton(alice.hwid_ms, 500, 300, MouseMuxSDK.BUTTON_LEFT_DOWN);
sdk.injectVirtualPointerWheel(bob.hwid_ms, 200, 100, 120);
sdk.injectVirtualKeyboard(alice.hwid_kb, 0x41, 0x100); // 'A' key down
// Convenience: instant down+up click
sdk.injectVirtualClick(alice.hwid_ms, 500, 300);
// Update name
sdk.setVirtualUserName(alice.hwid_ms, 'Alice (host)');
// Cleanup
sdk.destroyVirtualUser(alice.hwid_ms);
sdk.disableVirtualInput(); // destroys all remaining
Virtual users integrate with remote cursors automatically:
sdk.enableRemoteCursors(document.body, { showLabels: true });
sdk.enableVirtualInput();
const vu = sdk.createVirtualUser('Remote Viewer');
sdk.injectVirtualPointerMotion(vu.hwid_ms, 400, 200); // cursor appears
Colors are deterministic by HWID slot -- destroying and recreating at the same slot gives the same color.
For screen sharing, remote desktop, and mirroring apps, the SDK's CoordinateForwarder maps coordinates between a video element showing a captured screen and the target screen.
| Mode | How it works | When to use |
|---|---|---|
| Manual | Client-side JS: screenToElement -> normalize -> de-normalize to target screen |
Full client-side control |
| Server | Raw physical coords + transform ID sent to server; server does the mapping | Production use |
Both modes use (w-1) normalization to match the server's integer math exactly.
// 1. Connect and get screen list
await sdk.connectAsync('ws://localhost:41001');
sdk.requestScreenList();
// 2. Create forwarder
const fwd = sdk.createCoordinateForwarder(videoElement, screenIndex, {
mode: 'server'
});
await fwd.activate();
// 3. Forward coordinates in your handler
onPointerMotion: (data) => {
const params = fwd.buildMotionParams(vuMouseHwid, data.x, data.y);
if (params) sdk.send('pointer.motion.request.A2M', params);
}
// 4. Cleanup
fwd.destroy();
| Property | Type | Description |
|---|---|---|
mode |
'manual' or 'server' |
Forwarding mode |
active |
boolean |
Whether currently active |
transformId |
number or null |
Server-side transform ID (server mode) |
targetScreen |
object or null |
Target screen from screen list |
element |
HTMLElement |
Source DOM element |
screenIndex |
number |
Target screen index |
const info = await sdk.requestWindowInfo(hwnd);
// {
// hwnd: 12345, pid: 9876,
// title: 'Untitled - Notepad',
// monitor: 0, dpi: 144,
// state: 'normal', visible: true,
// window: { x: 100, y: 200, w: 800, h: 600 },
// client: { x: 108, y: 230, w: 784, h: 561 }
// }
const info = await sdk.trackWindow(hwnd);
sdk.registerHandlers({
// ... required handlers ...
onWindowInfo: (data) => {
console.log('Window changed:', data.title, data.state);
},
onWindowDestroyed: (data) => {
console.log('Window destroyed:', data.hwnd);
}
});
sdk.untrackWindow(hwnd); // stop tracking
// Convenience methods
await sdk.showWindow(hwnd);
await sdk.hideWindow(hwnd);
await sdk.minimizeWindow(hwnd);
await sdk.maximizeWindow(hwnd);
await sdk.restoreWindow(hwnd);
await sdk.foregroundWindow(hwnd);
await sdk.focusWindow(hwnd);
await sdk.topWindow(hwnd);
await sdk.enableWindow(hwnd);
await sdk.disableWindow(hwnd);
await sdk.closeWindow(hwnd);
await sdk.moveWindow(hwnd, 100, 200);
await sdk.resizeWindow(hwnd, 800, 600);
await sdk.setWindowTransparent(hwnd, 128); // 50% opacity
await sdk.setWindowTopmost(hwnd);
await sdk.clearWindowTopmost(hwnd);
await sdk.setWindowClickthrough(hwnd);
await sdk.clearWindowClickthrough(hwnd);
// Or use the generic method
await sdk.controlWindow(hwnd, 'minimize');
await sdk.controlWindow(hwnd, 'move', { x: 100, y: 200 });
await sdk.controlWindow(hwnd, 'resize', { w: 800, h: 600 });
await sdk.controlWindow(hwnd, 'transparent', { alpha: 128 });
| Action | Description | Params |
|---|---|---|
show |
Show window | -- |
hide |
Hide window | -- |
minimize |
Minimize to taskbar | -- |
maximize |
Maximize window | -- |
restore |
Restore from min/max | -- |
foreground |
Bring to foreground | -- |
focus |
Set keyboard focus | -- |
top |
Bring to top of Z-order | -- |
enable |
Enable mouse/keyboard input | -- |
disable |
Disable mouse/keyboard input | -- |
close |
Close window | -- |
move |
Move to position | x, y |
resize |
Resize window | w, h |
transparent |
Set opacity | alpha (0-255) |
topmost |
Always on top | -- |
no_topmost |
Remove always on top | -- |
clickthrough |
Make click-through | -- |
no_clickthrough |
Restore mouse input | -- |
The same handlers work for both MouseMux server input and native browser input:
sdk.registerHandlers({
onPointerMotion: (data) => {
// Works for BOTH server and native input
// data.inputSource: 'server' or 'native'
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) handleMotion(pos.x, pos.y, data.hwid);
},
onPointerButton: (data) => {
if (data.button & MouseMuxSDK.BUTTON_LEFT_DOWN) handleClick(data);
}
// ...
});
// Connect to server OR enable native input - handlers work either way
if (result.action === 'connect') {
sdk.connect(result.url);
} else {
sdk.enableNativeInput(document.body);
}
Rules:
addEventListener callsuseMouseMux flags in handlersdata.inputSource if you need to distinguish sourcessdk.bindClick() for UI buttons (works for both modes)onPointerMotion: (data) => {
const pos = sdk.coords.screenToElement(data.x, data.y, canvas);
if (pos) {
// pos.x, pos.y are element-relative
}
}
onPointerPen: (data) => {
const pressure = data.pressure / 1023; // normalize to 0-1
const minSize = 2, maxSize = 20;
const size = minSize + (maxSize - minSize) * pressure;
}
onKeyboard: (data) => {
const isDown = data.message === 0x100; // WM_KEYDOWN
const isUp = data.message === 0x101; // WM_KEYUP
if (data.vkey === 0x42 && isDown) setTool('brush'); // 'B'
if (data.vkey === 0x45 && isDown) setTool('eraser'); // 'E'
}
Declaratively bind clicks to elements:
sdk.bindClick('.color-swatch', (element, hwid) => {
const color = element.dataset.color;
sdk.setDeviceData(hwid, 'selectedColor', color);
element.style.borderColor = sdk.getDeviceColor(hwid);
});
sdk.bindClick('.answer-card', (element, hwid) => {
state.votes.set(hwid, parseInt(element.dataset.index));
element.style.boxShadow = '0 0 10px ' + sdk.getDeviceColor(hwid);
updateVoteDisplay();
});
sdk.clearClickBindings(); // remove all
sdk.enableRemoteCursors(document.body, {
showLabels: true,
autoHide: false,
customHTML: (hwid, device) => {
return '<div style="color:' + device.color + ';">●</div>';
}
});
sdk.disableRemoteCursors();
const result = await MouseMuxUI.showConnectionDialog({
appName: 'My App',
appDescription: 'Multi-user collaborative application',
defaultUrl: 'ws://localhost:41001',
showThemeOption: true,
showLanguageOption: true,
onThemeChange: (theme) => applyTheme(theme),
onLanguageChange: (lang) => updateStrings()
});
if (result.action === 'connect') {
sdk.connect(result.url);
} else if (result.action === 'skip') {
sdk.enableNativeInput(document.body);
}
MouseMuxUI.showBanner({
appName: 'My App',
appVersion: '1.0.0',
appDate: '2026-03-15',
appTime: '11:31:56',
sdk: sdk,
showThemeToggle: true,
showLanguageSelector: true
});
MouseMuxUI.toast('Connected!', { type: 'success' });
MouseMuxUI.toast('Warning message', { type: 'warning' });
MouseMuxUI.toast('Error occurred', { type: 'error', duration: 5000 });
MouseMuxUI.toast('Info', { type: 'info' });
const confirmed = await ui.confirm('Are you sure?', {
title: 'Confirm Action',
confirmText: 'Yes',
cancelText: 'No'
});
const danger = await ui.confirmDanger('Delete all data?', {
title: 'Danger',
confirmText: 'Delete'
});
9 languages supported: English, German, Spanish, French, Italian, Japanese, Korean, Dutch, Chinese.
// SDK locales only
await MouseMuxI18n.init({
localesPath: '../websdk/locales',
defaultLanguage: 'en'
});
// SDK + app locales
await MouseMuxI18n.init({
localesPath: '../websdk/locales',
appLocales: {
path: './locales',
namespace: 'myapp'
}
});
MouseMuxI18n.t('sdk.connected'); // SDK string
MouseMuxI18n.t('myapp.brushSize'); // App string
MouseMuxI18n.t('sdk.timeoutWarning', { minutes: 5 }); // With params
await MouseMuxI18n.setLanguage('de');
const lang = MouseMuxI18n.getLanguage();
MouseMuxI18n.getInstance().on('languageChanged', (lang) => {
updateUIStrings();
});
{
"_meta": { "language": "English", "code": "en" },
"appName": "My App",
"brushSize": "Brush Size",
"clearCanvas": "Clear Canvas",
"greeting": "Hello, {{name}}!"
}
Test your app without a MouseMux server:
try {
await sdk.connectAsync('ws://localhost:41001', { timeout: 2000 });
} catch (err) {
console.log('Server unavailable, using native input');
sdk.enableNativeInput(document.body);
}
if (sdk.isNativeInputEnabled()) {
console.log('Running in debug mode');
}
sdk.disableNativeInput();
Native input uses HWID 0x3FFF for the single debug device.
The server sends a systemId string on connect -- a machine fingerprint that uniquely identifies the host system.
{CPUID}-{MAC}-{MACHINEGUID}-{SID}-{LICENSE}
Four 8-character uppercase hex fields (zero-padded), followed by a variable-length license string.
Example: A1B2C3D4-E5F60718-9A0B1C2D-3E4F5061-000000000000000000000000-000
const sysId = sdk.getSystemId();
if (sdk.serverInfo) {
console.log('System ID:', sdk.serverInfo.systemId);
console.log('Version:', sdk.serverInfo.version);
}
sdk.on('server.info.notify.M2A', (data) => {
console.log('System ID:', data.systemId);
});
function parseSystemId(systemId) {
const parts = systemId.split('-');
return {
cpuid: parts[0],
mac: parts[1],
machineGuid: parts[2],
sid: parts[3],
license: parts.slice(4).join('-')
};
}
const { contextBridge, ipcRenderer } = require('electron');
contextBridge.exposeInMainWorld('mmxElectron', {
getTransform: () => ipcRenderer.invoke('mmx-get-transform'),
onTransformUpdate: (cb) => {
const handler = (_, t) => cb(t);
ipcRenderer.on('mmx-transform-update', handler);
return () => ipcRenderer.removeListener('mmx-transform-update', handler);
},
onShutdown: (cb) => {
const handler = async () => {
await cb();
ipcRenderer.send('mmx-shutdown-complete');
};
ipcRenderer.on('mmx-shutdown', handler);
return () => ipcRenderer.removeListener('mmx-shutdown', handler);
}
});
Important: The onShutdown handler ensures the SDK can send client.logout.request.A2M and wait for the response before the window closes. Without this, virtual users and captures may be orphaned.
const { screen, ipcMain } = require('electron');
function pushTransform() {
const transform = {
zoomFactor: win.webContents.getZoomFactor(),
contentBoundsDip: win.getContentBounds(),
displays: screen.getAllDisplays().map(d => ({
id: d.id,
scaleFactor: d.scaleFactor,
boundsDip: d.bounds
}))
};
win.webContents.send('mmx-transform-update', transform);
}
const debounce = (fn, ms) => {
let timeout;
return (...args) => {
clearTimeout(timeout);
timeout = setTimeout(() => fn(...args), ms);
};
};
win.on('resize', debounce(pushTransform, 50));
win.on('move', debounce(pushTransform, 50));
ipcMain.handle('mmx-get-transform', pushTransform);
// Graceful shutdown
let shutdownComplete = false;
ipcMain.on('mmx-shutdown-complete', () => {
shutdownComplete = true;
win.destroy();
});
win.on('close', (e) => {
if (!shutdownComplete) {
e.preventDefault();
win.webContents.send('mmx-shutdown');
setTimeout(() => { shutdownComplete = true; win.destroy(); }, 2000);
}
});
myapp/
+-- index.html # Entry point
+-- sdk-path.js # SDK path configuration
+-- app.json # App metadata
+-- css/
| +-- app.css # Styles
+-- js/
| +-- app-config.js # Constants and configuration
| +-- app-state.js # State management
| +-- app-ui.js # UI helpers and rendering
| +-- app-sdk.js # SDK integration
| +-- app-init.js # Initialization
+-- locales/
+-- en.json # English strings
+-- de.json # German strings
Apps must follow this exact order:
(async function initializeApp() {
// 1. Load app metadata
let appInfo = { name: 'My App', info: { make: '2.2.36' } };
try {
const response = await fetch('./app.json');
if (response.ok) appInfo = await response.json();
} catch (e) { console.warn('Could not load app.json'); }
// 2. Create SDK instance
const sdk = new MouseMuxSDK({
appName: appInfo.name,
appVersion: appInfo.info?.make || '2.2.36',
autoConnect: false
});
// 3. Register handlers (before connection dialog!)
sdk.registerHandlers({ /* all 10 handlers */ });
// 4. Initialize i18n
await MouseMuxI18n.init({
localesPath: MOUSEMUX_SDK_PATH + 'locales',
appLocales: { path: './locales', namespace: 'myapp' }
});
// 5. Show connection dialog
const result = await MouseMuxUI.showConnectionDialog({
appName: appInfo.name,
showThemeOption: true,
showLanguageOption: true
});
// 6. Connect or enable native input
if (result.action === 'connect') {
sdk.connect(result.url);
} else {
sdk.enableNativeInput(document.body);
}
// 7. Show status banner
MouseMuxUI.showBanner({
appName: appInfo.name,
appVersion: appInfo.info?.make,
appDate: appInfo.date,
appTime: appInfo.time,
sdk: sdk,
showThemeToggle: true
});
})();
Declare which operating modes your app needs:
{
"mode": {
"hard": ["multiplex"],
"hint": ["switched"],
"help": "Works best in Switched mode for independent cursors"
}
}
| Field | Type | Description |
|---|---|---|
hard |
string[] |
Required modes -- app will not function without these |
hint |
string[] |
Suggested modes -- app works best with these |
help |
string |
Human-readable explanation |
Mode values: "native", "switched", "multiplex", "multi-keyboard"
| Option | Type | Default | Description |
|---|---|---|---|
appName |
string |
'Unknown App' |
Application name (sent to server on login) |
appVersion |
string |
'0.0.0' |
Application version |
appBuildDate |
string |
'unknown' |
Application build date |
url |
string |
'ws://localhost:41001' |
WebSocket server URL |
autoConnect |
boolean |
true |
Auto-connect on construction |
strictValidation |
boolean |
false |
Enable strict field validation on messages |
| Method | Returns | Description |
|---|---|---|
connect(url?) |
void |
Connect to WebSocket server |
connectAsync(url?, options?) |
Promise |
Connect with Promise and timeout |
tryAutoConnect() |
Promise<boolean> |
Try auto-connect silently |
disconnect() |
void |
Disconnect from server |
disconnectAsync(reason?) |
Promise |
Graceful disconnect with logout |
isConnected() |
boolean |
Check connection status |
send(type, params?) |
boolean |
Send message to server |
sendLog(level, message) |
boolean |
Send log message to server |
| Method | Returns | Description |
|---|---|---|
getDevice(hwid) |
object |
Get device state |
getAllDevices() |
Map |
Get all devices |
getLastActiveDevice() |
number |
Get last active HWID |
isMyDevice(hwid) |
boolean |
Check if local device |
getMyDevices() |
Set |
Get all local HWIDs |
setDeviceData(hwid, key, value) |
void |
Store custom data |
getDeviceData(hwid, key) |
any |
Get custom data |
getAllDeviceData(hwid) |
object |
Get all custom data |
getDeviceColor(hwid) |
string |
Get auto-assigned color |
setColorPalette(colors) |
void |
Set custom color palette |
getColorPalette() |
string[] |
Get current palette |
| Method | Returns | Description |
|---|---|---|
requestUserList() |
boolean |
Request user list from server |
getUserId(hwid) |
number |
Get user ID for any device |
isSameUser(hwid1, hwid2) |
boolean |
Check if same user |
getDevicesForUser(userId) |
number[] |
Get all HWIDs for a user |
getUserForDevice(hwid) |
object |
Get full user object |
getDeviceName(hwid) |
string |
Get friendly device name |
getUsers() |
Map |
Get all users |
getStats() |
object |
Get event statistics |
getSystemId() |
string |
Get system ID of connected server |
getServerInfo(options?) |
Promise |
Get server info |
requestServerInfo() |
void |
Request server info |
requestServerPath(what, options?) |
Promise |
Resolve path key to filesystem path |
requestHardwareIDs(options?) |
Promise |
Request virtual HWIDs |
requestServerMode() |
void |
Request current server mode |
| Method | Returns | Description |
|---|---|---|
requestScreenList() |
boolean |
Request monitor/screen list |
activateTransform(params) |
boolean |
Activate coordinate transform |
updateTransform(params) |
boolean |
Update transform viewport |
deactivateTransform(params) |
boolean |
Deactivate transform |
createCoordinateForwarder(el, screenIdx, opts?) |
CoordinateForwarder |
Create coordinate forwarder |
| Method | Returns | Description |
|---|---|---|
requestWindowInfo(hwnd, options?) |
Promise<Object> |
One-shot window info query |
trackWindow(hwnd, options?) |
Promise<Object> |
Subscribe to live window updates |
untrackWindow(hwnd) |
boolean |
Unsubscribe from window updates |
controlWindow(hwnd, action, params?, options?) |
Promise<Object> |
Control a window |
showWindow(hwnd) |
Promise |
Show window |
hideWindow(hwnd) |
Promise |
Hide window |
minimizeWindow(hwnd) |
Promise |
Minimize window |
maximizeWindow(hwnd) |
Promise |
Maximize window |
restoreWindow(hwnd) |
Promise |
Restore window |
foregroundWindow(hwnd) |
Promise |
Bring to foreground |
focusWindow(hwnd) |
Promise |
Set keyboard focus |
topWindow(hwnd) |
Promise |
Bring to top |
closeWindow(hwnd) |
Promise |
Close window |
moveWindow(hwnd, x, y) |
Promise |
Move window |
resizeWindow(hwnd, w, h) |
Promise |
Resize window |
setWindowTransparent(hwnd, alpha) |
Promise |
Set opacity (0-255) |
setWindowTopmost(hwnd) |
Promise |
Always on top |
clearWindowTopmost(hwnd) |
Promise |
Remove always on top |
setWindowClickthrough(hwnd) |
Promise |
Make click-through |
clearWindowClickthrough(hwnd) |
Promise |
Restore mouse input |
sdk.coords)| Method | Returns | Description |
|---|---|---|
screenToElement(x, y, el) |
{x,y} or null |
Convert to element coords |
screenToContentCss(x, y) |
{x,y} or null |
Convert to CSS coords |
isInElement(x, y, el) |
boolean |
Check if point is in element |
elementAtPoint(x, y) |
Element or null |
Get element at screen coords |
hasTransformCache() |
boolean |
Check conversion accuracy |
hasElectronBridge() |
boolean |
Check if Electron bridge available |
onTransformUpdate(cb) |
function |
Subscribe to transform updates |
getElementPhysicalBounds(el) |
{x,y,w,h} or null |
Get element bounds in physical pixels |
findClickedElement(x, y, selector) |
Element or null |
Find element at coords |
| Method | Returns | Description |
|---|---|---|
bindClick(selector, callback) |
function |
Bind click to selector |
clearClickBindings() |
void |
Remove all bindings |
enableNativeInput(container) |
void |
Enable native input mode |
disableNativeInput() |
void |
Disable native input mode |
isNativeInputEnabled() |
boolean |
Check if native input active |
| Method | Returns | Description |
|---|---|---|
enableVirtualInput() |
void |
Enable virtual input mode |
disableVirtualInput() |
void |
Disable and destroy all virtual users |
isVirtualInputEnabled() |
boolean |
Check if enabled |
createVirtualUser(name?) |
{hwid_ms, hwid_kb, name, color} |
Create virtual user |
destroyVirtualUser(hwid_ms) |
void |
Destroy a virtual user |
getVirtualUsers() |
Map |
Get all active virtual users |
setVirtualUserName(hwid_ms, name) |
void |
Update display name |
injectVirtualPointerMotion(hwid, x, y) |
void |
Inject pointer motion |
injectVirtualPointerButton(hwid, x, y, button) |
void |
Inject button event |
injectVirtualPointerWheel(hwid, x, y, delta) |
void |
Inject wheel event |
injectVirtualKeyboard(hwid, vkey, message, scan?, flags?) |
void |
Inject keyboard event |
injectVirtualClick(hwid, x, y, domButton?) |
void |
Instant down+up click |
| Method | Returns | Description |
|---|---|---|
enableRemoteCursors(container, opts) |
void |
Show remote cursors |
disableRemoteCursors() |
void |
Hide remote cursors |
| Method | Returns | Description |
|---|---|---|
on(event, callback) |
void |
Register event listener |
off(event, callback) |
void |
Remove event listener |
emit(event, data) |
void |
Emit event |
| Method | Returns | Description |
|---|---|---|
MouseMuxSDK.probe(url, options?) |
Promise |
Test server availability |
*.request.A2M -- App to MouseMux (outgoing commands)*.notify.M2A -- MouseMux to App (incoming events)All A2M commands return a response with ok: true/false and optional error string.
| Message | Description |
|---|---|
server.info.notify.M2A |
Server info (version, systemId, protocol_version) |
server.mode.notify.M2A |
Server mode (array: ["multiplex", "multi-keyboard"]) |
server.ping.notify.M2A |
Keepalive ping (SDK auto-responds) |
server.shutdown.notify.M2A |
Server shutting down |
server.timeout.warning.notify.M2A |
Timeout warning |
server.timeout.stopped.notify.M2A |
Session ended due to timeout |
screen.list.notify.M2A |
Screen/monitor configuration |
screen.change.notify.M2A |
Screen config changed |
user.list.notify.M2A |
Full user list |
user.create.notify.M2A |
User created |
user.dispose.notify.M2A |
User removed |
user.changed.notify.M2A |
User state changed |
pointer.motion.notify.M2A |
Pointer moved |
pointer.button.notify.M2A |
Button changed |
pointer.wheel.notify.M2A |
Wheel scroll |
pointer.pen.notify.M2A |
Pen input |
keyboard.key.notify.M2A |
Key event |
window.info.notify.M2A |
Tracked window changed |
window.destroyed.notify.M2A |
Tracked window destroyed |
| Message | Description |
|---|---|
client.login.request.A2M |
Login to server (automatic) |
client.logout.request.A2M |
Logout from server |
client.pong.request.A2M |
Pong response (automatic) |
client.log.request.A2M |
Send log message |
user.create.request.A2M |
Create virtual user |
user.dispose.request.A2M |
Remove virtual user |
user.list.request.A2M |
Request user list |
user.hwid.request.A2M |
Request available HWIDs |
pointer.motion.request.A2M |
Move pointer |
pointer.button.request.A2M |
Send button event |
pointer.wheel.request.A2M |
Send wheel event |
pointer.capture.request.A2M |
Capture mouse |
keyboard.key.request.A2M |
Send keyboard event |
cursor.hide.request.A2M |
Show/hide cursor |
cursor.type.request.A2M |
Set cursor type |
screen.list.request.A2M |
Request screen list |
transform.activate.request.A2M |
Activate coordinate transform |
window.info.request.A2M |
Query window info |
window.track.request.A2M |
Subscribe to window changes |
window.control.request.A2M |
Control a window |
server.info.request.A2M |
Request server info |
server.mode.request.A2M |
Request server mode |
server.path.request.A2M |
Resolve path key |
theme.list.request.A2M |
Request cursor theme list |
The Web SDK is included with every MouseMux app. Download any app from our App Library to get started -- each app contains a complete, working websdk/ folder structure.
What's included:
sdk/core/, sdk/state/, sdk/helpers/, sdk/input/)sdk/ui.js)locales/)See these apps in our App Library for working examples:
| App | Demonstrates |
|---|---|
| Multi-Paint | Collaborative drawing with brush/eraser tools |
| Team Vote | Multi-user voting and quiz system |
| Whiteboard | Shared whiteboard with shapes and text |
| SCADA Simulator | Industrial control panel demonstration |
| Service Desk | Multi-user customer support interface |
| Sound Board | Collaborative music application |
Each app demonstrates different SDK features and patterns. Download any app to explore its source code.
| Problem | Solution |
|---|---|
| Cannot connect | Verify MouseMux is running on ws://localhost:41001 |
| Connection drops | Check network stability, enable reconnect |
| Wrong URL | Check WebSocket URL in connection dialog |
| Problem | Solution |
|---|---|
| Coordinates wrong | Check sdk.coords.hasTransformCache() |
| Multi-monitor problems | Ensure displays array is populated |
| Electron zoom issues | Verify preload bridge is set up |
| Problem | Solution |
|---|---|
| Handlers not firing | Call registerHandlers() before connect() |
| Missing handlers error | Provide all 10 required handlers |
| Handler throws error | Add try-catch in handler code |
For more information or custom integration support, contact [email protected]