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