"icy-name": "Music Assistant",
"icy-pub": "0",
}
-FLOW_MAX_SAMPLE_RATE = 96000
+FLOW_MAX_SAMPLE_RATE = 192000
FLOW_MAX_BIT_DEPTH = 24
# handle raw pcm
if output_codec.is_pcm():
player = self.stream_controller.mass.players.get(child_player_id)
- player_max_bit_depth = 32 if player.supports_24bit else 16
+ player_max_bit_depth = 24 if player.supports_24bit else 16
output_sample_rate = min(self.pcm_format.sample_rate, player.max_sample_rate)
output_bit_depth = min(self.pcm_format.bit_depth, player_max_bit_depth)
output_channels = await self.stream_controller.mass.config.get_player_config_value(
# handle raw pcm
if output_codec.is_pcm():
player = self.mass.players.get(queue_id)
- player_max_bit_depth = 32 if player.supports_24bit else 16
+ player_max_bit_depth = 24 if player.supports_24bit else 16
if flow_mode:
output_sample_rate = min(FLOW_MAX_SAMPLE_RATE, player.max_sample_rate)
output_bit_depth = min(FLOW_MAX_BIT_DEPTH, player_max_bit_depth)
# cleanup existing job first
if not existing_job.finished:
existing_job.stop()
-
+ queue_player = self.mass.players.get(queue_id)
+ pcm_bit_depth = 24 if queue_player.supports_24bit else 16
+ pcm_sample_rate = min(queue_player.max_sample_rate, 96000)
self.multi_client_jobs[queue_id] = stream_job = MultiClientStreamJob(
self,
queue_id=queue_id,
pcm_format=AudioFormat(
- # hardcoded pcm quality of 48/24 for now
- # TODO: change this to the highest quality supported by all child players ?
- content_type=ContentType.from_bit_depth(24),
- sample_rate=48000,
- bit_depth=24,
+ content_type=ContentType.from_bit_depth(pcm_bit_depth),
+ sample_rate=pcm_sample_rate,
+ bit_depth=pcm_bit_depth,
channels=2,
),
start_queue_item=start_queue_item,
else:
output_sample_rate = min(default_sample_rate, queue_player.max_sample_rate)
- player_max_bit_depth = 32 if queue_player.supports_24bit else 16
+ player_max_bit_depth = 24 if queue_player.supports_24bit else 16
output_bit_depth = min(default_bit_depth, player_max_bit_depth)
output_channels_str = await self.mass.config.get_player_config_value(
queue_player.player_id, CONF_OUTPUT_CHANNELS
import functools
import time
from collections.abc import Awaitable, Callable, Coroutine, Sequence
+from contextlib import suppress
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar
"""Catch UpnpError errors and check availability before and after request."""
player_id = kwargs["player_id"] if "player_id" in kwargs else args[0]
dlna_player = self.dlnaplayers[player_id]
+ dlna_player.last_command = time.time()
self.logger.debug(
- "Handling command %s for player %s - using args: %s %s",
+ "Handling command %s for player %s",
func.__name__,
dlna_player.player.display_name,
- str(args),
- str(kwargs),
)
if not dlna_player.available:
self.logger.warning("Device disappeared when trying to call %s", func.__name__)
last_seen: float = field(default_factory=time.time)
next_url: str | None = None
next_item: QueueItem | None = None
- supports_next_uri = True
- end_of_track_reached = False
+ supports_next_uri: bool | None = None
+ end_of_track_reached: float | None = None
+ last_command: float = field(default_factory=time.time)
def update_attributes(self):
"""Update attributes of the MA Player from DLNA state."""
self.player.elapsed_time_last_updated = (
self.device.media_position_updated_at.timestamp()
)
- if self.device.media_duration and self.player.corrected_elapsed_time:
- self.end_of_track_reached = (
- self.device.media_duration - self.player.corrected_elapsed_time
- ) < 15
+ # some dlna players get stuck at the end of the track and won't
+ # automatically play the next track, try to workaround that
+ if (
+ self.device.media_duration
+ and self.player.corrected_elapsed_time
+ and self.player.state == PlayerState.PLAYING
+ and (self.device.media_duration - self.player.corrected_elapsed_time) <= 10
+ ):
+ self.end_of_track_reached = time.time()
else:
# device is unavailable
self.player.available = False
async def cmd_stop(self, player_id: str) -> None:
"""Send STOP command to given player."""
dlna_player = self.dlnaplayers[player_id]
- dlna_player.end_of_track_reached = False
+ dlna_player.end_of_track_reached = None
dlna_player.next_url = None
assert dlna_player.device is not None
await dlna_player.device.async_stop()
# always clear queue (by sending stop) first
if dlna_player.device.can_stop:
await self.cmd_stop(player_id)
+ dlna_player.next_url = None
+ dlna_player.end_of_track_reached = None
didl_metadata = create_didl_metadata(self.mass, url, queue_item)
title = queue_item.name if queue_item else "Music Assistant"
await dlna_player.device.async_wait_for_can_play(10)
await dlna_player.device.async_play()
# force poll the device
- for sleep in (0, 1, 2):
+ for sleep in (1, 2):
await asyncio.sleep(sleep)
dlna_player.force_poll = True
await self.poll_player(dlna_player.udn)
try:
now = time.time()
do_ping = dlna_player.force_poll or (now - dlna_player.last_seen) > 60
- await dlna_player.device.async_update(do_ping=do_ping)
+ with suppress(ValueError):
+ await dlna_player.device.async_update(do_ping=do_ping)
dlna_player.last_seen = now if do_ping else dlna_player.last_seen
except UpnpError as err:
self.logger.debug("Device unavailable: %r", err)
await self._device_discovered(ssdp_udn, discovery_info["location"])
- await async_search(on_response, 60)
+ await async_search(on_response)
finally:
self._discovery_running = False
self.mass.create_task(self._run_discovery())
# reschedule self once finished
- self.mass.loop.call_later(300, reschedule)
+ self.mass.loop.call_later(120, reschedule)
async def _device_disconnect(self, dlna_player: DLNAPlayer) -> None:
"""
self.logger.debug("Ignoring disabled player: %s", udn)
return
+ is_sonos = "rincon" in udn.lower()
+
dlna_player = DLNAPlayer(
udn=udn,
player=Player(
address=description_url,
manufacturer="unknown",
),
+ max_sample_rate=48000 if is_sonos else 192000,
+ supports_24bit=True,
# disable sonos players by default in dlna
- enabled_by_default="rincon" not in udn.lower(),
+ enabled_by_default=not is_sonos,
),
description_url=description_url,
)
) -> None:
"""Handle state variable(s) changed event from DLNA device."""
udn = service.device.udn
-
dlna_player = self.dlnaplayers[udn]
- self.logger.debug(
- "Received event for Player %s: %s",
- dlna_player.player.display_name,
- service,
- )
if not state_variables:
# Indicates a failure to resubscribe, check if device is still available
):
dlna_player.force_poll = True
self.mass.create_task(self.poll_player(dlna_player.udn))
+ self.logger.debug(
+ "Received new state from event for Player %s: %s",
+ dlna_player.player.display_name,
+ state_variable.value,
+ )
dlna_player.last_seen = time.time()
self.mass.create_task(self._update_player(dlna_player))
dlna_player.next_item = next_item
# no need to try setting the next url if we already know the player does not support it
- if not dlna_player.supports_next_uri:
+ if dlna_player.supports_next_uri is False:
return
# send queue item to dlna queue
await dlna_player.device.async_set_next_transport_uri(next_url, title, didl_metadata)
except UpnpError:
dlna_player.supports_next_uri = False
- self.logger.info("Player does not support next uri")
+ self.logger.info(
+ "Player does not support next transport uri feature, "
+ "gapless playback is not possible."
+ )
+ else:
+ # log once if we detected that the player supports the next transport uri
+ if dlna_player.supports_next_uri is None:
+ dlna_player.supports_next_uri = True
+ self.logger.debug("Player supports the next transport uri feature.")
self.logger.debug(
"Enqued next track (%s) to player %s",
self.mass.players.update(dlna_player.udn)
# enqueue next item if needed
- if dlna_player.player.state == PlayerState.PLAYING and (
- not dlna_player.next_url or dlna_player.next_url == current_url
+ if (
+ dlna_player.player.state == PlayerState.PLAYING
+ and (not dlna_player.next_url or dlna_player.next_url == current_url)
+ # prevent race conditions at start/stop by doing this check
+ and (time.time() - dlna_player.last_command) > 10
):
self.mass.create_task(self._enqueue_next_track(dlna_player))
+ # try to detect a player that gets stuck at the end of the track
+ if (
+ dlna_player.end_of_track_reached
+ and dlna_player.next_url
+ and dlna_player.supports_next_uri
+ and time.time() - dlna_player.end_of_track_reached > 10
+ ):
+ self.logger.warning(
+ "Detected that the player is stuck at the end of the track, "
+ "enabling workaround for this player."
+ )
+ dlna_player.supports_next_uri = False
# if player does not support next uri, manual play it
if (
not dlna_player.supports_next_uri
and prev_state == PlayerState.PLAYING
and current_state == PlayerState.IDLE
and dlna_player.next_url
- and dlna_player.end_of_track_reached
):
+ self.logger.warning(
+ "Player does not support next_uri and end of track reached, "
+ "sending next url manually."
+ )
await self.cmd_play_url(dlna_player.udn, dlna_player.next_url, dlna_player.next_item)
dlna_player.end_of_track_reached = False
dlna_player.next_url = None