Feat: Use the player prefered (lossless) format in universal groups (#1949)
authorMaxim Raznatovski <nda.mr43@gmail.com>
Mon, 10 Feb 2025 15:28:04 +0000 (16:28 +0100)
committerGitHub <noreply@github.com>
Mon, 10 Feb 2025 15:28:04 +0000 (16:28 +0100)
* refactor: promote get_output_format to public

* feat: use the player preferred (lossless) audio format in universal groups

* docs: adjust ugp_stream doc comment

music_assistant/controllers/streams.py
music_assistant/providers/player_group/__init__.py
music_assistant/providers/player_group/ugp_stream.py

index 11b966bd3aac5aa9f3b279db4ad1b0148c97462f..dfe6934ee06a1201d2f19708ddfec902cd7b22cd 100644 (file)
@@ -284,7 +284,7 @@ class StreamsController(CoreController):
                 )
                 raise web.HTTPNotFound(reason=f"No streamdetails for Queue item: {queue_item_id}")
         # work out output format/details
-        output_format = await self._get_output_format(
+        output_format = await self.get_output_format(
             output_format_str=request.match_info["fmt"],
             player=queue_player,
             default_sample_rate=queue_item.streamdetails.audio_format.sample_rate,
@@ -385,7 +385,7 @@ class StreamsController(CoreController):
         flow_pcm_format = await self._select_flow_format(queue_player)
 
         # work out output format/details
-        output_format = await self._get_output_format(
+        output_format = await self.get_output_format(
             output_format_str=request.match_info["fmt"],
             player=queue_player,
             default_sample_rate=flow_pcm_format.sample_rate,
@@ -890,7 +890,7 @@ class StreamsController(CoreController):
             request.headers,
         )
 
-    async def _get_output_format(
+    async def get_output_format(
         self,
         output_format_str: str,
         player: Player,
index 3c1ee150f3dbdaf4fa6e90bc0b8e7428a67f4e36..3aa2eeca229317ec604fb4b398705423dde6cb40 100644 (file)
@@ -503,7 +503,7 @@ class PlayerGroupProvider(PlayerProvider):
         self.ugp_streams[player_id] = UGPStream(
             audio_source=audio_source, audio_format=UGP_FORMAT, base_pcm_format=UGP_FORMAT
         )
-        base_url = f"{self.mass.streams.base_url}/ugp/{player_id}.mp3"
+        base_url = f"{self.mass.streams.base_url}/ugp/{player_id}.flac"
 
         # set the state optimistically
         group_player.current_media = media
@@ -632,7 +632,7 @@ class PlayerGroupProvider(PlayerProvider):
         # handle resync/resume if group player was already playing
         if group_player.state == PlayerState.PLAYING and group_type == GROUP_TYPE_UNIVERSAL:
             child_player_provider = self.mass.players.get_player_provider(player_id)
-            base_url = f"{self.mass.streams.base_url}/ugp/{group_player.player_id}.mp3"
+            base_url = f"{self.mass.streams.base_url}/ugp/{group_player.player_id}.flac"
             await child_player_provider.play_media(
                 player_id,
                 media=PlayerMedia(
@@ -739,9 +739,15 @@ class PlayerGroupProvider(PlayerProvider):
             model_name = "Universal Group"
             manufacturer = self.name
             # register dynamic route for the ugp stream
-            route_path = f"/ugp/{group_player_id}.mp3"
             self._on_unload.append(
-                self.mass.streams.register_dynamic_route(route_path, self._serve_ugp_stream)
+                self.mass.streams.register_dynamic_route(
+                    f"/ugp/{group_player_id}.flac", self._serve_ugp_stream
+                )
+            )
+            self._on_unload.append(
+                self.mass.streams.register_dynamic_route(
+                    f"/ugp/{group_player_id}.mp3", self._serve_ugp_stream
+                )
             )
             can_group_with = {
                 # allow grouping with all providers, except the playergroup provider itself
@@ -891,10 +897,20 @@ class PlayerGroupProvider(PlayerProvider):
         """Serve the UGP (multi-client) flow stream audio to a player."""
         ugp_player_id = request.path.rsplit(".")[0].rsplit("/")[-1]
         child_player_id = request.query.get("player_id")  # optional!
-
-        # Right now we default to MP3 output format, since it's the most compatible
-        # TODO: use the player's preferred output format
-        output_format = AudioFormat(content_type=ContentType.MP3)
+        output_format_str = request.path.rsplit(".")[-1]
+
+        if child_player_id and (child_player := self.mass.players.get(child_player_id)):
+            # Use the preferred output format of the child player
+            output_format = await self.mass.streams.get_output_format(
+                output_format_str=output_format_str,
+                player=child_player,
+                default_sample_rate=UGP_FORMAT.sample_rate,
+                default_bit_depth=24,
+            )
+        elif output_format_str == "flac":
+            output_format = AudioFormat(content_type=ContentType.FLAC)
+        else:
+            output_format = AudioFormat(content_type=ContentType.MP3)
 
         if not (ugp_player := self.mass.players.get(ugp_player_id)):
             raise web.HTTPNotFound(reason=f"Unknown UGP player: {ugp_player_id}")
@@ -907,7 +923,7 @@ class PlayerGroupProvider(PlayerProvider):
         )
         headers = {
             **DEFAULT_STREAM_HEADERS,
-            "Content-Type": "audio/mp3",
+            "Content-Type": f"audio/{output_format_str}",
             "Accept-Ranges": "none",
             "Cache-Control": "no-cache",
             "Connection": "close",
index 434905e58b5aceb0e232650b44ac58bbf36e737f..e0c698762ebc061d3161f333ad51367597117d5a 100644 (file)
@@ -1,8 +1,9 @@
 """
 Implementation of a Stream for the Universal Group Player.
 
-Basically this is like a fake radio radio stream (MP3) format with multiple subscribers.
-The MP3 format is chosen because it is widely supported.
+Stream handler for Universal Groups, managing audio distribution to group members.
+Essentially, it multicasts an audio source to multiple client streams, allowing individual
+filter_params for each client.
 """
 
 from __future__ import annotations
@@ -25,8 +26,9 @@ class UGPStream:
     """
     Implementation of a Stream for the Universal Group Player.
 
-    Basically this is like a fake radio radio stream (MP3) format with multiple subscribers.
-    The MP3 format is chosen because it is widely supported.
+    Stream handler for Universal Groups, managing audio distribution to group members.
+    Essentially, it multicasts an audio source to multiple client streams, allowing individual
+    filter_params for each client.
     """
 
     def __init__(