I recently jumped on the hype train when it comes to streaming and picked up an Elgato Stream Deck. If you’re unfamiliar, these devices are essentially hotkey peripherals with LCD adjustable keys that allow you to quickly perform certain tasks. Could a keyboard shortcut get the job done? For a lot of tasks, definitely, but the Stream Deck software is where the magic comes in.
The Stream Deck software allows you to connect certain services or multi-stage shortcuts to a specific key, something a standard keyboard shortcut probably won’t do well. In addition, you’re able to design your own actions using simple JavaScript and HTML.
In this tutorial, we’re going to see how to create a Stream Deck action, one that sends HTTP requests to remote webhook services, using JavaScript.
Before getting too invested, I wanted to point out that I do have affiliate links scattered throughout this tutorial. If you’re interested in a Stream Deck, please consider using my link so that I get a small commission.
There are a few requirements that must be met prior to starting this tutorial:
While plugin development is easy with JavaScript, you must have physical hardware to test with. There is no simulator or emulator, and very little can be tested in a web browser. The Stream Deck communicates between the hardware and the application running on your computer through web sockets. To be able to debug what’s happening on the Stream Deck hardware, you’ll need to change some system settings, and while not difficult, could be a little scary.
While the development experience is nearly the same between macOS, Windows, and Linux, I’ll be using Windows and will be referencing certain Windows specific things. If at any point something doesn’t make sense, it’s worth checking out the Stream Deck developer documentation.
The example used in this tutorial is just one of many possible Stream Deck use cases. I built the plugin because it solved a need that I had personally when it came to my tasks and streams.
Imagine that you need to communicate with a service and there isn’t already a Stream Deck action for that service. Take Discord, a popular gaming chat and video service which doesn’t have an official plugin for the Stream Deck. Discord will let you create bots in the form of webhooks. So what this means is that you create a webhook for a channel within your Discord server and it provides you a link. When you send a message to that link, it shares it with your channel.
The concept of webhooks with the Stream Deck is essentially, I have a webhook URL that expects a message in a specific format. I want to create a key on my hardware that sends that message to that service when pressed. This service might be Discord, or it might be something different like Zapier, Automate.io, or IFTTT. It doesn’t matter, and while unofficial plugins exist for each of these services, it might be a good idea to make something that works for any service.
So let’s use the example of Discord. With cURL, we could communicate with the Discord webhook like this:
cURL -X POST \
-H "Content-Type: application/json"
-d '{ "content": "The stream is starting now!" }'
https://DISCORD_WEBHOOK_URL
Throughout this tutorial, we’re going to be trying to reproduce the above cURL statement, while keeping it universal for other webhook enabled services as well.
There are a few ways that you can get started when it comes to building Stream Deck plugins. The official documentation recommends looking at the existing plugins in your installation directory and using them as a template. I found this to be too complicated for someone just starting out. Instead, I recommend downloading the plugin template that Elgato released.
When it comes to plugin development, you’ll need a parent directory with a properly formatted namespace, but beyond that, it doesn’t really matter. Take com.nraboy.webhooks.sdPlugin for example, which is my parent level directory. Your directory will need to end with .sdPlugin, otherwise the build tool won’t know what to do.
At its core, a Stream Deck plugin is composed of a manifest.json file, a property inspector HTML file, and a main plugin HTML file. The naming convention only matters when it comes to the manifest.json file because within that file, you define the other file paths. This manifest.json file must exist at the root of your project directory and it might look something like this:
{
"Actions": [
{
"Icon": "resources/actionIcon",
"Name": "Webhooks",
"States": [
{
"Image": "resources/actionDefaultImage",
"TitleAlignment": "bottom",
"FontSize": "10"
}
],
"SupportedInMultiActions": true,
"Tooltip": "Send an HTTP request to a remote webhook API.",
"UUID": "com.nraboy.webhooks.action"
}
],
"SDKVersion": 2,
"Author": "Nic Raboy",
"CodePath": "main.html",
"Description": "Send an HTTP request to a remote webhook API.",
"Name": "Webhooks",
"Icon": "resources/pluginIcon",
"URL": "https://www.nraboy.com",
"PropertyInspectorPath": "propertyinspector.html",
"Version": "1.0",
"OS": [
{
"Platform": "mac",
"MinimumVersion": "10.11"
},
{
"Platform": "windows",
"MinimumVersion": "10"
}
],
"Software": {
"MinimumVersion" : "4.1"
}
}
Notice that in the above file, the CodePath
, PropertyInspectorPath
, Icon
, and Image
fields reference where to find each of the appropriate files that define a plugin. It’s worth consulting the Stream Deck developer documentation for more insight into what each of the JSON fields represents.
The two main parts of any Stream Deck plugin are the property inspector and the main plugin. The property inspector is what you’d find in the desktop application. It’s where you’d define any configuration while setting a key on the device. The main plugin is what would typically execute logic when there is interaction with the hardware.
Let’s first take a look at the propertyinspector.html file, which in this example, exists at the root of the project. The propertyinspector.html file might contain something like the following:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>Stream Deck Webhooks</title>
<link rel="stylesheet" href="sdpi.css">
<script src="common.js"></script>
</head>
<body>
<div class="sdpi-wrapper">
<div class="sdpi-item">
<div class="sdpi-item-label">Webhook URL</div>
<input class="inspector sdpi-item-value" id="webhookurl" value="">
</div>
<div class="sdpi-item" id="select_single">
<div class="sdpi-item-label">Method</div>
<select class="sdpi-item-value select inspector" id="webhookmethod">
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
</div>
<div type="textarea" class="sdpi-item" id="message_only">
<div class="sdpi-item-label">Webhook Payload</div>
<span class="sdpi-item-value textarea">
<textarea class="inspector" type="textarea" id="webhookpayload">{}</textarea>
</span>
</div>
<div class="sdpi-item">
<div class="sdpi-item-label">Are you done?</div>
<button class="sdpi-item-value" id="save" onclick="save()">Save</button>
</div>
</div>
<script>
// Logic in here ...
</script>
</body>
</html>
If you’re using the template, you’ll have a common.js file and a sdpi.css file included with it. You won’t need to touch these files unless you want to further customize the plugin beyond what’s typical.
In this above HTML, we have four different HTML elements. We have a text field for our webhook URL, a select menu for choosing what type of HTTP method to use, a text area for the request payload, and a button to save everything on the Stream Deck. Take note that each input element has an inspector
class name. We’re doing this so we can quickly pull each user defined input along with the id
that goes with it.
We can get this information and save it in the <script>
tag for our logic. This <script>
tag might look like the following:
<script>
if($SD) {
$SD.on("connected", function (jsonObj) {
console.log(`[connected] ${JSON.stringify(jsonObj)}`);
if(jsonObj.hasOwnProperty('actionInfo')) {
settings = Utils.getProp(jsonObj, 'actionInfo.payload.settings', {});
document.getElementById("webhookurl").value = settings.webhookurl || "https://";
document.getElementById("webhookmethod").value = settings.webhookmethod || "GET";
document.getElementById("webhookpayload").value = settings.webhookpayload || "{}";
}
});
};
const save = function() {
if($SD) {
var payload = {};
[].forEach.call(document.querySelectorAll(".inspector"), element => {
payload[element.id] = element.value;
});
$SD.api.sendToPlugin($SD.uuid, $SD.actionInfo["action"], payload);
}
}
</script>
Within the common.js file we have access to the $SD
wrapper. This makes it easier when it comes to interacting with the plugin and the property inspector. Behind the scenes, everything is controlled with web sockets, so the helper class makes life a bit easier.
When the connection between the property inspector and the plugin is established, a payload with the current persist settings is sent. We can take those settings and use them to set each of our HTML elements, otherwise default to a value if no such persisted settings exist for the key.
In the save
function which is attached to the <button>
element, we loop through each input with the inspector
class name and construct a payload to be sent. An example of this payload might look like the following:
{
"webhookurl": "https://",
"webhookmethod": "POST",
"webhookpayload": "{}"
}
The payload to be sent in the save
method should not be confused with the payload to be sent to the webhook service in question. The payload we are talking about here is the payload to be sent from the property inspector to the plugin on the Stream Deck hardware. Basically sent from desktop to device.
Each key on the Stream Deck has a UUID, so we’re sending it with the payload using the $SD.api.sendToPlugin
method.
With the property inspector ready, we can take a look at the plugin side of things, which is defined as main.html per the manifest.json file. Open the main.html file which should exist at the root of your project and include the following:
<!DOCTYPE HTML>
<html>
<head>
<title>com.nraboy.webhooks</title>
<meta charset="utf-8" />
</head>
<body>
<script src="common.js"></script>
<script>
$SD.on('connected', (jsonObj) => connected(jsonObj));
function connected(jsonObj) {
console.log(`[connected] ${JSON.stringify(jsonObj)}`);
$SD.on('com.nraboy.webhooks.action.willAppear', (jsonObj) => action.onWillAppear(jsonObj));
$SD.on('com.nraboy.webhooks.action.keyUp', (jsonObj) => action.onKeyUp(jsonObj));
$SD.on('com.nraboy.webhooks.action.didReceiveSettings', (jsonObj) => action.onDidReceiveSettings(jsonObj));
$SD.on('com.nraboy.webhooks.action.propertyInspectorDidAppear', (jsonObj) => {});
$SD.on('com.nraboy.webhooks.action.propertyInspectorDidDisappear', (jsonObj) => {});
$SD.on('com.nraboy.webhooks.action.sendToPlugin', (jsonObj) => action.onSendToPlugin(jsonObj));
};
const action = {
onDidReceiveSettings: (jsonObj) => {
console.log(`[onDidReceiveMessage] ${JSON.stringify(jsonObj)}`);
},
onWillAppear: (jsonObj) => {
console.log(`[onWillAppear] ${JSON.stringify(jsonObj)}`);
$SD.api.sendToPropertyInspector(jsonObj.context, Utils.getProp(jsonObj, "payload.settings", {}), jsonObj.action);
},
onSendToPlugin: (jsonObj) => {
console.log(`[onSendToPlugin] ${JSON.stringify(jsonObj)}`);
if(jsonObj.payload) {
$SD.api.setSettings(jsonObj.context, jsonObj.payload);
}
},
onKeyUp: (jsonObj) => {
console.log(`[onKeyUp] ${JSON.stringify(jsonObj)}`);
if (!jsonObj.payload.settings || !jsonObj.payload.settings.webhookurl) {
$SD.api.showAlert(jsonObj.context);
return;
}
fetch(jsonObj.payload.settings.webhookurl, {
"method": jsonObj.payload.settings.webhookmethod,
"headers": {
"content-type": "application/json"
},
"body": jsonObj.payload.settings.webhookpayload
}).then(result => $SD.api.showOk(jsonObj.context), error => $SD.api.showAlert(jsonObj.context));
}
};
</script>
</body>
</html>
Because we like helper methods, we are including the common.js file in the main.html file. Within the core <script>
tag, we configure our listeners as defined in the Stream Deck developer documentation.
The most important listeners for us are the onSendToPlugin
and onKeyUp
listeners. The rest, while included in the above example, aren’t technically being used.
So let’s look at the onSendToPlugin
listener:
onSendToPlugin: (jsonObj) => {
console.log(`[onSendToPlugin] ${JSON.stringify(jsonObj)}`);
if(jsonObj.payload) {
$SD.api.setSettings(jsonObj.context, jsonObj.payload);
}
},
In the propertyinspector.html file, we made use of the $SD.api.sendToPlugin
method. In the main.html file, we are listening for that message and in this case are persisting it to the device settings through the $SD.api.setSettings
method. The jsonObj.context
variable in this case is the UUID so we properly save data to the correct keys.
If we wanted to, we could include an onDidReceiveMessage
in the propertyinspector.html file if we wanted to do something when the settings were saved. However, we don’t really care, so we won’t listen for it.
So at this point the settings were sent to the plugin and they were saved. Now we need to do something with them when the key was pressed.
onKeyUp: (jsonObj) => {
console.log(`[onKeyUp] ${JSON.stringify(jsonObj)}`);
if (!jsonObj.payload.settings || !jsonObj.payload.settings.webhookurl) {
$SD.api.showAlert(jsonObj.context);
return;
}
fetch(jsonObj.payload.settings.webhookurl, {
"method": jsonObj.payload.settings.webhookmethod,
"headers": {
"content-type": "application/json"
},
"body": jsonObj.payload.settings.webhookpayload
}).then(result => $SD.api.showOk(jsonObj.context), error => $SD.api.showAlert(jsonObj.context));
}
The jsonObj
variable contains the settings that were previously persisted. When the key is pressed, we make sure the settings exist. We could and probably should do better validation on this data, but for this example it is fine. If we’re sure the data looks good, we can do a fetch
operation in JavaScript to make an HTTP request using the data in our settings.
Upon success, a success icon will be shown on the Stream Deck. Upon failure, a failure icon will be shown on the Stream Deck.
That’s all the coding required for this particular example. However, before you can make use of the plugin, you’ll need a set of images:
icon | size | @2x |
---|---|---|
action image | 20x20 | 40x40 |
category icon | 28x28 | 56x56 |
key icon | 72x72 | 144x144 |
These images are straight-forward for the most part with the exception of the action image which should comply with certain colors. The documentation recommends using #D8D8D8 with a transparent background, but it’s up to you if you want to break the rules.
So by now you should have your code and images done. Each code file and image file should be correctly referenced in the manifest.json file at the root of your project. Now we need to build it.
Download the distribution tool for your operating system and execute the following:
DistributionTool.exe -b -i com.nraboy.webhooks.sdPlugin -o Release
Remember, I’m on Windows, so use the command found in the documentation for your operating system.
If there are no build errors, you should end up with a file that you can double click to install.
Developing a plugin for the Stream Deck will likely not go without a hitch. The process is a bit strange, and there could be bugs in your plugin as well as the SDK or APIs that Eglato offers in the Stream Deck. You’re going to want to troubleshoot what’s happening at every stage and to do that, you’re going to need to change some configurations on your computer.
Take Windows for example. You’re going to need to edit a registry.
Consult the documentation to see what you need to do to make this possible. However, once you do this, you can visit http://localhost:23654 in your web browser to see console output of each component in your project.
This is incredibly beneficial when it comes to troubleshooting.
You just saw how to create a custom plugin for the Elgato Stream Deck! In this example we saw how to send HTTP requests to a remote web service which is great for a lot of circumstances, but there are plenty of other plugin scenarios that can be accomplished.
If you want to play around with this project, you can find it on GitHub. You’ll be able to check out the code as well as a release to be installed on your Stream Deck hardware.
As previously mentioned, if you’re looking to buy a Stream Deck, check out one of my affiliate links, so I get a little credit for the purchase.
A video version of this tutorial can be found below.