Skip to content

Comments

Add group mute support to volume controls#1404

Open
scyto wants to merge 5 commits intomusic-assistant:mainfrom
scyto:fix/group-volume-mute
Open

Add group mute support to volume controls#1404
scyto wants to merge 5 commits intomusic-assistant:mainfrom
scyto:fix/group-volume-mute

Conversation

@scyto
Copy link

@scyto scyto commented Jan 27, 2026

Summary

  • Group mute button calls new group_volume_mute server API instead of looping through individual members
  • Group mute icon reflects aggregate state (muted only when ALL members are muted)
  • Bottom bar mute icon is also group-aware
  • Child player volume sliders remain interactive when muted (server uses ATTR_MUTE_LOCK to preserve mute state during group volume changes)
  • Add playerCommandGroupVolumeMute and playerCommandGroupMuteToggle API methods
  • Fix main volume slider not calling API (upstream regression from bb0dbf9)

Context

This is the frontend counterpart to music-assistant/server#3034 which adds the cmd_group_volume_mute API endpoint and ATTR_MUTE_LOCK attribute.

The maintainer's commit d160077 added a frontend-only workaround that loops through group members calling individual mute, with a TODO: "revisit this when api/server supports group mute toggle". This PR replaces that workaround with a single server API call.

Note: This PR also fixes an upstream regression where the main volume slider's @update:model-value handler was reduced to just mainDisplayVolume = $event (no API call), making the volume slider a visual-only no-op. The fix restores the API call and makes it group-aware.

Behavior changes

Action Before After
Click group mute button Only leader muted All members mute/unmute together via server API
Group mute icon Reflected leader's state Muted only when ALL members are muted
Group volume slider (with muted members) Auto-unmuted members Muted members stay muted (ATTR_MUTE_LOCK)
Child player slider (muted) Slider disabled Slider interactive, volume changes preserved
Main volume slider Not calling API (regression) Calls group_volume or volume_set correctly
Solo player No change No change

Files changed

  • VolumeControl.vueisGroupMuted() helper, group-aware mute icon/slider logic, volume slider API fix
  • VolumeBtn.vue — group-aware mute icon in bottom bar
  • index.ts — new playerCommandGroupVolumeMute and playerCommandGroupMuteToggle API methods
  • helpers.tshandlePlayerMuteToggle uses single group API call instead of member loop
  • utils.tsgetVolumeIconComponent accepts optional isMuted override parameter

Dependencies

Requires music-assistant/server#3034 for the group_volume_mute server API.

Test plan

  • Click group mute button → all members mute
  • Click group unmute button → all members unmute
  • Mute 2 of 4 members individually → group icon shows unmuted
  • Mute all 4 members individually → group icon shows muted
  • Drag group volume slider with muted members → muted members stay muted
  • Drag muted child player slider → volume changes, player stays muted
  • Drag group volume slider → volume actually changes (API called)
  • Solo player mute/unmute behavior unchanged
  • Solo player volume slider works correctly

🤖 Generated with Claude Code

@scyto
Copy link
Author

scyto commented Jan 27, 2026

demo docker image ghcr.io/scyto/ma-server-patched:group-mute
updated with latest commits 2026-02-03

@scyto
Copy link
Author

scyto commented Feb 7, 2026

backend PR now merged

@scyto scyto force-pushed the fix/group-volume-mute branch from feff91a to e7acd72 Compare February 14, 2026 00:36
@scyto
Copy link
Author

scyto commented Feb 14, 2026

@marcelveldt updated to fix upstream bug and rebase this PR fork/branch with latest frontend changes

as alwasy built and tested, dev image is at image: ghcr.io/scyto/ma-server-patched:group-mute

@scyto scyto force-pushed the fix/group-volume-mute branch from e7acd72 to 628b6eb Compare February 14, 2026 00:49
@marcelveldt marcelveldt requested a review from stvncode February 18, 2026 19:11
@scyto
Copy link
Author

scyto commented Feb 18, 2026

@stvncode i used your feedback to help me think about how to push claude to help me be better, i hope this addresses what you were getting at, this approach seems simpler to me and removed 15 lines of duplicative and custom code from my original PR.

these commits have been tested on my live (patched) production system

Commit 1: Deduplicate isGroupMuted and fix mute toggle logic

Problem:

isGroupMuted was copy-pasted in two components (VolumeControl.vue as a function, VolumeBtn.vue as a computed), and playerCommandGroupMuteToggle in the API class used !leader.volume_muted to decide mute/unmute — which could disagree with the icon that used the all-members isGroupMuted check.

Fix:

Extracted isGroupMuted to a single shared function in helpers.ts
handlePlayerMuteToggle now calls api.playerCommandGroupVolumeMute(id, !isGroupMuted(player)) directly, so the toggle always matches the icon
Removed playerCommandGroupMuteToggle from the API class (it encoded the wrong logic)

Commit 2: Make getVolumeIconComponent group-mute aware

Problem:

Every caller of getVolumeIconComponent had to know about group mute and either pass isGroupMuted(player) explicitly or duplicate the icon-selection logic entirely (as VolumeBtn did with 15 lines of hand-rolled code). Player.vue didn't pass it at all — a bug.

Fix:

Changed getVolumeIconComponent in utils.ts to fall back to isGroupMuted(player) instead of just player.volume_muted when no explicit isMuted override is passed. This is backward-compatible since isGroupMuted returns !!player.volume_muted for non-group players.
VolumeControl.vue drops the third arg from its icon call
VolumeBtn.vue replaces 15 lines of duplicated icon logic with a single getVolumeIconComponent call
Player.vue gets correct group mute icons for free with no code change

@stvncode
Copy link
Contributor

@scyto Thanks for the changes, this looks better 😃
Can i let you rebase your pr ? You have conflicts. For the code, i have nothing to say. Will test and approve after the rebase if everything's correct

scyto and others added 3 commits February 20, 2026 17:19
- Use server's group_volume_mute API (PR #3034) instead of looping through
  individual members
- isGroupMuted() shows muted state only when ALL group members are muted
- Group volume slider disabled only when entire group is muted
- Child player sliders remain interactive when muted (server uses ATTR_MUTE_LOCK)
- Bottom bar mute icon reflects aggregate group mute state
- Fix main volume slider not calling API (upstream regression from bb0dbf9)

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Extract isGroupMuted to shared helper in helpers.ts to remove
duplication between VolumeControl.vue and VolumeBtn.vue. Fix mute
toggle using leader's volume_muted flag instead of the all-members
isGroupMuted check, which caused the toggle action to disagree with
the displayed icon state.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Move group mute check into getVolumeIconComponent so callers no longer
need to pass isGroupMuted explicitly. This removes duplicated icon
logic from VolumeBtn and fixes Player OSD not reflecting group mute.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@scyto scyto force-pushed the fix/group-volume-mute branch from c6df3b9 to 13ec301 Compare February 21, 2026 01:20
- Pass isGroupMuted(player) explicitly to getVolumeIconComponent so the
  icon reflects aggregate group mute state correctly
- Pass individual child mute state (!!childPlayer.volume_muted) for
  child player icons instead of computing group state
- Skip members with null volume_muted in isGroupMuted (players that
  don't support mute control)
- Only disable mute button for non-group players when mute_control is
  PLAYER_CONTROL_NONE

Co-Authored-By: Claude Opus 4.6 <[email protected]>
@scyto
Copy link
Author

scyto commented Feb 21, 2026

Status update — group mute now fully working

After rebasing onto the latest upstream, group mute broke for protocol-synced players (e.g., Sendspin). The root cause was on the server side: cmd_group_volume_mute used player.group_members (raw property, empty for protocol-synced players) instead of player.state.group_members (computed state with protocol-translated members). The existing cmd_group_volume already used player.state correctly — this was an inconsistency in our function.

Frontend changes in this push (commit 5f04cd7):

  • Pass isGroupMuted(player) explicitly to getVolumeIconComponent so the icon reflects aggregate group mute state correctly
  • Pass individual child mute state (!!childPlayer.volume_muted) for child player icons
  • Skip members with null volume_muted in isGroupMuted (players that don't support mute control)
  • Only disable mute button for non-group players when mute_control == PLAYER_CONTROL_NONE

Server fix required:

Both PRs are needed for full group mute functionality with protocol-synced players.

@scyto
Copy link
Author

scyto commented Feb 21, 2026

@stvncode sorry about that churn some of the other work going on by marcel broke me when i rebased and it took a while for me to get claude to figure it out

I built and tested a complete docker image ghcr.io/scyto/ma-server-patched:group-mute without the server change the child mutes worked but the the group mute didn't, i see the server change already got merged.

This was the final analysis


ME: was protocol synced players the new upstream feature that broke us?

Yes, exactly. The upstream protocol linking refactor changed the contract for how group members are accessed:

Before the refactor: player.group_members (raw) contained all members directly, so your original cmd_group_volume_mute code worked fine.

After the refactor: For protocol-synced players (like your Sendspin setup), raw group_members can be empty. The real member list now lives in player.state.group_members, which runs through __final_group_members to translate protocol player IDs to visible player IDs.

The upstream code was updated accordingly — cmd_group_volume already uses player.state.group_members. But since cmd_group_volume_mute was your new code in the PR, it didn't get that update during the refactor. After rebasing onto the refactored code, the mismatch surfaced.

ME: will group mute still work for non-protocol synced players?

Yes. The player.state.group_members property is a superset of player.group_members — it includes everything from the raw property plus protocol-translated members.

For non-protocol-synced players, the __final_group_members logic in player.py essentially passes through the raw group_members unchanged (since there are no protocol IDs to translate). So player.state.group_members returns the same list as player.group_members.

This is also confirmed by the fact that cmd_group_volume (the volume slider for groups) has been using player.state.group_members all along and works for all player types — our fix just makes cmd_group_volume_mute consistent with that existing pattern.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants