From: Marcel van der Veldt Date: Wed, 19 Feb 2025 18:33:24 +0000 (+0100) Subject: Fix: Use a (consistent) config entry for the output codec X-Git-Url: https://git.kitaultman.com/?a=commitdiff_plain;h=3f43578786bef93eae3065114bff865c54a67415;p=music-assistant-server.git Fix: Use a (consistent) config entry for the output codec --- diff --git a/music_assistant/constants.py b/music_assistant/constants.py index 514a49c9..9240fe01 100644 --- a/music_assistant/constants.py +++ b/music_assistant/constants.py @@ -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, ) diff --git a/music_assistant/controllers/player_queues.py b/music_assistant/controllers/player_queues.py index f281411e..a7acf55f 100644 --- a/music_assistant/controllers/player_queues.py +++ b/music_assistant/controllers/player_queues.py @@ -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", diff --git a/music_assistant/controllers/streams.py b/music_assistant/controllers/streams.py index 1a039a31..909fa7ec 100644 --- a/music_assistant/controllers/streams.py +++ b/music_assistant/controllers/streams.py @@ -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( diff --git a/music_assistant/helpers/ffmpeg.py b/music_assistant/helpers/ffmpeg.py index 525d159b..bc02ce3e 100644 --- a/music_assistant/helpers/ffmpeg.py +++ b/music_assistant/helpers/ffmpeg.py @@ -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: diff --git a/music_assistant/providers/bluesound/__init__.py b/music_assistant/providers/bluesound/__init__.py index 08f03eb1..534f728b 100644 --- a/music_assistant/providers/bluesound/__init__.py +++ b/music_assistant/providers/bluesound/__init__.py @@ -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, ) diff --git a/music_assistant/providers/chromecast/__init__.py b/music_assistant/providers/chromecast/__init__.py index 1836896a..32f12f79 100644 --- a/music_assistant/providers/chromecast/__init__.py +++ b/music_assistant/providers/chromecast/__init__.py @@ -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 diff --git a/music_assistant/providers/dlna/__init__.py b/music_assistant/providers/dlna/__init__.py index 5497b4e0..b2c9dae0 100644 --- a/music_assistant/providers/dlna/__init__.py +++ b/music_assistant/providers/dlna/__init__.py @@ -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: diff --git a/music_assistant/providers/fully_kiosk/__init__.py b/music_assistant/providers/fully_kiosk/__init__.py index b096db65..33e09cdd 100644 --- a/music_assistant/providers/fully_kiosk/__init__.py +++ b/music_assistant/providers/fully_kiosk/__init__.py @@ -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 diff --git a/music_assistant/providers/hass_players/__init__.py b/music_assistant/providers/hass_players/__init__.py index 87465827..3d6b5fd7 100644 --- a/music_assistant/providers/hass_players/__init__.py +++ b/music_assistant/providers/hass_players/__init__.py @@ -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 = { diff --git a/music_assistant/providers/player_group/__init__.py b/music_assistant/providers/player_group/__init__.py index b826a268..48f56cd7 100644 --- a/music_assistant/providers/player_group/__init__.py +++ b/music_assistant/providers/player_group/__init__.py @@ -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, ) diff --git a/music_assistant/providers/slimproto/__init__.py b/music_assistant/providers/slimproto/__init__.py index 02ff024b..8314d005 100644 --- a/music_assistant/providers/slimproto/__init__.py +++ b/music_assistant/providers/slimproto/__init__.py @@ -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, diff --git a/music_assistant/providers/sonos/provider.py b/music_assistant/providers/sonos/provider.py index f18bd47a..7937f534 100644 --- a/music_assistant/providers/sonos/provider.py +++ b/music_assistant/providers/sonos/provider.py @@ -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", diff --git a/music_assistant/providers/sonos_s1/__init__.py b/music_assistant/providers/sonos_s1/__init__.py index 1746f9bc..c06056e3 100644 --- a/music_assistant/providers/sonos_s1/__init__.py +++ b/music_assistant/providers/sonos_s1/__init__.py @@ -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))