How the Music Assistant Discovery Controller Finds Devices Using mDNS
The DiscoveryController creates a single AsyncServiceBrowser that aggregates mDNS service types from all provider manifests, listens for Zeroconf advertisements on the network, and forwards resolved AsyncServiceInfo objects to providers via the on_mdns_service_state_change callback, while also supporting synchronous-style device lookups through async_find_mdns_service.
The Music Assistant server relies on mDNS (Zeroconf) to automatically discover network players like Sonos, AirPlay, and Spotify Connect devices. The DiscoveryController in music_assistant/controllers/discovery/controller.py centralizes this discovery logic, eliminating duplicate network traffic by maintaining one browser instance that serves all providers. This architecture ensures thread-safe handling of concurrent device announcements while providing both push-based callbacks and pull-based lookup methods for providers.
The Three-Phase mDNS Discovery Workflow
The DiscoveryController implements a three-phase architecture that separates browser initialization, event handling, and on-demand resolution.
Phase 1: Service Type Aggregation and Browser Setup
When the controller starts, the setup() method scans every registered provider's manifest for mdns_discovery entries. These entries declare which service types the provider can handle, such as _raop._tcp.local. for AirPlay or _sonos._tcp.local. for Sonos devices.
all_types: set[str] = set()
for manifest in self.mass.get_provider_manifests():
if manifest.mdns_discovery:
all_types.update(manifest.mdns_discovery)
If the aggregated set contains service types, the controller initializes a single AsyncServiceBrowser from the zeroconf library. This browser listens for all declared service types simultaneously, registering the _on_mdns_service_state_change handler as its callback. The implementation in controller.py (lines 217-235) ensures only one browser instance runs regardless of how many providers require mDNS discovery.
Phase 2: Handling Service State Changes
Each time Zeroconf detects a service state change—whether a device appears, updates, or disappears—the browser invokes _on_mdns_service_state_change. This method constructs an AsyncServiceInfo object and requests the full DNS record from the network.
async def process_mdns_state_change(provider):
lock = self._mdns_locks.setdefault(provider.instance_id, asyncio.Lock())
if state_change == ServiceStateChange.Removed:
info = None
else:
info = AsyncServiceInfo(service_type, name)
await info.async_request(zeroconf, 3000)
async with lock:
await provider.on_mdns_service_state_change(name, state_change, info)
The controller maintains per-provider locks (self._mdns_locks) to prevent race conditions when multiple devices announce simultaneously. After resolving the service info, the controller forwards the data to every loaded provider that registered the corresponding service type. Providers implement on_mdns_service_state_change to instantiate player objects from the resolved IP addresses and TXT records.
Phase 3: On-Demand Device Lookup
Some protocols require resolving a specific device by exact name after initial discovery. The async_find_mdns_service method (lines 153-198 in controller.py) supports this synchronous-style lookup pattern used by providers like AirPlay.
The method first scans the internal Zeroconf cache for a matching entry, handling MAC address prefixes (e.g., aa:bb:cc:dd:ee:ff@DeviceName) using the RAOP_MAC_PREFIX regex. If no match exists, it registers an asyncio.Event in self._mdns_waiters and waits for the next mDNS announcement, re-checking the cache until the timeout expires or a match appears.
Implementation Details
Browser Lifecycle Management
The controller manages the browser lifecycle through two private methods:
_setup_mdns_browser(): Creates theAsyncServiceBrowserinstance duringsetup()_cancel_mdns_browser(): Handles shutdown duringclose(), supporting both the modernasync_cancel()coroutine and legacycancel()method for compatibility
This centralized approach ensures network resources are properly released when the server shuts down.
Replay for Late-Loading Providers
When a provider loads after the server has already discovered devices, the _replay_mdns_discovery method iterates over the Zeroconf cache and manually invokes on_mdns_service_state_change with ServiceStateChange.Added for each cached entry. This guarantees that late-loading providers see the same device inventory as those loaded at startup.
Code Examples
Provider Implementation: Reacting to mDNS Announcements
Providers subclassing the base provider model implement on_mdns_service_state_change to handle device arrivals. The Sonos provider demonstrates this pattern:
# music_assistant/providers/sonos/provider.py
async def on_mdns_service_state_change(self, name, state_change, info):
if state_change is ServiceStateChange.Added and info:
ip = info.parsed_addresses()[0]
port = info.port
await self._handle_new_player(ip, port)
On-Demand Lookup: Resolving Specific Devices
The AirPlay provider uses async_find_mdns_service to resolve RAOP services by exact device name:
# music_assistant/providers/airplay/provider.py
async def add_player(self, device_name: str):
raop_info = await self.mass.discovery.async_find_mdns_service(
service_type="_raop._tcp.local.",
name_filter=device_name,
timeout=10
)
if raop_info:
ip = raop_info.parsed_addresses()[0]
await self._setup_raop_player(ip, raop_info.port)
Stand-Alone Discovery Script
To manually trigger mDNS discovery outside the standard provider lifecycle:
import asyncio
from music_assistant import MusicAssistant
async def main():
ma = MusicAssistant()
await ma.start()
info = await ma.discovery.async_find_mdns_service(
service_type="_spotify-connect._tcp.local.",
name_filter="LivingRoom",
timeout=5
)
if info:
print(f"Found at {info.parsed_addresses()[0]}:{info.port}")
asyncio.run(main())
Summary
- The DiscoveryController aggregates mdns_discovery service types from all provider manifests into a single set before creating the browser.
- A single AsyncServiceBrowser instance listens for all service types, forwarding events to providers via the on_mdns_service_state_change callback.
- Per-provider asyncio Locks prevent race conditions when processing concurrent mDNS announcements.
- The async_find_mdns_service method enables synchronous-style lookups by scanning the Zeroconf cache and waiting for announcements if necessary.
- The replay mechanism ensures late-loading providers receive discovery events for devices already present on the network.
Frequently Asked Questions
What is the role of AsyncServiceBrowser in the DiscoveryController?
The AsyncServiceBrowser is the Zeroconf component that listens for mDNS service announcements on the network. The DiscoveryController creates exactly one browser instance that monitors all service types declared across all provider manifests (such as _raop._tcp.local. or _sonos._tcp.local.), aggregating these into a single listener to optimize network resource usage rather than creating separate browsers for each provider.
How does the DiscoveryController handle simultaneous mDNS events from multiple devices?
The controller maintains a dictionary of asyncio.Lock objects mapped to provider instance IDs (self._mdns_locks). When processing a state change in process_mdns_state_change, it acquires the lock specific to that provider before invoking on_mdns_service_state_change. This ensures that even if multiple devices announce simultaneously, each provider processes events sequentially without race conditions.
Can providers request mDNS lookups on demand after initial discovery?
Yes. Providers call async_find_mdns_service with a specific service type and name filter to resolve a device by exact name. This method first checks the Zeroconf cache, handles MAC address prefixes using regex parsing, and if no match exists, waits for the next network announcement using an asyncio.Event mechanism. This is essential for protocols like AirPlay that need to resolve device connections after the initial browser discovery.
What happens if a provider loads after devices have already been discovered?
The controller implements a replay mechanism through _replay_mdns_discovery. When a new provider loads, this method iterates over the existing Zeroconf cache and manually invokes on_mdns_service_state_change with ServiceStateChange.Added for each cached service type matching the provider's manifest. This ensures consistent device visibility regardless of provider initialization timing.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →