mirror of
https://github.com/librespot-org/librespot.git
synced 2026-04-27 08:15:50 +03:00
[PR #430] [MERGED] Gapless play and improved notifications #921
Labels
No labels
A-Alsa
SpotifyAPI
Tokio 1.0
audio
bug
can't reproduce
compilation
dependencies
duplicate
enhancement
good first issue
help wanted
high priority
imported
imported
invalid
new api
pull-request
question
reverse engineering
wiki
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/librespot#921
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/librespot-org/librespot/pull/430
Author: @kaymes
Created: 2/2/2020
Status: ✅ Merged
Merged: 2/27/2020
Merged by: @sashahilton00
Base:
dev← Head:gapless_play📝 Commits (10+)
5784b46Prepare for gapless play.9eef690Some clean up8756341Remove debug message349e182Smarter handling of preloading and loading of tracks that are already loaded.6fed8d0Make preloading work.981b76bKeep the sink open499824arust fmtead794fCorrect notifications when loading the same track again.18d1181Clean up some code3f111a9Suppress sending loading state to Spotify unless we actually need to load a track.📊 Changes
7 files changed (+1470 additions, -463 deletions)
View changed files
📝
audio/src/fetch.rs(+9 -0)📝
connect/src/spirc.rs(+370 -123)📝
examples/play.rs(+4 -2)📝
metadata/src/lib.rs(+3 -0)📝
playback/src/player.rs(+1067 -324)📝
src/main.rs(+12 -10)📝
src/player_event_handler.rs(+5 -4)📄 Description
This PR allows librespot to perform gapless playback (issue #21). As far as I can see it's working well, but given the extent of the changes, it requires a good amount of testing.
Features of this PR so far:
The reason for having "additional features" in this one PR is that the necessary changes to the plumbing between the player and spirc made them come out naturally.
And now to the more technical details:
One of the first realisations was, that for gapless play there has to be better comunication between the player and spirc. Initially I thought about beefing up the future that notifies spirc about the end of a track, but then I realised, that this would kind of duplicate the player events channel that is already there. Thus, I decided to beef up the player events channel instead and get rid of the existing future. The player can now feed multiple player event channels and spirc takes one of them to listen to player events. This allows spirc to know about when to preload the next track. As a by product, it also allows spirc to have more detailed information about the player's status, in particular the current playback position.
To be able to preload tracks, loading of tracks has to happen in a non-blocking way. Initially I started to convert PlayerInternal::load_track() into a proper future. However, I eventually realised, that the call to seek() is blocking and can't be converted to proper async code easily. Thus load_track() now spawns a new (short lived) thread to load a track and put the result onto a one-shot channel. Thus, we effectively get a future that loads the next track.
I turned PlayerInternal into a future so run() is now replaced by poll(). But it still contains blocking code as before. Thus, it must not be spawned onto the tokio reactor but instead run via wait() in a dedicated thread. That's the same as before, the thread just does player.wait() instead of player.run(). The reason for this is to give PlayerInternal's main loop a context to poll multiple channels and futures as needed without having to resort to timed cycles and similar crutches.
The code is set up such that the player emits a TimeToPreloadNextTrack event when playback is 30 seconds or less to the end of the track AND the remainder of the track has been downloaded. This is the signal to SpircTask to examine the playlist (using preview_next_track()) and call player.preload_track() to have the player start with the download.
In the old code, spirc uses the protobuf frame it communicates with the server with to store its own state machine (e.g. store the playing status). This was no longer sufficient. E.g. when the status is kPlayStatusLoading, spirc must know whether it is loading to play or loading to pause. Thus, I gave SpircTask its own state (SpircPlayStatus) which allows it to accurately track what is going on.
Next steps:
The added events also allow to expose more information to external programs through run_program_on_events. E.g. volume changes now emit events on the channel which would make fixing #410 trivial.
🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.