librespot-org / librespot

Open Source Spotify client library
MIT License
4.86k stars 616 forks source link

Error and termination when audio interface is removed or not available #1057

Closed tweisberger closed 1 year ago

tweisberger commented 2 years ago

Describe the bug When the audio device is removed while Spotify connected to librespot endpoint or when connect while audio interface is not present, librespot terminates. librespot should handle this gracefully. This requires manual intervention.

This use case is using an external DAC connected to USB. When the DAC is set to another input away from USB, it removes the connection. Expected behavior would be to gracefully disconnect from Spotify. Or in the case where the audio device is not present, reject an incoming connection.

To reproduce Steps to reproduce the behavior 1: Audio interface is removed after connect:

  1. Set DAC to USB input (connection the device running librespot).
  2. Launch librespot with sudo librespot --name Livingroom --bitrate 320 --format S16 --mixer softvol --initial-volume 70 --volume-ctrl log --volume-range 60 --autoplay --cache /var/local/www/spotify_cache --disable-audio-cache --backend alsa --device _audioout --onevent /var/local/www/commandw/spotevent.sh
  3. Connect with Spotify on iOS (or macOS) client
  4. Change input of DAC away from USB
  5. See error

Behavior 2: Audio interface is not present when connecting :

  1. Set DAC to input other then USB.
  2. Launch librespot with sudo librespot --name Livingroom --bitrate 320 --format S16 --mixer softvol --initial-volume 70 --volume-ctrl log --volume-range 110 --autoplay --cache /var/local/www/spotify_cache --disable-audio-cache --backend alsa --device _audioout --onevent /var/local/www/commandw/spotevent.sh
  3. Connect with Spotify on iOS (or macOS) client
  4. See error

Log

Behavior 1 : [2022-08-31T22:09:38Z WARN librespot_playback::audio_backend::alsa] Error writing from AlsaSink buffer to PCM, trying to recover, ALSA function 'snd_pcm_writei' failed with error 'EIO: I/O error' [2022-08-31T22:09:38Z ERROR librespot_playback::player] Audio Sink Error On Write: <AlsaSink> ALSA function 'snd_pcm_recover' failed with error 'EIO: I/O error' pi@moode:~ $

Behavior 2: [2022-08-31T22:07:05Z TRACE librespot_playback::player] == Starting sink == ALSA lib pcm_hw.c:1829:(_snd_pcm_hw_open) Invalid value for card [2022-08-31T22:07:05Z ERROR librespot_playback::player] Audio Sink Error Connection Refused: <AlsaSink> Device _audioout May be Invalid, Busy, or Already in Use, ALSA function 'snd_pcm_open' failed with error 'ENOENT: No such file or directory' pi@moode:~ $

Host

Connecting Device

Additional context In the case of moOde, librespot terminates, and the device becomes unusable until the user connects to the web ui and manually restarts librespot

JasonLG1979 commented 2 years ago

This is not a bug. This is a user/consumer error. It is up to moode to:

  1. Make sure the device is available before allowing librespot to use it.

And

  1. Handle services existing because of errors.
JasonLG1979 commented 2 years ago

Ok a further breakdown now that I'm home.

When the audio device is removed while Spotify connected to librespot endpoint or when connect while audio interface is not present, librespot terminates. librespot should handle this gracefully. This requires manual intervention.

In that case librespot logs an error message and exits with an exit status of 1 indicating an error. This is expected behavior.

This use case is using an external DAC connected to USB. When the DAC is set to another input away from USB, it removes the connection. Expected behavior would be to gracefully disconnect from Spotify. Or in the case where the audio device is not present, reject an incoming connection.

The same as above. On dev at least and possibly on master(?) librespot does disconnect from Spotify.

When librespot is not playing it closes the device and when it starts playing it tries to open the device. We do not monitor the status of devices we are not using and I'm unaware of any plans to do so.

There are blocking sink events you can subscribe to via an onevent script that will allow you to tell when librespot is about to try to open the device and/or has closed the device. In your onevent script you can do whatever you need to do to make sure the device you have told librespot to use actually exists and is available to use.

JasonLG1979 commented 2 years ago

You can use --emit-sink-events --onevent=path/to/my/script Events are sent as environment variables.

An example script:

#!/usr/bin/python3
import os

player_event = os.getenv('PLAYER_EVENT')

# The player thread will block until this script exits in all cases.
if player_event == 'sink':
    status = os.getenv('SINK_STATUS')
    if status == 'running':
        # The device is not open yet.
        # Do stuff to make sure the device you have told librespot
        # to use actually exists and is available to use.
        # The device will be opened after the script exits.
        print('The device is about to be opened.')
    elif status == 'temporarily_closed':
        # *Transient State* 
        # The device has been temporarily closed, for example librespot is loading.
        print('The device is closed, but more than likely will be reopened very shortly.')
    elif status == 'closed':
        # The device was closed before this script was called,
        # for example librespot is stopped or paused.
        # Other things are free to use the device after the script exits.
        print('The device has been closed.')
roderickvd commented 2 years ago

Principally it is a fair use case as some good DACs indeed may turn off their USB circuitry when switched to another source, to minimize electrical noise.

However does this bug still happen if you stop Spotify first? Then switch input and back, start Spotify playback? From a code review I think that might work.

Otherwise on a failed write we could consider reopening the device one time only, then retry the write.

JasonLG1979 commented 2 years ago

Principally it is a fair use case as some good DACs indeed may turn off their USB circuitry when switched to another source, to minimize electrical noise.

It's essentially the equivalent of unplugging a DAC during playback.

However does this bug still happen if you stop Spotify first? Then switch input and back, start Spotify playback? From a code review I think that might work.

If Spotify stops then librespot closes the device. But all these things happen asyc so you would have to stop well in advance to prevent player from pushing bytes to the device.

Otherwise on a failed write we could consider reopening the device one time only, then retry the write.

That's basically what try_recover in the ALSA Sink does. If we get the error in player it is fatal to the sink. What I can do is stop the sink and signal a disconnect from player. I just so happen to make that possible in my latest PR, but I still stand by my statement that it's not our responsibility to keep track of devices, that's the job of whatever daemonizes librespot. I will finish up the bits to make it work.

Currently the best thing for moode to do is just restart librespot when it happens. That's what systemd does and although it's not elegant it works fine.

roderickvd commented 2 years ago

That's basically what try_recover in the ALSA Sink does. If we get the error in player it is fatal to the sink. What I can do is stop the sink and signal a disconnect from player. I just so happen to make that possible in my latest PR, but I still stand by my statement that it's not our responsibility to keep track of devices, that's the job of whatever daemonizes librespot. I will finish up the bits to make it work.

I agree. If it's relatively easy though I guess it'd be a quick win.

JasonLG1979 commented 2 years ago

I got you one better. Instead of stopping and disconnecting I made it so that it just tells Spotify we're paused when the sink errors out.

So when a device disappears we just tell Spotify we're paused. While it's gone you can smash play all you want and you'll get a bunch of errors in the log but we won't exit or crash. After the device shows back up if you hit play it continues playback were you left off.

I tested it by unplugging and plugging my DAC and it seems to work pretty well.

JasonLG1979 commented 2 years ago

Basically the same thing will happen if a user tells librespot to use an invalid device. We won't exit or crash but you'll get a lot of errors and Spotify will just refuse to play no matter how hard you smash the play button.

roderickvd commented 2 years ago

Great idea!

JasonLG1979 commented 2 years ago

Ok I added it to that massive PR. Give it a test.

JasonLG1979 commented 2 years ago

There is still no way currently to programmatically tell librespot to pause from the outside without using the spotify api though. I've got a REST style command API in the works so users and consumers can tell librespot to do basic commands. I'm thinking all POST's. Info would still be gotten from events.

roderickvd commented 1 year ago

Closing this now I think that @JasonLG1979 made quite some effort on this a few months ago.