diff --git a/cmd/daemon/controls.go b/cmd/daemon/controls.go index 6cccdca..1133673 100644 --- a/cmd/daemon/controls.go +++ b/cmd/daemon/controls.go @@ -14,6 +14,48 @@ import ( "time" ) +func (p *AppPlayer) prefetchNext() { + next := p.state.tracks.PeekNext() + if next == nil { + return + } + + nextId := librespot.SpotifyIdFromUri(next.Uri) + if p.secondaryStream != nil && p.secondaryStream.Is(nextId) { + return + } + + log.Debugf("prefetching %s %s", nextId.Type(), nextId.Uri()) + + var err error + p.secondaryStream, err = p.player.NewStream(nextId, *p.app.cfg.Bitrate, 0) + if err != nil { + log.WithError(err).Warnf("failed prefetching stream for %s", nextId) + return + } + + log.Infof("prefetched track \"%s\" (uri: %s, duration: %dms)", + p.primaryStream.Media.Name(), nextId.Uri(), p.primaryStream.Media.Duration()) +} + +func (p *AppPlayer) schedulePrefetchNext() { + if p.state.player.IsPaused || p.primaryStream == nil { + p.prefetchTimer.Reset(time.Duration(math.MaxInt64)) + return + } + + untilTrackEnd := time.Duration(p.primaryStream.Media.Duration()-int32(p.player.PositionMs())) * time.Millisecond + untilTrackEnd -= 30 * time.Second + if untilTrackEnd < 10*time.Second { + p.prefetchTimer.Reset(time.Duration(math.MaxInt64)) + + go p.prefetchNext() + } else { + p.prefetchTimer.Reset(untilTrackEnd) + log.Tracef("scheduling prefetch in %.0fs", untilTrackEnd.Seconds()) + } +} + func (p *AppPlayer) handlePlayerEvent(ev *player.Event) { switch ev.Type { case player.EventTypePlaying: @@ -185,6 +227,7 @@ func (p *AppPlayer) loadCurrentTrack(paused bool) error { p.state.player.IsPlaying = true p.state.player.IsBuffering = false p.updateState() + p.schedulePrefetchNext() p.app.server.Emit(&ApiEvent{ Type: ApiEventTypeMetadata, @@ -253,6 +296,7 @@ func (p *AppPlayer) addToQueue(track *connectpb.ContextTrack) { p.state.player.PrevTracks = p.state.tracks.PrevTracks() p.state.player.NextTracks = p.state.tracks.NextTracks() p.updateState() + p.schedulePrefetchNext() } func (p *AppPlayer) setQueue(prev []*connectpb.ContextTrack, next []*connectpb.ContextTrack) { @@ -260,6 +304,7 @@ func (p *AppPlayer) setQueue(prev []*connectpb.ContextTrack, next []*connectpb.C p.state.player.PrevTracks = p.state.tracks.PrevTracks() p.state.player.NextTracks = p.state.tracks.NextTracks() p.updateState() + p.schedulePrefetchNext() } func (p *AppPlayer) play() error { @@ -283,6 +328,8 @@ func (p *AppPlayer) play() error { p.state.player.PositionAsOfTimestamp = streamPos p.state.player.IsPaused = false p.updateState() + p.schedulePrefetchNext() + return nil } @@ -300,6 +347,8 @@ func (p *AppPlayer) pause() error { p.state.player.PositionAsOfTimestamp = streamPos p.state.player.IsPaused = true p.updateState() + p.schedulePrefetchNext() + return nil } @@ -318,6 +367,7 @@ func (p *AppPlayer) seek(position int64) error { p.state.player.Timestamp = time.Now().UnixMilli() p.state.player.PositionAsOfTimestamp = position p.updateState() + p.schedulePrefetchNext() p.app.server.Emit(&ApiEvent{ Type: ApiEventTypeSeek, diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 5138d44..3d058ea 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -13,6 +13,7 @@ import ( "go-librespot/zeroconf" "golang.org/x/exp/rand" "gopkg.in/yaml.v3" + "math" "os" "strings" "time" @@ -74,6 +75,9 @@ func (app *App) newAppPlayer(creds any) (_ *AppPlayer, err error) { countryCode: new(string), } + // start a dummy timer for prefetching next media + appPlayer.prefetchTimer = time.AfterFunc(time.Duration(math.MaxInt64), appPlayer.prefetchNext) + if appPlayer.sess, err = session.NewSessionFromOptions(&session.Options{ DeviceType: app.deviceType, DeviceId: app.deviceId, diff --git a/cmd/daemon/player.go b/cmd/daemon/player.go index 24d22f8..d269b0f 100644 --- a/cmd/daemon/player.go +++ b/cmd/daemon/player.go @@ -15,6 +15,7 @@ import ( "math" "strings" "sync" + "time" ) type AppPlayer struct { @@ -35,6 +36,8 @@ type AppPlayer struct { state *State primaryStream *player.Stream secondaryStream *player.Stream + + prefetchTimer *time.Timer } func (p *AppPlayer) handleAccesspointPacket(pktType ap.PacketType, payload []byte) error { @@ -106,6 +109,8 @@ func (p *AppPlayer) handleDealerMessage(msg dealer.Message) error { return fmt.Errorf("failed inactive state put: %w", err) } + p.schedulePrefetchNext() + if p.app.cfg.ZeroconfEnabled { p.logout <- p } diff --git a/tracks/tracks.go b/tracks/tracks.go index 97fb5ea..109eb39 100644 --- a/tracks/tracks.go +++ b/tracks/tracks.go @@ -156,6 +156,19 @@ func (tl *List) GoStart() bool { return true } +func (tl *List) PeekNext() *connectpb.ContextTrack { + if tl.playingQueue && len(tl.queue) > 1 { + return tl.queue[1] + } + + iter := tl.tracks.iterHere() + if iter.next() { + return iter.get().item + } + + return nil +} + func (tl *List) GoNext() bool { if tl.playingQueue { tl.queue = tl.queue[1:]