CrossFadeMode,
EventType,
MediaType,
+ MetadataMode,
ProviderType,
)
from music_assistant.models.errors import MediaNotFoundError, QueueEmpty
"Cache-Control": "no-cache",
}
- # for now, only support icy metadata on MP3 streams to prevent issues
- # https://github.com/music-assistant/hass-music-assistant/issues/603
- # in the future we could expand this support:
- # by making exceptions for players that do also support ICY on other content types
- # and/or metaint value such as Kodi.
- # another future expansion is to just get the PCM frames here and encode
- # for each inidvidual player with or without ICY...
- if queue_stream.output_format == ContentType.MP3:
- # use the default/recommended metaint size of 8192
- # https://cast.readme.io/docs/icy
+ # ICY-metadata headers depend on settings
+ metadata_mode = queue_stream.queue.settings.metadata_mode
+ if metadata_mode != MetadataMode.DISABLED:
headers["icy-name"] = "Music Assistant"
headers["icy-pub"] = "1"
- # use the default/recommended metaint size of 8192
- headers["icy-metaint"] = str(ICY_CHUNKSIZE)
+ headers["icy-metaint"] = str(queue_stream.output_chunksize)
resp = web.StreamResponse(headers=headers)
try:
self.signal_next: bool = False
self._runner_task: Optional[asyncio.Task] = None
self._prev_chunk: bytes = b""
+ if queue.settings.metadata_mode == MetadataMode.LEGACY:
+ # use the legacy/recommended metaint size of 8192 bytes
+ self.output_chunksize = ICY_CHUNKSIZE
+ else:
+ self.output_chunksize = get_chunksize(
+ output_format, pcm_sample_rate, pcm_bit_depth
+ )
if autostart:
self.mass.create_task(self.start())
# Read bytes from final output and send chunk to child callback.
chunk_num = 0
- if self.output_format == ContentType.MP3:
- # use the icy compatible static chunksize (iter_chunks of x size)
- get_chunks = ffmpeg_proc.iter_chunked(ICY_CHUNKSIZE)
- else:
- # all other: prefer chunksize that fits 1 second belonging to output type
- # but accept less (iter any chunk of max chunk size)
- get_chunks = ffmpeg_proc.iter_any(
- get_chunksize(
- self.output_format,
- self.pcm_sample_rate,
- self.pcm_bit_depth,
- self.pcm_channels,
- )
- )
- async for chunk in get_chunks:
+ async for chunk in ffmpeg_proc.iter_chunked(self.output_chunksize):
chunk_num += 1
if len(self.connected_clients) == 0:
crossfade_duration = self.queue.settings.crossfade_duration
crossfade_size = sample_size_per_second * crossfade_duration
# buffer_duration has some overhead to account for padded silence
- buffer_duration = (crossfade_duration or 2) * 2 if track_count > 1 else 1
+ buffer_duration = (crossfade_duration or 1) * 2
# predict total size to expect for this track from duration
stream_duration = (queue_track.duration or 0) - seek_position
self.queue.signal_update()
buffer = b""
bytes_written = 0
+ seconds_streamed = 0
# handle incoming audio chunks
async for chunk in get_media_stream(
self.mass,
chunk_size=sample_size_per_second,
):
- seconds_streamed = bytes_written / sample_size_per_second
+ seconds_streamed += 1
+ self.seconds_streamed += 1
seconds_in_buffer = len(buffer) / sample_size_per_second
- queue_track.streamdetails.seconds_streamed = seconds_streamed
+ # try to make a rough assumption of how many seconds the player has in buffer
+ player_in_buffer = self.seconds_streamed - (
+ time() - self.streaming_started
+ )
#### HANDLE FIRST PART OF TRACK
streamdetails.media_type == MediaType.ANNOUNCEMENT
or not stream_duration
or stream_duration < buffer_duration
+ or player_in_buffer < buffer_duration
):
# handle edge case where we have a previous chunk in buffer
# and the next track is too short
) -> AsyncGenerator[bytes, None]:
"""Get radio audio stream from HTTP, including metadata retrieval."""
headers = {"Icy-MetaData": "1"}
- timeout = ClientTimeout(total=0, connect=30, sock_read=120)
+ timeout = ClientTimeout(total=0, connect=30, sock_read=600)
async with mass.http_session.get(url, headers=headers, timeout=timeout) as resp:
headers = resp.headers
meta_int = int(headers.get("icy-metaint", "0"))
buffer = b""
buffer_all = False
bytes_received = 0
- timeout = ClientTimeout(total=0, connect=30, sock_read=120)
+ timeout = ClientTimeout(total=0, connect=30, sock_read=600)
async with mass.http_session.get(url, headers=headers, timeout=timeout) as resp:
is_partial = resp.status == 206
buffer_all = seek_position and not is_partial
ALL = "all" # repeat entire queue
+class MetadataMode(Enum):
+ """Enum with stream metadata modes."""
+
+ DISABLED = "disabled" # do not notify icy support
+ DEFAULT = "default" # enable icy if player requests it, default chunksize
+ LEGACY = "legacy" # enable icy but with legacy 8kb chunksize, requires mp3
+
+
class PlayerState(Enum):
"""Enum for the (playback)state of a player."""
import random
from typing import TYPE_CHECKING, Any, Dict, Optional
-from .enums import ContentType, CrossFadeMode, RepeatMode
+from .enums import ContentType, CrossFadeMode, MetadataMode, RepeatMode
if TYPE_CHECKING:
from .player_queue import PlayerQueue
self._stream_type: ContentType = queue.player.stream_type
self._max_sample_rate: int = queue.player.max_sample_rate
self._announce_volume_increase: int = 15
+ self._metadata_mode: MetadataMode = MetadataMode.DEFAULT
@property
def repeat_mode(self) -> RepeatMode:
return self._repeat_mode
@repeat_mode.setter
- def repeat_mode(self, enabled: bool) -> None:
+ def repeat_mode(self, mode: RepeatMode) -> None:
"""Set repeat enabled setting."""
- if self._repeat_mode != enabled:
- self._repeat_mode = enabled
+ if self._repeat_mode != mode:
+ self._repeat_mode = mode
self._on_update("repeat_mode")
@property
# for now we use default python random function
# can be extended with some more magic based on last_played and stuff
next_items = random.sample(next_items, len(next_items))
-
items = played_items + [cur_item] + next_items
asyncio.create_task(self._queue.update_items(items))
self._on_update("shuffle_enabled")
self._announce_volume_increase = volume_increase
self._on_update("announce_volume_increase")
+ @property
+ def metadata_mode(self) -> MetadataMode:
+ """Return metadata mode setting."""
+ return self._metadata_mode
+
+ @metadata_mode.setter
+ def metadata_mode(self, mode: MetadataMode) -> None:
+ """Set metadata mode setting."""
+ if self._metadata_mode != mode:
+ self._metadata_mode = mode
+ self._on_update("metadata_mode")
+
def to_dict(self) -> Dict[str, Any]:
"""Return dict from settings."""
return {
"stream_type": self.stream_type.value,
"max_sample_rate": self.max_sample_rate,
"announce_volume_increase": self.announce_volume_increase,
+ "metadata_mode": self.metadata_mode.value,
}
def from_dict(self, d: Dict[str, Any]) -> None:
self._announce_volume_increase = int(
d.get("announce_volume_increase", self._announce_volume_increase)
)
+ self._metadata_mode = MetadataMode(
+ d.get("metadata_mode", self._metadata_mode.value)
+ )
async def restore(self) -> None:
"""Restore state from db."""