/
/
/
# What does this implement/fix? `Player.volume_control` answers "where should a volume command go right now", but a few callers were using it to answer a different question: "which interface will actually carry the audio for the stream about to start". When nothing is streaming yet, the first question falls back to a fixed per-domain order (cast, then DLNA, then AirPlay, then Sendspin), so the second question got the wrong answer — and only got the right one if the caller happened to run after the output was marked active. That ordering dependency is what made an AirPlay speaker join a group at full volume (#5668). That fix reordered the calls; this one separates the two questions so the order stops mattering. It also closes the same gap on the mute side, which had no fix yet: an AirPlay speaker's mute is a latch that is only ever cleared by an explicit unmute. Mute the player while it streams over AirPlay, then stop playback, and the parent starts reporting the idle Cast side's (unmuted) state — the mute disappears from the UI with nothing left to unmute, while the speaker itself stays latched. The next stream then starts silent and ignores volume changes, and an announcement sent to it plays inaudibly. On Apple devices the device-feedback path that would normally clear it is disabled, so it never recovers. **Related issue (if applicable):** - follow-up to #5668 ## Types of changes - [ ] Bugfix (non-breaking change which fixes an issue) — `bugfix` - [ ] New feature (non-breaking change which adds functionality) — `new-feature` - [ ] Enhancement to an existing feature — `enhancement` - [ ] New music/player/metadata/plugin provider — `new-provider` - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) — `breaking-change` - [ ] Refactor (no behaviour change) — `refactor` - [ ] Documentation only — `documentation` - [x] Maintenance / chore — `maintenance` - [ ] CI / workflow change — `ci` - [ ] Dependencies bump — `dependencies` ### Changes - Added `Player.volume_control_for_output()` and `Player.mute_control_for_output()`, which resolve the control for one named output instead of the ambient one. No fallback to a sibling interface: if the output carrying the audio doesn't own the control, nothing does. - AirPlay's stream-start sync now uses those, so its volume decision no longer depends on the parent already pointing at it. - That sync now covers mute too, clearing a stale latch left behind by a control that doesn't own this output. - `volume_control` / `mute_control` themselves are unchanged — every other caller genuinely wants the ambient answer. ## Checklist - [x] The code change is tested and works locally. - [x] `pre-commit run --all-files` passes. - [x] `pytest` passes, and tests have been added/updated under `tests/` where applicable. - [ ] For changes to shared models, the companion PR in `music-assistant/models` is linked. - [ ] For changes affecting the UI, the companion PR in `music-assistant/frontend` is linked. - [x] I have read and complied with the project's [AI Policy](https://github.com/music-assistant/.github/blob/main/AI_POLICY.md) for any AI-assisted contributions. - [ ] I have [raised a PR against the documentation repository](https://github.com/music-assistant/music-assistant.io/blob/main/CONTRIBUTING.md) targeting the main or beta branch as appropriate.