streamdeck_flutter 0.4.0 copy "streamdeck_flutter: ^0.4.0" to clipboard
streamdeck_flutter: ^0.4.0 copied to clipboard

PlatformmacOS

Build Elgato Stream Deck plugins with Flutter. Renders UI offscreen, slices frames into key-sized tiles, and pushes them via the Stream Deck WebSocket protocol.

streamdeck_flutter #

A Flutter framework for building Elgato Stream Deck plugins. Renders Flutter UI headless (no on-screen window), captures each action's region as a key-sized tile or touchscreen strip, and pushes them as base64 JPEG via the Stream Deck WebSocket protocol. Input events from the Stream Deck arrive as a typed event stream, surfaced through StreamDeckAction callbacks.

Platform: macOS only.

Requirements #

macOS App Sandbox must be disabled. The plugin connects to the Stream Deck app via a local WebSocket, which is blocked by the default sandbox. Disable it in your entitlements:

<key>com.apple.security.app-sandbox</key>
<false/>

Or add the network client entitlement:

<key>com.apple.security.network.client</key>
<true/>

Quick Start #

1. Add the dependency #

dependencies:
  streamdeck_flutter: ^0.4.0

2. Create your plugin #

import 'package:flutter/material.dart';
import 'package:streamdeck_flutter/streamdeck_flutter.dart';

const _binary = 'my_plugin';
const _uuid = 'com.example.my-plugin';

void main(List<String> args) {
  HeadlessBinding.ensureInitialized();
  runHeadless(MyPlugin(connection: ConnectionInfo.fromArgs(args)));
}

class MyPlugin extends StatelessWidget {
  const MyPlugin({super.key, required this.connection});

  final ConnectionInfo connection;

  @override
  Widget build(BuildContext context) {
    return StreamDeckPlugin(
      connection: connection,
      manifest: Manifest(
        uuid: _uuid,
        name: 'My Plugin',
        author: 'Your Name',
        description: 'A Stream Deck plugin built with Flutter.',
        icon: 'assets/imgs/plugin-icon',
        version: '1.0.0.0',
        codePath: '$_binary.app/Contents/MacOS/$_binary',
        actions: [
          ManifestAction(
            id: 'counter',
            name: 'Counter',
            tooltip: 'A simple tap counter.',
            icon: 'assets/imgs/action-icon',
            states: const [ManifestState(image: 'assets/imgs/action-icon')],
            build: (context) => const CounterAction(),
          ),
        ],
        os: const [ManifestOS(platform: 'mac', minimumVersion: '10.15')],
        software: const ManifestSoftware(minimumVersion: '6.9'),
      ),
    );
  }
}

3. Create an action widget #

Each ManifestAction.build is the WidgetBuilder for that action — the Stream Deck event for an action context is dispatched to its build. Inside, the action info is available via StreamDeck.actionOf(context). Use StreamDeckAction to wrap your UI — it handles routing to Keypad or Dial based on the controller type.

class CounterAction extends StatefulWidget {
  const CounterAction({super.key});

  @override
  State<CounterAction> createState() => _CounterActionState();
}

class _CounterActionState extends State<CounterAction> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    return StreamDeckAction(
      onKeyDown: () => setState(() => _count++),
      onDialRotate: (ticks) => setState(() => _count += ticks),
      onDialDown: () => setState(() => _count = 0),
      builder: (context) {
        final isEncoder = StreamDeck.actionOf(context)?.payload.controller == ActionType.encoder;
        return Container(
          color: Colors.black,
          child: Center(
            child: Text(
              isEncoder ? '$_count taps' : '$_count',
              style: TextStyle(color: Colors.white, fontSize: isEncoder ? 20 : 32),
            ),
          ),
        );
      },
    );
  }
}

4. Build and install #

flutter pub run streamdeck_flutter build -i com.example.my-plugin

Builds the app, bundles the plugin, links it, and restarts the Stream Deck app.

Action Types #

Keypad + Encoder (default) #

ManifestAction(
  id: 'my-action',
  name: 'My Action',
  states: const [ManifestState(image: 'assets/imgs/icon')],
  encoder: const ManifestEncoder(
    triggerDescription: ManifestTriggerDescription(
      rotate: 'Adjust value',
      push: 'Confirm',
      touch: 'Toggle',
    ),
  ),
)

Keypad only #

ManifestAction.keypad(
  id: 'key-action',
  name: 'Key Action',
  states: const [ManifestState(image: 'assets/imgs/icon')],
)

Encoder only #

ManifestAction.encoder(
  id: 'dial-action',
  name: 'Dial Action',
  states: const [ManifestState(image: 'assets/imgs/icon')],
)

StreamDeckAction Widget #

Wrap your action UI in StreamDeckAction. It provides a single builder that renders for both keypad and encoder — check StreamDeck.actionOf(context)?.payload.controller to differentiate:

StreamDeckAction(
  onKeyDown: () { /* key pressed */ },
  onKeyUp: () { /* key released */ },
  onDialRotate: (ticks) { /* dial rotated */ },
  onDialDown: () { /* dial pressed */ },
  onDialUp: () { /* dial released */ },
  onTouchTap: (position) { /* touchscreen tapped */ },
  onSettings: (json) { /* settings changed */ },
  builder: (context) => MyWidget(),
)

Settings #

Access action settings:

final action = StreamDeck.actionOf(context)!;
final color = action.payload.setting<String>('color') ?? '#FFFFFF';

Persist settings:

final client = StreamDeck.clientOf(context);
client.setSettings(settings: {'color': '#FF0000'});

CLI Commands #

# Build and install (profile mode)
flutter pub run streamdeck_flutter build -i com.example.my-plugin

# Build in release mode
flutter pub run streamdeck_flutter build -i com.example.my-plugin -m release

# Build and attach for development (debug mode + hot reload)
flutter pub run streamdeck_flutter run -i com.example.my-plugin

# Unlink before building (clean install)
flutter pub run streamdeck_flutter run -i com.example.my-plugin -u

Architecture #

Stream Deck App (WS Server)
    |  WebSocket JSON
Flutter macOS App (WS Client / Plugin)
    |-- StreamDeckPlugin      -> top-level widget, manifest, per-action build dispatch
    |-- Client                -> connects, registers, sends setImage/setFeedback
    |-- Raster                -> captures each RepaintBoundary, change detection, JPEG encode
    '-- StreamDeckAction      -> maps the SD event stream to Flutter callbacks

runHeadless mounts the plugin in a detached render tree (its own RenderView/PipelineOwner) flushed every frame, so the macOS window is never shown. Each action's RepaintBoundary is captured independently at the device's reported pixel ratio (the Stream Deck + reports 1.0). Only changed tiles are sent (FNV hash comparison) to minimize WebSocket traffic, and JPEG encoding runs on a background isolate to keep the heavy work off the render thread.

The manifest.json is generated automatically by the plugin on startup from the Manifest Dart object. Action UUIDs use short IDs (e.g. 'counter') that are auto-prefixed with the plugin UUID (e.g. com.example.my-plugin.counter).

Encoder Layout (canvas.json) #

The framework renders encoder touchscreen strips as pixel data using a canvas.json layout file bundled as a package asset. The manifest encoder block is generated automatically with the correct path. You do not need to call setFeedbackLayout at runtime — the Stream Deck app reads the layout from the manifest.

Assets (icons, layouts, UI files) are discovered automatically from the built app's AssetManifest.bin and symlinked into the plugin bundle. No manual asset copying is needed.

0
likes
150
points
96
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Build Elgato Stream Deck plugins with Flutter. Renders UI offscreen, slices frames into key-sized tiles, and pushes them via the Stream Deck WebSocket protocol.

Repository (GitHub)
View/report issues

Topics

#streamdeck #elgato #macos #plugin

License

BSD-3-Clause (license)

Dependencies

args, flutter, freezed_annotation, image, json_annotation, logging, rxdart, standard_message_codec, streamdeck_client

More

Packages that depend on streamdeck_flutter

Packages that implement streamdeck_flutter