- queue_id: queue_id of the playerqueue to handle the command.
"""
- # always fetch the underlying player so we can raise early if its not available
- player = self.mass.players.get(queue_id, True)
- if player.announcement_in_progress:
- self.logger.warning("Ignore queue command: An announcement is in progress")
- return
if queue := self.get(queue_id):
queue.stream_finished = None
# forward the actual stop command to the player provider
self._queue_items[queue_id] = queue_items
# always call update to calculate state etc
self.on_player_update(player, {})
+ self.mass.signal_event(EventType.QUEUE_ADDED, object_id=queue_id, data=queue)
def on_player_update(
self,
import asyncio
import functools
-from contextlib import suppress
+import time
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, cast
from music_assistant.common.helpers.util import get_changed_values
)
await self.cmd_stop(player.player_id)
# wait for the player to stop
- with suppress(TimeoutError):
- await self.wait_for_state(player, PlayerState.IDLE, 10)
- # a small amount of pause before the volume command
- # prevents that the last piece of music is very loud
- await asyncio.sleep(0.2)
+ await self.wait_for_state(player, PlayerState.IDLE, 10, 0.4)
# adjust volume if needed
# in case of a (sync) group, we need to do this for all child players
prev_volumes: dict[str, int] = {}
)
await self.play_media(player_id=player.player_id, media=announcement)
# wait for the player(s) to play
- with suppress(TimeoutError):
- await self.wait_for_state(player, PlayerState.PLAYING, 10)
- self.logger.debug(
- "Announcement to player %s - waiting on the player to stop playing...",
- player.display_name,
- )
+ await self.wait_for_state(player, PlayerState.PLAYING, 10, minimal_time=0.1)
# wait for the player to stop playing
if not announcement.duration:
media_info = await parse_tags(announcement.custom_data["url"])
- announcement.duration = media_info.duration
- with suppress(TimeoutError):
- await self.wait_for_state(player, PlayerState.IDLE, (announcement.duration or 60) + 3)
+ announcement.duration = media_info.duration or 60
+ media_info.duration += 2
+ await self.wait_for_state(
+ player,
+ PlayerState.IDLE,
+ max(announcement.duration * 2, 60),
+ announcement.duration + 2,
+ )
self.logger.debug(
"Announcement to player %s - restore previous state...", player.display_name
)
# TODO !!
async def wait_for_state(
- self, player: Player, wanted_state: PlayerState, timeout: float = 60.0
+ self,
+ player: Player,
+ wanted_state: PlayerState,
+ timeout: float = 60.0,
+ minimal_time: float = 0,
) -> None:
"""Wait for the given player to reach the given state."""
- async with asyncio.timeout(timeout):
- while player.state != wanted_state:
- await asyncio.sleep(0.1)
+ start_timestamp = time.time()
+ self.logger.debug(
+ "Waiting for player %s to reach state %s", player.display_name, wanted_state
+ )
+ try:
+ async with asyncio.timeout(timeout):
+ while player.state != wanted_state:
+ await asyncio.sleep(0.1)
+
+ except TimeoutError:
+ self.logger.debug(
+ "Player %s did not reach state %s within the timeout of %s seconds",
+ player.display_name,
+ wanted_state,
+ timeout,
+ )
+ elapsed_time = round(time.time() - start_timestamp, 2)
+ if elapsed_time < minimal_time:
+ self.logger.debug(
+ "Player %s reached state %s too soon (%s vs %s seconds) - add fallback sleep...",
+ player.display_name,
+ wanted_state,
+ elapsed_time,
+ minimal_time,
+ )
+ await asyncio.sleep(minimal_time - elapsed_time)
+ else:
+ self.logger.debug(
+ "Player %s reached state %s within %s seconds",
+ player.display_name,
+ wanted_state,
+ elapsed_time,
+ )
bits_per_sample: int
format: str
bit_rate: int
- duration: int | None
+ duration: float | None
tags: dict[str, str]
has_cover_image: bool
filename: str
),
format=raw["format"]["format_name"],
bit_rate=int(raw["format"].get("bit_rate", 320)),
- duration=int(float(raw["format"].get("duration", 0))) or None,
+ duration=float(raw["format"].get("duration", 0)) or None,
tags=tags,
has_cover_image=has_cover_image,
filename=raw["format"]["filename"],
if not tags.duration and file_size and tags.bit_rate:
# estimate duration from filesize/bitrate
tags.duration = int((file_size * 8) / tags.bit_rate)
+ if not tags.duration and tags.raw.get("format", {}).get("duration"):
+ tags.duration = float(tags.raw["format"]["duration"])
return tags
except (KeyError, ValueError, JSONDecodeError, InvalidDataError) as err:
msg = f"Unable to retrieve info for {file_path}: {err!s}"
chunk_index = 0
timeout = ClientTimeout(total=0, connect=30, sock_read=600)
headers = {}
+ # if seek_position and streamdetails.size:
+ # chunk_count = ceil(streamdetails.size / 2048)
+ # chunk_index = int(chunk_count / streamdetails.duration) * seek_position
+ # skip_bytes = chunk_index * 2048
+ # headers["Range"] = f"bytes={skip_bytes}-"
+
+ # NOTE: Seek with using the Range header is not working properly
+ # causing malformed audio so this is a temporary patch
+ # by just skipping chunks
if seek_position and streamdetails.size:
chunk_count = ceil(streamdetails.size / 2048)
- chunk_index = int(chunk_count / streamdetails.duration) * seek_position
- skip_bytes = chunk_index * 2048
- headers["Range"] = f"bytes={skip_bytes}-"
+ skip_chunks = int(chunk_count / streamdetails.duration) * seek_position
+ else:
+ skip_chunks = 0
buffer = bytearray()
streamdetails.data["start_ts"] = utc_timestamp()
async for chunk in resp.content.iter_chunked(2048):
buffer += chunk
if len(buffer) >= 2048:
+ if chunk_index >= skip_chunks:
+ continue
if chunk_index % 3 > 0:
yield bytes(buffer[:2048])
else: