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
ImDevinC
left a comment
There was a problem hiding this comment.
Couple things to help keep code clean
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
I just had a chance to test this today, and when I joined a voice channel, it immediately crashed StreamController. My setup was as follows:
|
…ptions UserVolume: - Replace set_top/center/bottom_label() calls with set_label(position=) to avoid ActionCore state lookup errors (state: 1) during voice events - Add _set_label_if_changed() helper that only calls set_label() when the text actually changes, eliminating volume % flickering during speaking updates - Guard on_ready() event subscriptions with _events_connected flag to prevent duplicate callbacks being stacked on subsequent on_ready() calls, which was the cause of repeated state errors on channel join/leave - Guard request_current_voice_channel() with _requested_initial_voice_state so it fires only once per action instance - Preserve self mic input volume in _on_voice_state_update: skip volume field for is_self entries so incoming voice-state data cannot overwrite the locally-managed input volume controlled via set_input_volume ChangeVoiceChannel: - Store channel name from GET_CHANNEL response for use as a button label - Add 'Channel name label position' ComboRow (default: none) to independently position channel name alongside server name - Add 'none' option to server name label position ComboRow - Add 'Server name font size' and 'Channel name font size' SpinRows (6-32pt) - Move label rendering outside the connected/observer branch so guild name is displayed in all states, not only the empty-channel fallback - Add on_change callbacks to all label config rows so position, font size, show-self, and badge corner changes re-render the button immediately - Reset _channel_name on channel ID change alongside other cached guild state main.py: - Mark ThreadPoolExecutor worker threads as daemon threads via initializer so they do not block StreamController's shutdown join loop, restoring the clean exit path and 'Have a nice day' message on Ctrl+C
Follow-up to 017610c. - main.py: remove the ThreadPoolExecutor thread initializer that attempted to set daemon=True at worker startup. - actions/ChangeVoiceChannel.py: guard guild icon task submission in _on_get_guild with try/except and fall back to synchronous fetch when submit fails. This keeps GET_GUILD callbacks from failing hard if the pool cannot accept a new task, while preserving thumbnail rendering behavior.
Yea looks like some of the refactors or new features made it a lot less stable. I tried to address these in the last 2 commits. It's working for me now with the FlatPak version of StreamController. If it still happens for you, let me know if you're on the FlatPak or just running it natively. |
… toggle behavior - Call _start_watching_configured_channel() during render to recover from startup event ordering and populate guild/channel labels + thumbnail without requiring an initial button press. - Update _on_change_channel toggle logic to disconnect only when already in the configured channel; if connected elsewhere, pressing now switches/joins the configured channel as expected.
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