Media controls (MPRIS, Windows SMTC); stop old tracks when switching

This commit is contained in:
sarah committed 2026-10-09 18:57:03 +02:00
1 parent e2d7515e08
commit d7f5eba1c2
14 files changed
+799 -26

No files matched your search

+27 -7
View File
@@ -37,6 +37,9 @@ class Player:
self.OnEnded: Callable[[], None] = lambda: None
self.OnChanged: Callable[[], None] = lambda: None # playing/paused, duration, source
self.OnError: Callable[[str], None] = lambda Message: None
# Others following along (the system media controls): called with
# "Changed", "Started" or "Seeked" after the owner's own callbacks.
self.Watchers: list[Callable[[str], None]] = []
# --- Position -----------------------------------------------------------
@@ -51,6 +54,12 @@ class Player:
self._Position = max(0, int(Milliseconds))
self._PositionAt = time.monotonic()
def _Tell(self, Kind: str) -> None:
if Kind == "Changed":
self.OnChanged()
for Watcher in self.Watchers:
Watcher(Kind)
# --- Events from the client ---------------------------------------------
def _StateChanged(self, Audio: fta.Audio, e: fta.AudioStateChangeEvent) -> None:
@@ -61,6 +70,7 @@ class Player:
self._SetPosition(self._Position)
self.Playing = True
self.OnStarted()
self._Tell("Started")
elif e.state == fta.AudioState.COMPLETED:
self.Playing = False
self._SetPosition(0)
@@ -72,7 +82,7 @@ class Player:
if self.Playing:
self._SetPosition(self.Position)
self.Playing = False
self.OnChanged()
self._Tell("Changed")
def _PositionChanged(self, Audio: fta.Audio, e: fta.AudioPositionChangeEvent) -> None:
if Audio is self.Audio and self.Playing:
@@ -86,7 +96,7 @@ class Player:
if Value > 0:
self.Duration = Value
self._Ready.set()
self.OnChanged()
self._Tell("Changed")
def _Loaded(self, Audio: fta.Audio) -> None:
if Audio is self.Audio:
@@ -109,9 +119,16 @@ class Player:
Old, self.Audio = self.Audio, None
if Old is None:
return
# Taking a service off the page doesn't stop its native player: it
# would play on, unseen. Stop and free it first.
for Method in (Old.pause, Old.release):
try:
await asyncio.wait_for(Method(), 2)
except (asyncio.TimeoutError, RuntimeError):
pass
if Old in self.Page.services:
self.Page.services.remove(Old)
self.Page.update() # disposes the old service and its native player
self.Page.update()
async def Load(self, FilePath: str, Start: int = 0, Play: bool = True, Duration: int = 0) -> bool:
"""Switches to FilePath and (if Play) starts at Start (ms). Duration is
@@ -138,7 +155,7 @@ class Player:
self.Audio = Audio
self.Page.services.append(Audio)
self.Page.update()
self.OnChanged()
self._Tell("Changed")
if Play:
return await self.Play(Start)
return True
@@ -166,6 +183,7 @@ class Player:
if not await self._Call(Audio.seek, ft.Duration(milliseconds=int(Position))):
return False
self._SetPosition(Position) # it played from 0 until the seek landed
self._Tell("Seeked")
return True
return await self._Call(Audio.play, ft.Duration(milliseconds=0))
@@ -174,7 +192,7 @@ class Player:
self._SetPosition(self.Position)
self.Playing = False
await self._Call(self.Audio.pause)
self.OnChanged()
self._Tell("Changed")
async def Toggle(self) -> None:
if self.Playing:
@@ -193,7 +211,8 @@ class Player:
await self._Call(self.Audio.seek, ft.Duration(milliseconds=Milliseconds))
else:
# Paused or not started: remember it, Play starts from there.
self.OnChanged()
self._Tell("Changed")
self._Tell("Seeked")
async def Stop(self) -> None:
"""Back to nothing loaded."""
@@ -202,10 +221,11 @@ class Player:
self.Playing = False
self.Duration = 0
self._SetPosition(0)
self.OnChanged()
self._Tell("Changed")
def SetVolume(self, Volume: float) -> None:
self.Volume = max(0.0, min(1.0, Volume))
if self.Audio is not None:
self.Audio.volume = self.Volume
self.Audio.update()
self._Tell("Volume")