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.json from Dart code.
  • Device layouts with key sizes, gaps, margins, and encoder dimensions for all Stream Deck models.
  • Scoped clientclient.scoped('context-id') returns a lightweight wrapper that auto-fills the action context on all send methods.

Libraries

streamdeck_client