/
/
/
1"""Constants for the AirPlay provider."""
2
3from __future__ import annotations
4
5from dataclasses import replace
6from enum import IntEnum, StrEnum
7from typing import Final
8
9from music_assistant_models.enums import ContentType, PlayerFeature
10from music_assistant_models.media_items import AudioFormat
11
12from music_assistant.constants import CONF_ENTRY_SYNC_ADJUST
13from music_assistant.controllers.streams.constants import SEEK_WAIT_THRESHOLD
14
15DOMAIN = "airplay"
16
17
18class StreamingProtocol(IntEnum):
19 """AirPlay streaming protocol versions."""
20
21 RAOP = 1 # AirPlay 1 (RAOP)
22 AIRPLAY2 = 2 # AirPlay 2
23
24
25class AirPlayRemoteCommand(StrEnum):
26 """Transport commands received from an AirPlay receiver."""
27
28 PLAY = "play"
29 PAUSE = "pause"
30 PLAY_PAUSE = "play_pause"
31 NEXT = "next"
32 PREVIOUS = "previous"
33
34
35class ClockReadiness(StrEnum):
36 """
37 How a receiver's clock readiness resolved for an anchor decision.
38
39 Only PROJECTED carries an instant; the rest all mean "anchor on the lead
40 alone", but for very different reasons - one is a device that will not play
41 at all, and treating them alike hides it.
42 """
43
44 # The binary projected when the receiver's clock becomes usable.
45 PROJECTED = "projected"
46 # NTP timing: there is no receiver clock to wait for.
47 NOT_APPLICABLE = "not_applicable"
48 # The receiver never answered our PTP clock and will render silence.
49 STALLED = "stalled"
50 # Nothing arrived within the wait: a slow device (retryable) or a receiver
51 # whose readiness went unreported.
52 UNREPORTED = "unreported"
53
54
55CONF_PASSWORD: Final[str] = "password"
56# Storage-only marker (no config entry) set when the device rejected the stored
57# password, so the player keeps asking for setup across restarts until a working
58# password is entered.
59CONF_PASSWORD_INVALID: Final[str] = "password_invalid"
60# Provider marker that the stored password verdicts were reviewed once. Releases
61# that could not tell a password challenge apart from a flat refusal wrote the
62# key above for both, so what they left behind is no evidence about a password
63# and is dropped a single time; a device that really challenges marks itself
64# again on its next connect.
65CONF_PASSWORD_MARKERS_REVIEWED: Final[str] = "password_markers_reviewed"
66CONF_IGNORE_VOLUME: Final[str] = "ignore_volume"
67CONF_ENCRYPTION: Final[str] = "encryption"
68# Advanced per-device streaming mode: pins the protocol/timing lane for
69# receivers whose automatic route misbehaves. Options are offered per device
70# capability; Automatic is the default and the setting is only ever written by
71# the user â a failing automatic route is reported, never switched away from.
72CONF_STREAMING_MODE: Final[str] = "streaming_mode"
73# Per-device 24-bit toggle, only offered for devices that advertise 24-bit
74# support. Defaults per device family (see default_hires_enabled).
75CONF_ENABLE_HIRES: Final[str] = "enable_hires"
76# Provider marker that the compatibility-mode pins were reset once. Earlier
77# releases switched a player here themselves when its native control channel
78# failed (usually a network dropout), pinning it to a lane many devices reject
79# outright, so those machine-written values are returned to Automatic a single
80# time; a deliberate choice can simply be made again.
81CONF_COMPAT_PINS_REVIEWED: Final[str] = "compat_pins_reviewed"
82STREAMING_MODE_AUTO: Final[str] = "auto"
83STREAMING_MODE_AP2_PTP: Final[str] = "ap2_ptp"
84STREAMING_MODE_AP2_NTP: Final[str] = "ap2_ntp"
85STREAMING_MODE_AP2_COMPAT: Final[str] = "ap2_compat"
86STREAMING_MODE_RAOP: Final[str] = "raop"
87CONF_STORED_VOLUME: Final[str] = "stored_volume"
88CONF_COMPANION_CREDENTIALS: Final[str] = "companion_credentials"
89CONF_MRP_CREDENTIALS: Final[str] = "mrp_credentials"
90CONF_NATIVE_MRP_CREDENTIALS: Final[str] = "native_mrp_credentials"
91
92# Bundle id of the Music Assistant tvOS dashboard app, launched over Companion on
93# eligible Apple TVs (see tvos/docs/launch-contract.md).
94TVOS_APP_BUNDLE_ID: Final[str] = "io.music-assistant.tvos"
95
96AIRPLAY_DISCOVERY_TYPE: Final[str] = "_airplay._tcp.local."
97COMPANION_DISCOVERY_TYPE: Final[str] = "_companion-link._tcp.local."
98MRP_DISCOVERY_TYPE: Final[str] = "_mediaremotetv._tcp.local."
99RAOP_DISCOVERY_TYPE: Final[str] = "_raop._tcp.local."
100DACP_DISCOVERY_TYPE: Final[str] = "_dacp._tcp.local."
101
102# Fallback ports for the two streaming services above, for when discovery has none.
103AIRPLAY_DEFAULT_PORT: Final[int] = 7000
104RAOP_DEFAULT_PORT: Final[int] = 5000
105
106# Floor for a late joiner's anchor, and the one it rests on whenever the binary
107# reports no readiness projection ([STATUS] clock_ready). A joiner cannot
108# honour an instant in the past, and that second case has no device evidence at
109# all, so this carries it the whole way: the command's trip to the binary, the
110# binary's own 250 ms floor, and the receiver seating the anchor. The value is
111# field-proven, not derived from those.
112# It is NOT headroom for the binary's post-commit clock verification, which
113# arms only when the receiver has still not probed by the time it reads the
114# START, and only for an anchor that clears the receiver queue depth plus
115# 500 ms - past ~2 s of effective depth, an anchor this close leaves no room.
116AIRPLAY_LATE_JOIN_MIN_HEADROOM_MS: Final[int] = 2500
117# How long a group start, a join or the Sendspin bridge waits for the binary's
118# first receiver-clock projection ([STATUS] clock_ready with state=probing|
119# ready). The binary emits its first line right after [STATUS] connected and
120# refreshes it about every 250 ms, and the projection exists from the receiver's
121# first probe (~1078 ms after connect on a cold device), so this only has to
122# cover a slower device before giving up and anchoring on the lead alone.
123# Independent of the binary's own stall report (state=stalled), which is a
124# slower, higher-confidence diagnosis of a receiver that never answers at all: a
125# wait that times out here has simply run out of planning time and falls back,
126# while a stall says the speaker will not play. Keep them apart - tightening the
127# stall report to meet this deadline would trade the margin that keeps it free
128# of false alarms.
129AIRPLAY_CLOCK_READY_TIMEOUT_MS: Final[int] = 2500
130# Lead added on top of a reported readiness instant, wherever one is waited for.
131# The binary refuses to place an anchor inside its own 250 ms floor, measured
132# from when IT reads the START command rather than when the server sends it (the
133# same trap as AIRPLAY_START_LEAD_MS), so the lead carries that floor plus 250 ms
134# for the command reaching the binary and for the convergence error of a
135# projection made from the receiver's very first probe.
136AIRPLAY_CLOCK_READY_LEAD_MS: Final[int] = 500
137# Default receiver buffer depth per device family: (manufacturer wildcard,
138# model wildcard, firmware wildcard) -> depth in ms, matched case-insensitively
139# in order, first match wins; unmatched devices stay on Automatic (the binary's
140# stock depth). The table is EMPTY since the buffered (type 103) stream became
141# the auto-route for the receivers that used to need a deepened queue: the
142# LinkPlay pipelines that starved on the realtime stream (WiiM at 1750 ms,
143# Edifier MS50A silent below 2500 ms) manage their own buffer on the buffered
144# stream and play fine on Automatic. The per-player depth setting remains as
145# an advanced override; extend the table only for devices that starve on the
146# route they actually take.
147AIRPLAY_BUFFER_DEPTH_DEFAULTS: Final[tuple[tuple[str, str, str, int], ...]] = ()
148# Per-player override of the splice receiver-queue depth in ms (0 = automatic).
149CONF_BUFFER_DEPTH: Final[str] = "buffer_depth"
150# How long a plain (non-join) START waits for the binary's [STATUS] started ack.
151# A strict buffered receiver can reject its anchor until its clock is seated;
152# cliairplay then makes up to 12 attempts, 500 ms apart. Cover that 5.5-second
153# retry span plus control-response and status-delivery margin.
154AIRPLAY_START_ACK_TIMEOUT_MS: Final[int] = 7000
155# A join can additionally withhold its ack while receiver-clock verification
156# settles. Keep its independently named bound aligned with the buffered retry
157# span too; command failures still answer either wait immediately.
158AIRPLAY_JOIN_START_ACK_TIMEOUT_MS: Final[int] = 7000
159# How far the content a corrected anchor actually cut may fall short of the cut
160# it asked for before the reported media position is re-based and the shortfall
161# reported. The binary derives the cut it took from the bytes it discarded, so a
162# few ms of byte quantization is expected and correcting for it would only
163# jitter the base; anything larger means the cut really did end early (the input
164# ran out inside it, or a teardown settled it) and the position is over-advanced
165# by that much for the rest of the anchor.
166AIRPLAY_CONTENT_CUT_TOLERANCE_MS: Final[int] = 20
167# Anchor leads for a readiness-confirmed START (cold and warm alike), solo and
168# group: the session only anchors after the binary confirmed the connection
169# ([STATUS] connected) and the new audio flowing ([STATUS] audio), so a lead no
170# longer guesses at setup or transcoder spin-up time. Both cover the receiver
171# re-anchor (accepted down to ~150 ms in the flush-ladder measurements) plus
172# the command's trip down the pipe; the group one also covers fanning the
173# shared instant out to every member. The pipe margin is what keeps them
174# workable: the binary rejects any instant inside its own 250 ms floor,
175# measured from when IT reads the command, and corrects a miss to that floor
176# plus another full lead â so a lead sitting exactly on the floor misses by the
177# delivery time every time and turns a start into a group-wide re-anchor ladder.
178AIRPLAY_START_LEAD_MS: Final[int] = 400
179AIRPLAY_GROUP_START_LEAD_MS: Final[int] = 500
180# Cold GROUP starts anchor further out: a receiver on a brand-new session
181# still acquires its PTP slave lock (~1.7-2.3 s measured on Sonos) and cannot
182# render at an anchor inside that window - it starts late and the group opens
183# audibly out of sync. Warm re-anchors reuse a locked clock and keep the
184# short leads above; solo cold starts have no sync partner to miss.
185AIRPLAY_COLD_GROUP_START_LEAD_MS: Final[int] = 2500
186
187# How long a start waits for the source to hand over its first audio before it
188# judges the members on what they never received. A seek may land up to
189# SEEK_WAIT_THRESHOLD seconds ahead of what the source has produced, and the
190# wait has to outlast the producer covering that; the margin covers its own
191# spin-up. It is a backstop rather than a budget: this runs under the player
192# lock, so a producer that neither delivers nor gives up would otherwise hold
193# every command for the player behind it.
194AIRPLAY_FEED_START_TIMEOUT: Final[float] = SEEK_WAIT_THRESHOLD + 5
195# Hard cap on how long the stdin EOF withheld for a predicted replacement stream
196# is held. What normally releases that wait is the queue itself: it clears the
197# transition on any failure between rotating its stream session and the
198# play_media that carries the replacement (an item that fails to load, a
199# provider error), and that is the signal no replacement is coming. This only
200# covers a transition that neither completes nor clears. It sits past the load
201# that carries a replacement - the queue's buffer prepare (BUFFER_READY_TIMEOUT,
202# 15s) plus the provider source slot its producer may wait out first - so a slow
203# but real seek is never cut short into a cold restart.
204AIRPLAY_REPLACEMENT_EOF_TIMEOUT: Final[float] = 35.0
205# How often the queue is asked whether it is still loading that replacement.
206# It only bounds how quickly a cleared transition is noticed, so it trades no
207# accuracy for a poll this cheap (one dict lookup).
208AIRPLAY_REPLACEMENT_POLL_INTERVAL: Final[float] = 1.0
209# Margin added on top of a member's reported warm lead (the splice-timeline
210# queue depth; that timeline is the default for every native AirPlay 2 session)
211# when anchoring a warm re-start: covers the command round-trips between the
212# flush acks and the shared START so every member's skip target lands beyond
213# its queued audio.
214AIRPLAY_SPLICE_LEAD_MARGIN_MS: Final[int] = 150
215
216# Floor for the late-join PCM ring, which has to hold every sample between the
217# audible position and the write head: a joiner's anchor maps onto content the
218# group was already fed but has not played yet. That distance - the write-head
219# lead - is the sum of every buffer between the session's byte counter and the
220# speaker, and it is far larger than the binary's own ring alone:
221# ~5.7 s the per-member ffmpeg and the pipes around it (measured 5.2 s
222# steady / 5.7 s peak at 44.1 kHz/16-bit, which is the worst case in
223# SECONDS - the same buffers hold ~4.3 s at the 32-bit hi-res carrier
224# rate, and low-delay ffmpeg flags do not shrink them)
225# ~4.0 s the cliairplay ring: the binary's own lead (2000 ms default,
226# clamped to the device-reported window) plus its 2 s of slack
227# ~2.0 s the receiver's own buffer
228# ~11.7 s in total, against 8.9-11.0 s measured across native AirPlay 2
229# sessions (Apple TV and Sonos, joining in both directions). This floor is that
230# sum rounded up, and it is only a floor: the lead is measured per session and
231# the ring grows to match, because a receiver that reports a wider lead window
232# or buffers more deeply moves the sum with nothing to announce it.
233AIRPLAY_LATE_JOIN_RING_MIN_SECONDS: Final[float] = 12.0
234# Grown on top of the largest lead a session has actually shown, so a lead that
235# drifts up between two joins cannot clip the oldest sample a joiner needs.
236AIRPLAY_LATE_JOIN_RING_MARGIN_SECONDS: Final[float] = 2.0
237# Hard bound on the ring, which is per session (one ring feeds every member) and
238# costs byte_rate x seconds. Bounded in BYTES rather than seconds on purpose:
239# the seconds the ring must hold are largest at the LOWEST byte rate (see the
240# measurements above), so a single byte bound buys ~35 s at 44.1 kHz/16-bit -
241# three times the measured lead, where the seconds are actually needed - and
242# still ~16 s at the 32-bit hi-res carrier, where the pipeline holds fewer
243# seconds anyway. 6 MiB per playing group is a few percent of the server's idle
244# footprint.
245AIRPLAY_LATE_JOIN_RING_MAX_BYTES: Final[int] = 6 * 1024 * 1024
246
247# Delays (seconds) between automatic re-join attempts for a group member whose
248# cliairplay process died unexpectedly mid-session (e.g. the device rode out a
249# network blackout longer than the binary's own keepalive tolerance). A device
250# recovering from a network dropout typically needs tens of seconds to come
251# back, so the ladder stretches to a few minutes; every attempt re-validates
252# that the group still plays and the player was not repurposed meanwhile, and
253# the whole schedule is abandoned as soon as either no longer holds.
254AIRPLAY_REJOIN_ATTEMPT_DELAYS: Final[tuple[int, ...]] = (5, 15, 30, 60, 120)
255
256# Shared audible instant for a native announcement over a live stream: now +
257# the largest member span + this margin. A member can only mix the clip into
258# audio it has not delivered yet, and its span (warm_lead_ms on the splice
259# timeline, the reported lead_ms otherwise) is how far its delivery head runs
260# ahead of the audible position - so the earliest instant EVERY member can
261# honor lies one max-span out. The margin covers fanning the command out over
262# the pipes and the per-member arm processing, so one shared instant stays
263# feasible for all members.
264AIRPLAY_ANNOUNCE_AT_MARGIN_MS: Final[int] = 300
265# Span assumed for a member whose binary reported neither a warm lead nor a
266# device lead (both read 0 = unreported): the binary's own default playback
267# lead, which bounds how far its delivery head can run ahead.
268AIRPLAY_ANNOUNCE_FALLBACK_SPAN_MS: Final[int] = 2000
269# Music gain (dB) under the clip while it plays; the binary ramps the duck in
270# and out itself. <= -60 mutes the music entirely. -18 dB puts the music
271# clearly in the background under speech (-12 was field-judged too shallow).
272AIRPLAY_ANNOUNCE_DUCK_DB: Final[int] = -18
273# Silence prepended to every announcement clip. The binary holds the music duck
274# for the whole clip file, so this is a window in which the music is already
275# ducked and the announcement has not started yet: the announcement volume is
276# raised inside it, where it cannot be heard as the music getting louder. It
277# has to cover the duck's ramp plus the round trip of the volume command.
278AIRPLAY_ANNOUNCE_DUCK_LEAD_S: Final[float] = 0.5
279# Silence appended to every announcement clip, so the volume restore has a
280# cushion to land in. The binary holds the duck for the whole clip, so the
281# restore lands while the music is still ducked instead of racing the duck's
282# 200 ms tail ramp (a restore that lands after the ramp plays a moment of
283# full-level music at the still-bumped device volume).
284AIRPLAY_ANNOUNCE_DUCK_TAIL_S: Final[float] = 1.0
285# On top of the lead to the commanded instant: how long to wait for a member's
286# announce_started before treating that member as not announcing. An outdated
287# binary silently ignores the unknown command, so this bounded wait is also what
288# detects that, and the announcement then fails instead of playing nowhere.
289AIRPLAY_ANNOUNCE_STARTED_TIMEOUT_MS: Final[int] = 3000
290# On top of the clip's audible end: how long to wait for announce_done. The
291# wait stays bounded because a queue that ends mid-clip emits its eof, which
292# ends the status stream while the clip still plays out over the drain.
293AIRPLAY_ANNOUNCE_DONE_TIMEOUT_MS: Final[int] = 5000
294# Pad after the clip's audible end before the pre-announcement volume is
295# restored: covers the jitter between the acked instant and true audibility.
296AIRPLAY_ANNOUNCE_VOLUME_RESTORE_PAD_MS: Final[int] = 500
297# Where inside the ducked lead-in the announcement volume is raised: past the
298# duck's own ramp, with the rest of the lead left for the command to reach the
299# receiver before the announcement itself becomes audible.
300AIRPLAY_ANNOUNCE_VOLUME_BUMP_DELAY_MS: Final[int] = 200
301# The AirPlay volume parameter is linear dB: 0..100 maps onto -30..0 dB on
302# every flow (libraop raopcl_float_volume, reused verbatim by the native AP2
303# SET_PARAMETER path), so one volume point is exactly 0.3 dB of output. This
304# makes the announcement volume bump's effect on the music bed computable, and
305# the duck is deepened by the same amount to keep the music's perceived level
306# at the configured duck depth.
307AIRPLAY_VOLUME_DB_PER_POINT: Final[float] = 0.3
308
309# Cover art is rendered to a local JPEG for the binary to embed (the binary
310# does not fetch URLs). 512px keeps the SET_PARAMETER payload small while still
311# looking sharp on speaker apps and the Apple TV now-playing screen.
312AIRPLAY_ARTWORK_SIZE: Final[int] = 512
313# How long a track-change metadata push waits for that render, so the artwork
314# can ride the SENDMETA bundle and the receiver rewrites its now-playing state
315# once instead of twice (bare replace, then artwork). A render that misses the
316# budget is not abandoned: it keeps running and is delivered with the
317# stand-alone ARTWORK command once it completes.
318AIRPLAY_ARTWORK_RENDER_TIMEOUT: Final[float] = 1.5
319EXTERNAL_ARTWORK_PATH_PREFIX: Final[str] = "external_artwork"
320
321# Per-protocol credential storage keys
322CONF_RAOP_CREDENTIALS: Final[str] = "raop_credentials"
323CONF_AIRPLAY_CREDENTIALS: Final[str] = "airplay_credentials"
324
325# AirPlay serves the shared sync-adjust control as a non-advanced (always visible)
326# setting: the binary handles the playback lead automatically and does not apply
327# device-reported render latency, so sync_adjust is the primary way to compensate
328# a device wired to a TV / AV receiver / amplifier that adds its own audio delay.
329# The AirPlay-scoped strings spell out the sign; the shared entry stays advanced
330# for other providers.
331CONF_ENTRY_SYNC_ADJUST_AIRPLAY = replace(CONF_ENTRY_SYNC_ADJUST, advanced=False)
332
333# Interactive setup-flow input keys (transient PIN/password form fields and the
334# optional "set up now?" choice for the control pairing steps).
335CONF_PAIRING_PIN: Final[str] = "pairing_pin"
336# every AirPlay pairing PIN (streaming, Companion, MRP) is 4 digits
337PAIRING_PIN_FORMAT: Final[str] = "####"
338CONF_PAIRING_PASSWORD: Final[str] = "pairing_password"
339CONF_COMPANION_PAIRING_PIN: Final[str] = "companion_pairing_pin"
340CONF_MRP_PAIRING_PIN: Final[str] = "mrp_pairing_pin"
341CONF_PAIR_NOW: Final[str] = "pair_now"
342
343FALLBACK_VOLUME: Final[int] = 20
344AIRPLAY_VOLUME_MUTE: Final[float] = -144.0
345# How long a volume we sent ourselves keeps the device's own volume reports from
346# being acted on. A receiver echoes every level it is given back over DACP, and
347# an echo that arrives after the next level was already sent would otherwise be
348# read as the user turning the knob and written straight back to the device.
349AIRPLAY_VOLUME_ECHO_GRACE_S: Final[float] = 2.0
350
351AIRPLAY_PCM_FORMAT = AudioFormat(
352 content_type=ContentType.from_bit_depth(16), sample_rate=44100, bit_depth=16
353)
354# Sample rates advertised for a receiver that supports 24-bit (AirPlay 2 flow
355# only). At 24-bit the cliairplay binary expects raw s32le on stdin and truncates
356# to 24-bit ALAC internally.
357AIRPLAY_HIRES_SAMPLE_RATES: Final[list[tuple[int, int]]] = [(44100, 24), (48000, 24)]
358
359# Bits in the audioFormat bit space (shared with the receiver's /info format
360# tables) that mark 24-bit ALAC: 44.1 kHz and 48 kHz respectively. Receivers
361# understate these - the Apple TV lists them for its buffered stream only, yet
362# renders them fine on the realtime stream - so a device advertising either bit
363# on either stream is treated as 24-bit capable.
364AIRPLAY_HIRES_AUDIO_FORMATS: Final[int] = (1 << 19) | (1 << 21)
365
366BASE_PLAYER_FEATURES: Final[set[PlayerFeature]] = {
367 PlayerFeature.PLAY_MEDIA,
368 PlayerFeature.PLAY_ANNOUNCEMENT,
369 PlayerFeature.SET_MEMBERS,
370 PlayerFeature.MULTI_DEVICE_DSP,
371 PlayerFeature.VOLUME_SET,
372 PlayerFeature.VOLUME_MUTE,
373}
374
375
376PIN_REQUIRED = 0x8
377PASSWORD_BIT = 0x80
378LEGACY_PAIRING_BIT = 0x200
379
380# Provider setting: opt-in for the shared PTP daemon's per-packet timing trace
381# (Announce/Sync/Follow_Up) when verbose logging is active. Off by default â
382# the trace floods the log and only matters for clock-sync debugging.
383CONF_VERBOSE_PTP_LOGGING: Final[str] = "verbose_ptp_logging"
384
385# The cliairplay binary tags no log levels on its output, so a genuine problem is
386# recognised by keyword and promoted to a warning that stays visible at normal levels.
387CLI_PROBLEM_MARKERS: Final[tuple[str, ...]] = ("error", "cannot", "failed", "unable")
388# Bound on how many of those promoted lines the shared PTP daemon may produce per
389# window before the rest are counted instead of logged. The markers above are
390# deliberately broad, and "error" is ordinary vocabulary in clock telemetry
391# (offset error, path delay error), so with the daemon's per-packet trace running
392# at ~10 lines/s a single matching line would otherwise fill the log at WARNING.
393# A burst still gets through, which is what a real one-shot daemon failure is.
394PTP_DAEMON_WARN_BURST: Final[int] = 5
395PTP_DAEMON_WARN_WINDOW: Final[float] = 60.0
396