/
/
/
1"""Constants for the Streams Controller."""
2
3from __future__ import annotations
4
5from enum import StrEnum
6from typing import Final
7
8from music_assistant_models.enums import VolumeNormalizationMode
9
10from music_assistant.helpers.util import get_total_system_memory, meets_memory_target
11
12# What the volume normalization preference falls back to.
13DEFAULT_VOLUME_NORMALIZATION_MODE: Final = VolumeNormalizationMode.FALLBACK_DYNAMIC
14
15# Modes that are only ever an outcome, never something to ask for: SOURCE is set by a
16# source that levels its own audio and UNKNOWN is what an unrecognised value
17# deserializes to. Neither is offered as a preference, and one that reaches the config
18# anyway is not honoured as one - it would otherwise be handed straight back as the
19# mode to apply, which for SOURCE also means claiming a source levelled the audio.
20OUTCOME_ONLY_NORMALIZATION_MODES: Final = (
21 VolumeNormalizationMode.SOURCE,
22 VolumeNormalizationMode.UNKNOWN,
23)
24
25
26class BufferMode(StrEnum):
27 """Buffer mode determines buffer behavior."""
28
29 SEEKABLE = "seekable"
30 ROLLING = "rolling"
31
32
33class BufferSize(StrEnum):
34 """Buffer size presets for configuration."""
35
36 MINIMAL = "minimal"
37 BALANCED = "balanced"
38 MAXIMUM = "maximum"
39
40
41# Calculate total system memory once at module load time
42TOTAL_SYSTEM_MEMORY_GB: Final[float] = get_total_system_memory()
43
44# RAM thresholds for the buffer-size presets, as NOMINAL targets. Both are checked via
45# meets_memory_target(), which absorbs the gap between a host's nominal size and what it
46# reports (kernel MemTotal reservation plus any integrated-GPU carve-out), so a "4GB" box
47# (reporting ~3.8GB) and an "8GB" box (reporting ~7.4GB) both qualify for their tier.
48BALANCED_MIN_RAM_GB: Final[float] = 4.0
49MAXIMUM_MIN_RAM_GB: Final[float] = 8.0
50
51# Buffer size in seconds for each preset
52BUFFER_SIZE_MAP: Final[dict[str, int]] = {
53 BufferSize.MINIMAL: 60,
54 BufferSize.BALANCED: 300,
55 BufferSize.MAXIMUM: 1200,
56}
57
58# Buffer size for radio streams (short rolling buffer)
59RADIO_BUFFER_SIZE: Final[int] = 15
60
61# Ceiling on how fast a single queue item is handed to a player, once it has had its opening
62# burst. Music Assistant serves audio for listening, not for collecting: at twice playback the
63# player's buffer still grows in realtime, while pulling a whole catalogue takes about as long
64# as listening to it would. These are the fastest we go, not a target - a player that needs
65# feeding more gently (Chromecast is the known case) can be paced slower than this.
66# Do not remove this to "fix" slow buffering; raise the burst instead. See the usage policy.
67SINGLE_ITEM_READRATE: Final[str] = "1.2"
68SINGLE_ITEM_READRATE_INITIAL_BURST: Final[str] = "60"
69
70# Time to keep the flow stream response open after the last audio byte of a queue.
71# Players buffer a few seconds ahead of what they actually render; some of them drop
72# that buffer the moment the connection is closed, cutting off the end of the queue.
73# Holding the (idle) connection open gives them time to play it out first. Kept below
74# the webserver shutdown timeout so a lead-out never stalls a restart of the server.
75FLOW_STREAM_LEAD_OUT_SECONDS: Final[int] = 8
76
77
78# Configuration keys
79CONF_BUFFER_SIZE: Final[str] = "buffer_size"
80
81
82def get_available_buffer_sizes() -> list[BufferSize]:
83 """
84 Return the buffer-size presets allowed for this host's RAM.
85
86 Minimal is always available; Balanced needs ~4GB and Maximum ~8GB (both within the
87 reporting tolerance). When total memory is unknown (0.0, e.g. Windows) all presets are
88 offered (fail open).
89 """
90 if TOTAL_SYSTEM_MEMORY_GB == 0.0:
91 return [BufferSize.MINIMAL, BufferSize.BALANCED, BufferSize.MAXIMUM]
92 sizes = [BufferSize.MINIMAL]
93 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, BALANCED_MIN_RAM_GB):
94 sizes.append(BufferSize.BALANCED)
95 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, MAXIMUM_MIN_RAM_GB):
96 sizes.append(BufferSize.MAXIMUM)
97 return sizes
98
99
100def _get_default_buffer_size() -> str:
101 # Unknown memory (0.0) picks the conservative Minimal default, unlike the
102 # available-presets list which fails open â meets_memory_target() also fails open,
103 # so the 0.0 case is handled explicitly here before consulting it.
104 if TOTAL_SYSTEM_MEMORY_GB == 0.0:
105 return BufferSize.MINIMAL
106 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, MAXIMUM_MIN_RAM_GB):
107 return BufferSize.MAXIMUM
108 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, BALANCED_MIN_RAM_GB):
109 return BufferSize.BALANCED
110 return BufferSize.MINIMAL
111
112
113CONF_BUFFER_SIZE_DEFAULT: Final[str] = _get_default_buffer_size()
114CONF_ALLOW_CROSSFADE_SAME_ALBUM: Final[str] = "allow_crossfade_same_album"
115CONF_SMART_FADES_LOG_LEVEL: Final[str] = "smart_fades_log_level"
116
117# Maximum wait for a provider source-stream slot before a speculative attempt gives up.
118STREAM_SLOT_WAIT_TIMEOUT: Final[float] = 5.0
119
120# Total capacity budget when an actual playback start retries/reselects provider mappings.
121STREAM_SLOT_PLAYBACK_WAIT_TIMEOUT: Final[float] = 15.0
122
123# Maximum time spent searching other streaming providers for an alternative mapping
124# when every known candidate is capacity-saturated.
125STREAM_SLOT_MATCH_TIMEOUT: Final[float] = 5.0
126
127# Maximum seconds we wait for the buffer to catch up on a forward seek.
128# Beyond this, the stream is re-fetched at the seek position.
129SEEK_WAIT_THRESHOLD: Final[int] = 20
130
131# Streams webserver default port
132DEFAULT_PORT: Final[int] = 8097
133
134# Cache constants for resolved radio URLs
135CACHE_CATEGORY_RESOLVED_RADIO_URL: Final[int] = 100
136CACHE_PROVIDER: Final[str] = "audio"
137
138# StreamDetails.data key providers set to opt into the in-band title handoff.
139STREAMDETAILS_INBAND_TITLE_HANDOFF_KEY: Final[str] = "inband_title_handoff"
140# StreamDetails.data key where the streams controller records the in-band (ICY)
141# stream title after an opted-in provider takes ownership of stream_metadata
142# (StreamDetails.stream_title is a derived view whose setter would overwrite it).
143STREAMDETAILS_INBAND_TITLE_KEY: Final[str] = "inband_stream_title"
144