-
Notifications
You must be signed in to change notification settings - Fork 4
Description
Bug Description
In our application we have code that detects when the audio callback isn't called, and try to recreate the stream if that happens. However, when trying to clean up the old stream, calling AudioStreamHandle::eject() on it hangs forever.
Ejecting a stream that is running normally works.
Steps To Reproduce
- Start a stream (capture or playback, doesn't matter) using the pipewire backend
- Trigger a condition where the audio callback is no longer called. Either:
a. disconnect any connections from the stream node in pipewire patchbay tool likeqpwgraph,helvumorpw-viz
b. disconnect the (USB) sound card that the stream was running on (depending on pipewire/wireplumber config, it may get reconnected to a different stream though) - Call
AudioStreamHandle::eject()on it
Expected Behavior
The stream is ejected and cleaned up immediately.
Actual Behavior
The eject() call hangs, and the stream still exists in tools like pw-top, wpctl status, pipewire patchbay tools.
Once I trigger the audio callback being called again (i.e. by manualy connecting the stream's node to some device in a patchbay tool), it finally ejects and is cleaned up.
Environment
- OS: Linux (Gentoo, Arch)
- Audio driver (enable
infologs): pipewire - Rust Version: 1.90.0
- Project Version/Commit (see
Cargo.lock): 7f73ef5 (recentmainas of 2025-10-28)
Additional Context
The root cause is likely around
interflow/src/backends/pipewire/stream.rs
Lines 194 to 196 in 7f73ef5
| .process(move |stream, inner| { | |
| log::debug!("Processing stream"); | |
| inner.handle_commands(); |
We only ever process the commands inside the the audio callback. But pipewire doesn't call that one when the node is disconnected.
Pipewire allows many different types of callbacks/communication patterns on the stream and in its MainLoop, which would likely be a better way to process commands.
This is also related to #77
Aside 1
I see that interflow uses pipewire in somewhat non-canonical way: it spawns a pipewire MainLoop per every stream (which then spawns another "data loop" for the audio callback). I think the intended way is that a single MainLoop is spawned (more or less globally) and then streams are added/removed from that loop. If this something interflow wants to transition to?
Aside 2
Was the API AudioStreamHandle::eject() returning the callback back to the caller inspired by something?
I imagine getting the callback back may be handy in some cases (though not ours), though it complicates the implementation slightly: sending some one-shot "please drop and destroy yourself" to the stream would be more straightforward. (callers would be still able to get data back from the callback by wrapping it in something like Arc<Mutex<Option<...>>>, though that isn't elegant)
Aside 3
In the previous backends that we've used (portaudio, cpal), dropping the stream handle resulted in stopping and disposing of the stream. In interflow, the stream keeps running in this case, without any way to stop it afterwards (short of panicking in the callback).
Is this intentional?
Possible Solution
For our use I've implemented a hacky work-around in tonarino#5. It works for us, but I don't think it's upstreamable as-is (for example eject() takes up to a second).
If you have an idea how you want to address this architecturally I can try implementing that.
Metadata
Metadata
Assignees
Labels
Projects
Status