Beginning of reworked PlaybackManager - PARTIAL
This commit is contained in:
+71
-46
@@ -64,25 +64,26 @@ func NewPlaybackManager(
|
||||
// clamp to 99% to avoid any possible rounding issues
|
||||
scrobbleCfg.ThresholdPercent = clamp(scrobbleCfg.ThresholdPercent, 0, 99)
|
||||
pm := &PlaybackManager{
|
||||
ctx: ctx,
|
||||
sm: s,
|
||||
player: p,
|
||||
scrobbleCfg: scrobbleCfg,
|
||||
transcodeCfg: transcodeCfg,
|
||||
ctx: ctx,
|
||||
sm: s,
|
||||
player: p,
|
||||
scrobbleCfg: scrobbleCfg,
|
||||
transcodeCfg: transcodeCfg,
|
||||
nowPlayingIdx: -1,
|
||||
}
|
||||
p.OnTrackChange(func(tracknum int) {
|
||||
if tracknum >= len(pm.playQueue) {
|
||||
return
|
||||
}
|
||||
p.OnTrackChange(func() {
|
||||
pm.checkScrobble() // scrobble the previous song if needed
|
||||
if pm.player.GetStatus().State == player.Playing {
|
||||
pm.playTimeStopwatch.Start()
|
||||
}
|
||||
pm.nowPlayingIdx = tracknum
|
||||
pm.nowPlayingIdx++
|
||||
pm.curTrackTime = float64(pm.playQueue[pm.nowPlayingIdx].Duration)
|
||||
pm.sendNowPlayingScrobble() // Must come before invokeOnChangeCallbacks b/c track may immediately be scrobbled
|
||||
pm.invokeOnSongChangeCallbacks()
|
||||
pm.doUpdateTimePos()
|
||||
if pm.nowPlayingIdx < len(pm.playQueue)-1 {
|
||||
pm.setTrack(pm.nowPlayingIdx+1, true)
|
||||
}
|
||||
})
|
||||
p.OnSeek(func() {
|
||||
pm.doUpdateTimePos()
|
||||
@@ -95,6 +96,7 @@ func NewPlaybackManager(
|
||||
pm.doUpdateTimePos()
|
||||
pm.invokeOnSongChangeCallbacks()
|
||||
pm.invokeNoArgCallbacks(pm.onStopped)
|
||||
pm.nowPlayingIdx = -1
|
||||
})
|
||||
p.OnPaused(func() {
|
||||
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 {
|
||||
if !appendToQueue {
|
||||
p.player.Stop()
|
||||
p.nowPlayingIdx = 0
|
||||
p.nowPlayingIdx = -1
|
||||
p.playQueue = nil
|
||||
}
|
||||
nums := util.Range(len(tracks))
|
||||
if shuffle {
|
||||
util.ShuffleSlice(nums)
|
||||
}
|
||||
needToSetNext := appendToQueue && len(tracks) > 0 && p.nowPlayingIdx == len(p.playQueue)-1
|
||||
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
|
||||
// (tracking play count increases, favorite, and rating) without messing up
|
||||
// other views' track models
|
||||
tr := *tracks[i]
|
||||
p.playQueue = append(p.playQueue, &tr)
|
||||
}
|
||||
if needToSetNext {
|
||||
p.setTrack(p.nowPlayingIdx+1, true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -241,7 +235,7 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool
|
||||
if p.replayGainCfg.Mode == ReplayGainAuto {
|
||||
p.SetReplayGainMode(player.ReplayGainAlbum)
|
||||
}
|
||||
return p.player.PlayTrackAt(firstTrack)
|
||||
return p.PlayTrackAt(firstTrack)
|
||||
}
|
||||
|
||||
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 {
|
||||
p.SetReplayGainMode(player.ReplayGainTrack)
|
||||
}
|
||||
return p.player.PlayTrackAt(firstTrack)
|
||||
return p.PlayTrackAt(firstTrack)
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlayTrack(trackID string) error {
|
||||
@@ -267,11 +261,15 @@ func (p *PlaybackManager) PlayTrack(trackID string) error {
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlayFromBeginning() error {
|
||||
return p.player.PlayTrackAt(0)
|
||||
return p.PlayTrackAt(0)
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -325,7 +323,7 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
|
||||
|
||||
func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
|
||||
newQueue := make([]*mediaprovider.Track, 0, len(p.playQueue)-len(trackIDs))
|
||||
rmCount := 0
|
||||
//rmCount := 0
|
||||
idSet := sharedutil.ToSet(trackIDs)
|
||||
isPlayingTrackRemoved := false
|
||||
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
|
||||
p.checkScrobble()
|
||||
}
|
||||
if err := p.player.RemoveTrackAt(i - rmCount); err == nil {
|
||||
rmCount++
|
||||
} else {
|
||||
log.Printf("error removing track: %v", err.Error())
|
||||
// did not remove this track
|
||||
newQueue = append(newQueue, tr)
|
||||
}
|
||||
// if err := p.player.RemoveTrackAt(i - rmCount); err == nil {
|
||||
// rmCount++
|
||||
//} else {
|
||||
var err error
|
||||
log.Printf("error removing track: %v", err.Error())
|
||||
// did not remove this track
|
||||
newQueue = append(newQueue, tr)
|
||||
//}
|
||||
} else {
|
||||
// not removing this track
|
||||
newQueue = append(newQueue, tr)
|
||||
@@ -359,9 +358,9 @@ func (p *PlaybackManager) RemoveTracksFromQueue(trackIDs []string) {
|
||||
// Stop playback and clear the play queue.
|
||||
func (p *PlaybackManager) StopAndClearPlayQueue() {
|
||||
p.player.Stop()
|
||||
p.player.ClearPlayQueue()
|
||||
p.doUpdateTimePos()
|
||||
p.playQueue = nil
|
||||
p.nowPlayingIdx = -1
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
|
||||
@@ -423,17 +422,17 @@ func (p *PlaybackManager) SetNextLoopMode() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, cb := range p.onLoopModeChange {
|
||||
cb(p.player.GetLoopMode())
|
||||
}
|
||||
//for _, cb := range p.onLoopModeChange {
|
||||
// cb(p.player.GetLoopMode())
|
||||
//}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error {
|
||||
if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil {
|
||||
return err
|
||||
}
|
||||
//if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil {
|
||||
// return err
|
||||
//}
|
||||
|
||||
for _, cb := range p.onLoopModeChange {
|
||||
cb(loopMode)
|
||||
@@ -443,7 +442,8 @@ func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error {
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) GetLoopMode() player.LoopMode {
|
||||
return p.player.GetLoopMode()
|
||||
return 0
|
||||
//return p.player.GetLoopMode()
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) PlayerStatus() player.Status {
|
||||
@@ -466,14 +466,17 @@ func (p *PlaybackManager) Volume() int {
|
||||
}
|
||||
|
||||
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 {
|
||||
if p.player.GetStatus().TimePos > 3 {
|
||||
if p.nowPlayingIdx == 0 || p.player.GetStatus().TimePos > 3 {
|
||||
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.
|
||||
@@ -501,6 +504,9 @@ func (p *PlaybackManager) Pause() error {
|
||||
}
|
||||
|
||||
func (p *PlaybackManager) Continue() error {
|
||||
if p.player.GetStatus().State == player.Stopped {
|
||||
return p.PlayFromBeginning()
|
||||
}
|
||||
return p.player.Continue()
|
||||
}
|
||||
|
||||
@@ -516,6 +522,25 @@ func (p *PlaybackManager) PlayPause() error {
|
||||
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
|
||||
func (p *PlaybackManager) checkScrobble() {
|
||||
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
|
||||
|
||||
+18
-129
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
@@ -58,9 +57,9 @@ type Player struct {
|
||||
haveRGainOpts bool
|
||||
audioExclusive bool
|
||||
status player.Status
|
||||
loopMode player.LoopMode
|
||||
seeking bool
|
||||
curPlaylistPos int64
|
||||
lenPlaylist int64
|
||||
prePausedState player.State
|
||||
clientName string
|
||||
equalizer Equalizer
|
||||
@@ -72,7 +71,7 @@ type Player struct {
|
||||
onStopped []func()
|
||||
onPlaying []func()
|
||||
onSeek []func()
|
||||
onTrackChange []func(int)
|
||||
onTrackChange []func()
|
||||
}
|
||||
|
||||
// Returns a new player.
|
||||
@@ -133,38 +132,19 @@ func (p *Player) Init(maxCacheMB int) error {
|
||||
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.
|
||||
func (p *Player) PlayFile(url string) error {
|
||||
log.Printf("Adding playback URL: %s", url)
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
}
|
||||
err := p.mpv.Command([]string{"loadfile", url, "replace"})
|
||||
if err == nil {
|
||||
p.setState(player.Playing)
|
||||
p.lenPlaylist = 1
|
||||
}
|
||||
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.
|
||||
func (p *Player) Stop() error {
|
||||
if !p.initialized {
|
||||
@@ -180,30 +160,24 @@ func (p *Player) Stop() error {
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
p.lenPlaylist = 0
|
||||
p.setState(player.Stopped)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Clears the play queue, except for the currently playing file.
|
||||
func (p *Player) ClearPlayQueue() error {
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
func (p *Player) SetNextFile(url string) error {
|
||||
if p.lenPlaylist > p.curPlaylistPos+1 {
|
||||
if err := p.mpv.Command([]string{"playlist-remove", strconv.Itoa(int(p.curPlaylistPos) + 1)}); err != nil {
|
||||
return err
|
||||
}
|
||||
p.lenPlaylist--
|
||||
}
|
||||
return p.mpv.Command([]string{"playlist-clear"})
|
||||
}
|
||||
|
||||
func (p *Player) SetFile(url string) error {
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
err := p.mpv.Command([]string{"loadfile", url, "append"})
|
||||
if err == nil {
|
||||
p.lenPlaylist++
|
||||
}
|
||||
if err := p.mpv.Command([]string{"stop"}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := p.mpv.Command([]string{"playlist-clear"}); err != nil {
|
||||
return err
|
||||
}
|
||||
return p.mpv.Command([]string{"loadfile", url, "append"})
|
||||
return err
|
||||
}
|
||||
|
||||
// Seeks within the currently playing track.
|
||||
@@ -218,27 +192,6 @@ func (p *Player) SeekSeconds(secs float64) error {
|
||||
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).
|
||||
// Unlike most Player functions, SetVolume can be called before Init,
|
||||
// to set the initial volume of the player on startup.
|
||||
@@ -325,22 +278,6 @@ func (p *Player) setPaused(paused bool) error {
|
||||
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
|
||||
func (p *Player) Pause() error {
|
||||
if p.status.State != player.Playing {
|
||||
@@ -362,57 +299,11 @@ func (p *Player) Continue() error {
|
||||
p.setState(p.prePausedState)
|
||||
}
|
||||
return err
|
||||
} else if p.status.State == player.Stopped {
|
||||
return p.PlayTrackAt(0)
|
||||
}
|
||||
|
||||
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.
|
||||
func (p *Player) GetStatus() player.Status {
|
||||
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,
|
||||
// or when playback begins at any time from the Stopped state.
|
||||
// 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)
|
||||
}
|
||||
|
||||
@@ -600,6 +491,7 @@ func (p *Player) eventHandler(ctx context.Context) {
|
||||
cb()
|
||||
}
|
||||
case mpv.EVENT_FILE_LOADED:
|
||||
p.curPlaylistPos, _ = p.getInt64Property("playlist-pos")
|
||||
if p.status.State == player.Paused {
|
||||
// seek while paused switches to a new file
|
||||
// mpv does not fire seek event in this case
|
||||
@@ -607,11 +499,8 @@ func (p *Player) eventHandler(ctx context.Context) {
|
||||
cb()
|
||||
}
|
||||
}
|
||||
if pos, err := p.getInt64Property("playlist-pos"); err == nil {
|
||||
p.curPlaylistPos = pos
|
||||
for _, cb := range p.onTrackChange {
|
||||
cb(int(pos))
|
||||
}
|
||||
for _, cb := range p.onTrackChange {
|
||||
cb()
|
||||
}
|
||||
case mpv.EVENT_IDLE:
|
||||
p.status.Duration = 0
|
||||
|
||||
+4
-44
@@ -4,57 +4,17 @@ import "github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
|
||||
type URLPlayer interface {
|
||||
BasePlayer
|
||||
AppendFile(url string) error
|
||||
}
|
||||
|
||||
type URLPlayerNew interface {
|
||||
BasePlayer
|
||||
SetFile(url string)
|
||||
SetNextFile(url string)
|
||||
PlayFile(url string) error
|
||||
SetNextFile(url string) error
|
||||
}
|
||||
|
||||
type TrackPlayer interface {
|
||||
BasePlayer
|
||||
AppendTrack(track *mediaprovider.Track) error
|
||||
}
|
||||
|
||||
type TrackPlayerNew interface {
|
||||
BasePlayer
|
||||
SetTrack(track *mediaprovider.Track) error
|
||||
PlayTrack(track *mediaprovider.Track) error
|
||||
SetNextTrack(track *mediaprovider.Track) error
|
||||
}
|
||||
|
||||
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
|
||||
Pause() error
|
||||
Stop() error
|
||||
@@ -72,7 +32,7 @@ type BasePlayerNew interface {
|
||||
OnStopped(func())
|
||||
OnPlaying(func())
|
||||
OnSeek(func())
|
||||
OnTrackChange(func(int))
|
||||
OnTrackChange(func())
|
||||
}
|
||||
|
||||
type ReplayGainPlayer interface {
|
||||
|
||||
Reference in New Issue
Block a user