Writing Plugins for the Elgato Stream Deck (NodeJS)

Objective in Brief

This article will show you how to write a simple plugin for the Elgato Stream Deck from scratch with NodeJS. It also shows how you can integrate a secondary language into this, for those for whom JS is not their primary language.

Introduction

Although streaming is on my list of things to do in 2021, I purchased my Stream Deck XL a few weeks ago as much to see how i could use it for developing tools than entering into the world of Twitch en all.

A shout out of Chrissy LeMaire and her post My setup after a year of livestreaming which gave me the original impetus to try out one of these button riddled marvels.

Elgato provide a readily accessible SDK for writing plugins for their device and I’ve spent the past week getting to grips with it.

NOTE FOR WINDOWS USERS:
The code below makes use of shell scripts with language specific shebangs. You will need to adjust these to use a suitable equivalent and update the codepath property in manifest.js accordingly. However, the code will remain exactly the same.

Plugin Background

Architecture

Essentially, you write plugins for the Stream Deck App, as it acts as a proxy to the device itself. Indeed, if you do not have the Stream Deck app open, your stream deck won’t operate beyond displaying the screensaver image. The document below illustrates this:

Plugins can utilise native Javascript in HTML documents, or alternatively a compiled command line tool, which can be written in a language of your choice.

Operation

Plugins are represented in the Stream Deck application on the right hand side of the window. The example below is a specific Custom group, including our Catme! plugin.

A plugin can contain one more more actions that can be used. To use one with the Stream Deck, you simply drag the action onto a specific key in the app. At this point, it is now also visible on your stream deck device and you can interact with it accordingly.

Plugins allow you to define the actions that occur, not only when a button is pressed, but also when other activities happen, such as an application you are monitoring is launched.

The Catme! Plugin

I decided that a good use of a plugin for a device costing over two hundred euros would be one that displayed a random cat image each time a button was pressed. Go figure… 🙂

Our Configuration

The basic software and hardware configuration the cat plugin has been developed on consists of:

  • Mac OSX, Big Sur – See note below for Windows systems and shell scripts.
  • PowerShell 7.1.0
  • NodeJS v15.50 (more about this below)
  • Elgato Stream Deck XL

Create New Plugin Directory

Plugins are stored in a specific directory under the generic Stream Deck plugin folder. On OSX systems, this is located by default at $HOME/Library/Application Support/com.elgato.StreamDeck/Plugins.

Making sure the Stream Deck application is closed, create a folder, com.example.timsapp.sdPlugin in the above directory. You’ll probably have noticed that folder names are in reverse DNS name format. This is intended for the purpose that may arise where you want your plugin to be available..

Create a Manifest File

Copy the text below, paste it into your editor of choice and save it in the folder you just renamed, saving the file as manifest.json, overwriting the existing file.

{
  "Actions": [
    {
      "Icon": "actionIcon",
      "Name": "Catme!",
      "States": [
        {
          "Image": "image",
          "TitleAlignment": "middle",
          "FontSize": "16"
        }
      ],
      "SupportedInMultiActions": false,
      "Tooltip": "This is a test application",
      "UUID": "com.example.timsapp.action"
    }
  ],
  "SDKVersion": 2,
  "Author": "Tim",
  "CodePath": "plugin",
  "Description": "This is a test application",
  "Name": "Tims App",
  "URL": "http://52.212.255.70",
  "Version": "1.4",
  "OS": [
    {
      "Platform": "mac",
      "MinimumVersion": "10.11"
    },
    {
      "Platform": "windows",
      "MinimumVersion": "10"
    }
  ],
  "Software": {
    "MinimumVersion": "4.1"
  }
}

You can find full documentation on the manifest file, manifest.json, here. However, the most important property to note at this time, with regards to the above, is “CodePath”. This refers to the filename of the plugins custom program that will be executed when the Stream Deck app starts up. Note that it’s location is relative to the directory of the plugin.

Process Initialisation Data

External plugins are invoked on loading of the Stream Deck app. The file specified in the CodePath property of manifest.json is launched (plugin in our case), passing argument parameters that are required for the intial registration of the plugin. This makes them readily accessible in any language that can process shell level arguments, such as PowerShell (via the $args special variable).

Here is an example of the format of data that gets sent on every initialisation.

-port
28196
-pluginUUID
77E0F22FBDF149124519C52E62BE4D2E
-registerEvent
registerPlugin
-info
{"application":{"language":"en","platform":"mac","version":"4.9.2.13193"},"devicePixelRatio":2,"devices":[{"id":"B32CC8E223C16EAB84570A06958A4860","name":"Stream Deck XL","size":{"columns":8,"rows":4},"type":2}],"plugin":{"version":"1.4"}}

You can find more specifics in the SDK documentation, but for the next stage, plugin registration, you will just need the port, pluginUUID, and registerEvent values.

Plugin Registration

After passing the initial details above, the Stream Deck app then handles the rest of plugin interactions exclusively via async communication over a websocket (including plugin registration). In addition to the creation of a websocket, the client plugin also needs to register handlers for the websocket events, so that interactions with the deck can be processed.

PowerShell and WebSockets Strife

The initial choice i wanted to do was to write the add-on completely in PowerShell. I had started out with it and successfully been able to obtain the arguments from the streamdeck app.

Unfortunately, the many hours I spent after trying to find a simple way to do the triumvirtate of websockets, asynchronous tasks and callbacks in PowerShell, my desire to keep it as simple as possible and minimise technical debt, led me away from using it in its entirety.

Instead, I went for an approach that still includes PowerShell in the mix, using NodeJS to handle registration, and any communication over the websocket, with the rest left to PowerShell to do. I’ve barely touched Javscript, but was able to put together something prettty quickly.

Create the NodeJS Plugin

We’re going to first ensure that two necessary packages are installed and available for our plug.

  • ws
  • shelljs

Then, we’ll create the file containing code which carries out the following:

  • Loads in the modules we installed above
  • Assign variables to the applicable elements in the arguments array.
  • Creates a websocket
  • Defines code for the ‘Open’ event being triggered to register the plugin
  • Defines code for the ‘Message’ event being triggered, detecting if the action was keyDown
  • If the action was keyDown, we fire off the PowerShell script, catme
  • Once the above is finished, start the setImage function, which instructs the app to reset the key image to that as defined in the manifest.

First, start a terminal session, and enter the following to change to the new plugin folder and install two packages we need. Leave your terminal session open.

cd "$HOME/Library/Application Support/com.elgato.StreamDeck/Plugins/com.example.timsapp.sdPlugin"
npm install ws
npm install shelljs

Next, start up your text editor of choice, and paste the following text in, saving it as a plugin

#! /usr/local/bin/node

var WS = require('ws');
var shell = require('shelljs');
var myArgs = process.argv.slice(2);
var port = myArgs[1];
var uuid = myArgs[3];
var registerEvent = myArgs[5];
var ws = new WS('ws://127.0.0.1:' + port);

var DestinationEnum = Object.freeze({
    HARDWARE_AND_SOFTWARE: 0,
    HARDWARE_ONLY: 1,
    SOFTWARE_ONLY: 2
});

function setImage(context, data) {
    var json = {
        event: 'setImage',
        context: context,
        payload: {
            image: data,
            target: DestinationEnum.HARDWARE_AND_SOFTWARE,
            state: 0
        }
    };
    ws.send(JSON.stringify(json));
}

ws.on('open', function () {
    var json = {
        event: registerEvent,
        uuid: uuid
    };
    ws.send(JSON.stringify(json));
});

ws.on('message', function (evt) {
    var jsonObj = JSON.parse(evt);
    if (jsonObj.event && jsonObj.event === 'keyDown') {
        shell.exec('./catme');
        setImage(jsonObj.context, null);
    }
});

Mark the plugin file as executable

Next up, we need change the file plugin so that the operating system sees it as an executable.
Going back to the terminal you left open, enter the following

chmod plugin +x

Register with thecatapi.com

You’ll need an api key in order to connect to the api service of thecatapi.

  • In your browser, go to https://thecatapi.com and signup
  • In due course you will have an api key that can be used in the following script

Create the PowerShell Script

I still wanted to show how simple it is to introduce PowerShell into the equation for your Elgato plugins. In this case, I’m performing the actions that connect to thecatapi.com and obtain a random image.

  • Again, go to your text editor of choice, and paste the code below.
  • Paste your api key between the quotes for x-api-key.
  • Save the file as catme
#! /usr/local/bin/pwsh
$headers = @{
  'x-api-key' = '<< insert your api key here>>'
}

$uri = 'https://api.thecatapi.com/v1/images/search'
$resp = Invoke-RestMethod -Uri $uri -Headers $headers

Invoke-WebRequest -Uri $resp.url -Headers $headers -OutFile ./image.png

Code Summary

  • We defined the header for the webrequest, containing the api key to be used for accessing the web service
  • The webrequest is executed and the output save/overwritten as image.png

Mark the powershell file as executable

Next up, we need change the file plugin so that the operating system sees it as an executable.
Going back to the terminal you left open, enter the following

chmod catme +x

Add an Instance of Catme!

We’re nearly there. All that’s left for us to do now is to add an instance of the plugin onto the deck.

Open the Stream Deck app, expand Custom on the right hand side of the window, and drag Catme! to a spare location on your deck.

At this point, we have carried out all the steps for making a working plugin.

Cat Away!

Now, you’re free to see as many pics of cats as long as you want. Just keep hitting that button. 🙂

You’ll notice on first run that the image on the tile is just a blue background with the question mark. This is simply because no image file exists at that point. Once you start pressing away, but images will appear. As an extra benefit, the last image you had on your deck before you quit the stream deck app will be the one you see appear on reload.

Conclusion

In this blog, we’ve gone through the following process of creating a real barebones plugin for the Elgato Deck. Although not necessary (all of the code can be done in NodeJS), we’ve shown how a secondary language can be used simply

We’ve :

  • Created a simple NodeJS script container handlers for the websocket connection and events
  • Demonstrated how to create a websocket and process its events easily.
  • Shown how we can launch secondary scripts from our plugin.
  • Written a PowerShell script which pulls down a cat image from the web and saves/overwrites it as image.png in the plugin directory.
Share