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