seek_position: int,
fade_in: bool,
output_format: ContentType,
+ is_alert: bool,
) -> QueueStream:
"""Start running a queue stream."""
# cleanup stale previous queue tasks
streamdetails = await get_stream_details(self.mass, first_item, queue.queue_id)
# work out pcm details
- if queue.settings.crossfade_mode == CrossFadeMode.ALWAYS:
+ if is_alert:
+ pcm_sample_rate = 41000
+ pcm_bit_depth = 16
+ pcm_channels = 2
+ pcm_resample = True
+ elif queue.settings.crossfade_mode == CrossFadeMode.ALWAYS:
pcm_sample_rate = min(96000, queue.max_sample_rate)
pcm_bit_depth = 24
pcm_channels = 2
pcm_bit_depth=pcm_bit_depth,
pcm_channels=pcm_channels,
pcm_resample=pcm_resample,
+ is_alert=is_alert,
autostart=True,
)
self.mass.create_task(self.cleanup_stale)
pcm_channels: int = 2,
pcm_floating_point: bool = False,
pcm_resample: bool = False,
+ is_alert: bool = False,
autostart: bool = False,
):
"""Init QueueStreamJob instance."""
self.pcm_channels = pcm_channels
self.pcm_floating_point = pcm_floating_point
self.pcm_resample = pcm_resample
+ self.is_alert = is_alert
self.url = queue.mass.streams.get_stream_url(stream_id, output_format)
self.mass = queue.mass
self.seconds_streamed += len(audio_chunk) / sample_size
del audio_chunk
# allow clients to only buffer max ~30 seconds ahead
- seconds_allowed = int(time() - self.streaming_started) + 30
+ seconds_allowed = int(time() - self.streaming_started)
diff = self.seconds_streamed - seconds_allowed
- if diff > 1:
+ if diff > 30:
self.logger.debug(
- "Player is buffering %s seconds ahead, slowing it down",
+ "Player is buffering %s seconds ahead, slowing it down a bit",
diff,
)
await asyncio.sleep(10)
)
continue
- if queue_track.name == "alert":
- self.pcm_resample = True
-
# check the PCM samplerate/bitrate
if not self.pcm_resample and streamdetails.bit_depth > self.pcm_bit_depth:
self.signal_next = True
queue_track.streamdetails.seconds_skipped = seek_position
fade_in_part = b""
cur_chunk = 0
- prev_chunk = None
+ prev_chunk = b""
bytes_written = 0
# handle incoming audio chunks
async for is_last_chunk, chunk in get_media_stream(
if len(chunk) == 0 and bytes_written == 0 and is_last_chunk:
# stream error: got empy first chunk ?!
self.logger.warning("Stream error on %s", queue_track.uri)
- elif cur_chunk == 1 and is_last_chunk:
- # audio only has one single chunk (alert?)
- bytes_written += len(chunk)
- yield chunk
- del chunk
elif cur_chunk == 1 and last_fadeout_data:
prev_chunk = chunk
del chunk
bytes_written += len(remaining_bytes)
del remaining_bytes
del chunk
- prev_chunk = None # needed to prevent this chunk being sent again
+ prev_chunk = b"" # needed to prevent this chunk being sent again
# HANDLE LAST PART OF TRACK
elif prev_chunk and is_last_chunk:
# last chunk received so create the last_part
del last_part
del remaining_bytes
del chunk
+ elif is_last_chunk:
+ # there is only one chunk (e.g. alert sound)
+ yield chunk
+ del chunk
# MIDDLE PARTS OF TRACK
else:
# middle part of the track
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
-from music_assistant.models.enums import EventType, MediaType, QueueOption, RepeatMode
+from music_assistant.models.enums import (
+ ContentType,
+ EventType,
+ MediaType,
+ QueueOption,
+ RepeatMode,
+)
from music_assistant.models.errors import MediaNotFoundError, MusicAssistantError
from music_assistant.models.event import MassEvent
items: List[QueueItem]
index: Optional[int]
position: int
+ repeat: RepeatMode
+ shuffle: bool
class PlayerQueue:
announce: Prepend the (TTS) alert with a small announce sound.
gain_correct: Adjust the gain of the alert sound (in dB).
"""
- if self._snapshot:
- self.logger.debug("Ignore play_alert: already in progress")
- # return
+ assert not self._snapshot, "Alert already in progress"
# create snapshot
await self.snapshot_create()
else:
raise MediaNotFoundError(f"Invalid uri: {uri}") from err
- # load queue with alert sound(s)
- await self.load(queue_items)
+ # start queue with alert sound(s)
+ self._items = queue_items
+ self._settings.repeat_mode = RepeatMode.OFF
+ self._settings.shuffle_enabled = False
+ await self.queue_stream_start(0, 0, False, is_alert=True)
- # wait for the alert to finish playing
- await self.stream.done.wait()
+ # wait for the player to finish playing
alert_done = asyncio.Event()
def handle_event(evt: MassEvent):
handle_event, EventType.QUEUE_UPDATED, self.queue_id
)
try:
+ await asyncio.wait_for(self.stream.done.wait(), 30)
await asyncio.wait_for(alert_done.wait(), 30)
+ except asyncio.TimeoutError:
+ self.logger.warning("Timeout while playing alert")
finally:
unsub()
# restore queue
async def snapshot_create(self) -> None:
"""Create snapshot of current Queue state."""
+ self.logger.debug("Creating snapshot...")
self._snapshot = QueueSnapShot(
powered=self.player.powered,
state=self.player.state,
items=self._items,
index=self._current_index,
position=self._current_item_elapsed_time,
+ repeat=self._settings.repeat_mode,
+ shuffle=self._settings.shuffle_enabled,
)
async def snapshot_restore(self) -> None:
# clear queue first
await self.clear()
# restore queue
+ self._settings.repeat_mode = self._snapshot.repeat
+ self._settings.shuffle_enabled = self._snapshot.shuffle
await self.update(self._snapshot.items)
self._current_index = self._snapshot.index
self._current_item_elapsed_time = self._snapshot.position
if not self._snapshot.powered:
await self.player.power(False)
# reset snapshot once restored
+ self.logger.debug("Restored snapshot...")
self._snapshot = None
async def play_index(
)
async def queue_stream_start(
- self, start_index: int, seek_position: int, fade_in: bool, passive: bool = False
+ self,
+ start_index: int,
+ seek_position: int,
+ fade_in: bool,
+ is_alert: bool = False,
+ passive: bool = False,
) -> QueueStream:
"""Start the queue stream runner."""
- output_format = self._settings.stream_type
+ if is_alert and ContentType.MP3 in self.player.supported_content_types:
+ # force MP3 for alert messages
+ output_format = ContentType.MP3
+ else:
+ output_format = self._settings.stream_type
if self.player.use_multi_stream:
# if multi stream is enabled, all child players should receive the same audio stream
expected_clients = len(get_child_players(self.player, True))
seek_position=seek_position,
fade_in=fade_in,
output_format=output_format,
+ is_alert=is_alert,
)
self._stream_id = stream.stream_id
# execute the play command on the player(s)