{**CONF_ENTRY_FLOW_MODE.to_dict(), "default_value": False, "value": False, "hidden": True}
)
+
CONF_ENTRY_AUTO_PLAY = ConfigEntry(
key=CONF_AUTO_PLAY,
type=ConfigEntryType.BOOLEAN,
PAUSE = "pause"
SYNC = "sync"
SEEK = "seek"
- ENQUEUE_NEXT = "enqueue_next"
PLAY_ANNOUNCEMENT = "play_announcement"
UNKNOWN = "unknown"
ConfigEntryType,
EventType,
MediaType,
- PlayerFeature,
PlayerState,
QueueOption,
RepeatMode,
if queue.repeat_mode == repeat_mode:
return # no change
queue.repeat_mode = repeat_mode
+ # ensure that we restart playback or trigger enqueue next if repeat mode changed
self.signal_update(queue_id)
+ if (
+ repeat_mode == RepeatMode.ONE
+ and queue.flow_mode
+ and queue.state == PlayerState.PLAYING
+ and queue.current_index != queue.index_in_buffer
+ ):
+ # edge case; repeat one enabled in flow mode but the
+ # flow stream had already loaded a new item in the buffer,
+ # we need to restart playback
+ self.mass.create_task(self.resume(queue_id))
+ else:
+ self.mass.create_task(self._enqueue_next(queue, queue.current_index))
@api_command("player_queues/play_media")
async def play_media(
elif prev_state["current_index"] != new_state["current_index"]:
queue.end_of_track_reached = False
- # handle enqueuing of next item to play
- if not queue.flow_mode or queue.stream_finished:
- self._check_enqueue_next(player, queue, prev_state, new_state)
+ # handle auto restart of queue in flow mode when repeat is enabled
+ if (
+ queue.flow_mode
+ and queue.repeat_mode != RepeatMode.OFF
+ and queue.stream_finished
+ and prev_state["state"] == PlayerState.PLAYING
+ and new_state["state"] == PlayerState.IDLE
+ ):
+ # flow mode and repeat mode is on, restart the queue
+ next_index = self._get_next_index(queue_id, queue.current_index, allow_repeat=True)
+ if next_index is not None:
+ self.mass.create_task(self.play_index(queue_id, next_index))
# do not send full updates if only time was updated
if changed_keys == {"elapsed_time"}:
raise QueueEmpty("No more (playable) tracks left in the queue.")
return next_item
+ def track_loaded_in_buffer(self, queue_id: str, item_id: str) -> None:
+ """Call when a player has (started) loading a track in the buffer."""
+ queue = self.get(queue_id)
+ if not queue:
+ msg = f"PlayerQueue {queue_id} is not available"
+ raise PlayerUnavailableError(msg)
+ queue.index_in_buffer = self.index_by_id(queue_id, item_id)
+ if queue.flow_mode:
+ return # nothing to do when flow mode is active
+ self.signal_update(queue_id)
+ # enqueue the next track as soon as the player reports
+ # it has started buffering the given queue item
+ self.mass.create_task(self._enqueue_next(queue, item_id))
+
# Main queue manipulation methods
def load(
base_key=queue_id,
)
)
+ # signal preload of next item (to ensure the player loads the correct next item)
+ if queue.index_in_buffer is not None:
+ task_id = f"enqueue_next_{queue.queue_id}"
+ self.mass.call_later(
+ 1, self._enqueue_next(queue, queue.index_in_buffer), task_id=task_id
+ )
+
# always send the base event
self.mass.signal_event(EventType.QUEUE_UPDATED, object_id=queue_id, data=queue)
# save state
await asyncio.sleep(5)
setattr(self, debounce_key, None)
- def _check_enqueue_next(
- self,
- player: Player,
- queue: PlayerQueue,
- prev_state: CompareState,
- new_state: CompareState,
- ) -> None:
- """Check if we need to enqueue the next item to the player itself."""
- if not queue.active:
- return
- if prev_state["state"] != PlayerState.PLAYING:
- return
+ async def _enqueue_next(self, queue: PlayerQueue, current_index: int | str) -> None:
+ """Enqueue the next item in the queue."""
if (player := self.mass.players.get(queue.queue_id)) and player.announcement_in_progress:
self.logger.warning("Ignore queue command: An announcement is in progress")
return
- current_item = self.get_item(queue.queue_id, queue.current_index)
- if not current_item:
- return # guard, just in case something bad happened
- if not current_item.duration:
- return
- # NOTE: 'seconds_streamed' can actually be 0 if there was a stream error!
- if current_item.streamdetails and current_item.streamdetails.seconds_streamed is not None:
- duration = current_item.streamdetails.seconds_streamed
- else:
- duration = current_item.duration
- seconds_remaining = int(duration - player.corrected_elapsed_time)
-
- async def _enqueue_next(current_index: int, supports_enqueue: bool = False) -> None:
- if (
- player := self.mass.players.get(queue.queue_id)
- ) and player.announcement_in_progress:
- self.logger.warning("Ignore queue command: An announcement is in progress")
- return
- with suppress(QueueEmpty):
- next_item = await self.preload_next_item(queue.queue_id, current_index)
- if supports_enqueue:
- await self.mass.players.enqueue_next_media(
- player_id=player.player_id,
- media=self.player_media_from_queue_item(next_item, queue.flow_mode),
- )
- return
- await self.play_index(queue.queue_id, next_item.queue_item_id)
-
- # handle queue fully played - clear it completely once the player stopped
- if (
- queue.stream_finished
- and queue.state == PlayerState.IDLE
- and self._get_next_index(queue.queue_id, queue.current_index) is None
- ):
- self.logger.debug("End of queue reached for %s", queue.display_name)
- self.clear(queue.queue_id)
- return
-
- # handle native enqueue next support of player
- if PlayerFeature.ENQUEUE_NEXT in player.supported_features:
- # we enqueue the next track after a new track
- # has started playing and (repeat) before the current track ends
- new_track_started = (
- new_state["state"] == PlayerState.PLAYING
- and prev_state["current_index"] != new_state["current_index"]
+ if isinstance(current_index, str):
+ current_index = self.index_by_id(queue.queue_id, current_index)
+ with suppress(QueueEmpty):
+ next_item = await self.preload_next_item(queue.queue_id, current_index)
+ await self.mass.players.enqueue_next_media(
+ player_id=player.player_id,
+ media=self.player_media_from_queue_item(next_item, queue.flow_mode),
)
- if (
- new_track_started
- or seconds_remaining == 15
- or int(player.corrected_elapsed_time) == 1
- ):
- self.mass.create_task(_enqueue_next(queue.current_index, True))
- return
-
- # player does not support enqueue next feature.
- # we wait for the player to stop after it reaches the end of the track
- if (
- (not queue.flow_mode or queue.repeat_mode in (RepeatMode.ALL, RepeatMode.ONE))
- # we have a couple of guards here to prevent the player starting
- # playback again when its stopped outside of MA's control
- and queue.stream_finished
- and queue.end_of_track_reached
- and queue.state == PlayerState.IDLE
- ):
- queue.stream_finished = None
- self.mass.create_task(_enqueue_next(queue.current_index, False))
- return
async def _get_radio_tracks(self, queue_id: str) -> list[MediaItemType]:
"""Call the registered music providers for dynamic tracks."""
queue_item.uri,
queue.display_name,
)
- queue.index_in_buffer = self.mass.player_queues.index_by_id(queue_id, queue_item_id)
+ self.mass.player_queues.track_loaded_in_buffer(queue_id, queue_item_id)
pcm_format = AudioFormat(
content_type=ContentType.from_bit_depth(output_format.bit_depth),
sample_rate=queue_item.streamdetails.audio_format.sample_rate,
queue_track.name,
queue.display_name,
)
- queue.index_in_buffer = self.mass.player_queues.index_by_id(
+ self.mass.player_queues.track_loaded_in_buffer(
queue.queue_id, queue_track.queue_item_id
)
"""
Handle enqueuing of the next (queue) item on the player.
- Only called if the player supports PlayerFeature.ENQUE_NEXT.
- Called about 1 second after a new track started playing.
- Called about 15 seconds before the end of the current track.
+ Called when player reports it started buffering a queue item
+ and when the queue items updated.
A PlayerProvider implementation is in itself responsible for handling this
so that the queue items keep playing until its empty or the player stopped.
PlayerFeature.VOLUME_SET,
PlayerFeature.VOLUME_MUTE,
PlayerFeature.PLAY_ANNOUNCEMENT, # see play_announcement method
- PlayerFeature.ENQUEUE_NEXT, # see play_media/enqueue_next_media methods
),
)
# register the player with the player manager
"""
Handle enqueuing of the next (queue) item on the player.
- Only called if the player supports PlayerFeature.ENQUE_NEXT.
- Called about 1 second after a new track started playing.
- Called about 15 seconds before the end of the current track.
+ Called when player reports it started buffering a queue item
+ and when the queue items updated.
A PlayerProvider implementation is in itself responsible for handling this
so that the queue items keep playing until its empty or the player stopped.
This will NOT be called if the end of the queue is reached (and repeat disabled).
This will NOT be called if the player is using flow mode to playback the queue.
"""
- # OPTIONAL - required only if you specified PlayerFeature.ENQUEUE_NEXT
# this method should handle the enqueuing of the next queue item on the player.
async def cmd_sync(self, player_id: str, target_player: str) -> None:
PlayerFeature.POWER,
PlayerFeature.VOLUME_MUTE,
PlayerFeature.VOLUME_SET,
- PlayerFeature.ENQUEUE_NEXT,
PlayerFeature.PAUSE,
),
enabled_by_default=enabled_by_default,
castplayer.player.supported_features = (
PlayerFeature.POWER,
PlayerFeature.VOLUME_SET,
- PlayerFeature.ENQUEUE_NEXT,
PlayerFeature.PAUSE,
)
PlayerFeature.VOLUME_SET,
)
-CONF_ENQUEUE_NEXT = "enqueue_next"
-
PLAYER_CONFIG_ENTRIES = (
- ConfigEntry(
- key=CONF_ENQUEUE_NEXT,
- type=ConfigEntryType.BOOLEAN,
- label="Player supports enqueue next/gapless",
- default_value=False,
- description="If the player supports enqueuing the next item for fluid/gapless playback. "
- "\n\nUnfortunately this feature is missing or broken on many DLNA players. \n"
- "Enable it with care. If music stops after one song, "
- "disable this setting (and use flow-mode instead).",
- ),
CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
CONF_ENTRY_CROSSFADE_DURATION,
CONF_ENTRY_ENFORCE_MP3,
def _set_player_features(self, dlna_player: DLNAPlayer) -> None:
"""Set Player Features based on config values and capabilities."""
dlna_player.player.supported_features = BASE_PLAYER_FEATURES
- player_id = dlna_player.player.player_id
- if self.mass.config.get_raw_player_config_value(player_id, CONF_ENQUEUE_NEXT, False):
- dlna_player.player.supported_features = (
- *dlna_player.player.supported_features,
- PlayerFeature.ENQUEUE_NEXT,
- )
CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
CONF_ENTRY_ENABLE_ICY_METADATA,
CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
- CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,
+ CONF_ENTRY_FLOW_MODE_ENFORCED,
CONF_ENTRY_HTTP_PROFILE,
ConfigEntry,
ConfigValueOption,
yield state
+async def _get_hass_media_player(
+ hass_prov: HomeAssistantProvider, entity_id: str
+) -> HassState | None:
+ """Return Hass state object for a single media_player entity."""
+ for state in await hass_prov.hass.get_states():
+ if state["entity_id"] == entity_id:
+ return state
+ return None
+
+
async def setup(
mass: MusicAssistant, manifest: ProviderManifest, config: ProviderConfig
) -> ProviderInstanceType:
"""Return all (provider/player specific) Config Entries for the given player (if any)."""
entries = await super().get_player_config_entries(player_id)
entries = entries + PLAYER_CONFIG_ENTRIES
- if player := self.mass.players.get(player_id):
- if PlayerFeature.ENQUEUE_NEXT not in player.supported_features:
- entries += (CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,)
+ if hass_state := await _get_hass_media_player(self.hass_prov, player_id):
+ hass_supported_features = MediaPlayerEntityFeature(
+ hass_state["attributes"]["supported_features"]
+ )
+ if MediaPlayerEntityFeature.MEDIA_ENQUEUE not in hass_supported_features:
+ entries += (CONF_ENTRY_FLOW_MODE_ENFORCED,)
+
return entries
async def cmd_stop(self, player_id: str) -> None:
supported_features.append(PlayerFeature.SYNC)
if MediaPlayerEntityFeature.PAUSE in hass_supported_features:
supported_features.append(PlayerFeature.PAUSE)
- if MediaPlayerEntityFeature.MEDIA_ENQUEUE in hass_supported_features:
- supported_features.append(PlayerFeature.ENQUEUE_NEXT)
if MediaPlayerEntityFeature.VOLUME_SET in hass_supported_features:
supported_features.append(PlayerFeature.VOLUME_SET)
if MediaPlayerEntityFeature.VOLUME_MUTE in hass_supported_features:
transition_duration = 0
metadata = {
- "item_id": media.queue_item_id or media.uri,
+ "item_id": media.uri,
"title": media.title,
"album": media.album,
"artist": media.artist,
"image_url": media.image_url,
"duration": media.duration,
+ "queue_id": media.queue_id,
+ "queue_item_id": media.queue_item_id,
}
queue = self.mass.player_queues.get(media.queue_id or player_id)
slimplayer.extra_data["playlist repeat"] = REPEATMODE_MAP[queue.repeat_mode]
PlayerFeature.VOLUME_SET,
PlayerFeature.PAUSE,
PlayerFeature.VOLUME_MUTE,
- PlayerFeature.ENQUEUE_NEXT,
),
can_sync_with=tuple(
x.player_id for x in self.slimproto.players if x.player_id != player_id
# update player state on player events
player.available = True
- player.current_item_id = (
- slimplayer.current_media.metadata.get("item_id")
- if slimplayer.current_media and slimplayer.current_media.metadata
- else slimplayer.current_url
- )
+ if slimplayer.current_media and (metadata := slimplayer.current_media.metadata):
+ player.current_media = PlayerMedia(
+ uri=metadata.get("item_id"),
+ title=metadata.get("title"),
+ album=metadata.get("album"),
+ artist=metadata.get("artist"),
+ image_url=metadata.get("image_url"),
+ duration=metadata.get("duration"),
+ queue_id=metadata.get("queue_id"),
+ queue_item_id=metadata.get("queue_item_id"),
+ )
+ else:
+ player.current_media = None
player.active_source = player.player_id
player.name = slimplayer.name
player.powered = slimplayer.powered
PLAYER_FEATURES_BASE = {
PlayerFeature.SYNC,
PlayerFeature.VOLUME_MUTE,
- PlayerFeature.ENQUEUE_NEXT,
PlayerFeature.PAUSE,
}
PLAYER_FEATURES = (
PlayerFeature.SYNC,
PlayerFeature.VOLUME_MUTE,
- PlayerFeature.ENQUEUE_NEXT,
PlayerFeature.PAUSE,
)
base_entries = await super().get_player_config_entries(player_id)
if not (self.sonosplayers.get(player_id)):
# most probably a syncgroup
- return (*base_entries, CONF_ENTRY_CROSSFADE, CONF_ENTRY_ENFORCE_MP3)
+ return (
+ *base_entries,
+ CONF_ENTRY_CROSSFADE,
+ CONF_ENTRY_ENFORCE_MP3,
+ CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
+ )
return (
*base_entries,
CONF_ENTRY_CROSSFADE,
PlayerFeature.SYNC,
PlayerFeature.VOLUME_MUTE,
PlayerFeature.VOLUME_SET,
- PlayerFeature.ENQUEUE_NEXT,
)
DURATION_SECONDS = "duration_in_s"
POSITION_SECONDS = "position_in_s"