)
SYNCGROUP_PREFIX: Final[str] = "syncgroup_"
VERBOSE_LOG_LEVEL: Final[int] = 5
+UGP_PREFIX: Final[str] = "ugp_"
player = self.get(player_id, True)
if player.announcement_in_progress:
return
+ # use stream server to host announcement on local network
+ # this ensures playback on all players, including ones that do not
+ # like https hosts and it also offers the pre-announce 'bell'
+ announcement_url = self.mass.streams.get_announcement_url(
+ player.player_id, url, use_pre_announce=use_pre_announce
+ )
try:
# mark announcement_in_progress on player
player.announcement_in_progress = True
+ self.logger.info(
+ "Playback announcement to player %s (with pre-announce: %s): %s",
+ player.display_name,
+ use_pre_announce,
+ url,
+ )
# check for native announce support
if PlayerFeature.PLAY_ANNOUNCEMENT in player.supported_features:
if prov := self.mass.get_provider(player.provider):
- await prov.play_announcement(player_id, url, use_pre_announce)
+ await prov.play_announcement(player_id, announcement_url)
return
# use fallback/default implementation
- await self._play_announcement(player, url, use_pre_announce)
+ await self._play_announcement(player, announcement_url)
finally:
player.announcement_in_progress = False
async def _play_announcement(
self,
player: Player,
- url: str,
- use_pre_announce: bool | None = None,
+ announcement_url: str,
) -> None:
"""Handle (default/fallback) implementation of the play announcement feature.
"""
if player.synced_to:
# redirect to sync master if player is group child
- self.mass.create_task(self.play_announcement(player.synced_to, url))
+ self.mass.create_task(self.play_announcement(player.synced_to, announcement_url))
return
if active_group := self._get_active_player_group(player):
# redirect to group player if playergroup is atcive
- self.mass.create_task(self.play_announcement(active_group.player_id, url))
+ self.mass.create_task(self.play_announcement(active_group.player_id, announcement_url))
return
- self.logger.info(
- "Playback announcement to player %s (with pre-announce: %s): %s",
- player.display_name,
- use_pre_announce,
- url,
- )
- # use stream server to host announcement on local network
- # this ensures playback on all players, including ones that do not
- # like https hosts and it also offers the pre-announce 'bell'
- url = self.mass.streams.get_announcement_url(player.player_id, url, use_pre_announce)
# create a queue item for the announcement so
# we can send a regular play-media call downstream
queue_item = QueueItem(
queue_id=player.player_id,
- queue_item_id=url,
+ queue_item_id=announcement_url,
name="Announcement",
duration=None,
streamdetails=StreamDetails(
provider="url",
- item_id=url,
+ item_id=announcement_url,
audio_format=AudioFormat(
- content_type=ContentType.try_parse(url),
+ content_type=ContentType.try_parse(announcement_url),
),
media_type=MediaType.ANNOUNCEMENT,
- direct=url,
- data=url,
+ direct=announcement_url,
+ data=announcement_url,
target_loudness=-10,
),
)
with suppress(TimeoutError):
await self.wait_for_state(player, PlayerState.IDLE, 5)
# increase volume a bit
- temp_volume = int(min(75, prev_volume * 1.5))
+ temp_volume = max(int(min(75, prev_volume) * 1.5), 15)
if temp_volume > prev_volume:
self.logger.debug(
"Announcement to player %s - setting temporary volume (%s)...",
CONF_OUTPUT_CHANNELS,
CONF_PUBLISH_IP,
SILENCE_FILE,
+ UGP_PREFIX,
VERBOSE_LOG_LEVEL,
)
from music_assistant.server.helpers.audio import LOGGER as AUDIO_LOGGER
self.mass = mass
self.pcm_audio_source = pcm_audio_source
self.pcm_format = pcm_format
+ self.auto_start = auto_start
self.expected_players: set[str] = set()
self.job_id = shortuuid.uuid()
self.bytes_streamed: int = 0
self._running = False
self.allow_start = asyncio.Event()
self._audio_task = asyncio.create_task(self._stream_job_runner())
- if auto_start:
- self.allow_start.set()
@property
def finished(self) -> bool:
try:
self._subscribed_players[player_id] = ffmpeg_proc
self.logger.debug("Subscribed player %s", player_id)
+ if self.auto_start and len(self._subscribed_players) == len(self.expected_players):
+ self.allow_start.set()
yield self
finally:
self._subscribed_players.pop(player_id, None)
if queue_item.media_type == MediaType.ANNOUNCEMENT:
return queue_item.queue_item_id
# handle request for (multi client) queue flow stream
- if queue_item.queue_item_id in ("flow", queue_item.queue_id) or flow_mode:
- # note: this will return an existing streamjonb if that was already created
- # e.g. in case of universal group player
+ if queue_item.queue_id.startswith(UGP_PREFIX):
+ # special case: we got forwarded a request from a Universal Group Player
+ # use the existing stream job that was already created by UGP
+ stream_job = self.mass.streams.stream_jobs[queue_item.queue_id]
+ return stream_job.resolve_stream_url(player_id, output_codec)
+
+ if flow_mode:
+ # create a new flow mode stream job session
pcm_format = AudioFormat(
content_type=ContentType.from_bit_depth(24),
sample_rate=FLOW_DEFAULT_SAMPLE_RATE,
This is called by player/sync group implementations to start streaming
the queue audio to multiple players at once.
"""
- if existing_job := self.stream_jobs.get(queue_id, None):
- if existing_job.pending:
- return existing_job
+ if existing_job := self.stream_jobs.pop(queue_id, None):
# cleanup existing job first
existing_job.stop()
self.stream_jobs[queue_id] = stream_job = QueueStreamJob(
This will NOT be called if the player is using flow mode to playback the queue.
"""
- async def play_announcement(
- self, player_id: str, announcement_url: str, use_pre_announce: bool = False
- ) -> None:
+ async def play_announcement(self, player_id: str, announcement_url: str) -> None:
"""Handle (provider native) playback of an announcement on given player."""
# will only be called for players with PLAY_ANNOUNCEMENT feature set.
raise NotImplementedError
from music_assistant.common.models.media_items import AudioFormat
from music_assistant.common.models.player import DeviceInfo, Player
from music_assistant.common.models.player_queue import PlayerQueue
-from music_assistant.constants import CONF_SYNC_ADJUST, VERBOSE_LOG_LEVEL
+from music_assistant.constants import CONF_SYNC_ADJUST, UGP_PREFIX, VERBOSE_LOG_LEVEL
from music_assistant.server.helpers.audio import get_media_stream
from music_assistant.server.helpers.process import AsyncProcess, check_output
from music_assistant.server.models.player_provider import PlayerProvider
-from music_assistant.server.providers.ugp import UGP_PREFIX
if TYPE_CHECKING:
from music_assistant.common.models.config_entries import ProviderConfig
CONF_PORT,
CONF_SYNC_ADJUST,
MASS_LOGO_ONLINE,
+ UGP_PREFIX,
VERBOSE_LOG_LEVEL,
)
from music_assistant.server.models.player_provider import PlayerProvider
-from music_assistant.server.providers.ugp import UGP_PREFIX
if TYPE_CHECKING:
from aioslimproto.models import SlimEvent
from music_assistant.common.models.errors import SetupFailedError
from music_assistant.common.models.media_items import AudioFormat
from music_assistant.common.models.player import DeviceInfo, Player
+from music_assistant.constants import UGP_PREFIX
from music_assistant.server.helpers.audio import get_media_stream
from music_assistant.server.helpers.process import AsyncProcess, check_output
from music_assistant.server.models.player_provider import PlayerProvider
-from music_assistant.server.providers.ugp import UGP_PREFIX
if TYPE_CHECKING:
from snapcast.control.group import Snapgroup
await self._enqueue_item(sonos_player, url=url, queue_item=queue_item)
- async def play_announcement(
- self, player_id: str, announcement_url: str, use_pre_announce: bool = False
- ) -> None:
+ async def play_announcement(self, player_id: str, announcement_url: str) -> None:
"""Handle (provider native) playback of an announcement on given player."""
- if use_pre_announce:
- announcement_url = self.mass.streams.get_announcement_url(
- player_id, announcement_url, True
- )
sonos_player = self.sonosplayers[player_id]
mass_player = self.mass.players.get(player_id)
- temp_volume = int(min(75, mass_player.volume_level * 1.5))
+ temp_volume = max(int(min(75, mass_player.volume_level) * 1.5), 15)
self.logger.debug(
"Playing announcement %s using websocket audioclip on %s",
announcement_url,
from music_assistant.common.models.media_items import AudioFormat
from music_assistant.common.models.player import DeviceInfo, Player
from music_assistant.common.models.queue_item import QueueItem
-from music_assistant.constants import CONF_CROSSFADE, CONF_GROUP_MEMBERS, SYNCGROUP_PREFIX
+from music_assistant.constants import (
+ CONF_CROSSFADE,
+ CONF_GROUP_MEMBERS,
+ SYNCGROUP_PREFIX,
+ UGP_PREFIX,
+)
from music_assistant.server.controllers.streams import (
FLOW_DEFAULT_BIT_DEPTH,
FLOW_DEFAULT_SAMPLE_RATE,
from music_assistant.server import MusicAssistant
from music_assistant.server.models import ProviderInstanceType
-UGP_PREFIX = "ugp_"
-
# ruff: noqa: ARG002
await self.cmd_power(player_id, True)
group_player = self.mass.players.get(player_id)
- await self.cmd_stop(player_id)
-
# create a multi-client stream job - all (direct) child's of this UGP group
# will subscribe to this multi client queue stream
pcm_format = AudioFormat(