Skip to content

Fall back to protocol-scoped key in get_player_config_value#3885

Open
flomotlik wants to merge 1 commit into
music-assistant:devfrom
flomotlik:fix/protocol-namespaced-config-fallback
Open

Fall back to protocol-scoped key in get_player_config_value#3885
flomotlik wants to merge 1 commit into
music-assistant:devfrom
flomotlik:fix/protocol-namespaced-config-fallback

Conversation

@flomotlik
Copy link
Copy Markdown

Problem

Universal Group (UGP) streams produce no audio for any DLNA-backed member (Teufel/Raumfeld, generic UPnP) because every member's HTTP fetch from the UGP stream server raises KeyError. Reproducible against a Universal Group whose members are universal_players wrapping DLNA protocol players.

File "music_assistant/providers/universal_group/player.py", line 394, in _serve_ugp_stream
    output_format = await self.mass.streams.audio.get_output_format(
File "music_assistant/controllers/streams/audio.py", line 1151
    player.player_id, CONF_SAMPLE_RATES, unpack_splitted_values=True
File "music_assistant/controllers/config.py", line 733, in get_player_config_value
    raise KeyError(msg)
KeyError: 'Config key sample_rates not found for player up501e2d1d53ca'

The same KeyError fires for CONF_HTTP_PROFILE immediately afterwards in _serve_ugp_stream itself. Both lookups go through ConfigController.get_player_config_value().

Root cause

Since 2.8.0 (CONF_PROTOCOL_KEY_SPLITTER = "||protocol||"), universal_player wrappers store per-protocol settings (sample_rates, http_profile, etc.) under <protocol_id>||protocol||<key> rather than at the top level of the player config. Every caller still asks for the bare key, so the top-level lookup fails. The UGP stream handler is the most visible victim, but the latent shape is general.

Live config of an affected universal_player (Teufel One M wrapping DLNA):

uuid:640e08e3-…||protocol||sample_rates      = ['44100||16', '48000||16']
uuid:640e08e3-…||protocol||http_profile      = 'no_content_length'

No top-level sample_rates or http_profile exists, so get_player_config_value(player_id, 'sample_rates') raises.

Fix

In ConfigController.get_player_config_value, when the bare key is missing and the player has an active_output_protocol, retry with f"{active_protocol}{CONF_PROTOCOL_KEY_SPLITTER}{key}". Original default / KeyError semantics are preserved when no protocol is active.

A single-point fix at the config layer covers every caller (get_output_format, select_flow_format, select_pcm_format, _serve_ugp_stream's http_profile lookup, and any future callers).

Verification

Applied locally on 2.8.7, then created a UGP with two DLNA-wrapped members and played a Spotify playlist via music_assistant.play_media. Pre-patch: KeyError stack trace per member fetch, group reports state=playing but speakers silent. Post-patch: no errors in logs, both speakers audible. Same result for a single-member UGP.

Happy to add a test if maintainers want one — get_player_config_value doesn't currently have direct coverage, so I'd add a fixture rather than extending an existing test.

Universal players that wrap a protocol player (DLNA, Sonos, etc.) store
per-protocol settings such as `sample_rates` and `http_profile` under
`<protocol_id>||protocol||<key>` rather than at the top level of the
player config. The universal_group flow stream handler calls
`get_player_config_value(player_id, CONF_SAMPLE_RATES, ...)` (and the
same for CONF_HTTP_PROFILE) against the universal_player's id, hits the
missing top-level key, and raises KeyError. The Stream server returns
HTTP 500 to every DLNA member fetching the UGP audio, so no audio is
played for any DLNA-backed group member.

This is reproducible with a Universal Group whose members are
DLNA-wrapped universal_players (e.g. Teufel/Raumfeld, generic UPnP).
The traceback ends in `_serve_ugp_stream` →
`StreamsAudio.get_output_format` → `get_player_config_value(...)` →
`KeyError: 'Config key sample_rates not found for player <id>'`.

When the bare key is missing and the player has an
`active_output_protocol`, retry with the protocol-namespaced key. All
existing behavior (`default`, `KeyError`) is preserved when no protocol
is active. The fix lives in the single chokepoint every caller goes
through, so each latent KeyError of the same shape (sample_rates,
http_profile, …) is fixed in one place.
@flomotlik
Copy link
Copy Markdown
Author

Alternative we tested — UGP-localized fix (rejected after testing)

Before settling on the config-layer fallback, I explored the more surgical option: change the UGP stream URL to address the member's active_output_protocol player ID instead of the universal_player ID, so _serve_ugp_stream's sample_rates / http_profile lookups resolve against the protocol player's config (which has those keys at the top level) without needing a fallback at all.

Two-line patch in music_assistant/providers/universal_group/player.py:

-uri=f"{base_url}?player_id={member.player_id}",
+uri=f"{base_url}?player_id={member.active_output_protocol or member.player_id}",

(same pattern in set_members).

Tested locally; here's what I observed

Scenario Result
Steady-state play, active_output_protocol already populated Works, audible.
First play after MA restart active_output_protocol is None on every member at the time the URL is built — it gets populated as a side effect of the subsequent _handle_play_media call. The patch silently falls back to member.player_id, the lookup raises KeyError, no audio.
Any subsequent play in the same session Works.

So this approach makes the bug intermittent rather than fixing it: every container restart / first-play-after-boot is silent, the second press works. From a user's seat: "press Styrbar, no music, press again, music plays" after every MA update — not acceptable for the user-visible Styrbar paths that motivated the fix.

The config-layer fallback in this PR doesn't have the race because it runs at lookup time (when the DLNA member fetches the stream URL), by which point _handle_play_media has already selected the protocol.

A heuristic-strengthened UGP-localized version (fall through to preferred_output_protocol config or the first-available output_protocols[]) might cover the cold-start case but starts to duplicate the protocol-selection logic that lives in _handle_play_media. Happy to switch to that approach if you'd prefer the smaller blast radius — let me know, or push back if there's a cleaner location for the fix that I'm not seeing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant