With all the time I'm spending on video chat these days, I was looking for an
easier way to manage my video setup. I was intrigued by the Elgato Stream Deck,
with its small footprint, and fancy LCD screen buttons -- but I run linux, and
(as usual) that requires a bit more effort.
Fortunately, there is plenty of effort that's already been put in by others in
the community. For those not looking to build something custom, there's the
streamdeck ui which allows
the configuration of different types of built-in actions, all configurable
While this is a great way to get started with the Stream Deck, I found myself
wanting more than the actions available with this tool. So I kept looking.
I soon found a blog post from Lorna
Mitchell, and the associated
streamdeck-tricks repo. Thanks
to their thorough explanation and ample references, I started tinkering with the
same underlying library they suggest
I've since learned of similar library that interfaces with the Stream
Deck (https://github.com/dh1tw/streamdeck) - while it looks equally compelling,
I had already started my work when I found this library. If you're starting
fresh, this library appears to be more actively developed.
Before I get into the app, let me introduce the way I think about the stream
deck, as this impacts what I built, as well as how the components relate to each
The largest level of organization is the Stream Deck itself. This layer handles
the physical communication with the device.
When it comes to creating Buttons on the device, I expect them to share some
resources - for example, Lorna mentions several buttons that all interact with
their audio mixer. I chose to group this type of complexity into what I called a
plugin. Specific plugins are discussed below.
The lowest level of organization is a button. Buttons have an appearance,
and generally take some action when pressed.
I spend a lot of time in Google Meet, and I sometimes forget the keystrokes for
muting audio and video, so the first plugin I built has dedicated buttons for
Prior to the streamdeck, I had used some keyboard shortcuts that called
xdotool, and the plugin basically just runs those.
xdotool is a bit arcane,
but it can be very powerful:
- It can find windows based on their title or window class:
xdotool search --name "Meet - "
- It can also send key presses to those windows without stealing your mouse and
keyboard focus. Even if the window is on a different virtual desktop! This is
done by combining the
- It can move your mouse and keyboard focus to the chosen window, if desired,
windowactivatecommand instead of
So, to mute my audio in Google Meet, i can run this command:
xdotool search --name "Meet -" windowfocus key ctrl+d
The streamdeck plugin just runs these commands for me, at the push of a
The next plugin I created was to control my video chat lighting, so I could turn
it off when not in use (to decrease eye strain), and adjust brightness as
needed. The lighting needs in my office change considerably depending on the
weather and time of day.
To achieve this, I invested in an Elgato Key Light Air. They are not the most
affordable light, but the build quality is excellent, and after using my
previous (cheap) option for a year, I deserved the upgrade :)
The Elgato Key lights are interesting, because they are operated by making calls
to an onboard api server in the light itself, over HTTP. This means that not
only was a golang client readily available, but i started thinking differently
about the streamdeck - as a way to not just run commands on my local machine,
but to interact with APIs and other connected devices directly!
The initial setup of the Elgato light was frustrating, as my dual-band wifi
network was not visible in the setup app, and required a fussy
Now that I could control the light, I realized that i wanted the buttons to
reflect whether the light was connected, so I would not press them if the light
was, for example, unplugged.
I first tried this out with the go-streamdeck library's
which allow you to apply visual effects over the normal button appearance. I
found that that this caused some corruption in the button appearance. In
hindsight, this was probably just me calling button update handlers too often,
from multiple goroutines.
But before I figured that out, I removed the use of Decorators, and created my
own ImageButton class, which implements the Button interface. It also has a
pre-set "disabled" appearance, which greys out the button, so i can tell at a
glance that my light is not available.
With all my current video chat needs addressed, it was time to tackle something
new: OBS Studio. OBS Studio is popular with streamers,
who use it to customize and combine video and audio sources. In addition to
Recording and Streaming, OBS supports a Virtual Camera, which can be used
transparently by most video chat software.
I know OBS has a lot of capabilities, but my current desire is to have a
"intermission"-style image replace my camera feed, while I step away briefly to
grab a snack, or a drink, before a meeting gets started.
To allow streamdeck control of OBS, i'm using the
plugin, and a go library for
obs. The only action I've
implemented so far is to switch Scenes. If you're a more advanced user of OBS,
you may want to use a library that is more actively maintained (like this
one), to get more recent features, like
the ability to start/stop the Virtual Camera.
The scene switching buttons use the same state-tracking built for the Keylight,
and grey out when obs is not connected. A goroutine watches the OBS connection
in the background, and will reconnect automatically.
So far, the streamdeck has improved a few routine tasks for me, and provided a
fun project. As I get more familiar with OBS, I expect I'll create more
elaborate configurations to switch between.
While I currently use the streamdeck plugged into my laptop, I also see
opportunity for a "headless" streamdeck, attached to something like a Raspberry
Pi Zero, which can serve as a physical interface to any connected API (e.g.
smart home lights).
Do you have a streamdeck? How do you use it? Leave a comment to let me know!