/
/
/
1"""Constants for the Streams Controller."""
2
3from __future__ import annotations
4
5from enum import StrEnum
6from typing import Final, Literal
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 stream output is handed to a player, once it has had its opening
62# burst. Music Assistant serves audio for listening, not for collecting: barely above
63# playback speed the player's buffer still grows, while pulling a whole catalogue takes
64# about as long as listening to it would. A gentle feed also keeps a realtime source's
65# banked head start resident for its end-of-track crossfade, and spares players with a
66# small input buffer (Chromecast is the known case).
67# Do not remove this pacing to "fix" slow buffering. See the usage policy.
68#
69# gapless_burst: players that must hold a whole opening chunk before they play gapless
70# (MusicCast is the known case). low_latency: live AudioSource streams, where whatever
71# the burst hands over sits in the player's buffer as listening delay.
72PacingProfile = Literal["default", "gapless_burst", "low_latency"]
73_PACING: Final[dict[PacingProfile, tuple[str, str]]] = {
74 "default": ("1.02", "3"),
75 "gapless_burst": ("1.2", "60"),
76 "low_latency": ("1.02", "0.5"),
77}
78
79
80def output_pacing_args(profile: PacingProfile = "default") -> list[str]:
81 """Return the ffmpeg pacing arguments for a stream handed to a player."""
82 readrate, burst = _PACING[profile]
83 return ["-readrate", readrate, "-readrate_initial_burst", burst]
84
85
86# Time to keep the flow stream response open after the last audio byte of a queue.
87# Players buffer a few seconds ahead of what they actually render; some of them drop
88# that buffer the moment the connection is closed, cutting off the end of the queue.
89# Holding the (idle) connection open gives them time to play it out first. Kept below
90# the webserver shutdown timeout so a lead-out never stalls a restart of the server.
91FLOW_STREAM_LEAD_OUT_SECONDS: Final[int] = 8
92
93
94# Configuration keys
95CONF_BUFFER_SIZE: Final[str] = "buffer_size"
96
97
98def get_available_buffer_sizes() -> list[BufferSize]:
99 """
100 Return the buffer-size presets allowed for this host's RAM.
101
102 Minimal is always available; Balanced needs ~4GB and Maximum ~8GB (both within the
103 reporting tolerance). When total memory is unknown (0.0, e.g. Windows) all presets are
104 offered (fail open).
105 """
106 if TOTAL_SYSTEM_MEMORY_GB == 0.0:
107 return [BufferSize.MINIMAL, BufferSize.BALANCED, BufferSize.MAXIMUM]
108 sizes = [BufferSize.MINIMAL]
109 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, BALANCED_MIN_RAM_GB):
110 sizes.append(BufferSize.BALANCED)
111 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, MAXIMUM_MIN_RAM_GB):
112 sizes.append(BufferSize.MAXIMUM)
113 return sizes
114
115
116def _get_default_buffer_size() -> str:
117 # Unknown memory (0.0) picks the conservative Minimal default, unlike the
118 # available-presets list which fails open â meets_memory_target() also fails open,
119 # so the 0.0 case is handled explicitly here before consulting it.
120 if TOTAL_SYSTEM_MEMORY_GB == 0.0:
121 return BufferSize.MINIMAL
122 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, MAXIMUM_MIN_RAM_GB):
123 return BufferSize.MAXIMUM
124 if meets_memory_target(TOTAL_SYSTEM_MEMORY_GB, BALANCED_MIN_RAM_GB):
125 return BufferSize.BALANCED
126 return BufferSize.MINIMAL
127
128
129CONF_BUFFER_SIZE_DEFAULT: Final[str] = _get_default_buffer_size()
130CONF_ALLOW_CROSSFADE_SAME_ALBUM: Final[str] = "allow_crossfade_same_album"
131CONF_SMART_FADES_LOG_LEVEL: Final[str] = "smart_fades_log_level"
132
133# Maximum wait for a provider source-stream slot before a speculative attempt gives up.
134STREAM_SLOT_WAIT_TIMEOUT: Final[float] = 5.0
135
136# Total capacity budget when an actual playback start retries/reselects provider mappings.
137STREAM_SLOT_PLAYBACK_WAIT_TIMEOUT: Final[float] = 15.0
138
139# Maximum time spent searching other streaming providers for an alternative mapping
140# when every known candidate is capacity-saturated.
141STREAM_SLOT_MATCH_TIMEOUT: Final[float] = 5.0
142
143# Maximum seconds we wait for the buffer to catch up on a forward seek.
144# Beyond this, the stream is re-fetched at the seek position.
145SEEK_WAIT_THRESHOLD: Final[int] = 20
146
147# Streams webserver default port
148DEFAULT_PORT: Final[int] = 8097
149
150# Cache constants for resolved radio URLs
151CACHE_CATEGORY_RESOLVED_RADIO_URL: Final[int] = 100
152CACHE_PROVIDER: Final[str] = "audio"
153
154# StreamDetails.data key providers set to opt into the in-band title handoff.
155STREAMDETAILS_INBAND_TITLE_HANDOFF_KEY: Final[str] = "inband_title_handoff"
156# StreamDetails.data key where the streams controller records the in-band (ICY)
157# stream title after an opted-in provider takes ownership of stream_metadata
158# (StreamDetails.stream_title is a derived view whose setter would overwrite it).
159STREAMDETAILS_INBAND_TITLE_KEY: Final[str] = "inband_stream_title"
160