Fix: Use a (consistent) config entry for the output codec
authorMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 19 Feb 2025 18:33:24 +0000 (19:33 +0100)
committerMarcel van der Veldt <m.vanderveldt@outlook.com>
Wed, 19 Feb 2025 18:33:24 +0000 (19:33 +0100)
13 files changed:
music_assistant/constants.py
music_assistant/controllers/player_queues.py
music_assistant/controllers/streams.py
music_assistant/helpers/ffmpeg.py
music_assistant/providers/bluesound/__init__.py
music_assistant/providers/chromecast/__init__.py
music_assistant/providers/dlna/__init__.py
music_assistant/providers/fully_kiosk/__init__.py
music_assistant/providers/hass_players/__init__.py
music_assistant/providers/player_group/__init__.py
music_assistant/providers/slimproto/__init__.py
music_assistant/providers/sonos/provider.py
music_assistant/providers/sonos_s1/__init__.py

index 514a49c9b56cc2fd736ee6031111ab3e7f8f0a3c..9240fe012277636754840bb5e7c8b6a5d637902a 100644 (file)
@@ -62,7 +62,6 @@ CONF_AUTO_PLAY: Final[str] = "auto_play"
 CONF_CROSSFADE: Final[str] = "crossfade"
 CONF_GROUP_MEMBERS: Final[str] = "group_members"
 CONF_HIDE_PLAYER: Final[str] = "hide_player"
-CONF_ENFORCE_MP3: Final[str] = "enforce_mp3"
 CONF_SYNC_ADJUST: Final[str] = "sync_adjust"
 CONF_TTS_PRE_ANNOUNCE: Final[str] = "tts_pre_announce"
 CONF_ANNOUNCE_VOLUME_STRATEGY: Final[str] = "announce_volume_strategy"
@@ -82,6 +81,7 @@ CONF_VOLUME_NORMALIZATION_FIXED_GAIN_TRACKS: Final[str] = "volume_normalization_
 CONF_POWER_CONTROL: Final[str] = "power_control"
 CONF_VOLUME_CONTROL: Final[str] = "volume_control"
 CONF_MUTE_CONTROL: Final[str] = "mute_control"
+CONF_OUTPUT_CODEC: Final[str] = "output_codec"
 
 # config default values
 DEFAULT_HOST: Final[str] = "0.0.0.0"
@@ -294,25 +294,34 @@ CONF_ENTRY_HIDE_PLAYER = ConfigEntry(
     default_value=False,
 )
 
-CONF_ENTRY_ENFORCE_MP3 = ConfigEntry(
-    key=CONF_ENFORCE_MP3,
-    type=ConfigEntryType.BOOLEAN,
-    label="Enforce (lossy) mp3 stream",
-    default_value=False,
-    description="By default, Music Assistant sends lossless, high quality audio "
-    "to all players. Some players can not deal with that and require the stream to be packed "
-    "into a lossy mp3 codec. \n\n "
-    "Only enable when needed. Saves some bandwidth at the cost of audio quality.",
-    category="audio",
+CONF_ENTRY_OUTPUT_CODEC = ConfigEntry(
+    key=CONF_OUTPUT_CODEC,
+    type=ConfigEntryType.STRING,
+    label="Output codec to use for streaming audio to the player",
+    default_value="flac",
+    options=[
+        ConfigValueOption("FLAC (lossless, compressed)", "flac"),
+        ConfigValueOption("MP3 (lossy)", "mp3"),
+        ConfigValueOption("AAC (lossy)", "aac"),
+        ConfigValueOption("WAV (lossless, uncompressed)", "wav"),
+    ],
+    description="Select the codec to use for streaming audio to this player. \n"
+    "By default, Music Assistant sends lossless, high quality audio to all players and prefers "
+    "the FLAC codec because it offers some compression while still remaining lossless \n\n"
+    "Some players however do not support FLAC and require the stream to be packed "
+    "into e.g. a lossy mp3 codec or you like to save some network bandwidth. \n\n "
+    "Choosing a lossy codec saves some bandwidth at the cost of audio quality.",
+    category="advanced",
 )
 
-CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED = ConfigEntry.from_dict(
-    {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True}
+CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3 = ConfigEntry.from_dict(
+    {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3"}
 )
-CONF_ENTRY_ENFORCE_MP3_HIDDEN = ConfigEntry.from_dict(
-    {**CONF_ENTRY_ENFORCE_MP3.to_dict(), "default_value": True, "hidden": True}
+CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3 = ConfigEntry.from_dict(
+    {**CONF_ENTRY_OUTPUT_CODEC.to_dict(), "default_value": "mp3", "hidden": True}
 )
 
+
 CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
     key=CONF_SYNC_ADJUST,
     type=ConfigEntryType.INTEGER,
@@ -548,6 +557,7 @@ BASE_PLAYER_CONFIG_ENTRIES = (
     CONF_ENTRY_TTS_PRE_ANNOUNCE,
     CONF_ENTRY_SAMPLE_RATES,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
+    CONF_ENTRY_OUTPUT_CODEC,
 )
 
 
index f281411ed6e429eb36f7382d5e2659242b2eb4bf..a7acf55f50df3c5e637df2aaf95fc0ca797db4e3 100644 (file)
@@ -799,7 +799,7 @@ class PlayerQueuesController(CoreController):
         async def play_media():
             await self.mass.players.play_media(
                 player_id=queue_id,
-                media=self.player_media_from_queue_item(queue_item, queue.flow_mode),
+                media=await self.player_media_from_queue_item(queue_item, queue.flow_mode),
             )
             await asyncio.sleep(2)
             setattr(queue, "transitioning", False)  # noqa: B010
@@ -1176,10 +1176,12 @@ class PlayerQueuesController(CoreController):
                 return index
         return None
 
-    def player_media_from_queue_item(self, queue_item: QueueItem, flow_mode: bool) -> PlayerMedia:
+    async def player_media_from_queue_item(
+        self, queue_item: QueueItem, flow_mode: bool
+    ) -> PlayerMedia:
         """Parse PlayerMedia from QueueItem."""
         media = PlayerMedia(
-            uri=self.mass.streams.resolve_stream_url(queue_item, flow_mode=flow_mode),
+            uri=await self.mass.streams.resolve_stream_url(queue_item, flow_mode=flow_mode),
             media_type=MediaType.FLOW_STREAM if flow_mode else queue_item.media_type,
             title="Music Assistant" if flow_mode else queue_item.name,
             image_url=MASS_LOGO_ONLINE,
@@ -1425,7 +1427,7 @@ class PlayerQueuesController(CoreController):
             return
         await self.mass.players.enqueue_next_media(
             player_id=queue_id,
-            media=self.player_media_from_queue_item(next_item, False),
+            media=await self.player_media_from_queue_item(next_item, False),
         )
         self.logger.debug(
             "Enqueued next track %s on queue %s",
index 1a039a319bbfa1c7e30390514857988d377ff60b..909fa7ec7da75b29910c2b34c3440dd1dd0a536f 100644 (file)
@@ -9,7 +9,6 @@ the upnp callbacks and json rpc api for slimproto clients.
 from __future__ import annotations
 
 import os
-import time
 import urllib.parse
 from collections.abc import AsyncGenerator
 from typing import TYPE_CHECKING
@@ -37,6 +36,7 @@ from music_assistant.constants import (
     CONF_ENTRY_ENABLE_ICY_METADATA,
     CONF_HTTP_PROFILE,
     CONF_OUTPUT_CHANNELS,
+    CONF_OUTPUT_CODEC,
     CONF_PUBLISH_IP,
     CONF_SAMPLE_RATES,
     CONF_VOLUME_NORMALIZATION_FIXED_GAIN_RADIO,
@@ -245,26 +245,24 @@ class StreamsController(CoreController):
         """Cleanup on exit."""
         await self._server.close()
 
-    def resolve_stream_url(
+    async def resolve_stream_url(
         self,
         queue_item: QueueItem,
         flow_mode: bool = False,
-        output_codec: ContentType = ContentType.FLAC,
+        player_id: str | None = None,
     ) -> str:
         """Resolve the stream URL for the given QueueItem."""
+        if not player_id:
+            player_id = queue_item.queue_id
+        output_codec = ContentType.try_parse(
+            await self.mass.config.get_player_config_value(player_id, CONF_OUTPUT_CODEC)
+        )
         fmt = output_codec.value
         # handle raw pcm without exact format specifiers
         if output_codec.is_pcm() and ";" not in fmt:
             fmt += f";codec=pcm;rate={44100};bitrate={16};channels={2}"
-        query_params = {}
         base_path = "flow" if flow_mode else "single"
-        url = f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}"  # noqa: E501
-        # we add a timestamp as basic checksum
-        # most importantly this is to invalidate any caches
-        # but also to handle edge cases such as single track repeat
-        query_params["ts"] = str(int(time.time()))
-        url += "?" + urllib.parse.urlencode(query_params)
-        return url
+        return f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}"  # noqa: E501
 
     async def serve_queue_item_stream(self, request: web.Request) -> web.Response:
         """Stream single queueitem audio to a player."""
@@ -1015,33 +1013,25 @@ class StreamsController(CoreController):
         supported_bit_depths: tuple[int] = tuple(int(x[1]) for x in supported_rates_conf)
 
         player_max_bit_depth = max(supported_bit_depths)
-        if content_type.is_pcm() or content_type == ContentType.WAV:
-            # parse pcm details from format string
-            output_sample_rate, output_bit_depth, output_channels = parse_pcm_info(
-                output_format_str
-            )
-            if content_type == ContentType.PCM:
-                # resolve generic pcm type
-                content_type = ContentType.from_bit_depth(output_bit_depth)
+        if default_sample_rate in supported_sample_rates:
+            output_sample_rate = default_sample_rate
         else:
-            if default_sample_rate in supported_sample_rates:
-                output_sample_rate = default_sample_rate
-            else:
-                output_sample_rate = max(supported_sample_rates)
-            output_bit_depth = min(default_bit_depth, player_max_bit_depth)
-            output_channels_str = self.mass.config.get_raw_player_config_value(
-                player.player_id, CONF_OUTPUT_CHANNELS, "stereo"
-            )
-            output_channels = 1 if output_channels_str != "stereo" else 2
+            output_sample_rate = max(supported_sample_rates)
+        output_bit_depth = min(default_bit_depth, player_max_bit_depth)
+        output_channels_str = self.mass.config.get_raw_player_config_value(
+            player.player_id, CONF_OUTPUT_CHANNELS, "stereo"
+        )
+        output_channels = 1 if output_channels_str != "stereo" else 2
         if not content_type.is_lossless():
             output_bit_depth = 16
             output_sample_rate = min(48000, output_sample_rate)
+        if output_format_str == "pcm":
+            content_type = ContentType.from_bit_depth(output_bit_depth)
         return AudioFormat(
             content_type=content_type,
             sample_rate=output_sample_rate,
             bit_depth=output_bit_depth,
             channels=output_channels,
-            output_format_str=output_format_str,
         )
 
     async def _select_flow_format(
index 525d159b2a936688d354ea295572d12877bf7ea8..bc02ce3efd4172a1985e5e69fc9defbbd71ec58e 100644 (file)
@@ -40,7 +40,7 @@ class FFMpeg(AsyncProcess):
         extra_input_args: list[str] | None = None,
         audio_output: str | int = "-",
         collect_log_history: bool = False,
-        loglevel: str = "error",
+        loglevel: str = "info",
     ) -> None:
         """Initialize AsyncProcess."""
         ffmpeg_args = get_ffmpeg_args(
@@ -82,7 +82,7 @@ class FFMpeg(AsyncProcess):
             else:
                 clean_args.append(arg)
         args_str = " ".join(clean_args)
-        self.logger.log(VERBOSE_LOG_LEVEL, "started with args: %s", args_str)
+        self.logger.debug("started with args: %s", args_str)
         self._logger_task = asyncio.create_task(self._log_reader_task())
         if isinstance(self.audio_input, AsyncGenerator):
             self._stdin_task = asyncio.create_task(self._feed_stdin())
@@ -141,7 +141,7 @@ class FFMpeg(AsyncProcess):
         cancelled = False
         try:
             start = time.time()
-            self.logger.log(VERBOSE_LOG_LEVEL, "Start reading audio data from source...")
+            self.logger.debug("Start reading audio data from source...")
             # use TimedAsyncGenerator to catch we're stuck waiting on data forever
             # don't set this timeout too low because in some cases it can indeed take a while
             # for data to arrive (e.g. when there is X amount of seconds in the buffer)
@@ -155,9 +155,7 @@ class FFMpeg(AsyncProcess):
                 if self.closed:
                     return
                 await self.write(chunk)
-            self.logger.log(
-                VERBOSE_LOG_LEVEL, "Audio data source exhausted in %.2fs", time.time() - start
-            )
+            self.logger.debug("Audio data source exhausted in %.2fs", time.time() - start)
             generator_exhausted = True
         except Exception as err:
             cancelled = isinstance(err, asyncio.CancelledError)
@@ -273,37 +271,49 @@ def get_ffmpeg_args(
         input_args += ["-i", input_path]
 
     # collect output args
-    output_args = []
+    output_args = [
+        "-ac",
+        str(output_format.channels),
+        "-channel_layout",
+        "mono" if output_format.channels == 1 else "stereo",
+    ]
     if output_path.upper() == "NULL":
         # devnull stream
-        output_args = ["-f", "null", "-"]
-    elif output_format.content_type == ContentType.UNKNOWN:
-        raise RuntimeError("Invalid output format specified")
+        output_path = "-"
+        output_args = ["-f", "null"]
     elif output_format.content_type == ContentType.AAC:
-        output_args = ["-f", "adts", "-c:a", "aac", "-b:a", "256k", output_path]
+        output_args = ["-f", "adts", "-c:a", "aac", "-b:a", "256k"]
     elif output_format.content_type == ContentType.MP3:
-        output_args = ["-f", "mp3", "-b:a", "320k", output_path]
-    else:
-        if output_format.content_type.is_pcm():
-            output_args += ["-acodec", output_format.content_type.name.lower()]
-        # use explicit format identifier for all other
-        output_args += [
+        output_args = ["-f", "mp3", "-b:a", "320k"]
+    elif output_format.content_type == ContentType.WAV:
+        pcm_format = ContentType.from_bit_depth(output_format.bit_depth)
+        output_args = [
+            # "-ar",
+            # str(output_format.sample_rate),
+            "-acodec",
+            pcm_format.name.lower(),
             "-f",
-            output_format.content_type.value,
+            "wav",
+        ]
+    elif output_format.content_type == ContentType.FLAC:
+        # use level 0 compression for fastest encoding
+        sample_fmt = "s32" if output_format.bit_depth > 16 else "s16"
+        output_args += ["-sample_fmt", sample_fmt, "-f", "flac", "-compression_level", "0"]
+    elif output_format.content_type.is_pcm():
+        # use explicit format identifier for pcm formats
+        output_args += [
             "-ar",
             str(output_format.sample_rate),
-            "-ac",
-            str(output_format.channels),
+            "-acodec",
+            output_format.content_type.name.lower(),
+            "-f",
+            output_format.content_type.value,
         ]
-        if not output_format.content_type.is_pcm() and output_format.content_type.is_lossless():
-            if output_format.bit_depth == 24:
-                output_args += ["-sample_fmt", "s32"]
-            elif output_format.bit_depth == 16:
-                output_args += ["-sample_fmt", "s16"]
-        if output_format.output_format_str == "flac":
-            # use level 0 compression for fastest encoding
-            output_args += ["-compression_level", "0"]
-        output_args += [output_path]
+    else:
+        raise RuntimeError("Invalid/unsupported output format specified")
+
+    # append (final) output path at the end of the args
+    output_args.append(output_path)
 
     # edge case: source file is not stereo - downmix to stereo
     if input_format.channels > 2 and output_format.channels == 2:
index 08f03eb19b4092e0ccfb8baba3dcda83cc9c109d..534f728bc4ea498958806c7a10912c4523cb54a1 100644 (file)
@@ -16,9 +16,9 @@ from zeroconf import ServiceStateChange
 from music_assistant.constants import (
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_FLOW_MODE_ENFORCED,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
+    CONF_ENTRY_OUTPUT_CODEC,
     VERBOSE_LOG_LEVEL,
 )
 from music_assistant.helpers.util import (
@@ -315,7 +315,7 @@ class BluesoundPlayerProvider(PlayerProvider):
             *base_entries,
             CONF_ENTRY_HTTP_PROFILE_FORCED_2,
             CONF_ENTRY_CROSSFADE,
-            CONF_ENTRY_ENFORCE_MP3,
+            CONF_ENTRY_OUTPUT_CODEC,
             CONF_ENTRY_FLOW_MODE_ENFORCED,
             CONF_ENTRY_ENABLE_ICY_METADATA,
         )
index 1836896acfa2a96df68ebd2934751ec78c27958a..32f12f79b00f441f7be27ef0948a17c5554ce1e6 100644 (file)
@@ -30,10 +30,9 @@ from pychromecast.socket_client import CONNECTION_STATUS_CONNECTED, CONNECTION_S
 
 from music_assistant.constants import (
     BASE_PLAYER_CONFIG_ENTRIES,
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE_DURATION,
     CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
-    CONF_ENTRY_ENFORCE_MP3,
+    CONF_ENTRY_OUTPUT_CODEC,
     CONF_MUTE_CONTROL,
     CONF_PLAYERS,
     CONF_POWER_CONTROL,
@@ -61,7 +60,7 @@ if TYPE_CHECKING:
 PLAYER_CONFIG_ENTRIES = (
     CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
     CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_ENFORCE_MP3,
+    CONF_ENTRY_OUTPUT_CODEC,
 )
 
 # originally/officially cast supports 96k sample rate (even for groups)
@@ -283,8 +282,6 @@ class ChromecastProvider(PlayerProvider):
     ) -> None:
         """Handle PLAY MEDIA on given player."""
         castplayer = self.castplayers[player_id]
-        if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False):
-            media.uri = media.uri.replace(".flac", ".mp3")
         queuedata = {
             "type": "LOAD",
             "media": self._create_cc_media_item(media),
@@ -298,8 +295,6 @@ class ChromecastProvider(PlayerProvider):
     async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle enqueuing of the next item on the player."""
         castplayer = self.castplayers[player_id]
-        if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False):
-            media.uri = media.uri.replace(".flac", ".mp3")
         next_item_id = None
         status = castplayer.cc.media_controller.status
         # lookup position of current track in cast queue
index 5497b4e02c5476c72114b4330f9d0a11258a05ec..b2c9dae076e27b82f1926b0b4a31873a176745f1 100644 (file)
@@ -28,13 +28,12 @@ from music_assistant_models.errors import PlayerUnavailableError
 from music_assistant_models.player import DeviceInfo, Player, PlayerMedia
 
 from music_assistant.constants import (
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE_DURATION,
     CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
     CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_FLOW_MODE_DEFAULT_ENABLED,
     CONF_ENTRY_HTTP_PROFILE,
+    CONF_ENTRY_OUTPUT_CODEC,
     CONF_PLAYERS,
     VERBOSE_LOG_LEVEL,
     create_sample_rates_config_entry,
@@ -60,7 +59,7 @@ if TYPE_CHECKING:
 PLAYER_CONFIG_ENTRIES = (
     CONF_ENTRY_CROSSFADE_FLOW_MODE_REQUIRED,
     CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_ENFORCE_MP3,
+    CONF_ENTRY_OUTPUT_CODEC,
     CONF_ENTRY_HTTP_PROFILE,
     CONF_ENTRY_ENABLE_ICY_METADATA,
     # enable flow mode by default because
@@ -294,8 +293,6 @@ class DLNAPlayerProvider(PlayerProvider):
     @catch_request_errors
     async def play_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle PLAY MEDIA on given player."""
-        if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False):
-            media.uri = media.uri.replace(".flac", ".mp3")
         dlna_player = self.dlnaplayers[player_id]
         # always clear queue (by sending stop) first
         if dlna_player.device.can_stop:
@@ -321,8 +318,6 @@ class DLNAPlayerProvider(PlayerProvider):
     async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle enqueuing of the next queue item on the player."""
         dlna_player = self.dlnaplayers[player_id]
-        if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, False):
-            media.uri = media.uri.replace(".flac", ".mp3")
         didl_metadata = create_didl_metadata(media)
         title = media.title or media.uri
         try:
index b096db65cec4a513d3f654538d9dfd800fdb9505..33e09cdd31ac367bb76d8bd5d196d4a7af83ffde 100644 (file)
@@ -14,11 +14,10 @@ from music_assistant_models.errors import PlayerUnavailableError, SetupFailedErr
 from music_assistant_models.player import DeviceInfo, Player, PlayerMedia
 
 from music_assistant.constants import (
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
     CONF_ENTRY_FLOW_MODE_ENFORCED,
+    CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
     CONF_IP_ADDRESS,
     CONF_PASSWORD,
     CONF_PORT,
@@ -160,7 +159,7 @@ class FullyKioskProvider(PlayerProvider):
             CONF_ENTRY_FLOW_MODE_ENFORCED,
             CONF_ENTRY_CROSSFADE,
             CONF_ENTRY_CROSSFADE_DURATION,
-            CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
+            CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
         )
 
     async def cmd_volume_set(self, player_id: str, volume_level: int) -> None:
@@ -190,8 +189,6 @@ class FullyKioskProvider(PlayerProvider):
         """Handle PLAY MEDIA on given player."""
         if not (player := self.mass.players.get(player_id)):
             return
-        if self.mass.config.get_raw_player_config_value(player_id, CONF_ENFORCE_MP3, True):
-            media.uri = media.uri.replace(".flac", ".mp3")
         await self._fully.playSound(media.uri, AUDIOMANAGER_STREAM_MUSIC)
         player.current_media = media
         player.elapsed_time = 0
index 87465827332db5a15b8a0e6c07911d0c8ce1e0c7..3d6b5fd729bbf1996efcdfa7eb5d3218801ae54b 100644 (file)
@@ -20,15 +20,14 @@ from music_assistant_models.errors import InvalidDataError, LoginFailed, SetupFa
 from music_assistant_models.player import DeviceInfo, Player, PlayerMedia
 
 from music_assistant.constants import (
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
     CONF_ENTRY_ENABLE_ICY_METADATA,
-    CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
-    CONF_ENTRY_ENFORCE_MP3_HIDDEN,
     CONF_ENTRY_FLOW_MODE_ENFORCED,
     CONF_ENTRY_HTTP_PROFILE,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
+    CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
+    CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3,
     HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES,
     create_sample_rates_config_entry,
 )
@@ -63,7 +62,7 @@ CONF_PLAYERS = "players"
 DEFAULT_PLAYER_CONFIG_ENTRIES = (
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
-    CONF_ENTRY_ENFORCE_MP3_DEFAULT_ENABLED,
+    CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
     CONF_ENTRY_HTTP_PROFILE,
     CONF_ENTRY_ENABLE_ICY_METADATA,
     CONF_ENTRY_FLOW_MODE_ENFORCED,
@@ -194,7 +193,7 @@ class HomeAssistantPlayers(PlayerProvider):
                 if bit_depth not in supported_bit_depths:
                     supported_bit_depths.append(bit_depth)
             if not supports_flac:
-                base_entries = (*base_entries, CONF_ENTRY_ENFORCE_MP3_HIDDEN)
+                base_entries = (*base_entries, CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3)
             return (
                 *base_entries,
                 # New ESPHome mediaplayer (used in Voice PE) uses FLAC 48khz/16 bits
@@ -255,11 +254,6 @@ class HomeAssistantPlayers(PlayerProvider):
 
     async def play_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle PLAY MEDIA on given player."""
-        if self.mass.config.get_player_config_value(
-            player_id,
-            CONF_ENFORCE_MP3,
-        ):
-            media.uri = media.uri.replace(".flac", ".mp3")
         player = self.mass.players.get(player_id, True)
         assert player
         extra_data = {
index b826a2681b9b5c8d365b787c1b15274455e4e361..48f56cd76ca3e6a43acd424f924939cc7cb68346 100644 (file)
@@ -46,7 +46,6 @@ from music_assistant.constants import (
     CONF_CROSSFADE,
     CONF_CROSSFADE_DURATION,
     CONF_ENABLE_ICY_METADATA,
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
     CONF_ENTRY_FLOW_MODE_ENFORCED,
@@ -55,6 +54,7 @@ from music_assistant.constants import (
     CONF_GROUP_MEMBERS,
     CONF_HTTP_PROFILE,
     CONF_MUTE_CONTROL,
+    CONF_OUTPUT_CODEC,
     CONF_POWER_CONTROL,
     CONF_SAMPLE_RATES,
     CONF_VOLUME_CONTROL,
@@ -301,7 +301,7 @@ class PlayerGroupProvider(PlayerProvider):
             CONF_ENABLE_ICY_METADATA,
             CONF_CROSSFADE,
             CONF_CROSSFADE_DURATION,
-            CONF_ENFORCE_MP3,
+            CONF_OUTPUT_CODEC,
             CONF_FLOW_MODE,
             CONF_SAMPLE_RATES,
         )
index 02ff024b14f1769053de071ec1fb6be95c687e3b..8314d0059e407cc58265fe94f689a2f26b796235 100644 (file)
@@ -42,15 +42,14 @@ from music_assistant_models.player import DeviceInfo, Player, PlayerMedia
 from music_assistant.constants import (
     CONF_CROSSFADE,
     CONF_CROSSFADE_DURATION,
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE,
     CONF_ENTRY_CROSSFADE_DURATION,
     CONF_ENTRY_DEPRECATED_EQ_BASS,
     CONF_ENTRY_DEPRECATED_EQ_MID,
     CONF_ENTRY_DEPRECATED_EQ_TREBLE,
-    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_HTTP_PROFILE_FORCED_2,
     CONF_ENTRY_OUTPUT_CHANNELS,
+    CONF_ENTRY_OUTPUT_CODEC,
     CONF_ENTRY_SYNC_ADJUST,
     CONF_PORT,
     CONF_SYNC_ADJUST,
@@ -311,7 +310,7 @@ class SlimprotoProvider(PlayerProvider):
                 CONF_ENTRY_DEPRECATED_EQ_TREBLE,
                 CONF_ENTRY_OUTPUT_CHANNELS,
                 CONF_ENTRY_CROSSFADE_DURATION,
-                CONF_ENTRY_ENFORCE_MP3,
+                CONF_ENTRY_OUTPUT_CODEC,
                 CONF_ENTRY_SYNC_ADJUST,
                 CONF_ENTRY_DISPLAY,
                 CONF_ENTRY_VISUALIZATION,
@@ -425,10 +424,6 @@ class SlimprotoProvider(PlayerProvider):
         async with TaskManager(self.mass) as tg:
             for slimplayer in self._get_sync_clients(player_id):
                 url = f"{base_url}&child_player_id={slimplayer.player_id}"
-                if self.mass.config.get_raw_player_config_value(
-                    slimplayer.player_id, CONF_ENFORCE_MP3, False
-                ):
-                    url = url.replace("flac", "mp3")
                 stream.expected_clients += 1
                 tg.create_task(
                     self._handle_play_url(
@@ -444,15 +439,9 @@ class SlimprotoProvider(PlayerProvider):
         """Handle enqueuing of the next queue item on the player."""
         if not (slimplayer := self.slimproto.get_player(player_id)):
             return
-        url = media.uri
-        if self.mass.config.get_raw_player_config_value(
-            slimplayer.player_id, CONF_ENFORCE_MP3, False
-        ):
-            url = url.replace("flac", "mp3")
-
         await self._handle_play_url(
             slimplayer,
-            url=url,
+            url=media.uri,
             media=media,
             enqueue=True,
             send_flush=False,
index f18bd47a6c8247972a163e4358711461fcc577a7..7937f534a4f02ec05c1994743fd080cbb2d844e8 100644 (file)
@@ -17,15 +17,15 @@ from aiohttp.client_exceptions import ClientError
 from aiosonos.api.models import SonosCapability
 from aiosonos.utils import get_discovery_info
 from music_assistant_models.config_entries import ConfigEntry, PlayerConfig
-from music_assistant_models.enums import ConfigEntryType, ContentType, PlayerState, ProviderFeature
+from music_assistant_models.enums import ConfigEntryType, PlayerState, ProviderFeature
 from music_assistant_models.errors import PlayerCommandFailed
 from music_assistant_models.player import DeviceInfo, PlayerMedia
 from zeroconf import ServiceStateChange
 
 from music_assistant.constants import (
     CONF_ENTRY_CROSSFADE,
-    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
+    CONF_ENTRY_OUTPUT_CODEC,
     MASS_LOGO_ONLINE,
     VERBOSE_LOG_LEVEL,
     create_sample_rates_config_entry,
@@ -156,7 +156,7 @@ class SonosPlayerProvider(PlayerProvider):
             *await super().get_player_config_entries(player_id),
             CONF_ENTRY_CROSSFADE,
             CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
-            CONF_ENTRY_ENFORCE_MP3,
+            CONF_ENTRY_OUTPUT_CODEC,
             create_sample_rates_config_entry(
                 max_sample_rate=48000, max_bit_depth=24, safe_max_bit_depth=24, hidden=True
             ),
@@ -450,12 +450,7 @@ class SonosPlayerProvider(PlayerProvider):
             limit=upcoming_window_size + previous_window_size,
             offset=max(queue_index - previous_window_size, 0),
         )
-        enforce_mp3 = self.mass.config.get_raw_player_config_value(
-            sonos_player_id, CONF_ENTRY_ENFORCE_MP3.key, CONF_ENTRY_ENFORCE_MP3.default_value
-        )
-        sonos_queue_items = [
-            self._parse_sonos_queue_item(item, enforce_mp3) for item in queue_items
-        ]
+        sonos_queue_items = [await self._parse_sonos_queue_item(item) for item in queue_items]
         result = {
             "includesBeginningOfQueue": offset == 0,
             "includesEndOfQueue": mass_queue.items <= (queue_index + len(sonos_queue_items)),
@@ -554,7 +549,7 @@ class SonosPlayerProvider(PlayerProvider):
             break
         return web.Response(status=204)
 
-    def _parse_sonos_queue_item(self, queue_item: QueueItem, enforce_mp3: bool) -> dict[str, Any]:
+    async def _parse_sonos_queue_item(self, queue_item: QueueItem) -> dict[str, Any]:
         """Parse a Sonos queue item to a PlayerMedia object."""
         available = queue_item.media_item.available if queue_item.media_item else True
         return {
@@ -563,9 +558,7 @@ class SonosPlayerProvider(PlayerProvider):
             "policies": {},
             "track": {
                 "type": "track",
-                "mediaUrl": self.mass.streams.resolve_stream_url(
-                    queue_item, output_codec=ContentType.MP3 if enforce_mp3 else ContentType.FLAC
-                ),
+                "mediaUrl": await self.mass.streams.resolve_stream_url(queue_item),
                 "contentType": "audio/flac",
                 "service": {
                     "name": "Music Assistant",
index 1746f9bc1073c85413e4dd1747606ceb1b66d802..c06056e3724a7a0d7e9a893a59927c4b97ffb7c4 100644 (file)
@@ -32,11 +32,10 @@ from soco.discovery import discover, scan_network
 
 from music_assistant.constants import (
     CONF_CROSSFADE,
-    CONF_ENFORCE_MP3,
     CONF_ENTRY_CROSSFADE,
-    CONF_ENTRY_ENFORCE_MP3,
     CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
     CONF_ENTRY_HTTP_PROFILE_FORCED_1,
+    CONF_ENTRY_OUTPUT_CODEC,
     VERBOSE_LOG_LEVEL,
     create_sample_rates_config_entry,
 )
@@ -184,14 +183,14 @@ class SonosPlayerProvider(PlayerProvider):
             return (
                 *base_entries,
                 CONF_ENTRY_CROSSFADE,
-                CONF_ENTRY_ENFORCE_MP3,
+                CONF_ENTRY_OUTPUT_CODEC,
                 CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
             )
         return (
             *base_entries,
             CONF_ENTRY_CROSSFADE,
             CONF_ENTRY_SAMPLE_RATES,
-            CONF_ENTRY_ENFORCE_MP3,
+            CONF_ENTRY_OUTPUT_CODEC,
             CONF_ENTRY_FLOW_MODE_HIDDEN_DISABLED,
             CONF_ENTRY_HTTP_PROFILE_FORCED_1,
         )
@@ -300,8 +299,6 @@ class SonosPlayerProvider(PlayerProvider):
                 "accept play_media command, it is synced to another player."
             )
             raise PlayerCommandFailed(msg)
-        if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3):
-            media.uri = media.uri.replace(".flac", ".mp3")
         didl_metadata = create_didl_metadata(media)
         await asyncio.to_thread(sonos_player.soco.play_uri, media.uri, meta=didl_metadata)
         self.mass.call_later(2, sonos_player.poll_speaker)
@@ -310,8 +307,6 @@ class SonosPlayerProvider(PlayerProvider):
     async def enqueue_next_media(self, player_id: str, media: PlayerMedia) -> None:
         """Handle enqueuing of the next queue item on the player."""
         sonos_player = self.sonosplayers[player_id]
-        if await self.mass.config.get_player_config_value(player_id, CONF_ENFORCE_MP3):
-            media.uri = media.uri.replace(".flac", ".mp3")
         didl_metadata = create_didl_metadata(media)
         # set crossfade according to player setting
         crossfade = bool(await self.mass.config.get_player_config_value(player_id, CONF_CROSSFADE))