StreamDeckのプラグインを作ってみる

はじめに

エンジニアの松原です。趣味のガジェット漁りで以前 StreamDeck を購入しました。エンジニアとして、デバイスに関して何かできないか調べていたところ、このデバイスではプラグインを自作できるようで、ソースコードにhtmlとJavascriptが使われているようでした。これらはフロントエンド開発でよく扱う言語のため、これまで培った経験を活用して今回自作のプラグインを作ることを記事にしてみました。

公式サイトのサンプルコードGitHubに上がっていたので、今回の記事ではこちらを参考にしつつ、自分なりにプラグイン開発について整理してみました。

github.com

プラグインの構造について

プラグイン開発をする前に、まずはカスタムプラグインがどのように実行されるかざっくり調べてみました。

プラグインのアーキテクチャ

StreamDeckの公式サイトにプラグインの構造について解説がありました。

developer.elgato.com

The Stream Deck software loads all the custom plugins when the application starts. Websocket APIs allow bidirectional communication between the plugins and the Stream Deck application using JSON.

自作のプラグインを使う場合、WebSocket経由でStreamDeck本体のアプリケーションと通信できるみたいですね。図で整理すると以下のような感じでしょうか。

それぞれスタンドアロンのアプリケーションとして動作させるのであれば、WebSocketが扱える環境ならどの言語環境でも利用できそうですが、もうちょっと調べてみる必要がありました(記事の後半で解説しています)。

プラグインの実行経路

StreamDeckの公式サイトにプラグインのマニフェストについて解説がありました。

そのうちmanifest.jsonについての記載があったので、調べたところ CodePath の記述でエントリーポイントとなるhtmlファイルを指定しているみたいです。

CodePath Required The relative path to the HTML/binary file containing the plugin code.

{
  "Actions": ..., 
  "SDKVersion": 2,
  "Author": "Elgato", 
  "CodePath": "code.html", 
  "Description": "This lets you display the number of times you pressed on the key.", 
  "Name": "Counter", 
  "Icon": "pluginIcon", 
  "URL": "https://www.elgato.com/gaming/stream-deck", 
  "Version": "1.2.0",
  "OS": ...,
  "Software": ...

カスタムプラグインはWindowsの場合、 [User Home Directory]/AppData/Roaming/Elgato/StreamDeck/Plugins に配置されるようです。

StreamDeck本体のアプリケーション(StreamDeck.exe)の実行時にこのディレクトリに入っている各manifest.jsonに書かれている情報を利用してプラグインが実行されるようです。アプリケーション起動時にのみプラグインをチェックするようなので、プラグインのソースコードの変更を反映したい場合はStreamDeck本体のアプリケーションを立ち上げなおす必要がありそうです。

Javascriptの実行経路

エントリーポイントになっているhtmlファイル内に書かれているJavascriptですが、通常のWebアプリケーション開発ではイベント処理である onloadイベントなどに紐づけて自動的に処理を実行する仕組みがあるのですが、どうやらその仕組みを使っていないようです(以下は公式のプラグインサンプルのNumberDisplayのhtmlファイルです)。

github.com

htmlファイル内に記載されているJavascriptのコードを実行する方法についてはStreamDeckの公式サイトのRegistration Procedureに記載がありました。

developer.elgato.com

for Javascript plugins, its connectElgatoStreamDeckSocket() is called with several parameters.

グローバルスコープに connectElgatoStreamDeckSocket() という名前の関数を置いておけば、StreamDeck本体側でその関数を実行するようです。

function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {
...
}

StreamDeckのイベントをNode.jsにブリッジするプラグインを作る

プラグイン側のコード

StreamDeckのイベントをNode.js側でハンドリングするために、Node.jsのサービス側で別のWebSocketサーバーを立てて、そこにイベントを送るプラグインを書いてみます。 少し長いですが、プラグインのhtmlファイルは以下のようなコードになります。ローカルネットワークのポート40000番のWebSocketサーバーに対してStreamDeckのイベントをそのまま渡しています。

<!DOCTYPE HTML>
<html>

<head>
  <title>com.blkcatman.websocketbridge</title>
  <meta charset="utf-8" />
</head>

<body>
  <script>
    var bridgeEnabled = false;
    var bridge = null 
    const connectToBridge = () => {
      bridge = new WebSocket("ws://127.0.0.1:40000");
      bridge.onopen = () => bridgeEnabled = true
      bridge.onclose = () => bridgeEnabled = false;
    };

    const localAction = {
      onKeyDown: function (context, settings, coordinates, userDesiredState) {},

      onKeyUp: function (context, settings, coordinates, userDesiredState) {
        if (bridgeEnabled) {
          bridge.send(JSON.stringify({
            settings,
            coordinates
          }));
        } else {
          connectToBridge();
        }
      },

      onWillAppear: function (context, settings, coordinates) {}
    };

    function connectElgatoStreamDeckSocket(inPort, inPluginUUID, inRegisterEvent, inInfo) {
      connectToBridge();
      let websocket = new WebSocket("ws://127.0.0.1:" + inPort);

      websocket.onopen = () =>  websocket.send(JSON.stringify({
        "event": inRegisterEvent,
        "uuid": inPluginUUID
      }));
      websocket.onclose = () => {};

      websocket.onmessage = (evt) => {
        var jsonObj = JSON.parse(evt.data);
        var event = jsonObj['event'];
        var context = jsonObj['context'];
        var pl = jsonObj['payload'] || {};

        if (event == "keyDown") {
          localAction.onKeyDown(context, pl.settings, pl.coordinates, pl.userDesiredState);
        }
        else if (event == "keyUp") {
          localAction.onKeyUp(context, pl.settings, pl.coordinates, pl.userDesiredState);
        }
        else if (event == "willAppear") {
          localAction.onWillAppear(context, pl.settings, pl.coordinates);
        }
      };
    };
  </script>

</body>
</html>

Node.js側のコード

Node.js側のコードは以下のようなコードになります。40000番ポートで接続を待っており、クライアントからメッセージがやってきた時にそのメッセージをコンソールログに出力しています。

const WebSocketServer = require('ws').Server;

const wss = new WebSocketServer({port: 40000});

wss.on('connection', (ws) => {
  ws.on('message', (message) => {
    const jsonObj = JSON.parse(message);
    if (jsonObj != null) {
      console.log(jsonObj);
    }
  });
});

プラグインをStreamDeckのアクションにバインドする

作ったプラグインを [User Home Directory]/AppData/Roaming/Elgato/StreamDeck/Plugins に配置し、StreamDeck.exeを再起動します。 再起動後、プラグインをStreamDeckのアクションにバインドします。ドラッグ&ドロップでプラグインを選んで設定します。

できたもの

Node.jsのサービスを実行し、StreamDeck本体のボタンをポチポチした結果が以下になります。

以下はNode.jsアプリケーション側のコンソールログになります。今回はプラグイン側でイベント内容の詳細を指定していないため、アクションの座標値のみが取得できています。 座標値については 公式ドキュメントのアーキテクチャ解説のCoordinatesを見ると対応関係が分かるかと思います。

{ settings: {}, coordinates: { column: 2, row: 1 } }
{ settings: {}, coordinates: { column: 1, row: 1 } }
{ settings: {}, coordinates: { column: 1, row: 1 } }
{ settings: {}, coordinates: { column: 0, row: 1 } }
{ settings: {}, coordinates: { column: 0, row: 2 } }
{ settings: {}, coordinates: { column: 1, row: 2 } }
{ settings: {}, coordinates: { column: 1, row: 2 } }
{ settings: {}, coordinates: { column: 2, row: 2 } }
{ settings: {}, coordinates: { column: 3, row: 2 } }
{ settings: {}, coordinates: { column: 4, row: 2 } }
{ settings: {}, coordinates: { column: 4, row: 0 } }

まとめ

この記事ではStreamDeckのプラグインを自作して、Node.jsのサービス側でStreamDeckのイベントを受け取る方法について紹介しました。
次回以降はNode.js側でさらに応用することを考えるか、ネイティブのプラグインの作り方について記事にできればと思います。