Skip to content

Add Audio Output Switcher to Plugin Store - #1103

Open
zomars wants to merge 4 commits into
SteamDeckHomebrew:mainfrom
zomars:add-audio-output-switcher
Open

zomars wants to merge 4 commits into
SteamDeckHomebrew:mainfrom
zomars:add-audio-output-switcher

Conversation

@zomars

@zomars zomars commented Aug 9, 2026

Copy link
Copy Markdown

Add Audio Output Switcher to Plugin Store

Audio Output Switcher lets you change the default audio output device directly from the Quick Access Menu, without a trip to desktop mode. It is for setups with somewhere to switch to — a dock, a TV, a headset, a USB DAC — and especially for devices where gamemode has no output picker at all: on a Steam Machine driving a TV, the QAM audio section is only a CEC volume slider. A handheld with nothing attached has exactly one output, because Steam presents its speakers and headphone jack as a single device, and the panel says so rather than showing a lone row you cannot press.

What it does:

  • Lists the outputs Steam exposes, with the active device marked and pinned to the top. Virtual sinks such as steam-streaming-playback are dropped, since picking one silences the speakers with nothing to explain why.
  • Switches the output with one tap, and already-playing streams follow the switch immediately rather than only new ones.
  • Adds a "Follow system default" row while an override is set, so there is a way back from a pinned device.
  • Optional controller shortcut: a button combo you record yourself cycles the output without opening the panel. Nothing is bound by default.
  • Device arrival and removal are events, so a wireless headset connecting or powering off shows up right away without reloading the panel.

The backend is the standard Python backend and declares no flags, so it runs as the ordinary user rather than root — no bundled binaries and no subprocesses. Switching goes through SteamClient.System.Audio, the same API Steam's own audio selector uses; the Python side exists only to persist the shortcut binding across restarts.

Plugin panel

Submodule is pinned at f2c44aa (v1.3.0).

Task Checklist

Developer

  • I am the original author or an authorized maintainer of this plugin.
  • I have abided by the licenses of the libraries I am utilizing, including attaching license notices where appropriate.
  • Generative AI was NOT used to write a majority of the code I am submitting.

Plugin

  • I have verified that my plugin works properly on the Stable and Beta update channels of SteamOS.
  • I have verified my plugin is unique or provides more/alternative functionality to a plugin already on the store.

Backend

  • No: I am using a custom backend other than Python.
  • No: I am using a tool or software from a 3rd party FOSS project that does not have it's dependencies statically linked.
  • No: I am using a custom binary that has all of it's dependencies statically linked.

Community

  • I have tested and left feedback on two other pull requests for new or updating plugins.
  • I have commented links to my testing report in this PR.

Testing

  • Tested by a third party on SteamOS Stable or Beta update channel.

@github-actions github-actions Bot added the not-plugin Not related to plugins themselves label Aug 9, 2026
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

Issues Found

  • Neither testing box is present in your description. Please include one of them per the Markdown comment above the testing section.

Next Steps

  1. If we found any issues above, please edit your pull request description to resolve them and leave a comment saying you've done so.
  2. For the quickest review, please see the Community section of the pull request template for how you can help other developers.
  3. Once your description is correct, a maintainer will review your pull request as soon as possible.

Thank you for your contribution! If you need any help, please reach out on our Discord server. ❤️

@github-actions

Copy link
Copy Markdown

This pull request is stale as no updates or testing reports have been posted within the last 7 days. Please ensure you are actively recruiting testers or resolving the issues discussed. If you do not remove the stale label or comment, this will be closed in 7 days. Please close this pull request if you need more time to resolve issues so we can keep our repo healthy.

@github-actions github-actions Bot added stale plugin-addition Adding a plugin to the Plugin Store labels Aug 17, 2026
@zomars
zomars marked this pull request as ready for review August 17, 2026 23:28
@zomars
zomars requested a review from a team as a code owner August 17, 2026 23:28
@github-actions github-actions Bot removed the stale label Aug 18, 2026
@zomars

zomars commented Aug 18, 2026

Copy link
Copy Markdown
Author

Testing reports I left on other plugin pull requests, per the Community checklist:

Both were tested on SteamOS Stable with Decky Loader 3.2.6.

Picks up the async pactl rewrite, the derived PipeWire runtime dir, and the
titleView fix.
@github-actions

Copy link
Copy Markdown

This pull request is stale as no updates or testing reports have been posted within the last 7 days. Please ensure you are actively recruiting testers or resolving the issues discussed. If you do not remove the stale label or comment, this will be closed in 7 days. Please close this pull request if you need more time to resolve issues so we can keep our repo healthy.

@github-actions github-actions Bot added the stale label Aug 26, 2026
@zomars

zomars commented Aug 26, 2026

Copy link
Copy Markdown
Author

Pin updated to v1.2.0 (ff2cc4b); description updated to match.

Switching now goes through SteamClient.System.Audio instead of pactl:

  • Playing streams follow the switch, handled by Steam.
  • Virtual sinks such as steam-streaming-playback are no longer listed.
  • Device add/remove are events, not a four-second poll.
  • New "Follow system default" row clears an override.
  • The Python backend only persists the shortcut binding; no subprocesses.

Still needs a third-party tester. Build plugin (AudioOutputSwitcher, true) has been waiting for maintainer approval since 18 Aug, so there is no CI build to install.

@AAGaming00 AAGaming00 removed the not-plugin Not related to plugins themselves label Aug 26, 2026
@Outpox

Outpox commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Testing report

Tested on a Steam Deck LCD, SteamOS Stable, Decky Loader v3.2.6, at the pinned commit ff2cc4b.

Built it from source: pnpm build and tsc --noEmit are clean, and the rebuilt dist/index.js is
byte-identical to the committed one. Installed it and drove the panel on hardware.

What worked

  • Switching outputs from the panel. pactl get-default-sink followed every tap, and a live audio
    stream moved across with it, no manual move-sink-input needed. That part is genuinely nice.
  • "Follow system default" appears only when an override is set, clears it correctly, and disappears again.
  • Device add and remove are live. I removed a sink with the panel open and it vanished without a reload.
  • Backend loads and unloads cleanly, nothing in the logs, no root needed.

Three things I would fix

  1. Closing the panel mid-capture leaves the capture armed. Nothing clears the capture global
    on unmount. Start recording a combo, close the QAM with the button (which by your own README
    never reaches the input stream), and the panel looks idle on reopen but is not: the shortcut
    stops firing, and the next two buttons held together in a game get saved as the new binding.
    I reproduced it with your own onButton / startCapture code: a saved L4+R4 binding silently
    became A+B. Clearing capture in the effect cleanup, plus a timeout, should cover it.

  2. On a plain Deck with nothing plugged in, the panel shows one disabled row and no explanation.
    PipeWire exposes Speaker and Headphones as separate sinks, but Steam collapses them into a single
    device, so SetDefaultDeviceOverride cannot reach that distinction. Your Steam Machine case is
    real and the README explains it well, but publish.description promises "every available audio
    output", which reads wrong on the device most store users own. Worth naming the audience in the
    description, and showing a line like "Only one output available, connect a headset, dock or TV"
    instead of a bare disabled row.

  3. The active device is not pinned to the top, contrary to the PR body and README. It is marked
    with , but the list is sorted alphabetically only. With a second device active it sat third.
    Either implement it or drop the claim. Note cycleOutput walks the same array, so pinning would
    change the shortcut's cycle order too.

Smaller stuff: the README still says "repolls" in the features list where the code uses events, and
plain virtual sinks do show up in GetDevices() (two module-null-sink sinks appeared with
bHasOutput: true), so the "virtual sinks never appear" note is stated more strongly than it holds.

@github-actions github-actions Bot removed the stale label Aug 27, 2026
@zomars

zomars commented Aug 30, 2026

Copy link
Copy Markdown
Author

Thank you — that is a genuinely useful report, and all three were real. Fixed in f2c44aa (v1.3.0), and the submodule here is repinned to it.

1. Capture surviving the panel. Correct, and worse than it looked: capture is a module global, so an abandoned recording both killed the shortcut (onButton returns early while it is set) and stayed armed to overwrite the binding. There is now a single endCapture() teardown, called from the panel effect's cleanup and from a ten-second deadline that re-arms on every input, so a long deliberate hold is never cut off but an abandoned one cannot outlive the panel.

I checked that the cleanup actually runs rather than assuming it: with an instrumented build on a Steam Machine, navigating away from the plugin page and closing it both fire the unmount, so the cleanup lands immediately and the deadline is the backstop for anything that does not. Re-armed a capture, closed the panel, then pressed the combo — it fires, and the stored binding is still [44, 45] rather than whatever was pressed next.

2. One row and no explanation. Fair, and the description was the bigger problem. publish.description no longer promises "every available output"; it names the setups the plugin is for — dock, TV, headset, DAC — and says outright that a handheld with nothing attached has one output to pick. The panel now explains the single-output case instead of showing a bare disabled row. I could not exercise that branch on hardware, since this box always has HDMI plus a headset; that one is code review only, and I would rather say so than imply otherwise.

3. Active device not pinned. You were right, the sort was alphabetical only. Pinned now — and pinned in the panel only, which your last sentence turned out to be the important part of. cycleOutput walks the same list, and pinning it there too would make "next" mean the first name every time, so a third device would never come up. It stays name-ordered; only the display reorders. Verified on device: with the headset active, the panel reads ● Razer Barracuda X 2.4 then HDMI / DisplayPort, against the alphabetical order.

Smaller stuff, both correct. README said "repolls" where the code uses RegisterForDeviceAdded/Removed; fixed. The virtual-sink claim was stated backwards, and on a machine that hosts Remote Play it is not just null sinks — steam-streaming-playback sat in the list as an ordinary selectable row, and picking it silences the speakers with nothing to explain why. Those are filtered now, structurally rather than by name: HDMI and a USB headset each report availableConfigs: [1] and a described currentConfig, while the virtual ones report no configs, an empty description, and connector and bus 0. If some build ever reports configs differently and that filter empties the list, the unfiltered list is shown instead, so nobody ends up with a blank panel.

One more, found while checking the above. RegisterForDeviceAdded and RegisterForDeviceRemoved return undefined on current builds — measured, both of them — so the panel's cleanup had no handle to unregister with and quietly did nothing. Every trip into the QAM left another live callback on Steam, re-reading the device list for the rest of the session. It now subscribes once at plugin load and fans out to the panel: measured on device, one registration per plugin load and none per panel open, where it was previously two per open.

Happy to have it looked at again whenever you have time — and no rush, you have already put more into this than the checklist asks for.

@github-actions

github-actions Bot commented Sep 6, 2026

Copy link
Copy Markdown

This pull request is stale as no updates or testing reports have been posted within the last 7 days. Please ensure you are actively recruiting testers or resolving the issues discussed. If you do not remove the stale label or comment, this will be closed in 7 days. Please close this pull request if you need more time to resolve issues so we can keep our repo healthy.

@github-actions github-actions Bot added the stale label Sep 6, 2026
@zomars

zomars commented Sep 11, 2026

Copy link
Copy Markdown
Author

Also waiting for merging. Got already a third party test report. Works both on Steam Deck and Steam Machine.

@github-actions github-actions Bot removed the stale label Sep 11, 2026
@github-actions

Copy link
Copy Markdown

This pull request is stale as no updates or testing reports have been posted within the last 7 days. Please ensure you are actively recruiting testers or resolving the issues discussed. If you do not remove the stale label or comment, this will be closed in 7 days. Please close this pull request if you need more time to resolve issues so we can keep our repo healthy.

@github-actions github-actions Bot added the stale label Sep 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

plugin-addition Adding a plugin to the Plugin Store stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants