streamdeck_client
Pure Dart client for the Elgato Stream Deck WebSocket protocol. Handles connection, reconnection, event parsing, manifest models, and device layout constants.
Usage
The Stream Deck app launches your plugin with CLI args: -port, -pluginUUID, -registerEvent, -info. Parse them with ConnectionInfo.fromArgs, connect, and react to events.
import 'dart:convert';
import 'dart:io';
import 'package:streamdeck_client/streamdeck_client.dart';
void main(List<String> args) async {
// Parse CLI args from the Stream Deck app.
final connection = ConnectionInfo.fromArgs(args);
final client = Client(connection);
await client.connect();
// Track which actions are visible on the device.
client.actions.listen((actions) {
for (final action in actions.values) {
print('Active: ${action.action} at '
'(${action.payload.coordinates.column}, '
'${action.payload.coordinates.row})');
}
});
// Listen to all events globally.
client.events.listen((event) {
switch (event) {
case ActionInfo():
// An action appeared on the device. ActionInfo is the
// willAppear event — it carries the action UUID, context,
// device, coordinates, settings, and controller type.
final scoped = client.scoped(event.context);
scoped.setTitle(title: 'Ready');
print('willAppear: ${event.action} '
'controller=${event.payload.controller}');
case WillDisappearEvent():
print('willDisappear: ${event.context}');
case KeyDownEvent():
client.showAlert(event.context);
print('keyDown: ${event.context}');
case KeyUpEvent(:final payload):
print('keyUp: state=${payload.state}');
case DialRotateEvent(:final payload):
print('dialRotate: ${payload.ticks} ticks');
case DialDownEvent():
print('dialDown');
case DialUpEvent():
print('dialUp');
case TouchTapEvent(:final payload):
print('touchTap: pos=${payload.tapPos}');
case DidReceiveSettingsEvent(:final payload):
print('settings: ${jsonEncode(payload.settings)}');
default:
break;
}
});
// Or listen to events for a specific action context.
// This is more efficient — events are filtered and cached.
client.eventsFor('some-context-id').listen((event) {
switch (event) {
case KeyDownEvent():
final scoped = client.scoped(event.context);
scoped.setSettings(settings: {'count': 42});
scoped.showOk();
default:
break;
}
});
// Keep the process alive.
await ProcessSignal.sigint.watch().first;
await client.dispose();
}
Connection Lifecycle
connect() → Connection.connecting → Connection.connected
↓ (socket drops)
Connection.disconnected
↓ (auto)
Connection.reconnecting → connected
↓ (max attempts)
Connection.failed
Monitor connection state:
client.connection.listen((state) {
print('Connection: $state');
});
// Or read synchronously:
if (client.connectionValue == Connection.connected) { ... }
Scoped Client
When working with a specific action, use scoped() to avoid passing the context on every call:
client.events.listen((event) {
if (event case ActionInfo()) {
final action = client.scoped(event.context);
action.setTitle(title: 'Hello');
action.setSettings(settings: {'key': 'value'});
action.showAlert();
// All calls auto-fill context from the scoped client.
}
});
Features
- WebSocket client with automatic reconnection (exponential backoff, configurable max attempts).
- Full protocol coverage: all received events (
keyDown,dialRotate,touchTap,willAppear, etc.) and sent commands (setImage,setFeedback,setSettings, etc.). - Typed event models using freezed —
toString,==,hashCode,copyWith,fromJson/toJson. - Manifest models for generating
manifest.jsonfrom Dart code. - Device layouts with key sizes, gaps, margins, and encoder dimensions for all Stream Deck models.
- Scoped client —
client.scoped('context-id')returns a lightweight wrapper that auto-fills the action context on all send methods.