From: Marvin Schenkel Date: Tue, 13 Jan 2026 14:18:31 +0000 (+0100) Subject: Fix issues with progress bar jumps and time overflow (#2959) X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=499012764e382047018214154029d10d858b03f1;p=music-assistant-server.git Fix issues with progress bar jumps and time overflow (#2959) * Progress * Progress * Fix duplicate playlog entries on stream restart * Use model for queuetimeupdate * Cleanup * Update music_assistant/controllers/streams/streams_controller.py Co-authored-by: Marcel van der Veldt * Feedback * Feedback --------- Co-authored-by: Marcel van der Veldt --- diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index 5bcd3899..6edcefa1 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -677,6 +677,7 @@ class PlayerQueuesController(CoreController): queue.current_index = None queue.current_item = None queue.elapsed_time = 0 + queue.elapsed_time_last_updated = time.time() queue.index_in_buffer = None self.update_items(queue_id, []) @@ -924,6 +925,7 @@ class PlayerQueuesController(CoreController): # this way the UI knows immediately that a new item is loading queue.current_item = self.get_item(queue_id, index) queue.elapsed_time = seek_position + queue.elapsed_time_last_updated = time.time() self.signal_update(queue_id) queue.index_in_buffer = index queue.flow_mode_stream_log = [] diff --git a/music_assistant/controllers/streams/streams_controller.py b/music_assistant/controllers/streams/streams_controller.py index 89896412..7f2ee975 100644 --- a/music_assistant/controllers/streams/streams_controller.py +++ b/music_assistant/controllers/streams/streams_controller.py @@ -590,6 +590,8 @@ class StreamsController(CoreController): if not start_queue_item: raise web.HTTPNotFound(reason=f"Unknown Queue item: {start_queue_item_id}") + queue.flow_mode_stream_log = [] + # select the highest possible PCM settings for this player flow_pcm_format = await self._select_flow_format(queue_player) diff --git a/music_assistant/providers/airplay/player.py b/music_assistant/providers/airplay/player.py index 18599f14..28ee5a2f 100644 --- a/music_assistant/providers/airplay/player.py +++ b/music_assistant/providers/airplay/player.py @@ -132,7 +132,12 @@ class AirPlayPlayer(Player): """Return the corrected elapsed time accounting for stream session restarts.""" if not self.stream or not self.stream.session: return super().corrected_elapsed_time or 0.0 - return time.time() - self.stream.session.last_stream_started + session = self.stream.session + elapsed = time.time() - session.last_stream_started - session.total_pause_time + if session.last_paused is not None: + current_pause = time.time() - session.last_paused + elapsed -= current_pause + return max(0.0, elapsed) async def get_config_entries( self, @@ -592,7 +597,16 @@ class AirPlayPlayer(Player): ) -> None: """Set the playback state from stream (RAOP or AirPlay2).""" if state is not None: + prev_state = self._attr_playback_state self._attr_playback_state = state + if self.stream and self.stream.session: + if prev_state == PlaybackState.PLAYING and state != PlaybackState.PLAYING: + self.stream.session.last_paused = time.time() + elif prev_state != PlaybackState.PLAYING and state == PlaybackState.PLAYING: + if self.stream.session.last_paused is not None: + pause_duration = time.time() - self.stream.session.last_paused + self.stream.session.total_pause_time += pause_duration + self.stream.session.last_paused = None if elapsed_time is not None: self._attr_elapsed_time = elapsed_time self._attr_elapsed_time_last_updated = time.time() diff --git a/music_assistant/providers/airplay/stream_session.py b/music_assistant/providers/airplay/stream_session.py index ca1d5a00..775bda66 100644 --- a/music_assistant/providers/airplay/stream_session.py +++ b/music_assistant/providers/airplay/stream_session.py @@ -65,6 +65,8 @@ class AirPlayStreamSession: # because we reuse an existing stream session for new play_media requests, # we need to track when the last stream was started self.last_stream_started: float = 0.0 + self.total_pause_time: float = 0.0 + self.last_paused: float | None = None self._clients_ready = asyncio.Event() self._first_chunk_received = asyncio.Event() @@ -206,6 +208,8 @@ class AirPlayStreamSession: old_audio_source_task.cancel() self._audio_source_task = new_audio_source_task self.last_stream_started = time.time() + self.wait_start + self.total_pause_time = 0.0 + self.last_paused = None for sync_client in self.sync_clients: sync_client.set_state_from_stream(state=None, elapsed_time=0) # ensure we cleanly wait for the old audio source task to finish diff --git a/music_assistant/providers/squeezelite/player.py b/music_assistant/providers/squeezelite/player.py index 17882e50..8a6a62f2 100644 --- a/music_assistant/providers/squeezelite/player.py +++ b/music_assistant/providers/squeezelite/player.py @@ -369,6 +369,7 @@ class SqueezelitePlayer(Player): self._attr_available = self.client.connected self._attr_name = self.client.name self._attr_powered = self.client.powered + old_state = self._attr_playback_state self._attr_playback_state = STATE_MAP[self.client.state] self._attr_volume_level = self.client.volume_level self._attr_volume_muted = self.client.muted @@ -377,8 +378,13 @@ class SqueezelitePlayer(Player): ip_address=self.client.device_address, manufacturer=self.client.device_type, ) - self._attr_elapsed_time = self.client.elapsed_seconds - self._attr_elapsed_time_last_updated = time.time() + if ( + old_state != PlaybackState.PLAYING + and self._attr_playback_state == PlaybackState.PLAYING + ): + # Invalidate elapsed time interpolation to avoid jumps when resuming from pause/stop + # We need this because some players (e.g. WiiM) keep sending increasing elapsed time + self._attr_elapsed_time_last_updated = time.time() # Update current media if available if self.client.current_media and (metadata := self.client.current_media.metadata): self._attr_current_media = PlayerMedia( @@ -458,8 +464,10 @@ class SqueezelitePlayer(Player): def _handle_player_heartbeat(self) -> None: """Process SlimClient elapsed_time update.""" - if self.client.state == SlimPlayerState.STOPPED: - # ignore server heartbeats when stopped + if self.playback_state != PlaybackState.PLAYING: + # ignore server heartbeats when not playing + # Some players keep sending heartbeat with increasing elapsed time + # even when paused (e.g. WiiM) return # elapsed time change on the player will be auto picked up # by the player manager.