DACP_DISCOVERY_TYPE: Final[str] = "_dacp._tcp.local."
AIRPLAY_OUTPUT_BUFFER_DURATION_MS: Final[int] = (
- 2000 # Read ahead buffer for cliraop. Output buffer duration for cliap2.
+ 2000 # Read ahead buffer for cliraop. Default output buffer duration for cliap2.
)
+AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS: Final[int] = 250 # Minimum output buffer duration permitted.
AIRPLAY2_MIN_LOG_LEVEL: Final[int] = 3 # Min loglevel to ensure stderr output contains what we need
-AIRPLAY2_CONNECT_TIME_MS: Final[int] = 2500 # Time in ms to allow AirPlay2 device to connect
+AIRPLAY2_CONNECT_TIME_MS: Final[int] = 3300 # Time in ms to allow AirPlay2 device to connect
RAOP_CONNECT_TIME_MS: Final[int] = 1000 # Time in ms to allow RAOP device to connect
# Per-protocol credential storage keys
CONF_RAOP_CREDENTIALS: Final[str] = "raop_credentials"
CONF_AIRPLAY_CREDENTIALS: Final[str] = "airplay_credentials"
+CONF_AIRPLAY_LATENCY: Final[str] = "airplay_latency"
# Legacy credential key (for migration)
CONF_AP_CREDENTIALS: Final[str] = "ap_credentials"
from .constants import (
AIRPLAY_DISCOVERY_TYPE,
AIRPLAY_FLOW_PCM_FORMAT,
+ AIRPLAY_OUTPUT_BUFFER_DURATION_MS,
+ AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS,
BASE_PLAYER_FEATURES,
BROKEN_AIRPLAY_WARN,
CACHE_CATEGORY_PREV_VOLUME,
CONF_ACTION_RESET_PAIRING,
CONF_ACTION_START_PAIRING,
CONF_AIRPLAY_CREDENTIALS,
+ CONF_AIRPLAY_LATENCY,
CONF_AIRPLAY_PROTOCOL,
CONF_ALAC_ENCODE,
CONF_ENCRYPTION,
features.add(PlayerFeature.PAUSE)
return features
+ @property
+ def output_buffer_duration_ms(self) -> int:
+ """Get the configured output buffer duration in milliseconds."""
+ return cast(
+ "int",
+ self.config.get_value(CONF_AIRPLAY_LATENCY, AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS),
+ )
+
async def get_config_entries(
self,
action: str | None = None,
required=False,
label="Device password",
description="Some devices require a password to connect/play.",
+ depends_on=CONF_AIRPLAY_PROTOCOL,
+ depends_on_value=StreamingProtocol.RAOP.value,
+ hidden=self.protocol != StreamingProtocol.RAOP,
category="protocol_generic",
advanced=True,
),
supported_sample_rates=[44100], supported_bit_depths=[16], hidden=True
),
ConfigEntry(
- key=CONF_IGNORE_VOLUME,
- type=ConfigEntryType.BOOLEAN,
- default_value=False,
- label="Ignore volume reports sent by the device itself",
+ key=CONF_AIRPLAY_LATENCY,
+ type=ConfigEntryType.INTEGER,
+ default_value=AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS,
+ range=(AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS, AIRPLAY_OUTPUT_BUFFER_DURATION_MS),
+ label="Milliseconds of data to buffer",
description=(
- "The AirPlay protocol allows devices to report their own volume "
- "level. \n"
- "For some devices this is not reliable and can cause unexpected "
- "volume changes. \n"
- "Enable this option to ignore these reports."
+ "The number of milliseconds of data to buffer\n"
+ "NOTE: This adds to the latency experienced for commencement "
+ "of playback. \n"
+ "Try increasing value if playback is unreliable."
),
category="protocol_generic",
- # TODO: remove depends_on when DACP support is added for AirPlay2
depends_on=CONF_AIRPLAY_PROTOCOL,
- depends_on_value=StreamingProtocol.RAOP.value,
- hidden=self.protocol != StreamingProtocol.RAOP,
+ depends_on_value=StreamingProtocol.AIRPLAY2.value,
+ hidden=self.protocol != StreamingProtocol.AIRPLAY2,
advanced=True,
),
]
# add new child to the existing stream (RAOP or AirPlay2) session (if any)
self._attr_group_members.append(player_id)
- if stream_session:
+ if stream_session and child_player_to_add is not None:
await stream_session.add_client(child_player_to_add)
# Ensure group leader includes itself in group_members when it has members
from music_assistant.helpers.process import AsyncProcess
from music_assistant.providers.airplay.constants import (
AIRPLAY2_MIN_LOG_LEVEL,
+ AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS,
CONF_AIRPLAY_CREDENTIALS,
+ CONF_AIRPLAY_LATENCY,
)
from music_assistant.providers.airplay.helpers import get_cli_binary
async def start(self, start_ntp: int) -> None:
"""Start cliap2 process."""
- cli_binary = await get_cli_binary(self.player.protocol)
assert self.player.airplay_discovery_info is not None
-
+ cli_binary = await get_cli_binary(self.player.protocol)
player_id = self.player.player_id
- sync_adjust = self.mass.config.get_raw_player_config_value(player_id, CONF_SYNC_ADJUST, 0)
+ sync_adjust = self.player.config.get_value(CONF_SYNC_ADJUST)
assert isinstance(sync_adjust, int)
+ latency = self.player.config.get_value(
+ CONF_AIRPLAY_LATENCY, AIRPLAY_OUTPUT_BUFFER_MIN_DURATION_MS
+ )
+ assert isinstance(latency, int)
txt_kv: str = ""
for key, value in self.player.airplay_discovery_info.decoded_properties.items():
txt_kv += f'"{key}={value}" '
# cliap2 is the binary that handles the actual streaming to the player
- # this binary leverages from the AirPlay2 support in owntones
+ # this binary leverages from the AirPlay2 support in OwnTone
# https://github.com/music-assistant/cliairplay
# Get AirPlay credentials if available (for Apple devices that require pairing)
str(self._cli_loglevel),
"--dacp_id",
prov.dacp_id,
- "--active_remote",
- self.active_remote_id,
"--pipe",
"-", # Use stdin for audio input
"--command_pipe",
self.commands_pipe.path,
+ "--latency",
+ str(latency),
]
# Add credentials for authenticated AirPlay devices (Apple TV, HomePod, etc.)
)
if "put delay detected" in line:
if "resetting all outputs" in line:
- logger.error("High packet loss detected, restarting playback...")
+ logger.error(
+ "Repeated output buffer low level detected, restarting playback..."
+ )
+ logger.info(
+ "Recommended to increase 'Milliseconds of data to buffer' in player "
+ "advanced settings"
+ )
self.mass.create_task(self.mass.players.cmd_resume(self.player.player_id))
else:
- logger.warning("Packet loss detected!")
+ logger.warning("Output buffer low level detected!")
if "end of stream reached" in line:
logger.debug("End of stream reached")
break
has_airplay2_client = any(
p.protocol == StreamingProtocol.AIRPLAY2 for p in self.sync_clients
)
- wait_start = AIRPLAY2_CONNECT_TIME_MS if has_airplay2_client else RAOP_CONNECT_TIME_MS
+ max_output_buffer_ms: int = 0
+ if has_airplay2_client:
+ max_output_buffer_ms = max(p.output_buffer_duration_ms for p in self.sync_clients)
+ wait_start = (
+ AIRPLAY2_CONNECT_TIME_MS + max_output_buffer_ms
+ if has_airplay2_client
+ else RAOP_CONNECT_TIME_MS
+ )
wait_start_seconds = wait_start / 1000
self.wait_start = wait_start_seconds
self.start_time = cur_time + wait_start_seconds