# How Player Providers Report Their State and Handle Media URIs in Music Assistant

> Learn how player providers in Music Assistant report state and handle media URIs. Discover the update_state and set_current_media methods. Optimize your media playback.

- Repository: [Music Assistant/server](https://github.com/music-assistant/server)
- Tags: internals
- Published: 2026-06-17

---

**Player providers in Music Assistant report state by calling `self.update_state()` after modifying internal attributes, and handle media URIs by storing them via `self.set_current_media()` before delegating to the underlying transport protocol.**

In the `music-assistant/server` architecture, every player provider—whether for Sonos, AirPlay, or MPD—inherits from the core `Player` model defined in [`music_assistant/models/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/player.py). Understanding how these player providers report their state and handle media URIs is essential for developing custom integrations or debugging playback synchronization issues. The system requires explicit state updates through standardized methods that feed the central `PlayerController`.

## Understanding the Core Player Model

The foundation of all player providers resides in **[`music_assistant/models/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/player.py)**, which defines the abstract `Player` base class. This class provides the infrastructure for **state reporting** and **media management**, requiring concrete providers to implement device-specific logic while delegating state updates to standardized methods. The `PlayerState` dataclass represents the canonical state snapshot that gets propagated to the controller and exposed via the API.

## How Player Providers Report State

State reporting follows a strict pattern where providers modify internal attributes—such as `_attr_playback_state` or `_attr_volume_level`—then explicitly notify the system via `update_state()`. This method validates the execution context, computes state changes, and signals the central controller when differences are detected.

### The update_state() Implementation

Located at lines 1309–1350 in [`music_assistant/models/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/player.py), the `update_state()` method performs several critical functions before broadcasting changes:

```python
def update_state(self, force_update: bool = False, signal_event: bool = True) -> None:
    """Update the PlayerState from the current state of the player."""
    self.mass.verify_event_loop_thread("player.update_state")
    self._cache.clear()
    
    prev_media_checksum = self._get_player_media_checksum()
    changed_values = self.__calculate_player_state()
    
    if prev_media_checksum != self._get_player_media_checksum():
        self.mass.call_later(1, self._on_player_media_updated, task_id=...)
    
    if len(changed_values) == 0 and not force_update:
        return
    
    if signal_event:
        self.mass.players.signal_player_state_update(self, changed_values)

```

The method first verifies it runs on the proper event loop thread using `mass.verify_event_loop_thread()`, then clears cached properties. It calculates a media checksum to detect URI changes, scheduling a debounced callback via `call_later()` when media transitions occur. If `changed_values` is empty and `force_update` is `False`, the method returns early to avoid unnecessary broadcasts. Otherwise, it signals the `PlayerController` through `mass.players.signal_player_state_update()`.

### Triggering State Updates from Providers

Concrete providers must explicitly invoke `self.update_state()` after modifying any state attributes. For example, in an MPD provider implementation found in the provider-specific [`player.py`](https://github.com/music-assistant/server/blob/main/player.py) files:

```python
async def play(self) -> None:
    await self._client.play()
    self._attr_playback_state = PlaybackState.PLAYING
    self.update_state()  # Report the new state to the controller

```

This pattern applies universally across all provider implementations, including Sonos and AirPlay variants located in `providers/*/player.py`. The provider manages the device communication, updates its internal attribute cache, then notifies the system to synchronize the global state.

## Handling Media URIs in Player Providers

When the core controller requests playback, it passes a `PlayerMedia` object containing the URI and metadata to the provider's `play_media()` method. The provider stores this information using `set_current_media()` before delegating to the underlying transport layer.

### The set_current_media() Helper

The `set_current_media()` method, implemented at lines 1352–1375 in [`music_assistant/models/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/player.py), populates the internal `_attr_current_media` attribute:

```python
def set_current_media(self, uri: str, media_type: MediaType = MediaType.UNKNOWN, 
                     title: str | None = None, artist: str | None = None, ...) -> None:
    if self._attr_current_media is None or clear_all:
        self._attr_current_media = PlayerMedia(uri=uri, media_type=media_type)
    self._attr_current_media.uri = uri
    if media_type != MediaType.UNKNOWN:
        self._attr_current_media.media_type = media_type
    if title:
        self._attr_current_media.title = title
    if artist:
        self._attr_current_media.artist = artist
    # ... additional metadata fields

```

This helper ensures the `PlayerMedia` instance always reflects the current URI while preserving or updating optional metadata such as title, artist, album, and image URLs.

### Typical play_media Implementation Flow

Concrete providers follow a consistent pattern when implementing `play_media()`:

```python
async def play_media(self, media: PlayerMedia) -> None:
    # 1. Store URI and metadata on the player instance

    self.set_current_media(
        uri=media.uri,
        media_type=media.media_type,
        title=media.title,
        artist=media.artist,
        album=media.album,
        image_url=media.image_url,
    )
    
    # 2. Delegate to transport-specific implementation

    await self._transport.send_play(media.uri)
    
    # 3. Update playback state and report the change

    self._attr_playback_state = PlaybackState.PLAYING
    self.update_state()

```

Variations exist only in the transport layer—whether using HTTP APIs for Sonos, `pympd` for MPD, or AirPlay sockets—while the state management logic remains identical across all providers.

## Complete Workflow Summary

The interaction between the core controller and player providers follows this sequence:

1. **Media request arrives** from the API or queue manager
2. **Core calls** `provider.play_media(media)` with a populated `PlayerMedia` object
3. **Provider stores the URI** via `self.set_current_media()` to update internal metadata
4. **Provider communicates** with the physical device through protocol-specific transport
5. **Provider updates attributes** like `_attr_playback_state` and elapsed time
6. **Provider calls** `self.update_state()` to calculate changes and signal the controller
7. **Controller broadcasts** the new `PlayerState` to connected clients and Home Assistant

## Summary

- **Player providers** inherit from the core `Player` class in [`music_assistant/models/player.py`](https://github.com/music-assistant/server/blob/main/music_assistant/models/player.py) and must explicitly report state changes.
- **`update_state()`** at lines 1309–1350 validates thread context, detects media changes via checksum comparison, and signals the `PlayerController` only when necessary.
- **`set_current_media()`** at lines 1352–1375 provides the standard mechanism for storing URIs and metadata before transport delegation.
- **Concrete implementations** in `providers/*/player.py` follow identical patterns: store media, talk to device, update attributes, call `update_state()`.
- **State consistency** is guaranteed because only `update_state()` propagates changes to the central controller, ensuring a single source of truth for the API and UI.

## Frequently Asked Questions

### When should a player provider call update_state()?

A provider must call `update_state()` immediately after modifying any state-tracking attributes such as `_attr_playback_state`, `_attr_volume_level`, or `_attr_current_media`. This includes after completing transport operations like `play()`, `pause()`, or `volume_set()`, ensuring the central controller receives a synchronized snapshot of the device's actual condition.

### How does Music Assistant detect media changes for UI updates?

The `update_state()` method calculates a checksum of the current media before and after state calculation using `_get_player_media_checksum()`. When the checksums differ, indicating a new URI or metadata, the method schedules a debounced callback via `mass.call_later()` to trigger UI refreshes without overwhelming the event loop with rapid successive changes.

### What happens if update_state() is called from the wrong thread?

The method invokes `self.mass.verify_event_loop_thread("player.update_state")` at entry, which raises an exception if the call originates outside the proper event loop context. This safety mechanism prevents race conditions and ensures thread-safe state management throughout the Music Assistant server.

### Can providers force a state update even when nothing has changed?

Yes, by passing `force_update=True` to `update_state()`, providers can trigger a broadcast of the current state regardless of whether `__calculate_player_state()` detected modifications. This is useful for initialization sequences or recovery scenarios where the provider needs to ensure the controller has the latest data even if internal attribute hashes remain identical.