Skip to content

Media playback control

from uda import Uda
with Uda() as uda:
track = uda.media.now_playing # None when nothing is playing
if track:
print(f"{track.title} — {track.artist}")
print(track.album, track.duration_ms, "ms")
print(uda.media.status) # 'playing' | 'paused' | 'stopped' | 'unknown'
const track = uda.media.nowPlaying; // null when nothing is playing
if (track) console.log(`${track.title} — ${track.artist}`);

Every field is owned and pre-joined for display: MPRIS publishes xesam:artist as an array, but a UI wants one string, so the backend performs the join and the core struct stays free of the wire types. An unpublished field is an empty string — a radio stream often has no album.

uda.media.send("play")
uda.media.send("pause")
uda.media.send("toggle") # switch between playing and paused
uda.media.send("next")
uda.media.send("previous")
uda.media.send("stop")

The full command set: play, pause, toggle, next, previous, stop.

status maps onto four values, with unknown as a first-class state rather than an error:

Value Meaning
playing a track is actively progressing
paused a track is selected and progress is halted
stopped nothing is loaded, or playback reached the end
unknown no session has focus, the player published nothing, or the value was unparseable

Never treat unknown as paused.

Platform Protocol Notes
Linux MPRIS v2 over the session bus any player exposing org.mpris.MediaPlayer2.*
Windows WinRT SMTC (GlobalSystemMediaTransportControls) reports the session holding system focus

Three SMTC quirks worth knowing, because they cost hours when missed:

  1. Every Try*Async() command returns bool, not a Result. false means the session refused the command (for example Next when the app does not enable it), while true only means the app accepted it — it can still fail afterwards. UDA maps false to a typed error so a caller can tell “sent” from “refused”.
  2. TimeSpan is in 100-nanosecond ticks. A zero Duration means “unknown” (a live stream), and is reported as absent rather than Some(0).
  3. “No current session” arrives as an Err, not a null. The absence of a player looks like a failure unless the caller reads it as a value — which is exactly why a machine with nothing playing answers Ok(None) / Ok(Unknown) and only commands raise NotSupported. This matches the Linux backend, where a session bus with no player name yields None.

The MPRIS Metadata dictionary deviates from its own specification in practice:

  • xesam:title is specified as an array of strings, but some players (mostly early versions) send a single string. UDA accepts both.
  • xesam:artist behaves the same way.
  • mpris:length is in microseconds; UDA converts to milliseconds. 0 denotes a live stream rather than a zero-length track, and a negative value is treated as invalid.
  • A type mismatch is not reported as an error; the value degrades to empty.

A player that refuses a command (pausing an already-paused stream, Next when the app disables the button) is still a successful call: the platform cannot distinguish “declined” from “done”, and reporting an error would make a perfectly normal toggle look like a failure.

Only a genuine transport failure — no session at all — produces an error.