How Music Assistant Handles Real-Time Updates and Events: Event Bus Architecture Explained

Music Assistant uses an internal event bus combined with a WebSocket API to push real-time updates to connected clients, enabling instant state synchronization across the ecosystem.

Music Assistant, the open-source media server (music-assistant/server), implements a robust publish-subscribe mechanism to handle real-time updates and events. The architecture centers on a core event bus that coordinates between internal controllers and external clients through WebSocket connections defined in the server codebase.

The Core Event Bus Architecture

The MusicAssistant class in music_assistant/mass.py serves as the central nervous system for real-time updates. It maintains a private set of subscribers (_subscribers) that receive notifications whenever state changes occur within the system.

Subscribing to Events

Clients and internal components register interest in specific events using the subscribe() method. This method accepts a callable (synchronous or asynchronous), along with optional filters for event types and object IDs.


# music_assistant/mass.py

def subscribe(
    self,
    cb_func: EventCallBackType,
    event_filter: EventType | tuple[EventType, ...] | None = None,
    id_filter: str | tuple[str, ...] | None = None,
) -> Callable[[], None]:
    # … registers the listener in self._subscribers …

The method returns an unsubscribe function that removes the callback when invoked.

Emitting Events with signal_event

When components need to broadcast changes, they invoke signal_event() on the MusicAssistant instance. This method constructs a MassEvent object and forwards it to all matching subscribers.


# music_assistant/mass.py

def signal_event(
    self,
    event: EventType,
    object_id: str | None = None,
    data: Any = None,
) -> None:
    # … builds MassEvent and notifies all subscribers …

The EventType enum, defined in music_assistant_models.enums, enumerates all possible events including PLAYER_UPDATED, PLAYBACK_STARTED, PROVIDERS_UPDATED, and PLAYER_VOLUME_CHANGED.

WebSocket Bridge for Client Delivery

External clients connect via the WebSocket API implemented in music_assistant/controllers/webserver/websocket_client.py. The WebsocketClientHandler class manages authenticated connections and bridges the internal event bus to the client socket.

Authentication and Subscription Setup

After successful authentication, the handler registers an internal forwarder callback through _subscribe_to_events(). This callback captures all events (or a filtered subset) and pushes them to the connected client.


# music_assistant/controllers/webserver/websocket_client.py

async def _handle_auth_command(self, msg: CommandMessage) -> None:
    # … authentication logic …

    self._subscribe_to_events()          # ← registers the event forwarder

# inside WebsocketClientHandler

def _subscribe_to_events(self) -> None:
    # Subscribe to all events (or a subset you care about)

    self._events_unsub_callback = self.mass.subscribe(
        self._send_message_sync,               # forward event to client

        event_filter=None,                     # None = all events

        id_filter=None,
    )

Synchronous Forwarding for Performance

The WebSocket handler uses _send_message_sync() rather than asynchronous methods to avoid executor overhead for large JSON payloads. This synchronous write places the encoded MassEvent directly into the client's output queue (_to_write), ensuring immediate delivery without blocking the main event loop.

End-to-End Event Flow Example

Consider a volume change scenario:

  1. PlayerController.set_volume() updates the player state and calls self.mass.signal_event(EventType.PLAYER_VOLUME_CHANGED, player_id, new_volume).
  2. The core event bus iterates through _subscribers and invokes matching callbacks.
  3. The WebsocketClientHandler forwarder receives the event and calls _send_message_sync().
  4. The client receives a JSON-encoded message:
{
  "event": "PLAYER_UPDATED",
  "object_id": "my_player_id",
  "data": {"state": "playing", "track": "Song A"}
}

Implementation Examples

Emitting Events from Custom Components

Custom providers or controllers can emit events using the following pattern:


# inside any controller or provider

from music_assistant_models.enums import EventType

def my_update(self):
    # …some state change…

    self.mass.signal_event(
        EventType.CORE_STATE_UPDATED,
        object_id=self.instance_id,
        data={"new_state": "running"},
    )

Programmatic Subscription

Background tasks or providers can subscribe to specific events programmatically:


# Typical usage inside a provider or background task

def on_player_update(event: MassEvent) -> None:
    print(f"Player {event.object_id} changed: {event.data}")

# Register the callback – only PLAYER_UPDATED events for a specific player

unsubscribe = mass.subscribe(
    on_player_update,
    event_filter=(EventType.PLAYER_UPDATED,),
    id_filter=("my_player_id",),
)

# When you no longer need the callback

unsubscribe()

Summary

  • Centralized event bus: The MusicAssistant class in music_assistant/mass.py manages all real-time updates through subscribe() and signal_event() methods.
  • Type-safe events: The EventType enum defines all possible events, from player state changes to provider updates.
  • WebSocket integration: The WebsocketClientHandler automatically forwards events to authenticated clients using synchronous message sending for optimal performance.
  • Flexible filtering: Subscribers can filter by event type and object ID to receive only relevant updates.
  • Thread safety: The signal_event() method validates calls originate from the main asyncio loop, ensuring safe operation across threads.

Frequently Asked Questions

How does Music Assistant ensure thread safety when signaling events?

The signal_event() method in music_assistant/mass.py validates that calls originate from the main asyncio loop thread. This safety check prevents race conditions when components running in different threads need to emit events, ensuring the event bus remains consistent.

What EventType values are available for real-time updates?

The EventType enum in music_assistant_models/enums.py defines all available events, including PLAYER_UPDATED, PLAYER_VOLUME_CHANGED, PLAYBACK_STARTED, PLAYBACK_PAUSED, PROVIDERS_UPDATED, and CORE_STATE_UPDATED. Controllers emit these events whenever corresponding state changes occur.

Can I filter events for specific objects or types?

Yes. The subscribe() method accepts event_filter and id_filter parameters. Pass a single EventType or tuple of types to filter events, and provide an object ID or tuple of IDs to restrict updates to specific entities. Pass None to receive all events.

Why does the WebSocket handler use synchronous message sending?

The WebsocketClientHandler uses _send_message_sync() to forward events because it avoids the overhead of the executor typically used for large JSON payloads. Since event messages are small and the method runs in the request thread, this approach delivers real-time updates immediately to the client's output queue without blocking the async event loop.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →