The Elgato Streamdeck is a programmable console with illuminated buttons which supports both Windows and MacOS, with third-party open source software is also available for linux support. It has taken the internet by storm and has been particularly well received by streamers/video content producers, as a quick search on youtube for ‘Elgato Streamdeck’ will confirm.
This is what the Elgato Streamdeck Mk. II looks like:
On MacOS, the buttons can be made to e.g. open websites. To run arbitrary commands, shell scripts can be executed. Alternatively, it can issue a HTTP(S) GET request when a button is pressed. For more involved automation, a simple locally-running server can be used to do whatever we want when a given HTTP endpoint/route is hit. Go and the Gin HTTP server framework are ideal for this task.
Using the Streamdeck on linux
The software you want here is streamdeck-ui.
The instructions at https://timothycrosley.github.io/streamdeck-ui/docs/installation/ubuntu/ worked well for me on Ubuntu/Pop!_OS 22.04 LTS, though I did run into an error with the QT library when launching:
Could not load the Qt platform plugin "xcb" in "" even though it was found.
which was solved with:
sudo apt-get install '^libxcb.*-dev' libx11-xcb-dev libglu1-mesa-dev libxrender-dev libxi-dev libxkbcommon-dev libxkbcommon-x11-dev
Using the Streamdeck to control Squeezebox Touch and Squeezebox Radio
As a specific example, I wanted to be able to control my Squeezebox Touch and Squeezebox Radio from my Streamdeck. To do this, a locally running server is used to send JSON commands to the Logitech Media Server when a HTTP endpoint is hit as a result of a button being pressed on the Streamdeck.
In the below, Gin is used to set up the following routes:
/lms/play
/lms/pause
/touch/volup
/touch/voldown
/radio/volup
/radio/voldown
which are then hit with a GET request by the Streamdeck when button is pressed, and send JSON to the Logitech Media Service with a POST to effect the action.
go mod init github.com/algrt-hm/steamdeck-intermediary
go get github.com/gin-gonic/gin
// server.go
package main
import (
"github.com/algrt-hm/steamdeck-intermediary/service"
"github.com/gin-gonic/gin"
)
func main() {
// playerMacAddressConst is the MAC address of the Squeezebox player we want to control
const touchMacAddress = "00:04:20:23:a1:b5"
const radioMacAddress = "00:04:20:2b:76:f6"
gin.SetMode(gin.ReleaseMode)
// instantiate server, includes logger and recovery middleware
server := gin.Default()
// Test / status endpoint
server.GET("/", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "Hello World!",
})
})
// Simple group: lms
lms := server.Group("/lms")
{
lms.GET("/play", func(c *gin.Context) {
code, body := service.LmsPost(touchMacAddress, service.LmsPlay)
c.String(code, body)
})
lms.GET("/pause", func(c *gin.Context) {
code, body := service.LmsPost(touchMacAddress, service.LmsPause)
c.String(code, body)
})
touch := lms.Group("/touch")
{
touch.GET("/voldown", func(c *gin.Context) {
code, body := service.LmsPost(touchMacAddress, service.LmsVolumeDown)
c.String(code, body)
})
touch.GET("/volup", func(c *gin.Context) {
code, body := service.LmsPost(touchMacAddress, service.LmsVolumeUp)
c.String(code, body)
})
}
radio := lms.Group("/radio")
{
radio.GET("/voldown", func(c *gin.Context) {
code, body := service.LmsPost(radioMacAddress, service.LmsVolumeDown)
c.String(code, body)
})
radio.GET("/volup", func(c *gin.Context) {
code, body := service.LmsPost(radioMacAddress, service.LmsVolumeUp)
c.String(code, body)
})
}
}
server.Run(":8080")
}
// service/lms-service.go
package service
import (
"bytes"
"encoding/json"
"io"
"log"
"net/http"
)
/*
Example of what we're doing here:
We send a post request to e.g: http://lms-host:9000/jsonrpc.js
The body looks like e.g. (for pause):
{"id":1,"method":"slim.request","params":["00:04:20:2b:76:f6",["pause"]]}
e.g. (for play):
{"id":1,"method":"slim.request","params":["00:04:20:2b:76:f6",["play"]]}
*/
// lmsHostnameConst is the hostname of the LMS server; here I am using a local DNS entry
// that is in my /etc/hosts file
const lmsHostnameConst = "lms-host"
// form of the JSON we send to the LMS server
type LmsService struct {
Id string `json:"id"`
Method string `json:"method"`
Params []any `json:"params"`
}
// checkErr will call log.Fatal if the error is not nil i.e. there is an error
func checkErr(err error) {
if err != nil {
log.Fatal(err)
}
}
// LmsSimple is a helper function to create a LmsService struct for a simple operation
// with a given player MAC address and verb (e.g. "play" or "pause")
func LmsSimple(playerMacAddress string, verb string) LmsService {
/*
Looks like e.g:
{
"id": "1",
"method": "slim.request",
"params": [
"00:04:20:2b:76:f6",
[
"pause"
]
]
}
*/
return LmsService{
Id: "1",
Method: "slim.request",
Params: []any{playerMacAddress, []string{verb}},
}
}
// LmsPlay is a helper function to create a LmsService struct for a "play" operation
func LmsPlay(playerMacAddress string) LmsService {
return LmsSimple(playerMacAddress, "play")
}
// LmsPause is a helper function to create a LmsService struct for a "pause" operation
func LmsPause(playerMacAddress string) LmsService {
return LmsSimple(playerMacAddress, "pause")
}
// LmsVolumeDown is a helper function to create a LmsService struct for a "volume down" operation
func LmsVolumeDown(playerMacAddress string) LmsService {
return LmsService{
Id: "1",
Method: "slim.request",
Params: []any{playerMacAddress, []string{"mixer", "volume", "-10"}},
}
}
// LmsVolumeUp is a helper function to create a LmsService struct for a "volume up" operation
func LmsVolumeUp(playerMacAddress string) LmsService {
return LmsService{
Id: "1",
Method: "slim.request",
Params: []any{playerMacAddress, []string{"mixer", "volume", "+10"}},
}
}
// LmsPost is a helper function to send a POST request to the LMS server
// with the given LmsService struct
// returning the HTTP status code and body text
func LmsPost(playerMacAddress string, fn func(string) LmsService) (int, string) {
postUrl := "http://" + lmsHostnameConst + ":9000/jsonrpc.js"
jsondata, err := json.Marshal(fn(playerMacAddress))
checkErr(err)
r, err := http.NewRequest("POST", postUrl, bytes.NewBuffer(jsondata))
checkErr(err)
r.Header.Set("Content-Type", "application/json")
client := &http.Client{}
res, err := client.Do(r)
checkErr(err)
defer res.Body.Close()
bodyText, err := io.ReadAll(res.Body)
checkErr(err)
return res.StatusCode, string(bodyText)
}
go build
./steamdeck-intermediary
We can then hit those endpoints from the streamdeck (example below):
These screenshots show MacOS. Using streamdeck-ui
on linux, the HTTP GET option is not baked-in, so e.g. curl -X GET http://hansolo:8080/lms/pause
is the command to have run on the button press.
That takes care of volume control: for a re-scan we can hit http://lms-host:9000/settings/index.html?p0=rescan
in the browser:
Reference
- https://adam.ac/blog/stream-deck-for-developers/
- https://stackoverflow.com/questions/68036484/qt6-qt-qpa-plugin-could-not-load-the-qt-platform-plugin-xcb-in-even-thou
- https://timothycrosley.github.io/streamdeck-ui/
- https://www.blisshq.com/music-library-management-blog/2014/12/16/three-ways-rescan-lms/