💻

(書きかけ)Go言語でElgato StreamDeckのプラグインを作る

2023/02/14に公開

クリエイター・配信者御用達ツールのElgato StreamDeckのプラグインをGo言語(バックエンド)とHTML5/CSS3/JS で作成します。
StreamDeckのプラグインのアーキテクチャとしては、キーが押された際の外部へのリクエスト送信や、StreamDeck本体に対して直接働きかけるバックエンド部分と、ユーザーが設定画面を開いた時のみ作成されるPropertyInspector(フロントエンドに該当)が存在します。


Pluginの構成図


バックエンド部(画像右、Plugin 1, Plugin 2の部分)


PropertyInspector(画像左下、青色でマークされた部分)

今回はPropertyInspectorは簡易的に済ませて、バックエンドの開発に注力しますが、機会があればPIの詳細な実装やWebAssemblyを使ったより複雑なシステムや、新規ウィンドウから設定画面を開く方法なども纏めてみようと思います。

今回は指定したメソッドでURLにアクセスし、取得した結果によって成功/警告表示をすだけのシンプルなアプリケーションを開発します。
成果物:https://github.com/FlowingSPDG

アーキテクチャとしては、バックエンドのアプリケーションとStreamDeckアプリがWebSocketで相互に接続し、フロントエンド(PropertyInspector/PIと呼称) はStreamDeckソフトウェアを経由してバックエンドと情報をやり取りする形になります。

用語集

  • Std/SD StreamDeckの略称, 15LEDモデルやXL,Mini, Plusなど全てを含みます。
  • PropertyInspector SDアプリ上でボタンをクリックすると出てくる部分。 HTML5/CSS3/JSで動きますが、便宜上「フロントエンド」ではなく正式名称である"PropertyInspector"か"PI"と呼称します。
  • Context 一般的にはGo言語のcontext.Contextインターフェイスを表しますが、今回はStreamDeck上で扱われる仮想ボタンを指します。物理的なボタンではなく、ユーザーが設定したボタンの情報をContextと呼ばれる文字列で扱います。 ボタンが移動されてもContextは正しく残ります。
  • Action ボタンに設定できる動作のこと。

公式にはJavaScript、C++, C#, Object-C の対応が謳われていますが、中身としてはmanifest.json中で指定したバイナリを所定の引数(後述)で起動しWebSocket通信を行なっているだけなので、外部に依存しないバイナリを生成出来る言語であればGoに限らずRustやnimなどでも(多分)可能です。 いくつか非公式のSDKもOSSで存在するようです。
Rust : https://github.com/mdonoughe/streamdeck-rs
Go : https://github.com/FlowingSPDG/streamdeck
C# : https://github.com/FritzAndFriends/StreamDeckToolkit
C#(関連ツール) : https://github.com/BarRaider/streamdeck-tools
(余談ですが、GitHub上ではネイティブのStreamDeckプラグインではなく、StreamDeck本体をUSB経由で直接コントロールするOSSも一部存在します。
ですが、今回はそのような形態やbitfocus/companionなどは使用せず、StreamDeckネイティブプラグインとして開発を行います。

PI上では、外部から"connectElgatoStreamDeckSocket"関数を叩くことで起動・WebSocketへ接続するアプローチとなっており、関数名の変化や最適化が行われると支障が出る為Vue, Reactなどのフレームワークは使用せず、代わりにBarRaider氏が公開しているEasyPIを使用します。
Elgato公式からもPI用のjsが公開されていますが、利便性の面から今回は使用しません。
https://github.com/elgatosf/streamdeck-javascript-sdk

事前の環境設定として、各種IDEのセットアップ、Go言語のインストールと以下ツールのインストールを行なってください。
また、バックエンドの処理でGenericsを使用する為、Go1.18以降が必要となります。

開発にはこちらのrepository templateを使用してください。

StreamDeckソフトウェアにプラグインが読み込まれると、バックエンドが起動しWebSocket通信を開始します。
正常に起動しているか確認するため、ログファイルを開いてみます。
Macでは~/Library/Logs/ElgatoStreamDeck/ , Windowsでは%appdata%\Elgato\StreamDeck\logs\ に吐き出されます。

StreamDeck上にボタンを配置すると、設定したWillAppearHandlerが起動します。
その状態でボタンを押すとKeyDownHandlerが起動していることがわかります。

PIはユーザーがボタンをクリックしてPIを開いた時のみHTMLの描画・WebSocketの接続が行われます。
PropertyInspectorの出力を確認するため、Chrome Dev Toolsで確認できるように設定を変更します。
Macの場合、以下を実行してください:
defaults write com.elgato.StreamDeck html_remote_debugging_enabled -bool YES
Windowsの場合、DWORDレジストリキーhtml_remote_debugging_enabledを"1"に変更してください。
@ HKEY_CURRENT_USER\Software\Elgato Systems GmbH\StreamDeck

その後、StreamDeckアプリを再起動し、http://localhost:23654/ にアクセスするとPropertyInspectorがリストで表示されます。(注: 現在アクティブなPIしか表示されないため、確認したいPIが存在しない場合SDアプリ上でボタンをクリックしてPIを表示してからリロードください)

各種イベントの説明
WillAppear - ボタンが出現したときに発行されます。注意点として、ユーザーがボタンを新規に配置した際だけではなく、プロファイルを切り替えた際やSDソフトウェアの再起動時、ページ切り替え時など幅広いタイミングで発行されるため、WillAppearが発生した際に設定を初期化すると、すでに設定が行われたボタンを初期化してしまうことになります。
事前に初期設定か確認するメソッドを用意したりすると良いでしょう(そもそもボタンを新規配置した際のイベントが無いのが良くない) 今回は使用しません
WillDisappear - ボタンが消える際に発行されるイベント。今回は使用しません
KeyDown - キーが押し込まれた際に発行されるイベント。設定情報も一緒についてくるので、このために内部でPIの設定を保持する必要はないです。 今回は「ボタンを押下すると特定の設定でHTTPのリクエストを出す」というプラグインの為、このイベントきっかけでリクエストの実行を行います。
DidReceiveSettings - バックエンド/PIから設定が保存された時に発行されます。 今回はバックエンド側に情報を保持する必要が無いため、使用しません。

StreamDeckのイベント以外でボタンに対して操作を行う場合
基本的にKeyDownなど主要なイベントのpayloadに設定が着いてくるため、バックエンド側で設定を保持する必要はありません。
しかし、CPU Usageやstreamdeck-vmix のように外部のイベントや恒常的にContextに対して変更を実施したい場合は、メモリ中に保持する必要があります。
その場合、event.Context をキーとして設定を保持するmapを作成し、WillAppearやDidReceiveSettingsを受けてvalueを更新する形にすると良いでしょう。
注意点として、mapの競合が発生する為goroutine safeなsync.Mapを使うかsync.Mutexでconcurrent ioの保護を行いましょう。

PIの開発
Property Inspector(設定画面)の開発を行います。
技術的には設定画面が開かれたとかにQTを使い指定したHTMLを描画し、グローバルに定義された"connectElgatoStreamDeckSocket"を起動しWebSocket経由で通信を行うというフローのようです。
前述した通り、今回はReact, Vueなどのフレームワークは使用せず、様々なプラグインの開発を行なっているBarRaider氏が公開しているEasyPIというjsを使用します。
(難読化を実施しない状態でフレームワークを使用するというのも手ですが、開発のスピード感とわかりやすさを優先する為素のJSを使用しています。もしReactなどで綺麗にPIに扱える方法があったらぜひ教えてください。Reactコンポーネント化したい)

各種actionごとにHTMLファイルを指定できるため、今回は"http_client.html"を作成し使用します。
設定項目としては、「対象のURL」「使用するメソッド」の2種類のため、まずバックエンドに対応した構造体を追加します。
次に、

manifestjsonの設定
https://developer.elgato.com/documentation/stream-deck/sdk/manifest/

DistributionToolでパッケージ化する
https://developer.elgato.com/documentation/stream-deck/sdk/packaging/

Discussion