From 9b1ee92a969fddb17eb9f742b4dd3ab4447d00b4 Mon Sep 17 00:00:00 2001 From: Marcel van der Veldt Date: Thu, 21 Mar 2024 14:17:57 +0100 Subject: [PATCH] Some fixes for Announcement feature (#1159) --- music_assistant/constants.py | 1 + music_assistant/server/controllers/players.py | 45 ++++++++++--------- music_assistant/server/controllers/streams.py | 21 +++++---- .../server/models/player_provider.py | 4 +- .../server/providers/airplay/__init__.py | 3 +- .../server/providers/slimproto/__init__.py | 2 +- .../server/providers/snapcast/__init__.py | 2 +- .../server/providers/sonos/__init__.py | 10 +---- .../server/providers/ugp/__init__.py | 11 ++--- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 44b9f328..04ac16a9 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -91,3 +91,4 @@ CONFIGURABLE_CORE_CONTROLLERS = ( ) SYNCGROUP_PREFIX: Final[str] = "syncgroup_" VERBOSE_LOG_LEVEL: Final[int] = 5 +UGP_PREFIX: Final[str] = "ugp_" diff --git a/music_assistant/server/controllers/players.py b/music_assistant/server/controllers/players.py index f49f0dca..b8b3d476 100644 --- a/music_assistant/server/controllers/players.py +++ b/music_assistant/server/controllers/players.py @@ -603,16 +603,28 @@ class PlayerController(CoreController): 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 @@ -1069,8 +1081,7 @@ class PlayerController(CoreController): 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. @@ -1088,38 +1099,28 @@ class PlayerController(CoreController): """ 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, ), ) @@ -1141,7 +1142,7 @@ class PlayerController(CoreController): 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)...", diff --git a/music_assistant/server/controllers/streams.py b/music_assistant/server/controllers/streams.py index 1b9e2fd9..cde50047 100644 --- a/music_assistant/server/controllers/streams.py +++ b/music_assistant/server/controllers/streams.py @@ -37,6 +37,7 @@ from music_assistant.constants import ( CONF_OUTPUT_CHANNELS, CONF_PUBLISH_IP, SILENCE_FILE, + UGP_PREFIX, VERBOSE_LOG_LEVEL, ) from music_assistant.server.helpers.audio import LOGGER as AUDIO_LOGGER @@ -103,6 +104,7 @@ class QueueStreamJob: 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 @@ -112,8 +114,6 @@ class QueueStreamJob: 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: @@ -233,6 +233,8 @@ class QueueStreamJob: 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) @@ -427,9 +429,14 @@ class StreamsController(CoreController): 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, @@ -475,9 +482,7 @@ class StreamsController(CoreController): 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( diff --git a/music_assistant/server/models/player_provider.py b/music_assistant/server/models/player_provider.py index e7cd5f69..22aad036 100644 --- a/music_assistant/server/models/player_provider.py +++ b/music_assistant/server/models/player_provider.py @@ -133,9 +133,7 @@ class PlayerProvider(Provider): 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 diff --git a/music_assistant/server/providers/airplay/__init__.py b/music_assistant/server/providers/airplay/__init__.py index 0daee42e..3d9a6404 100644 --- a/music_assistant/server/providers/airplay/__init__.py +++ b/music_assistant/server/providers/airplay/__init__.py @@ -40,11 +40,10 @@ from music_assistant.common.models.enums import ( 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 diff --git a/music_assistant/server/providers/slimproto/__init__.py b/music_assistant/server/providers/slimproto/__init__.py index 08caea85..c66d73c8 100644 --- a/music_assistant/server/providers/slimproto/__init__.py +++ b/music_assistant/server/providers/slimproto/__init__.py @@ -53,10 +53,10 @@ from music_assistant.constants import ( 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 diff --git a/music_assistant/server/providers/snapcast/__init__.py b/music_assistant/server/providers/snapcast/__init__.py index ed4475bd..1f7ce0f3 100644 --- a/music_assistant/server/providers/snapcast/__init__.py +++ b/music_assistant/server/providers/snapcast/__init__.py @@ -33,10 +33,10 @@ from music_assistant.common.models.enums import ( 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 diff --git a/music_assistant/server/providers/sonos/__init__.py b/music_assistant/server/providers/sonos/__init__.py index e69482b1..0737d52d 100644 --- a/music_assistant/server/providers/sonos/__init__.py +++ b/music_assistant/server/providers/sonos/__init__.py @@ -402,17 +402,11 @@ class SonosPlayerProvider(PlayerProvider): 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, diff --git a/music_assistant/server/providers/ugp/__init__.py b/music_assistant/server/providers/ugp/__init__.py index 8998adf1..43cf0a88 100644 --- a/music_assistant/server/providers/ugp/__init__.py +++ b/music_assistant/server/providers/ugp/__init__.py @@ -29,7 +29,12 @@ from music_assistant.common.models.enums import ( 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, @@ -44,8 +49,6 @@ if TYPE_CHECKING: from music_assistant.server import MusicAssistant from music_assistant.server.models import ProviderInstanceType -UGP_PREFIX = "ugp_" - # ruff: noqa: ARG002 @@ -174,8 +177,6 @@ class UniversalGroupProvider(PlayerProvider): 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( -- 2.34.1