296 lines
7.5 KiB
Go
296 lines
7.5 KiB
Go
|
package streamdeck
|
||
|
|
||
|
import (
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"log"
|
||
|
"net/url"
|
||
|
"os"
|
||
|
"os/signal"
|
||
|
"time"
|
||
|
|
||
|
"github.com/gorilla/websocket"
|
||
|
)
|
||
|
|
||
|
// EventDelegate receives callbacks for Stream Deck SDK events
|
||
|
type EventDelegate interface {
|
||
|
OnConnected(*websocket.Conn)
|
||
|
OnWillAppear(*EvWillAppear)
|
||
|
OnTitleParametersDidChange(*EvTitleParametersDidChange)
|
||
|
OnPropertyInspectorConnected(*EvSendToPlugin)
|
||
|
OnSendToPlugin(*EvSendToPlugin)
|
||
|
OnApplicationDidLaunch(*EvApplication)
|
||
|
OnApplicationDidTerminate(*EvApplication)
|
||
|
}
|
||
|
|
||
|
// StreamDeck SDK APIs
|
||
|
type StreamDeck struct {
|
||
|
Port string
|
||
|
PluginUUID string
|
||
|
RegisterEvent string
|
||
|
Info string
|
||
|
delegate EventDelegate
|
||
|
conn *websocket.Conn
|
||
|
done chan struct{}
|
||
|
}
|
||
|
|
||
|
// NewStreamDeck prepares StreamDeck struct
|
||
|
func NewStreamDeck(port, pluginUUID, registerEvent, info string) *StreamDeck {
|
||
|
return &StreamDeck{
|
||
|
Port: port,
|
||
|
PluginUUID: pluginUUID,
|
||
|
RegisterEvent: registerEvent,
|
||
|
Info: info,
|
||
|
done: make(chan struct{}),
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SetDelegate sets the delegate for receiving Stream Deck SDK event callbacks
|
||
|
func (sd *StreamDeck) SetDelegate(ed EventDelegate) {
|
||
|
sd.delegate = ed
|
||
|
}
|
||
|
|
||
|
func (sd *StreamDeck) register() error {
|
||
|
reg := evRegister{Event: sd.RegisterEvent, UUID: sd.PluginUUID}
|
||
|
data, err := json.Marshal(reg)
|
||
|
log.Println(string(data))
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
err = sd.conn.WriteMessage(websocket.TextMessage, data)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Connect establishes WebSocket connection to StreamDeck software
|
||
|
func (sd *StreamDeck) Connect() error {
|
||
|
u := url.URL{Scheme: "ws", Host: fmt.Sprintf("127.0.0.1:%s", sd.Port)}
|
||
|
c, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
sd.conn = c
|
||
|
|
||
|
err = sd.register()
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("failed register: %v", err)
|
||
|
}
|
||
|
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnConnected(sd.conn)
|
||
|
}
|
||
|
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// Close closes the websocket connection, defer after Connect
|
||
|
func (sd *StreamDeck) Close() {
|
||
|
sd.conn.Close()
|
||
|
}
|
||
|
|
||
|
func (sd *StreamDeck) onPropertyInspectorMessage(value string, ev *EvSendToPlugin) error {
|
||
|
switch value {
|
||
|
case "propertyInspectorConnected":
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnPropertyInspectorConnected(ev)
|
||
|
}
|
||
|
default:
|
||
|
log.Printf("Unknown property_inspector value: %s\n", value)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (sd *StreamDeck) onSendToPlugin(ev *EvSendToPlugin) error {
|
||
|
payload := make(map[string]*json.RawMessage)
|
||
|
err := json.Unmarshal(*ev.Payload, &payload)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("onSendToPlugin payload unmarshal: %v", err)
|
||
|
}
|
||
|
if raw, ok := payload["property_inspector"]; ok {
|
||
|
var value string
|
||
|
err := json.Unmarshal(*raw, &value)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("onSendToPlugin unmarshal property_inspector value: %v", err)
|
||
|
}
|
||
|
err = sd.onPropertyInspectorMessage(value, ev)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("onPropertyInspectorMessage: %v", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnSendToPlugin(ev)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (sd *StreamDeck) spawnMessageReader() {
|
||
|
defer close(sd.done)
|
||
|
for {
|
||
|
_, message, err := sd.conn.ReadMessage()
|
||
|
if err != nil {
|
||
|
log.Println("read:", err)
|
||
|
return
|
||
|
}
|
||
|
log.Printf("recv: %s", message)
|
||
|
|
||
|
var objmap map[string]*json.RawMessage
|
||
|
err = json.Unmarshal(message, &objmap)
|
||
|
if err != nil {
|
||
|
log.Fatal("message unmarshal", err)
|
||
|
}
|
||
|
var event string
|
||
|
err = json.Unmarshal(*objmap["event"], &event)
|
||
|
if err != nil {
|
||
|
log.Fatal("event unmarshal", err)
|
||
|
}
|
||
|
switch event {
|
||
|
case "willAppear":
|
||
|
var ev EvWillAppear
|
||
|
err := json.Unmarshal(message, &ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("willAppear unmarshal", err)
|
||
|
}
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnWillAppear(&ev)
|
||
|
}
|
||
|
case "titleParametersDidChange":
|
||
|
var ev EvTitleParametersDidChange
|
||
|
err := json.Unmarshal(message, &ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("titleParametersDidChange unmarshal", err)
|
||
|
}
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnTitleParametersDidChange(&ev)
|
||
|
}
|
||
|
case "sendToPlugin":
|
||
|
var ev EvSendToPlugin
|
||
|
err := json.Unmarshal(message, &ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("onSendToPlugin event unmarshal", err)
|
||
|
}
|
||
|
err = sd.onSendToPlugin(&ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("onSendToPlugin", err)
|
||
|
}
|
||
|
case "applicationDidLaunch":
|
||
|
var ev EvApplication
|
||
|
err := json.Unmarshal(message, &ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("applicationDidLaunch unmarshal", err)
|
||
|
}
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnApplicationDidLaunch(&ev)
|
||
|
}
|
||
|
case "applicationDidTerminate":
|
||
|
var ev EvApplication
|
||
|
err := json.Unmarshal(message, &ev)
|
||
|
if err != nil {
|
||
|
log.Fatal("applicationDidTerminate unmarshal", err)
|
||
|
}
|
||
|
if sd.delegate != nil {
|
||
|
sd.delegate.OnApplicationDidTerminate(&ev)
|
||
|
}
|
||
|
default:
|
||
|
log.Printf("Unknown event: %s\n", event)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ListenAndWait processes messages and waits until closed
|
||
|
func (sd *StreamDeck) ListenAndWait() {
|
||
|
go sd.spawnMessageReader()
|
||
|
|
||
|
interrupt := make(chan os.Signal, 1)
|
||
|
signal.Notify(interrupt, os.Interrupt)
|
||
|
|
||
|
for {
|
||
|
select {
|
||
|
case <-sd.done:
|
||
|
return
|
||
|
case <-interrupt:
|
||
|
log.Println("interrupt")
|
||
|
|
||
|
// Cleanly close the connection by sending a close message and then
|
||
|
// waiting (with timeout) for the server to close the connection.
|
||
|
err := sd.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, ""))
|
||
|
if err != nil {
|
||
|
log.Println("write close:", err)
|
||
|
return
|
||
|
}
|
||
|
select {
|
||
|
case <-sd.done:
|
||
|
case <-time.After(time.Second):
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SendToPropertyInspector sends a payload to the Property Inspector
|
||
|
func (sd *StreamDeck) SendToPropertyInspector(action, context string, payload interface{}) error {
|
||
|
event := evSendToPropertyInspector{Action: action, Event: "sendToPropertyInspector",
|
||
|
Context: context, Payload: payload}
|
||
|
data, err := json.Marshal(event)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("sendToPropertyInspector: %v", err)
|
||
|
}
|
||
|
err = sd.conn.WriteMessage(websocket.TextMessage, data)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setTitle write: %v", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetTitle dynamically changes the title displayed by an instance of an action
|
||
|
func (sd *StreamDeck) SetTitle(context, title string) error {
|
||
|
event := evSetTitle{Event: "setTitle", Context: context, Payload: evSetTitlePayload{
|
||
|
Title: title,
|
||
|
Target: 0,
|
||
|
}}
|
||
|
data, err := json.Marshal(event)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setTitle: %v", err)
|
||
|
}
|
||
|
err = sd.conn.WriteMessage(websocket.TextMessage, data)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setTitle write: %v", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetSettings saves persistent data for the action's instance
|
||
|
func (sd *StreamDeck) SetSettings(context string, payload interface{}) error {
|
||
|
event := evSetSettings{Event: "setSettings", Context: context, Payload: payload}
|
||
|
data, err := json.Marshal(event)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setSettings: %v", err)
|
||
|
}
|
||
|
err = sd.conn.WriteMessage(websocket.TextMessage, data)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setSettings write: %v", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// SetImage dynamically changes the image displayed by an instance of an action
|
||
|
func (sd *StreamDeck) SetImage(context string, bts []byte) error {
|
||
|
b64 := base64.StdEncoding.EncodeToString(bts)
|
||
|
event := evSetImage{Event: "setImage", Context: context, Payload: evSetImagePayload{
|
||
|
Image: fmt.Sprintf("data:image/png;base64, %s", b64),
|
||
|
Target: 0,
|
||
|
}}
|
||
|
data, err := json.Marshal(event)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setImage: %v", err)
|
||
|
}
|
||
|
err = sd.conn.WriteMessage(websocket.TextMessage, data)
|
||
|
if err != nil {
|
||
|
return fmt.Errorf("setImage write: %v", err)
|
||
|
}
|
||
|
return nil
|
||
|
}
|