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, [])
# 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 = []
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)
"""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,
) -> 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()
# 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()
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
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
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(
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.