Music Assistant Discovery Controller: mDNS and Zeroconf Player Detection Explained
The Music Assistant DiscoveryController uses a shared AsyncZeroconf instance to browse mDNS service records and broadcasts the server as a _mass._tcp.local. service, automatically dispatching state changes to providers that declare supported service types in their manifests.
The Music Assistant server repository implements a sophisticated network discovery mechanism that enables automatic detection of media players. The DiscoveryController, located in music_assistant/controllers/discovery/controller.py, orchestrates both mDNS/Zeroconf and SSDP/UPnP protocols to discover devices like Chromecast, AirPlay, and Sonos players without manual configuration.
How the Discovery Controller Works
The DiscoveryController combines two complementary protocols to discover network devices. mDNS/Zeroconf discovers service records advertised by devices (such as _raop._tcp.local. for AirPlay or _googlecast._tcp.local. for Chromecast) using the zeroconf Python package. SSDP/UPnP handles broadcast-based discovery of DLNA devices using async_upnp_client.
The controller is instantiated once at server start and performs five core responsibilities: creating a shared AsyncZeroconf instance, registering the Music Assistant server on the network, running a global AsyncServiceBrowser, dispatching mDNS state changes to providers, and maintaining cache-aware lookups for reliable player resolution.
Zeroconf Service Creation and Interface Selection
The _create_aiozc method initializes the AsyncZeroconf instance based on network adapter configuration. The helper function get_zeroconf_args in music_assistant/helpers/util.py inspects host network adapters via ifaddr.get_adapters() to determine whether Zeroconf operates in IPv4-only, IPv6-only, or dual-stack mode.
On macOS and FreeBSD, the controller forces IPv4-only mode because the IPVersion.All socket cannot join IPv4 multicast groups on these platforms. When the configuration entry CONF_ZEROCONF_INTERFACES is set to "all", the function returns an explicit list of concrete IP addresses rather than using InterfaceChoice.Default, ensuring devices on secondary network adapters are discovered.
Global mDNS Service Browser
The _setup_mdns_browser method creates a single AsyncServiceBrowser that listens for all service types required by loaded providers. When a provider declares mdns_discovery entries in its manifest (such as ["_googlecast._tcp.local."] for Chromecast), the browser automatically includes these service types.
State changes trigger the _on_mdns_service_state_change callback, which processes events at a verbose log level. The callback creates an AsyncServiceInfo object for added or updated services, requests details with a 3000ms timeout, and dispatches the event to all registered providers via their on_mdns_service_state_change coroutine.
Provider Integration and Service Registration
Providers declare their discovery requirements in their manifest files using the mdns_discovery key. For example, the Chromecast provider includes "mdns_discovery": ["_googlecast._tcp.local."] in its manifest configuration.
When a provider loads, the controller executes _replay_mdns_discovery to ensure cached entries for already-seen devices are immediately reported. This guarantees that devices discovered before the provider initialized are not missed. The controller also manages the _mass._tcp.local. service registration via _register_mass_service, announcing the Music Assistant server to the network.
Manual Service Discovery and Caching
The async_find_mdns_service method enables synchronous queries against the Zeroconf cache with optional waiting for new events. This supports reliable "player-by-name" lookups, such as finding a specific AirPlay device by its service name.
The method accepts a service_type, optional name_filter, and timeout parameter. When querying the cache, it extracts IP addresses using get_primary_ip_address_from_zeroconf and ports via helper functions from music_assistant/helpers/util.py.
Code Examples
Querying a specific player by name
# Assume `mass` is the MusicAssistant instance
service_type = "_raop._tcp.local."
name = "LivingRoomTV"
info = await mass.discovery.async_find_mdns_service(
service_type=service_type,
name_filter=name,
timeout=5.0,
)
if info:
from music_assistant.helpers.util import get_primary_ip_address_from_zeroconf, get_port_from_zeroconf
ip = get_primary_ip_address_from_zeroconf(info)
port = get_port_from_zeroconf(info)
print(f"Found {name} at {ip}:{port}")
else:
print("Device not found")
Creating a custom provider with mDNS discovery
// myprovider/manifest.json
{
"name": "MyDevice",
"mdns_discovery": ["_mydevice._tcp.local."]
}
# myprovider/__init__.py
from zeroconf import ServiceStateChange
async def on_mdns_service_state_change(self, name, state, info):
if state is ServiceStateChange.Added:
from music_assistant.helpers.util import get_primary_ip_address_from_zeroconf
ip = get_primary_ip_address_from_zeroconf(info)
self.log.info("MyDevice discovered at %s", ip)
elif state is ServiceStateChange.Removed:
self.log.info("MyDevice %s left the network", name)
Configuring all interfaces for discovery
# In Music Assistant configuration
zeroconf_interfaces: "all"
Setting CONF_ZEROCONF_INTERFACES to "all" forces get_zeroconf_args(use_all_interfaces=True) to return an explicit list of IP addresses, avoiding the default InterfaceChoice.Default that might miss devices on secondary adapters.
Summary
- The DiscoveryController in
music_assistant/controllers/discovery/controller.pymanages both mDNS/Zeroconf and SSDP discovery protocols. - Interface selection automatically adapts to IPv4, IPv6, or dual-stack based on system adapters, with macOS/FreeBSD forced to IPv4-only mode.
- Providers declare discovery needs via
mdns_discoveryentries in their manifests, receiving callbacks viaon_mdns_service_state_change. - The
async_find_mdns_servicemethod enables cache-aware lookups by service name with configurable timeouts. - The controller replays cached mDNS entries when providers load to ensure no devices are missed during startup.
Frequently Asked Questions
How does Music Assistant handle mDNS discovery on multi-homed systems?
On systems with multiple network interfaces, the get_zeroconf_args function in music_assistant/helpers/util.py inspects adapters via ifaddr.get_adapters(). When CONF_ZEROCONF_INTERFACES is set to "all", it returns an explicit list of IP addresses for each interface rather than using InterfaceChoice.Default, ensuring Zeroconf listens on all available networks.
What happens when a provider loads after devices have already been discovered?
The controller executes _replay_mdns_discovery when a provider initializes, which scans the Zeroconf cache and dispatches cached service entries to the provider's on_mdns_service_state_change method. This guarantees that devices discovered before the provider loaded are immediately reported without waiting for new mDNS broadcasts.
How can I manually look up a specific player's IP address using the DiscoveryController?
Use the async_find_mdns_service method with the appropriate service type and name filter. The method queries the Zeroconf cache synchronously and waits up to the specified timeout for new entries if the device isn't cached. Extract the IP using get_primary_ip_address_from_zeroconf from music_assistant/helpers/util.py.
Why does Music Assistant force IPv4-only mode on macOS and FreeBSD?
According to the source code in music_assistant/helpers/util.py, the IPVersion.All socket option cannot properly join IPv4 multicast groups on macOS and FreeBSD systems. To ensure reliable player detection on these platforms, get_zeroconf_args forces IPv4-only mode when it detects these operating systems.
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 →