fix(linux): use pw-link for PipeWire isolation (resilient to WirePlumber) - #49
Draft
m96-chan wants to merge 1 commit into
Draft
fix(linux): use pw-link for PipeWire isolation (resilient to WirePlumber)#49m96-chan wants to merge 1 commit into
m96-chan wants to merge 1 commit into
Conversation
…move On Linux with PipeWire + WirePlumber, per-PID capture was leaking audio from other apps. Two root causes (Issue #48): 1. `pulse.sink_input_move()` moves the target's sink-input to our null-sink, but WirePlumber's `stream-restore` / `follow-default-target` policies immediately move it back. The API reports success while isolation silently fails. 2. `pw-record --target=<name>` is redirected by the session manager to the default sink's monitor in multi-sink environments, so the recorder picks up the system mix. Replace the PipeWireStrategy isolation path with explicit `pw-link` connections by global port id: - Spawn `pw-record --target=0 -P node.name=proctap_pw_rec_<pid>` so the recorder's input ports stay unconnected until we link them. - Resolve the target app's `Stream/Output/Audio` node(s), the tap null-sink, and the recorder node from a single `pw-dump` snapshot. - Producer-side: link target node out-ports onto the tap's playback ports (original speaker route is left intact — user-created explicit port links are exempt from WirePlumber's session policy). - Capture-side: link tap monitor ports onto the recorder input ports. - Cleanup unloads the null-sink; PipeWire garbage-collects every attached link, so no explicit pw-link teardown is needed. PulseAudioStrategy and PipeWireNativeStrategy are unchanged. Tests cover the pure pw-dump / pw-link helpers (no daemon required); end-to-end verification requires Ubuntu + PipeWire + WirePlumber. Refs: #48 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owner
Author
|
Recently testing this module...now. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #48.
Summary
sink_input_move+ name-targetedpw-recordisolation inPipeWireStrategywith explicitpw-linkconnections resolved by global port id from apw-dumpsnapshotpw-recordis spawned with--target=0and a per-PIDnode.name, then its input ports are wired to the tap null-sink's monitor by global port id, so the session manager cannot redirect it to the default sink's monitorRoot cause (per #48)
On PipeWire + WirePlumber (Ubuntu 24.04 / VRChat repro), the previous strategy silently failed:
pulse.sink_input_move()succeeds, but WirePlumber'sstream-restore/follow-default-targetpolicies immediately revert the move. The API reports success while isolation is broken.pw-record --target=<name>is redirected by the session manager to the default sink's monitor in multi-sink environments, so the recorder picks up the full system mix.Fix
module-null-sink(proctap_pw_isolated_<pid>) as the dedicated capture target.pw-record --target=0 -P node.name=proctap_pw_rec_<pid> --format=s16 ...so the recorder's input ports stay unconnected until we link them explicitly.pw-dumpsnapshot and resolve:Stream/Output/Audionode(s) (every one, since some apps register multiple streams),pw-recordnode,audio.channel).pw-linktwo hops by global port id (deterministic, collision-free under sharedVRChat.exenode names):pw-linkteardown is needed.What's NOT touched
PulseAudioStrategy— pure PulseAudio environments are not subject to WirePlumber's session policy.PipeWireNativeStrategy— uses the native C API directly, nopw-recordsubprocess.Tests
tests/test_linux_pw_link_helpers.py(32 tests, no PipeWire daemon required) covers the pure helpers:pw-dumpJSON parsing, node lookup by name / by PID, port resolution by direction + channel, idempotent-link detection (File exists), and the channel-matching loop in_pw_link_nodes.--python-version 3.10 --ignore-missing-imports).Test plan (manual / reviewer)
proctap --pid <one_pid> --stdout | ffplay -f s16le -ar 48000 -ac 2 -i pipe:0— verify only the targeted app is audible, the other is absentpw-link -lshows the target node's outputs linked to both the original sink andproctap_pw_isolated_<pid>playback portspw-link -lalso showsproctap_pw_isolated_<pid>monitor →proctap_pw_rec_<pid>inputpw-cli ls Nodeshows neitherproctap_pw_isolated_<pid>norproctap_pw_rec_<pid>, and the target app's playback route is unchangedPulseAudioStrategy)Notes
linux.py(linked in [Bug]: Linux (PipeWire + WirePlumber) でアプリ音声が他アプリ音声が混入する #48). This PR adopts the samepw-linktwo-hop architecture, scoped to a minimal change insidePipeWireStrategy.🤖 Generated with Claude Code