Beginning of reworked PlaybackManager - PARTIAL

This commit is contained in:
Drew Weymouth
2023-12-22 15:50:39 -08:00
parent 0cd58cb51b
commit d9eca8d807
3 changed files with 93 additions and 219 deletions
+71 -46
View File
@@ -64,25 +64,26 @@ func NewPlaybackManager(
// clamp to 99% to avoid any possible rounding issues // clamp to 99% to avoid any possible rounding issues
scrobbleCfg.ThresholdPercent = clamp(scrobbleCfg.ThresholdPercent, 0, 99) scrobbleCfg.ThresholdPercent = clamp(scrobbleCfg.ThresholdPercent, 0, 99)
pm := &PlaybackManager{ pm := &PlaybackManager{
ctx: ctx, ctx: ctx,
sm: s, sm: s,
player: p, player: p,
scrobbleCfg: scrobbleCfg, scrobbleCfg: scrobbleCfg,
transcodeCfg: transcodeCfg, transcodeCfg: transcodeCfg,
nowPlayingIdx: -1,
} }
p.OnTrackChange(func(tracknum int) { p.OnTrackChange(func() {
if tracknum >= len(pm.playQueue) {
return
}
pm.checkScrobble() // scrobble the previous song if needed pm.checkScrobble() // scrobble the previous song if needed
if pm.player.GetStatus().State == player.Playing { if pm.player.GetStatus().State == player.Playing {
pm.playTimeStopwatch.Start() pm.playTimeStopwatch.Start()
} }
pm.nowPlayingIdx = tracknum pm.nowPlayingIdx++
pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration) pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration)
pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled
pm.invokeOnSongChangeCallbacks() pm.invokeOnSongChangeCallbacks()
pm.doUpdateTimePos() pm.doUpdateTimePos()
if pm.nowPlayingIdx < len(pm.playQueue)-1 {
pm.setTrack(pm.nowPlayingIdx+1, true)
}
}) })
p.OnSeek(func() { p.OnSeek(func() {
pm.doUpdateTimePos() pm.doUpdateTimePos()
@@ -95,6 +96,7 @@ func NewPlaybackManager(
pm.doUpdateTimePos() pm.doUpdateTimePos()
pm.invokeOnSongChangeCallbacks() pm.invokeOnSongChangeCallbacks()
pm.invokeNoArgCallbacks(pm.onStopped) pm.invokeNoArgCallbacks(pm.onStopped)
pm.nowPlayingIdx = -1
}) })
p.OnPaused(func() { p.OnPaused(func() {
pm.playTimeStopwatch.Stop() pm.playTimeStopwatch.Stop()
@@ -205,32 +207,24 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, appendToQueue bool, sh
func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueue, shuffle bool) error { func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueue, shuffle bool) error {
if !appendToQueue { if !appendToQueue {
p.player.Stop() p.player.Stop()
p.nowPlayingIdx = 0 p.nowPlayingIdx = -1
p.playQueue = nil p.playQueue = nil
} }
nums := util.Range(len(tracks)) nums := util.Range(len(tracks))
if shuffle { if shuffle {
util.ShuffleSlice(nums) util.ShuffleSlice(nums)
} }
needToSetNext := appendToQueue && len(tracks) > 0 && p.nowPlayingIdx == len(p.playQueue)-1
for _, i := range nums { for _, i := range nums {
if urlP, ok := p.player.(player.URLPlayer); ok {
url, err := p.sm.Server.GetStreamURL(tracks[i].ID, p.transcodeCfg.ForceRawFile)
if err != nil {
return err
}
urlP.AppendFile(url)
} else if trP, ok := p.player.(player.TrackPlayer); ok {
trP.AppendTrack(tracks[i])
} else {
panic("unsupported player type")
}
// ensure a deep copy of the track info so that we can maintain our own state // ensure a deep copy of the track info so that we can maintain our own state
// (tracking play count increases, favorite, and rating) without messing up // (tracking play count increases, favorite, and rating) without messing up
// other views' track models // other views' track models
tr := *tracks[i] tr := *tracks[i]
p.playQueue = append(p.playQueue, &tr) p.playQueue = append(p.playQueue, &tr)
} }
if needToSetNext {
p.setTrack(p.nowPlayingIdx+1, true)
}
return nil return nil
} }
@@ -241,7 +235,7 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool
if p.replayGainCfg.Mode == ReplayGainAuto { if p.replayGainCfg.Mode == ReplayGainAuto {
p.SetReplayGainMode(player.ReplayGainAlbum) p.SetReplayGainMode(player.ReplayGainAlbum)
} }
return p.player.PlayTrackAt(firstTrack) return p.PlayTrackAt(firstTrack)
} }
func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffle bool) error { func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffle bool) error {
@@ -251,7 +245,7 @@ func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffl
if p.replayGainCfg.Mode == ReplayGainAuto { if p.replayGainCfg.Mode == ReplayGainAuto {
p.SetReplayGainMode(player.ReplayGainTrack) p.SetReplayGainMode(player.ReplayGainTrack)
} }
return p.player.PlayTrackAt(firstTrack) return p.PlayTrackAt(firstTrack)
} }
func (p *PlaybackManager) PlayTrack(trackID string) error { func (p *PlaybackManager) PlayTrack(trackID string) error {
@@ -267,11 +261,15 @@ func (p *PlaybackManager) PlayTrack(trackID string) error {
} }
func (p *PlaybackManager) PlayFromBeginning() error { func (p *PlaybackManager) PlayFromBeginning() error {
return p.player.PlayTrackAt(0) return p.PlayTrackAt(0)
} }
func (p *PlaybackManager) PlayTrackAt(idx int) error { func (p *PlaybackManager) PlayTrackAt(idx int) error {
return p.player.PlayTrackAt(idx) if idx < 0 || idx >= len(p.playQueue) {
return errors.New("track index out of range")
}
p.nowPlayingIdx = idx - 1
return p.setTrack(idx, false)
} }
func (p *PlaybackManager) PlayRandomSongs(genreName string) { func (p *PlaybackManager) PlayRandomSongs(genreName string) {
@@ -325,7 +323,7 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) { func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs)) newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs))
rmCount := 0 //rmCount := 0
idSet := sharedutil.ToSet(trackIDs) idSet := sharedutil.ToSet(trackIDs)
isPlayingTrackRemoved := false isPlayingTrackRemoved := false
for i, tr := range p.playQueue { for i, tr := range p.playQueue {
@@ -336,13 +334,14 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
// If we are removing the currently playing track, we need to scrobble it // If we are removing the currently playing track, we need to scrobble it
p.checkScrobble() p.checkScrobble()
} }
if err := p.player.RemoveTrackAt(i - rmCount); err == nil { // if err := p.player.RemoveTrackAt(i - rmCount); err == nil {
rmCount++ // rmCount++
} else { //} else {
log.Printf("error removing track: %v", err.Error()) var err error
// did not remove this track log.Printf("error removing track: %v", err.Error())
newQueue = append(newQueue, tr) // did not remove this track
} newQueue = append(newQueue, tr)
//}
} else { } else {
// not removing this track // not removing this track
newQueue = append(newQueue, tr) newQueue = append(newQueue, tr)
@@ -359,9 +358,9 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
// Stop playback and clear the play queue. // Stop playback and clear the play queue.
func (p *PlaybackManager) StopAndClearPlayQueue() { func (p *PlaybackManager) StopAndClearPlayQueue() {
p.player.Stop() p.player.Stop()
p.player.ClearPlayQueue()
p.doUpdateTimePos() p.doUpdateTimePos()
p.playQueue = nil p.playQueue = nil
p.nowPlayingIdx = -1
} }
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) { func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
@@ -423,17 +422,17 @@ func (p *PlaybackManager) SetNextLoopMode() error {
if err != nil { if err != nil {
return err return err
} }
for _, cb := range p.onLoopModeChange { //for _, cb := range p.onLoopModeChange {
cb(p.player.GetLoopMode()) // cb(p.player.GetLoopMode())
} //}
return nil return nil
} }
func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error { func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error {
if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil { //if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil {
return err // return err
} //}
for _, cb := range p.onLoopModeChange { for _, cb := range p.onLoopModeChange {
cb(loopMode) cb(loopMode)
@@ -443,7 +442,8 @@ func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error {
} }
func (p *PlaybackManager) GetLoopMode() player.LoopMode { func (p *PlaybackManager) GetLoopMode() player.LoopMode {
return p.player.GetLoopMode() return 0
//return p.player.GetLoopMode()
} }
func (p *PlaybackManager) PlayerStatus() player.Status { func (p *PlaybackManager) PlayerStatus() player.Status {
@@ -466,14 +466,17 @@ func (p *PlaybackManager) Volume() int {
} }
func (p *PlaybackManager) SeekNext() error { func (p *PlaybackManager) SeekNext() error {
return p.player.SeekNext() if p.CurrentPlayer().GetStatus().State == player.Stopped {
return nil
}
return p.PlayTrackAt(p.nowPlayingIdx + 1)
} }
func (p *PlaybackManager) SeekBackOrPrevious() error { func (p *PlaybackManager) SeekBackOrPrevious() error {
if p.player.GetStatus().TimePos > 3 { if p.nowPlayingIdx == 0 || p.player.GetStatus().TimePos > 3 {
return p.player.SeekSeconds(0) return p.player.SeekSeconds(0)
} }
return p.player.SeekPrevious() return p.PlayTrackAt(p.nowPlayingIdx - 1)
} }
// Seek to given absolute position in the current track by seconds. // Seek to given absolute position in the current track by seconds.
@@ -501,6 +504,9 @@ func (p *PlaybackManager) Pause() error {
} }
func (p *PlaybackManager) Continue() error { func (p *PlaybackManager) Continue() error {
if p.player.GetStatus().State == player.Stopped {
return p.PlayFromBeginning()
}
return p.player.Continue() return p.player.Continue()
} }
@@ -516,6 +522,25 @@ func (p *PlaybackManager) PlayPause() error {
return errors.New("unreached - invalid player state") return errors.New("unreached - invalid player state")
} }
func (p *PlaybackManager) setTrack(idx int, next bool) error {
if urlP, ok := p.player.(player.URLPlayer); ok {
url, err := p.sm.Server.GetStreamURL(p.playQueue[idx].ID, p.transcodeCfg.ForceRawFile)
if err != nil {
return err
}
if next {
return urlP.SetNextFile(url)
}
return urlP.PlayFile(url)
} else if trP, ok := p.player.(player.TrackPlayer); ok {
if next {
return trP.SetNextTrack(p.playQueue[idx])
}
return trP.PlayTrack(p.playQueue[idx])
}
panic("Unsupported player type")
}
// call BEFORE updating p.nowPlayingIdx // call BEFORE updating p.nowPlayingIdx
func (p *PlaybackManager) checkScrobble() { func (p *PlaybackManager) checkScrobble() {
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
+18 -129
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log"
"math" "math"
"strconv" "strconv"
@@ -58,9 +57,9 @@ type Player struct {
haveRGainOpts bool haveRGainOpts bool
audioExclusive bool audioExclusive bool
status player.Status status player.Status
loopMode player.LoopMode
seeking bool seeking bool
curPlaylistPos int64 curPlaylistPos int64
lenPlaylist int64
prePausedState player.State prePausedState player.State
clientName string clientName string
equalizer Equalizer equalizer Equalizer
@@ -72,7 +71,7 @@ type Player struct {
onStopped []func() onStopped []func()
onPlaying []func() onPlaying []func()
onSeek []func() onSeek []func()
onTrackChange []func(int) onTrackChange []func()
} }
// Returns a new player. // Returns a new player.
@@ -133,38 +132,19 @@ func (p *Player) Init(maxCacheMB int) error {
return nil return nil
} }
// Appends the given file to the play queue.
// Note that the Player API does not provide methods to read
// the play queue. Clients are expected to maintain their own play queue model.
func (p *Player) AppendFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"loadfile", url, "append"})
}
// Plays the specified file, clearing the previous play queue, if any. // Plays the specified file, clearing the previous play queue, if any.
func (p *Player) PlayFile(url string) error { func (p *Player) PlayFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized { if !p.initialized {
return ErrUnitialized return ErrUnitialized
} }
err := p.mpv.Command([]string{"loadfile", url, "replace"}) err := p.mpv.Command([]string{"loadfile", url, "replace"})
if err == nil { if err == nil {
p.setState(player.Playing) p.setState(player.Playing)
p.lenPlaylist = 1
} }
return err return err
} }
// Removes the item at the given index from the internal playqueue.
func (p *Player) RemoveTrackAt(idx int) error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)})
}
// Stops playback and clears the play queue. // Stops playback and clears the play queue.
func (p *Player) Stop() error { func (p *Player) Stop() error {
if !p.initialized { if !p.initialized {
@@ -180,30 +160,24 @@ func (p *Player) Stop() error {
} }
} }
if err == nil { if err == nil {
p.lenPlaylist = 0
p.setState(player.Stopped) p.setState(player.Stopped)
} }
return err return err
} }
// Clears the play queue, except for the currently playing file. func (p *Player) SetNextFile(url string) error {
func (p *Player) ClearPlayQueue() error { if p.lenPlaylist > p.curPlaylistPos+1 {
if !p.initialized { if err := p.mpv.Command([]string{"playlist-remove", strconv.Itoa(int(p.curPlaylistPos) + 1)}); err != nil {
return ErrUnitialized return err
}
p.lenPlaylist--
} }
return p.mpv.Command([]string{"playlist-clear"}) err := p.mpv.Command([]string{"loadfile", url, "append"})
} if err == nil {
p.lenPlaylist++
func (p *Player) SetFile(url string) error {
if !p.initialized {
return ErrUnitialized
} }
if err := p.mpv.Command([]string{"stop"}); err != nil { return err
return err
}
if err := p.mpv.Command([]string{"playlist-clear"}); err != nil {
return err
}
return p.mpv.Command([]string{"loadfile", url, "append"})
} }
// Seeks within the currently playing track. // Seeks within the currently playing track.
@@ -218,27 +192,6 @@ func (p *Player) SeekSeconds(secs float64) error {
return err return err
} }
// Seeks to the beginning of the previous track,
// or if no previous track, seeks to the beginning of the current track.
func (p *Player) SeekPrevious() error {
if !p.initialized {
return ErrUnitialized
}
if pos, err := p.getInt64Property("playlist-pos"); err == nil && pos == 0 {
return p.SeekSeconds(0)
}
return p.mpv.Command([]string{"playlist-prev"})
}
// Seeks to the next track in the play queue, if any.
func (p *Player) SeekNext() error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-next"})
}
// Sets the volume of the player (0-100). // Sets the volume of the player (0-100).
// Unlike most Player functions, SetVolume can be called before Init, // Unlike most Player functions, SetVolume can be called before Init,
// to set the initial volume of the player on startup. // to set the initial volume of the player on startup.
@@ -325,22 +278,6 @@ func (p *Player) setPaused(paused bool) error {
return err return err
} }
// Start playback from the specified track index in the play queue.
func (p *Player) PlayTrackAt(idx int) error {
// check if we have anything to play
if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) {
return nil
}
err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)})
if p.GetStatus().State == player.Paused {
err = p.setPaused(false)
}
if err == nil {
p.setState(player.Playing)
}
return err
}
// Pause playback and update the player state // Pause playback and update the player state
func (p *Player) Pause() error { func (p *Player) Pause() error {
if p.status.State != player.Playing { if p.status.State != player.Playing {
@@ -362,57 +299,11 @@ func (p *Player) Continue() error {
p.setState(p.prePausedState) p.setState(p.prePausedState)
} }
return err return err
} else if p.status.State == player.Stopped {
return p.PlayTrackAt(0)
} }
return nil return nil
} }
// Get the loop mode of the player.
func (p *Player) GetLoopMode() player.LoopMode {
return p.loopMode
}
// Set the loop mode of the player.
func (p *Player) SetLoopMode(mode player.LoopMode) error {
if !p.initialized {
return ErrUnitialized
}
// Return early if player is already in specified mode
if mode == p.loopMode {
return nil
}
switch mode {
case player.LoopNone:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case player.LoopAll:
if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case player.LoopOne:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil {
return err
}
}
p.loopMode = mode
return nil
}
// Get the current status of the player. // Get the current status of the player.
func (p *Player) GetStatus() player.Status { func (p *Player) GetStatus() player.Status {
if !p.initialized { if !p.initialized {
@@ -539,7 +430,7 @@ func (p *Player) OnSeek(cb func()) {
// Registers a callback which is invoked when the currently playing track changes, // Registers a callback which is invoked when the currently playing track changes,
// or when playback begins at any time from the Stopped state. // or when playback begins at any time from the Stopped state.
// Callback is invoked with the index of the currently playing track (zero-based). // Callback is invoked with the index of the currently playing track (zero-based).
func (p *Player) OnTrackChange(cb func(int)) { func (p *Player) OnTrackChange(cb func()) {
p.onTrackChange = append(p.onTrackChange, cb) p.onTrackChange = append(p.onTrackChange, cb)
} }
@@ -600,6 +491,7 @@ func (p *Player) eventHandler(ctx context.Context) {
cb() cb()
} }
case mpv.EVENT_FILE_LOADED: case mpv.EVENT_FILE_LOADED:
p.curPlaylistPos, _ = p.getInt64Property("playlist-pos")
if p.status.State == player.Paused { if p.status.State == player.Paused {
// seek while paused switches to a new file // seek while paused switches to a new file
// mpv does not fire seek event in this case // mpv does not fire seek event in this case
@@ -607,11 +499,8 @@ func (p *Player) eventHandler(ctx context.Context) {
cb() cb()
} }
} }
if pos, err := p.getInt64Property("playlist-pos"); err == nil { for _, cb := range p.onTrackChange {
p.curPlaylistPos = pos cb()
for _, cb := range p.onTrackChange {
cb(int(pos))
}
} }
case mpv.EVENT_IDLE: case mpv.EVENT_IDLE:
p.status.Duration = 0 p.status.Duration = 0
+4 -44
View File
@@ -4,57 +4,17 @@ import "github.com/dweymouth/supersonic/backend/mediaprovider"
type URLPlayer interface { type URLPlayer interface {
BasePlayer BasePlayer
AppendFile(url string) error PlayFile(url string) error
} SetNextFile(url string) error
type URLPlayerNew interface {
BasePlayer
SetFile(url string)
SetNextFile(url string)
} }
type TrackPlayer interface { type TrackPlayer interface {
BasePlayer BasePlayer
AppendTrack(track *mediaprovider.Track) error PlayTrack(track *mediaprovider.Track) error
}
type TrackPlayerNew interface {
BasePlayer
SetTrack(track *mediaprovider.Track) error
SetNextTrack(track *mediaprovider.Track) error SetNextTrack(track *mediaprovider.Track) error
} }
type BasePlayer interface { type BasePlayer interface {
// Transport
PlayTrackAt(idx int) error
Continue() error
Pause() error
Stop() error
SeekPrevious() error
SeekNext() error
SeekSeconds(secs float64) error
IsSeeking() bool
SetVolume(int) error
GetVolume() int
GetStatus() Status
ClearPlayQueue() error
RemoveTrackAt(idx int) error
SetLoopMode(LoopMode) error
GetLoopMode() LoopMode
// Event API
OnPaused(func())
OnStopped(func())
OnPlaying(func())
OnSeek(func())
OnTrackChange(func(int))
}
type BasePlayerNew interface {
Continue() error Continue() error
Pause() error Pause() error
Stop() error Stop() error
@@ -72,7 +32,7 @@ type BasePlayerNew interface {
OnStopped(func()) OnStopped(func())
OnPlaying(func()) OnPlaying(func())
OnSeek(func()) OnSeek(func())
OnTrackChange(func(int)) OnTrackChange(func())
} }
type ReplayGainPlayer interface { type ReplayGainPlayer interface {