Some fixes for Announcement feature (#1159)
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Thu, 21 Mar 2024 13:17:57 +0000 (14:17 +0100)
committerGitHub <noreply@github.com>
Thu, 21 Mar 2024 13:17:57 +0000 (14:17 +0100)
music_assistant/constants.py
music_assistant/server/controllers/players.py
music_assistant/server/controllers/streams.py
music_assistant/server/models/player_provider.py
music_assistant/server/providers/airplay/__init__.py
music_assistant/server/providers/slimproto/__init__.py
music_assistant/server/providers/snapcast/__init__.py
music_assistant/server/providers/sonos/__init__.py
music_assistant/server/providers/ugp/__init__.py

index 44b9f328d50cf73bc9206a0c406fdd52ae05e15b..04ac16a9f654190f8d36c8de075299c25b2623e0 100644 (file)
@@ -91,3 +91,4 @@ CONFIGURABLE_CORE_CONTROLLERS = (
 )
 SYNCGROUP_PREFIX: Final[str] = "syncgroup_"
 VERBOSE_LOG_LEVEL: Final[int] = 5
+UGP_PREFIX: Final[str] = "ugp_"
index f49f0dca0158e04b5588cadf95eececed50bebce..b8b3d4763fd5d742c9064d0f74f06122a388d09b 100644 (file)
@@ -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)...",
index 1b9e2fd9acf2021dc4e0e7f3c557c2b06b5742ad..cde50047c7a59e5be0ea551b39c1d09b7fb66c75 100644 (file)
@@ -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(
index e7cd5f69a87ff730a6fce73b3054cc36fb7aa448..22aad036eb94675e06b97b2bcb5aca88aa4be993 100644 (file)
@@ -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
index 0daee42e18d456bc1d0deda7d554e2748dc1fed9..3d9a640404c9d595cc307e48c54ba92b388a121f 100644 (file)
@@ -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
index 08caea85deed43f3e5ef02106118e47f470384ea..c66d73c87cfee7927c8a712d3853c1decea54343 100644 (file)
@@ -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
index ed4475bd51d265b0e4fdf0f8d07fd972b9716844..1f7ce0f39b02fd818644537ff8009f069653fe44 100644 (file)
@@ -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
index e69482b1f8b409d8b78044c4df1670e63ef3cf80..0737d52d4aa92813cc6187676e2e9a9471bb8d1a 100644 (file)
@@ -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,
index 8998adf1cb7e3f0288b5723e31ceaf23db2dd113..43cf0a88b74895fc0c50a6194a3e9bb0e958410e 100644 (file)
@@ -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(