host/audio: per-stream routing via libpipewire (session 3 of 4)
When opts.app is set, a dedicated OS thread runs a libpipewire MainLoop, subscribes to the registry, and writes target.object to the "default" metadata so WirePlumber reroutes matching streams to our per-PID null-sink. Activation is now opts.app.is_some() OR the existing PIXELPASS_AUDIO_VIA_NULL_SINK env var (kept for no-filter dogfooding). Threading: tokio side spawns a std::thread; the two sides bridge via pipewire::channel for cmd→thread (Shutdown) and tokio::sync::mpsc for event→tokio (FirstRoutedStream). Cross-thread quit goes through the libpipewire channel so MainLoop is only mutated from its own thread. Shutdown clears target.object on every routed stream before quitting so WirePlumber doesn't log orphans. Routing decisions: - Filter is case-insensitive equality on application.name (predictable; no surprise matches from substring). - target.object is written as Spa:Id with the sink's object.serial. - Default-sink loopback stays loaded until the first stream is actually routed — avoids viewer silence if the user picks an app that isn't producing sound yet. On first route, the event task takes() the loopback module ID and unloads it. Session 2 picker explainer + (app pick saved: ...) banner softening both removed; banner is back to plain app-audio=NAME. Verified end-to-end cross-machine: desktop host with Strawberry selected, laptop viewer over mpv. Strawberry audible on the laptop; YouTube playback started on the desktop was NOT audible on the laptop. Routing isolates the filtered app. Session 4 still open: recreate loopback when the last filtered stream disappears (avoid silence), handle app-disappears-mid-session, multi-instance, --repair coupling for orphan sink cleanup. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+13
-7
@@ -119,13 +119,19 @@ pub async fn start(opts: &HostOpts) -> Result<CaptureHandle> {
|
||||
let key_interval = (opts.framerate * 2).to_string();
|
||||
let bitrate = opts.bitrate.to_string();
|
||||
|
||||
// PIXELPASS_AUDIO_VIA_NULL_SINK=1 routes audio through a per-PID
|
||||
// PipeWire null-sink + default-sink loopback instead of tapping the
|
||||
// default sink's monitor directly. Same audio captured either way at
|
||||
// this stage; the null-sink path is groundwork for per-app filtering
|
||||
// in a follow-up.
|
||||
let audio_routing = if std::env::var_os("PIXELPASS_AUDIO_VIA_NULL_SINK").is_some() {
|
||||
Some(Routing::start().context("audio routing setup failed")?)
|
||||
// Audio routing activates when either:
|
||||
// - `opts.app` is set (per-stream rerouting to a per-PID null-sink),
|
||||
// - or `PIXELPASS_AUDIO_VIA_NULL_SINK=1` is set (no app filter, just
|
||||
// captures everything via the null-sink → useful for development
|
||||
// and dogfooding the loopback path before app filtering is picked).
|
||||
let routing_requested =
|
||||
opts.app.is_some() || std::env::var_os("PIXELPASS_AUDIO_VIA_NULL_SINK").is_some();
|
||||
let audio_routing = if routing_requested {
|
||||
Some(
|
||||
Routing::start(opts)
|
||||
.await
|
||||
.context("audio routing setup failed")?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user