Media control as scenarios in Hammerspoon
I’d like to have an “inner digital mixer” tied to one control device: to quickly control media and sound in all open apps. I haven’t found a full solution for this yet: what I’ve tried adds latency and sometimes behaves unstably.
With Hammerspoon I managed to get closer to the feeling I want, at least at the level of a practical minimum: controlling playback in specific apps quickly and predictably. It’s not a mixer in the full sense, but it already radically speeds up working with media.
The principle
Not a “universal pause button” but scenarios for specific apps:
activate the right app → let the focus “settle” → send the right command → have a backup path if needed.
1) Spotify: play/pause directly
For Spotify the AppleScript command playpause is used.
The plus of this approach: it isn’t key emulation, the command goes straight to Spotify and doesn’t depend on which window is active.
2) YouTube / Twitch and similar players: focus → k
For YouTube and Twitch the same pattern is used:
-
the app is activated
-
exit from the Space layer before sending k, so the layer doesn’t intercept the key
-
a short delay
-
k is sent (the standard play/pause in players)
Important: exiting the layer here isn’t “cosmetics” but an engineering detail. If the layer stays active, k may be interpreted as a layer command rather than a player command.
Example:
spaceLayer:bind({}, "t", function()
local app = hs.application.find("Twitch")
if not app then return hs.alert.show("Twitch is not running") end
app:activate()
spaceLayer:exit()
hs.timer.doAfter(0.05, function() hs.eventtap.keyStroke({}, "k") end)
end)
Breakdown:
-
spaceLayer:bind({}, “t”, …): inside the Space layer the T key starts the scenario.
-
hs.application.find(“Twitch”): find the running Twitch (otherwise show an alert).
-
app:activate(): focus Twitch.
-
spaceLayer:exit(): exit the layer so that k isn’t intercepted by the layer’s hotkeys.
-
hs.timer.doAfter(0.05, …): a small delay for the focus to switch.
-
hs.eventtap.keyStroke({}, “k”): send k (play/pause).
3) Safari: the same principle of “catching the right player”
Separate scenarios are set up for Safari, with the same meaning:
-
focus the browser
-
a short pause
-
send k to the active tab or player
This is convenient when media plays in different places: the command first puts the context in order and only then presses “pause”.
4) Apple Music and Podcasts: focus → space + delay
Music and Podcasts use a different “control language”:
-
the app is activated
-
a small delay (so the focus really moves)
-
space is sent as play/pause
The delay matters here: without it some commands go “nowhere” or to the wrong interface element.
5) Apple Books: UI scripting + fallback (the most reliable option)
Books is made as resilient as possible, because the interface and states there can be capricious:
-
the app is activated
-
through System Events a press of Play/Pause in the menu is tried (different menu names and RU/EN are taken into account)
-
if the menu doesn’t work, the fallback: sending space to the app
In essence this is a “dual-circuit system”: first the most reliable path (the menu), then the backup (the key).
In the next post I’ll show the basic HTTP server code in Hammerspoon and the structure of the endpoints.