Support media control buttons via MPRIS #27

Merged
lucas merged 4 commits from agent/issue-1-media-actions into main 2026-11-13 14:27:45 +01:00
Collaborator

Adds a media action type that drives MPRIS players on the D-Bus session bus, so the transport buttons finally have something to bind to.

Closes: #1

What is in here

  • MprisPlayerRegistry (no glib, unit-tested): which players exist, their playback status and history, and the rule that ranks them — status band, then recency, then configured entry order, then appearance order, with a bus-name tiebreak so the result cannot depend on hash order. Matching reuses NodeMatcher over every dot-aligned suffix of the bus name (with the instance_<n> segment stripped) plus the player's Identity.
  • MediaService: session-bus connection, NameOwnerChanged tracking, one org.mpris.MediaPlayer2.Player proxy per player (plus a short-lived one for Identity), asynchronous method calls, and listener notification on every change. A missing session bus prints one warning and leaves all media actions inert.
  • Blinking LEDs in LedManager, with a single shared timer; Set cancels blinking for the control it writes.
  • Rewind / FastForward added to the LED enum — they carry LEDs on the device but were not addressable. Side effect: the startup reset now clears them too.
  • Action_Media: play-pause, stop, next, previous, seek-backward, seek-forward; optional target and seconds. Play LED solid while playing, blinking while paused; Stop LED lit while stopped; seek LEDs show the button, not the player, and scrubbing repeats every 200 ms after a 400 ms hold.
  • MediaService& threaded through IActionBase::Init and the action factory (clean cutover, no overloads kept), README documentation, and dbus.socket added to the user unit's After=.

Verification

  • nanoKontroller2_mpris_tests (new ctest target) covers instance-suffix stripping, suffix matching, Identity matching and the full selection ranking, including the no-match and unknown-bus-name paths. Built and run locally: all checks pass.
  • Every source file was compile-checked against glib/gio and WirePlumber headers; the full CMake build runs in CI (this container has neither pkg-config nor the dev packages).
  • Not verified on hardware: nobody is at the device in this session, so the end-to-end checks from the plan (LED behaviour, seek timing, the DBUS_SESSION_BUS_ADDRESS=unix:path=/nonexistent degraded path) are untested. In particular, the assumption that Rewind/FastForward really have LEDs comes from the issue, not from code — if they do not light up, the two LED enum members and the LED writes in the seek branch can be dropped without touching anything else.
Adds a `media` action type that drives MPRIS players on the D-Bus session bus, so the transport buttons finally have something to bind to. Closes: #1 ## What is in here - **`MprisPlayerRegistry`** (no glib, unit-tested): which players exist, their playback status and history, and the rule that ranks them — status band, then recency, then configured entry order, then appearance order, with a bus-name tiebreak so the result cannot depend on hash order. Matching reuses `NodeMatcher` over every dot-aligned suffix of the bus name (with the `instance_<n>` segment stripped) plus the player's `Identity`. - **`MediaService`**: session-bus connection, `NameOwnerChanged` tracking, one `org.mpris.MediaPlayer2.Player` proxy per player (plus a short-lived one for `Identity`), asynchronous method calls, and listener notification on every change. A missing session bus prints one warning and leaves all media actions inert. - **Blinking LEDs** in `LedManager`, with a single shared timer; `Set` cancels blinking for the control it writes. - **`Rewind` / `FastForward` added to the `LED` enum** — they carry LEDs on the device but were not addressable. Side effect: the startup reset now clears them too. - **`Action_Media`**: `play-pause`, `stop`, `next`, `previous`, `seek-backward`, `seek-forward`; optional `target` and `seconds`. Play LED solid while playing, blinking while paused; Stop LED lit while stopped; seek LEDs show the button, not the player, and scrubbing repeats every 200 ms after a 400 ms hold. - `MediaService&` threaded through `IActionBase::Init` and the action factory (clean cutover, no overloads kept), README documentation, and `dbus.socket` added to the user unit's `After=`. ## Verification - `nanoKontroller2_mpris_tests` (new ctest target) covers instance-suffix stripping, suffix matching, `Identity` matching and the full selection ranking, including the no-match and unknown-bus-name paths. Built and run locally: all checks pass. - Every source file was compile-checked against glib/gio and WirePlumber headers; the full CMake build runs in CI (this container has neither pkg-config nor the dev packages). - **Not verified on hardware**: nobody is at the device in this session, so the end-to-end checks from the plan (LED behaviour, seek timing, the `DBUS_SESSION_BUS_ADDRESS=unix:path=/nonexistent` degraded path) are untested. In particular, the assumption that `Rewind`/`FastForward` really have LEDs comes from the issue, not from code — if they do not light up, the two `LED` enum members and the LED writes in the seek branch can be dropped without touching anything else.
Media control needs to pick exactly one player per button press, even
when several MPRIS players are on the session bus. This adds the pure
bookkeeping half of that: the set of known players with their playback
status and history, plus the rule that ranks them.

The ranking puts the status band first so an audible player wins over
one that merely played recently, then recency, then the order of the
configured target entries, then the order of appearance; a bus-name
comparison breaks remaining ties so the result cannot depend on the
hashing of the underlying unordered map.

Players are matched through NodeMatcher over every dot-aligned suffix
of their well-known bus name plus their MPRIS Identity, with the
per-process `instance_<n>` segment stripped so a pattern can name a
player without knowing its pid. Signals for a bus name the registry
does not know are silent no-ops, because a vanish racing a property
change is normal traffic rather than an error.

The file deliberately pulls in nothing from glib, so the ranking rules
are unit-tested in their own dependency-free test target.

Issue: #1
Co-Authored-By: Claude Opus 5 via omp-deck
A paused media player needs a third LED state besides on and off. The
blink timer lives in LedManager rather than in the action that wants
it: it is shared by every blinking LED, so the number of GLib timeouts
does not grow with the configuration and all blinking LEDs flash in
phase instead of drifting apart.

Set now cancels blinking for the control it writes, so a definite state
always wins and no caller has to remember to stop the flashing first.
The phase flips go through an internal write that bypasses that
cancellation, and ResetState drops the timer along with the cached
states.

Issue: #1
Co-Authored-By: Claude Opus 5 via omp-deck
Both transport buttons carry an LED on the device, but they were absent
from the LED enum, which is what makes a control addressable at all:
IsLedCapable tests membership in that enum. Adding them lets an action
light them, and makes the startup reset clear them like every other
LED — a visible change on devices where they were left lit by another
tool.

Issue: #1
Co-Authored-By: Claude Opus 5 via omp-deck
feat(actions): add media action controlling MPRIS players
All checks were successful
Build / build (pull_request) Successful in 51s
5bed60602f
Transport buttons could be named in the configuration but had nothing
to bind to: every action type was a PipeWire node action. This adds a
`media` type that talks to media players over MPRIS on the D-Bus
session bus, covering play-pause, stop, track skip and seeking.

MediaService owns the bus connection and one proxy per player, and
turns name-owner and property changes into registry updates plus a
notification to the actions, so an action never talks to D-Bus by
itself and its LED follows the player even when the player is driven
from its own UI. Identity is read through a second, short-lived proxy
because GDBusProxy caches only its own interface's properties. All
calls are asynchronous: a player that stops answering must not stall
the main loop. A missing session bus is reported once and leaves the
service permanently inactive, without a reconnect loop — a session bus
that is gone means the session is gone.

A press acts on the single top-ranked player rather than fanning out
like the node actions, because transport controls on several players
at once is not a useful gesture. Play and Stop show the selected
player's state (blinking meaning paused), while the seek buttons light
their LED as press feedback and scrub while held, one step on press,
then every 200 ms after a 400 ms hold. Those timers are also stopped
from the destructor, so a release lost to an unplugged device cannot
leave an unattended scrub running.

Record and Cycle stay unmapped: MPRIS models no recording.

Issue: #1
Co-Authored-By: Claude Opus 5 via omp-deck
omp-deck changed title from WIP: Support media control buttons via MPRIS to Support media control buttons via MPRIS 2026-11-13 14:20:12 +01:00
lucas approved these changes 2026-11-13 14:27:38 +01:00
lucas merged commit 9a3922b462 into main 2026-11-13 14:27:45 +01:00
lucas deleted branch agent/issue-1-media-actions 2026-11-13 14:27:46 +01:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
lucas/nanoKontroller2!27
No description provided.