Conversation
… and user count - Show Discord server thumbnail (guild icon) on voice channel buttons; fall back to server name label when no icon is available. Label position (top / center / bottom) is user-configurable. - When connected to the configured channel, render a composited grid of up to 4 circular user avatars. A green speaking ring animates around each avatar in real time as those users speak (SPEAKING_START/STOP events). - 'Show my own avatar' toggle: include or exclude your own avatar from the grid while still counting yourself toward the user total. - Observer mode (not joined): display a user-count badge in a configurable corner (top-left / top-right / bottom-left / bottom-right) over the guild icon or fallback voice-channel icon so you can see channel occupancy at a glance without being connected. - Live updates: VOICE_STATE_CREATE/DELETE events trigger a GET_CHANNEL re-fetch rather than directly mutating per-button state, preventing cross-button counter contamination when multiple ChangeVoiceChannel buttons are on the same deck. - Re-subscribe to voice state events after leaving a channel, since Discord silently drops those subscriptions on leave. - Guild info fetch is decoupled from voice-state subscription so the server thumbnail populates immediately on launch even if the subscription is not yet active.
ImDevinC
left a comment
There was a problem hiding this comment.
Thanks for the PR. To help keep things organized, I think it would be best to move most of the logic in these actions to a helper file of some kind as it really muddies up the logic of "the action" vs everything else
- Move HTTP requests out of the action and into backend.py. New fetch_avatar() and fetch_guild_icon() methods on Backend perform the requests.get calls and return raw bytes; the action's existing thread-pool tasks call these methods and decode the bytes into PIL Images locally. This keeps blocking I/O out of the action layer without adding new events or infrastructure. - Scope label clearing to the configured position only. _render_button() previously blanked all three label slots on every render, which would erase any labels the user placed in the other positions. Now only the slot managed by this action (the configured label_position) is cleared.
|
…isplay - Tap touchbar to toggle mute on the displayed user (self-mute uses mic mute; other users use local per-user mute) - Mute state shown as dimmed overlay with red slash on avatar - Both UserVolume and ChangeVoiceChannel now render avatars in an overlapping stack instead of a grid, with the active user (selected or speaking) moved to the centre front - Detect current voice channel on startup so actions reflect state when StreamController launches mid-session - Expose current user avatar hash from backend auth response so self avatars render correctly for users with profile pictures
ce34a38 to
f6673f6
Compare
There was a problem hiding this comment.
Pull request overview
Adds a “dynamic voice channel” button experience by rendering live Discord voice channel state (guild thumbnail/name, user count, avatars, and speaking indicators) directly onto the Stream Deck key face.
Changes:
- Introduces new RPC/event plumbing for guild fetches and speaking + voice-state events.
- Enhances ChangeVoiceChannel to render guild icon/name, live user-count badge (observer mode), and avatar/speaking overlays (connected mode).
- Updates UserVolume to show avatar stacks + speaking state and adds new configuration/UI options.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
settings.py |
Guards settings UI when backend is not yet initialized/authenticated. |
main.py |
Adds new EventHolders for GET_GUILD, speaking, and voice-state events; hardens backend setup. |
discordrpc/asyncdiscord.py |
Adds get_guild(guild_id) RPC command helper. |
backend.py |
Adds guild fetch, speaking subscribe/unsubscribe, current-user avatar tracking, and CDN fetch helpers. |
actions/UserVolume.py |
Adds avatar/speaking rendering and a new “control self mic volume” toggle (plus mute toggle). |
actions/ChangeVoiceChannel.py |
Major UI/rendering upgrade for live channel display, guild icon/name, speaking rings, and user-count badge. |
actions/avatar_utils.py |
New shared image/placeholder/ring/badge composition utilities for avatar rendering. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
- Remove dead image_bytes branch and use backend.fetch_avatar() in ChangeVoiceChannel._fetch_avatar - Clean up _fetching_avatars on all early-return paths - Unsubscribe voice states and speaking before clearing state in _on_channel_id_changed - Remove unused _current_channel and show_self assignments - Implement Backend.set_input_volume for self mic volume control
feat(voice): Live voice channel display, avatars, speaking rings & user volume controls
Summary
Enhances the Change Voice Channel and User Volume actions with live visual feedback — overlapping avatar stacks, speaking indicators, mute overlays, and real-time user tracking — similar to native Discord Stream Deck integrations.
Change Voice Channel
Server thumbnail / name
GET_GUILD.Connected mode
SPEAKING_START/SPEAKING_STOPevents.Observer mode — user count badge
Startup detection
GET_SELECTED_VOICE_CHANNEL, so the display reflects state correctly when StreamController launches mid-session.User Volume
Overlapping avatar display
Tap-to-mute
Mute overlay
Self-volume control
Shared avatar utilities (
avatar_utils.py)Rendering logic extracted into a shared module used by both actions:
compose_overlapping_avatars()— stacks avatars with configurable front-user positioningCorrectness
VOICE_STATE_CREATE/DELETEevent data from Discord contains nochannel_id, so naively updating_userson every event caused all buttons to show the wrong count. Events now trigger aGET_CHANNELre-fetch for each button's own watched channel;_on_get_channelfully reconciles the user list from the authoritativevoice_statessnapshot.VOICE_STATE_*subscriptions when the local user leaves a channel. The button now resets_watching_channel_idon leave and re-subscribes immediately, keeping the observer-mode count live.GET_SELECTED_VOICE_CHANNELresponse data is normalised to matchVOICE_CHANNEL_SELECTevent format, so actions initialise correctly on launch.New backend / event infrastructure
discordrpc/asyncdiscord.pyget_guild(guild_id)backend.pyget_guild(),subscribe_speaking()/unsubscribe_speaking(),set_user_mute(),current_user_avatarproperty, dispatch routing forGET_GUILD,SPEAKING_START/STOP,VOICE_STATE_*main.pyGET_GUILD,SPEAKING_START,SPEAKING_STOP,VOICE_STATE_CREATE,VOICE_STATE_DELETEConfiguration options
top/center/bottom)bottomontop-left/top-right/bottom-left/bottom-right)bottom-rightoff