[Feature] Pause player on output device disconnect
Describe what you want
When the audio output device I am using with termusic disconnects from the laptop (because e.g., my earbuds die), the player should pause immediately.
Do you have already an idea for the implementation?
I would make this feature opt-out (or opt-in?) by adding a boolean setting to tui.toml/behavior.
I would add a handler for whatever the "output device disconnect" event is, filtering out devices which are not being used by termusic at the moment. The handler would check if this feature is currently on. If it is, and the player is not already paused, it should pause it.
This is a great feature to have, i am using this on my android player for example, but i dont know if this is realistically possible within termusic. The problem is that we are currently using cpal to handle streaming audio to the system, which to my knowledge, does not provide a way to listen to "DeviceDisconnected" events or such (https://github.com/RustAudio/cpal/issues/373). Additionally, we cant rely on the stream stopping as we dont directly stream to the output device, we go through the system's audios server (like pulseaudio / pipewire and other systems' audio servers), which means that the streams will just continue to work while there is no device on the other side.
I've been able to make this work in some other applications by checking for DeviceNotAvailable in cpal's error callback. Unfortunately this is not yet exposed in rodio, but there was a PR to expose it just a few weeks ago so it should be in the next release.
I've been able to make this work in some other applications by checking for DeviceNotAvailable in cpal's error callback.
I would be interested to see example code and hear the context about this, because to my knowledge, at least in pipewire, there should not be any error when trying to connect as a source while no output device is available or goes unavailable. Did you maybe continuously check for device changes or are you on a different audio service?
I've been able to make this work in some other applications by checking for DeviceNotAvailable in cpal's error callback. Unfortunately this is not yet exposed in rodio, but there was a https://github.com/RustAudio/rodio/pull/708 just a few weeks ago so it should be in the next release.
Also regarding this, to my knowledge rodio is quite exchangeable, so we could also just completely implement the output to cpal ourself, while still using rodio's mixing features, but it is good to know about this new handling.
Ah yes, you're correct about pipewire. It's been a few years since I worked on this particular issue so the details are a bit fuzzy, but it's coming back to me a bit more now. Unfortunately, the behavior for this is quite platform-dependent.
What happens on Linux for me is that rather than invoking the error callback, cpal will stop invoking the data callback for a brief period of time. I started working on another library similar to rodio that uses a ring buffer to communicate between the decoder thread and the audio thread. It's set up to use a timeout to detect if we're not able to write the entire buffer to the output device within the buffer's duration. If this happens, the device does not have enough data to keep producing sound, so the stream has likely stalled. Once you detect this condition, you can reset or pause the stream. You can see this happening if you add a println to the error handler in the repl example. It's not an exact science, but it's worked well enough for me.
If I recall correctly, Windows and MacOS have different sets of quirks. I believe Windows does invoke the error callback with the DeviceNotAvailable error under certain conditions. I've also had it hang indefinitely instead of only pausing briefly like it does on Linux, which is what prompted the timeout solution. MacOS is more complex because it will attempt to switch the audio device seamlessly if you've told it to use the default one. However, I was able to detect the device change if I configured cpal to use a specific output device instead of using default_output_config. I haven't tried Android or iOS, so I'm not sure what will happen there.
The short answer is that resetting/pausing the stream after either a delay in cpal's data callback or getting a DeviceNotAvailable error in the error callback has worked so far as a cross-platform solution. I wouldn't be surprised if there are some caveats that I'm missing though.