)
+def create_output_codec_config_entry(
+ hidden: bool = False, default_value: str = "flac"
+) -> ConfigEntry:
+ """Create output codec config entry based on player specific helpers."""
+ conf_entry = ConfigEntry.from_dict(CONF_ENTRY_OUTPUT_CODEC.to_dict())
+ conf_entry.hidden = hidden
+ conf_entry.default_value = default_value
+ return conf_entry
+
+
CONF_ENTRY_SYNC_ADJUST = ConfigEntry(
key=CONF_SYNC_ADJUST,
type=ConfigEntryType.INTEGER,
options: list[ConfigValueOption] = []
default_value: list[str] = []
+ if not supported_sample_rates and max_sample_rate is None:
+ supported_sample_rates = [44100]
+ if not supported_bit_depths and max_bit_depth is None:
+ supported_bit_depths = [16]
+
for option in CONF_ENTRY_SAMPLE_RATES.options:
option_value = cast(str, option.value)
sample_rate_str, bit_depth_str = option_value.split(MULTI_VALUE_SPLITTER, 1)
# actually store changes (if the above did not raise)
conf_key = f"{CONF_PLAYERS}/{player_id}"
self.set(conf_key, config.to_raw())
+ # always update player attributes to calculate e.g. player controls etc.
+ self.mass.players.update(config.player_id, force_update=True)
# send config updated event
self.mass.signal_event(
EventType.PLAYER_CONFIG_UPDATED,
object_id=config.player_id,
data=config,
)
- self.mass.players.update(config.player_id, force_update=True)
+
# return full player config (just in case)
return await self.get_player_config(player_id)
"""Handle playback/select of given plugin source on player."""
plugin_source = plugin_prov.get_source()
player.active_source = plugin_source.id
- stream_url = self.mass.streams.get_plugin_source_url(plugin_source.id, player.player_id)
+ stream_url = await self.mass.streams.get_plugin_source_url(
+ plugin_source.id, player.player_id
+ )
await self.play_media(
player_id=player.player_id,
media=PlayerMedia(
base_path = "flow" if flow_mode else "single"
return f"{self._server.base_url}/{base_path}/{queue_item.queue_id}/{queue_item.queue_item_id}.{fmt}" # noqa: E501
+ async def get_plugin_source_url(
+ self,
+ plugin_source: str,
+ player_id: str,
+ ) -> str:
+ """Get the url for the Plugin Source stream/proxy."""
+ 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}"
+ return f"{self._server.base_url}/pluginsource/{plugin_source}/{player_id}.{fmt}"
+
async def serve_queue_item_stream(self, request: web.Request) -> web.Response:
"""Stream single queueitem audio to a player."""
self._log_request(request)
# like https hosts and it also offers the pre-announce 'bell'
return f"{self.base_url}/announcement/{player_id}.{content_type.value}?pre_announce={use_pre_announce}" # noqa: E501
- def get_plugin_source_url(
- self,
- plugin_source: str,
- player_id: str,
- output_codec: ContentType = ContentType.FLAC,
- ) -> str:
- """Get the url for the Plugin Source stream/proxy."""
- 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}"
- return f"{self._server.base_url}/pluginsource/{plugin_source}/{player_id}.{fmt}"
-
async def get_queue_flow_stream(
self,
queue: PlayerQueue,
}
if self.write_access:
music_features.add(ProviderFeature.PLAYLIST_TRACKS_EDIT)
+ music_features.add(ProviderFeature.PLAYLIST_CREATE)
return music_features
@property
CONF_ENTRY_HTTP_PROFILE,
CONF_ENTRY_HTTP_PROFILE_FORCED_2,
CONF_ENTRY_OUTPUT_CODEC_DEFAULT_MP3,
- CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC,
- CONF_ENTRY_OUTPUT_CODEC_ENFORCE_MP3,
HIDDEN_ANNOUNCE_VOLUME_CONFIG_ENTRIES,
+ create_output_codec_config_entry,
create_sample_rates_config_entry,
)
from music_assistant.helpers.datetime import from_iso_string
class ESPHomeSupportedAudioFormat(TypedDict):
"""ESPHome Supported Audio Format."""
- format: str # flac or mp3
+ format: str # flac, wav or mp3
sample_rate: int # e.g. 48000
num_channels: int # 1 for announcements, 2 for media
purpose: int # 0 for media, 1 for announcements
# optimized config for new ESPHome mediaplayer
supported_sample_rates: list[int] = []
supported_bit_depths: list[int] = []
- supports_flac: bool = False
- for supported_format in player.extra_data["esphome_supported_audio_formats"]:
- if supported_format["purpose"] != 0:
- continue
- if supported_format["format"] == "flac":
- supports_flac = True
+ codec: str | None = None
+ supported_formats: list[ESPHomeSupportedAudioFormat] = player.extra_data[
+ "esphome_supported_audio_formats"
+ ]
+ # sort on purpose field, so we prefer the media pipeline
+ # but allows fallback to announcements pipeline if no media pipeline is available
+ supported_formats.sort(key=lambda x: x["purpose"])
+ for supported_format in supported_formats:
+ codec = supported_format["format"]
if supported_format["sample_rate"] not in supported_sample_rates:
supported_sample_rates.append(supported_format["sample_rate"])
bit_depth = supported_format["sample_bytes"] * 8
if bit_depth not in supported_bit_depths:
supported_bit_depths.append(bit_depth)
- if not supports_flac:
- 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
CONF_ENTRY_FLOW_MODE_ENFORCED,
CONF_ENTRY_HTTP_PROFILE_FORCED_2,
- CONF_ENTRY_OUTPUT_CODEC_ENFORCE_FLAC,
+ create_output_codec_config_entry(True, codec),
CONF_ENTRY_ENABLE_ICY_METADATA_HIDDEN,
create_sample_rates_config_entry(
supported_sample_rates=supported_sample_rates,