Merge pull request #481 from dweymouth/playback-cmd-queue

Prep for Jukebox mode - add queue to accumulate playback commands
This commit is contained in:
Drew Weymouth
2024-09-11 17:01:06 -07:00
committed by GitHub
14 changed files with 588 additions and 232 deletions
+1 -3
View File
@@ -377,9 +377,7 @@ func (a *App) LoadSavedPlayQueue() error {
return nil return nil
} }
if err := a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false); err != nil { a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false)
return err
}
if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) { if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) {
// TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact // TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact
a.PlaybackManager.PlayTrackAt(queue.TrackIndex) a.PlaybackManager.PlayTrackAt(queue.TrackIndex)
+20 -27
View File
@@ -9,16 +9,16 @@ import (
) )
type PlaybackHandler interface { type PlaybackHandler interface {
PlayPause() error PlayPause()
Stop() error Stop()
Pause() error Pause()
Continue() error Continue()
SeekBackOrPrevious() error SeekBackOrPrevious()
SeekNext() error SeekNext()
SeekSeconds(float64) error SeekSeconds(float64)
SeekBySeconds(float64) error SeekBySeconds(float64)
Volume() int Volume() int
SetVolume(int) error SetVolume(int)
} }
type IPCServer interface { type IPCServer interface {
@@ -57,14 +57,12 @@ func (s *serverImpl) createHandler() http.Handler {
w.WriteHeader(http.StatusNotFound) w.WriteHeader(http.StatusNotFound)
w.Write([]byte("The given path is not valid")) w.Write([]byte("The given path is not valid"))
}) })
m.HandleFunc(PingPath, s.makeSimpleEndpointHandler(func() error { return nil })) m.HandleFunc(PingPath, s.makeSimpleEndpointHandler(func() {}))
m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() error { m.HandleFunc(ShowPath, s.makeSimpleEndpointHandler(func() {
s.showFn() s.showFn()
return nil
})) }))
m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() error { m.HandleFunc(QuitPath, s.makeSimpleEndpointHandler(func() {
s.quitFn() s.quitFn()
return nil
})) }))
m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.pbHandler.Continue)) m.HandleFunc(PlayPath, s.makeSimpleEndpointHandler(s.pbHandler.Continue))
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause)) m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
@@ -77,7 +75,8 @@ func (s *serverImpl) createHandler() http.Handler {
m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) { m.HandleFunc(VolumePath, func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get("v") v := r.URL.Query().Get("v")
if vol, err := strconv.Atoi(v); err == nil { if vol, err := strconv.Atoi(v); err == nil {
s.writeSimpleResponse(w, s.pbHandler.SetVolume(vol)) s.pbHandler.SetVolume(vol)
s.writeOK(w)
} else { } else {
s.writeErr(w, err) s.writeErr(w, err)
} }
@@ -85,31 +84,25 @@ func (s *serverImpl) createHandler() http.Handler {
return m return m
} }
func (s *serverImpl) makeSimpleEndpointHandler(f func() error) func(http.ResponseWriter, *http.Request) { func (s *serverImpl) makeSimpleEndpointHandler(f func()) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
s.writeSimpleResponse(w, f()) f()
s.writeOK(w)
} }
} }
func (s *serverImpl) makeFloatEndpointHandler(f func(float64) error, queryParam string) func(http.ResponseWriter, *http.Request) { func (s *serverImpl) makeFloatEndpointHandler(f func(float64), queryParam string) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
v := r.URL.Query().Get(queryParam) v := r.URL.Query().Get(queryParam)
if val, err := strconv.ParseFloat(v, 64); err == nil { if val, err := strconv.ParseFloat(v, 64); err == nil {
s.writeSimpleResponse(w, f(val)) f(val)
s.writeOK(w)
} else { } else {
s.writeErr(w, err) s.writeErr(w, err)
} }
} }
} }
func (s *serverImpl) writeSimpleResponse(w http.ResponseWriter, err error) {
if err == nil {
s.writeOK(w)
} else {
s.writeErr(w, err)
}
}
func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) { func (s *serverImpl) writeOK(w http.ResponseWriter) (int, error) {
var r Response var r Response
b, err := json.Marshal(&r) b, err := json.Marshal(&r)
+7 -2
View File
@@ -298,11 +298,16 @@ type JukeboxProvider interface {
JukeboxStop() error JukeboxStop() error
JukeboxSeek(idx, seconds int) error JukeboxSeek(idx, seconds int) error
JukeboxClear() error JukeboxClear() error
JukeboxSet(trackID string) error
JukeboxAdd(trackID string) error JukeboxAdd(trackID string) error
JukeboxRemove(idx int) error JukeboxRemove(idx int) error
JukeboxSetVolume(vol int) error
JukeboxGetStatus() (*JukeboxStatus, error) JukeboxGetStatus() (*JukeboxStatus, error)
// Performs a Clear followed by an Add to set the queue
// to contain a single track
JukeboxSet(trackID string) error
// Sets the volume of the jukebox player (0-100)
JukeboxSetVolume(vol int) error
} }
type JukeboxStatus struct { type JukeboxStatus struct {
+3 -1
View File
@@ -1,6 +1,7 @@
package subsonic package subsonic
import ( import (
"fmt"
"strconv" "strconv"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
@@ -24,8 +25,9 @@ func (s *subsonicMediaProvider) JukeboxClear() error {
} }
func (s *subsonicMediaProvider) JukeboxSetVolume(vol int) error { func (s *subsonicMediaProvider) JukeboxSetVolume(vol int) error {
v := float64(vol) / 100
_, err := s.client.JukeboxControl("setGain", _, err := s.client.JukeboxControl("setGain",
map[string]string{"gain": strconv.Itoa(vol)}) map[string]string{"gain": fmt.Sprintf("%0.2f", v)})
return err return err
} }
+16 -10
View File
@@ -150,46 +150,51 @@ func (m *MPRISHandler) SupportedMimeTypes() ([]string, error) {
// OrgMprisMediaPlayer2PlayerAdapter implementation // OrgMprisMediaPlayer2PlayerAdapter implementation
func (m *MPRISHandler) Next() error { func (m *MPRISHandler) Next() error {
return m.pm.SeekNext() m.pm.SeekNext()
return nil
} }
func (m *MPRISHandler) Previous() error { func (m *MPRISHandler) Previous() error {
return m.pm.SeekBackOrPrevious() m.pm.SeekBackOrPrevious()
return nil
} }
func (m *MPRISHandler) Pause() error { func (m *MPRISHandler) Pause() error {
if m.pm.PlayerStatus().State == player.Playing { if m.pm.PlayerStatus().State == player.Playing {
return m.pm.PlayPause() m.pm.PlayPause()
} }
return nil return nil
} }
func (m *MPRISHandler) PlayPause() error { func (m *MPRISHandler) PlayPause() error {
return m.pm.PlayPause() m.pm.PlayPause()
return nil
} }
func (m *MPRISHandler) Stop() error { func (m *MPRISHandler) Stop() error {
return m.pm.Stop() m.pm.Stop()
return nil
} }
func (m *MPRISHandler) Play() error { func (m *MPRISHandler) Play() error {
switch m.pm.PlayerStatus().State { switch m.pm.PlayerStatus().State {
case player.Paused: case player.Paused:
return m.pm.PlayPause() m.pm.PlayPause()
case player.Stopped: case player.Stopped:
return m.pm.PlayFromBeginning() m.pm.PlayFromBeginning()
} }
return nil return nil
} }
func (m *MPRISHandler) Seek(offset types.Microseconds) error { func (m *MPRISHandler) Seek(offset types.Microseconds) error {
// MPRIS seek command is relative to current position // MPRIS seek command is relative to current position
return m.pm.SeekBySeconds(microsecondsToSeconds(offset)) m.pm.SeekBySeconds(microsecondsToSeconds(offset))
return nil
} }
func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error { func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error {
if m.curTrackPath == trackId { if m.curTrackPath == trackId {
return m.pm.SeekSeconds(microsecondsToSeconds(position)) m.pm.SeekSeconds(microsecondsToSeconds(position))
} }
return nil return nil
} }
@@ -297,7 +302,8 @@ func (m *MPRISHandler) Volume() (float64, error) {
} }
func (m *MPRISHandler) SetVolume(v float64) error { func (m *MPRISHandler) SetVolume(v float64) error {
return m.pm.SetVolume(int(v * 100)) m.pm.SetVolume(int(v * 100))
return nil
} }
func (m *MPRISHandler) Position() (int64, error) { func (m *MPRISHandler) Position() (int64, error) {
+222
View File
@@ -0,0 +1,222 @@
package backend
import (
"slices"
"sync"
"github.com/dweymouth/supersonic/backend/mediaprovider"
)
type playbackCommandType int
const (
cmdStop playbackCommandType = iota
cmdContinue
cmdPause
cmdPlayTrackAt // arg: int
cmdSeekSeconds // arg: float64
cmdSeekFwdBackN // arg: int
cmdVolume // arg: int
cmdLoopMode // arg: LoopMode
cmdStopAndClearPlayQueue
cmdUpdatePlayQueue // arg: []mediaprovider.MediaItem
cmdRemoveTracksFromQueue // arg: []int
// arg: []mediaprovider.MediaItem
// arg2: InsertMode
// arg3: bool (shuffle)
cmdLoadItems
cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode
)
type playbackCommand struct {
Type playbackCommandType
Arg any
Arg2 any
Arg3 any
}
// playbackCommandQueue is a queue to accumulate player commands from the UI
// commands are processed by the playback engine as fast as they can, but if
// more commands arrive before the player can respond to them, they will queue up
// and some commands may coalesce together (e.g. multiple volume commands into just one)
type playbackCommandQueue struct {
mutex sync.Mutex
queue []playbackCommand
cmdAvailable *sync.Cond
nextChan chan playbackCommand
}
func NewCommandQueue() *playbackCommandQueue {
c := &playbackCommandQueue{}
c.nextChan = make(chan playbackCommand)
c.cmdAvailable = sync.NewCond(&c.mutex)
go c.chanWriter()
return c
}
func (c *playbackCommandQueue) C() <-chan playbackCommand {
return c.nextChan
}
func (c *playbackCommandQueue) Stop() {
c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop},
playbackCommand{Type: cmdStop})
}
func (c *playbackCommandQueue) Continue() {
c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop},
playbackCommand{Type: cmdContinue})
}
func (c *playbackCommandQueue) Pause() {
c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop},
playbackCommand{Type: cmdPause})
}
func (c *playbackCommandQueue) PlayTrackAt(idx int) {
c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop, cmdPlayTrackAt},
playbackCommand{Type: cmdPlayTrackAt, Arg: idx})
}
func (c *playbackCommandQueue) StopAndClearPlayQueue() {
c.filterCommandsAndAdd([]playbackCommandType{cmdContinue, cmdPause, cmdStop, cmdStopAndClearPlayQueue},
playbackCommand{Type: cmdStopAndClearPlayQueue})
}
func (c *playbackCommandQueue) SetVolume(vol int) {
c.filterCommandsAndAdd([]playbackCommandType{cmdVolume},
playbackCommand{Type: cmdVolume, Arg: vol})
}
func (c *playbackCommandQueue) SetLoopMode(mode LoopMode) {
c.filterCommandsAndAdd([]playbackCommandType{cmdLoopMode},
playbackCommand{Type: cmdLoopMode, Arg: mode})
}
func (c *playbackCommandQueue) SeekSeconds(s float64) {
c.filterCommandsAndAdd([]playbackCommandType{cmdSeekSeconds},
playbackCommand{Type: cmdSeekSeconds, Arg: s})
}
func (c *playbackCommandQueue) SeekNext() {
c.seekBackOrFwd(1)
}
func (c *playbackCommandQueue) SeekBackOrPrevious() {
c.seekBackOrFwd(-1)
}
func (c *playbackCommandQueue) UpdatePlayQueue(items []mediaprovider.MediaItem) {
c.filterCommandsAndAdd([]playbackCommandType{cmdUpdatePlayQueue},
playbackCommand{Type: cmdUpdatePlayQueue, Arg: items})
}
func (c *playbackCommandQueue) RemoveItemsFromQueue(idxs []int) {
c.mutex.Lock()
c.queue = append(c.queue, playbackCommand{
Type: cmdRemoveTracksFromQueue,
Arg: idxs,
})
c.mutex.Unlock()
c.cmdAvailable.Signal()
}
func (c *playbackCommandQueue) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) {
c.mutex.Lock()
c.queue = append(c.queue, playbackCommand{
Type: cmdLoadRadioStation,
Arg: radio,
Arg2: insertMode,
})
c.mutex.Unlock()
c.cmdAvailable.Signal()
}
func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) {
c.mutex.Lock()
c.queue = append(c.queue, playbackCommand{
Type: cmdLoadItems,
Arg: items,
Arg2: insertQueueMode,
Arg3: shuffle,
})
c.mutex.Unlock()
c.cmdAvailable.Signal()
}
func (c *playbackCommandQueue) filterCommandsAndAdd(excludeTypes []playbackCommandType, command playbackCommand) {
c.mutex.Lock()
j := 0
for _, cmd := range c.queue {
if slices.Contains(excludeTypes, cmd.Type) {
continue
}
c.queue[j] = cmd
j++
}
c.queue = c.queue[:j]
c.queue = append(c.queue, command)
c.mutex.Unlock()
c.cmdAvailable.Signal()
}
func (c *playbackCommandQueue) seekBackOrFwd(direction int) {
// find the index of the last seekBackOrFwd command
// in the queue that can be coalesced with this one
lastIdx := -1
c.mutex.Lock()
done := false
for i := len(c.queue) - 1; i >= 0 && !done; i-- {
cmd := c.queue[i]
switch cmd.Type {
case cmdSeekFwdBackN:
lastIdx = i
case cmdRemoveTracksFromQueue, cmdLoadItems, cmdPlayTrackAt,
cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue:
// any queue-modifying command means we can't coalesce any
// more seekFwdBackN commands before here
done = true
}
}
if lastIdx == -1 {
// no coalescable seekFwdBackN commands, just append new one
c.queue = append(c.queue, playbackCommand{Type: cmdSeekFwdBackN, Arg: direction})
} else {
newQueue := make([]playbackCommand, 0, len(c.queue))
// copy over all cmds past the first coalescable idx
newQueue = append(newQueue, c.queue[0:lastIdx]...)
n := direction
for i := lastIdx; i < len(c.queue); i++ {
if cmd := c.queue[i]; cmd.Type == cmdSeekFwdBackN {
// coalesce this cmd with the new one
n += cmd.Arg.(int)
} else {
// copy over other non-seekFwdBackN command
newQueue = append(newQueue, cmd)
}
}
newQueue = append(newQueue, playbackCommand{
Type: cmdSeekFwdBackN,
Arg: n,
})
c.queue = newQueue
}
c.mutex.Unlock()
c.cmdAvailable.Signal()
}
func (c *playbackCommandQueue) chanWriter() {
for {
c.mutex.Lock()
for len(c.queue) == 0 {
c.cmdAvailable.Wait()
}
cmd := c.queue[0]
copy(c.queue, c.queue[1:])
c.queue = c.queue[:len(c.queue)-1]
c.mutex.Unlock()
c.nextChan <- cmd
}
}
+50 -15
View File
@@ -48,11 +48,12 @@ type playbackEngine struct {
latestTrackPosition float64 // cleared by checkScrobble latestTrackPosition float64 // cleared by checkScrobble
callbacksDisabled bool callbacksDisabled bool
playQueue []mediaprovider.MediaItem playQueue []mediaprovider.MediaItem
nowPlayingIdx int nowPlayingIdx int
isRadio bool isRadio bool
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
loopMode LoopMode noIncrementNextTrackChange bool // true iff the nowPlayingIndex should not be incremente don the next onTrackChange
loopMode LoopMode
// to pass to onSongChange listeners; clear once listeners have been called // to pass to onSongChange listeners; clear once listeners have been called
lastScrobbled *mediaprovider.Track lastScrobbled *mediaprovider.Track
@@ -119,8 +120,12 @@ func (p *playbackEngine) PlayTrackAt(idx int) error {
if idx < 0 || idx >= len(p.playQueue) { if idx < 0 || idx >= len(p.playQueue) {
return errors.New("track index out of range") return errors.New("track index out of range")
} }
p.nowPlayingIdx = idx - 1 p.noIncrementNextTrackChange = true
return p.setTrack(idx, false) err := p.setTrack(idx, false)
if err == nil {
p.nowPlayingIdx = idx
}
return err
} }
// Gets the curently playing media item, if any. // Gets the curently playing media item, if any.
@@ -183,6 +188,22 @@ func (p *playbackEngine) SeekBackOrPrevious() error {
return p.PlayTrackAt(p.nowPlayingIdx - 1) return p.PlayTrackAt(p.nowPlayingIdx - 1)
} }
func (p *playbackEngine) SeekFwdBackN(n int) error {
idx := p.nowPlayingIdx
if n < 0 && p.player.GetStatus().TimePos > 3 {
n += 1 // first seek back is just seek to beginning of current
}
if n == 0 || (idx == 0 && n < 0) {
return p.player.SeekSeconds(0) // seek back in current song
}
lastIdx := len(p.playQueue) - 1
if idx == lastIdx && n > 0 {
return nil // already on last track, nothing to seek next to
}
newIdx := minInt(lastIdx, maxInt(0, idx+n))
return p.PlayTrackAt(newIdx)
}
// Seek to given absolute position in the current track by seconds. // Seek to given absolute position in the current track by seconds.
func (p *playbackEngine) SeekSeconds(sec float64) error { func (p *playbackEngine) SeekSeconds(sec float64) error {
if p.isRadio { if p.isRadio {
@@ -213,14 +234,14 @@ func (p *playbackEngine) Continue() error {
// Load items into the play queue. // Load items into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // If replacing the current queue (!appendToQueue), playback will be stopped.
func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error {
newItems := p.deepCopyMediaItemSlice(items) newItems := deepCopyMediaItemSlice(items)
return p.doLoaditems(newItems, insertQueueMode, shuffle) return p.doLoaditems(newItems, insertQueueMode, shuffle)
} }
// Load tracks into the play queue. // Load tracks into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // If replacing the current queue (!appendToQueue), playback will be stopped.
func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error {
newTracks := p.copyTrackSliceToMediaItemSlice(tracks) newTracks := copyTrackSliceToMediaItemSlice(tracks)
return p.doLoaditems(newTracks, insertQueueMode, shuffle) return p.doLoaditems(newTracks, insertQueueMode, shuffle)
} }
@@ -279,7 +300,6 @@ func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, ins
func (p *playbackEngine) StopAndClearPlayQueue() { func (p *playbackEngine) StopAndClearPlayQueue() {
changed := len(p.playQueue) > 0 changed := len(p.playQueue) > 0
p.player.Stop() p.player.Stop()
p.doUpdateTimePos(false)
p.playQueue = nil p.playQueue = nil
p.nowPlayingIdx = -1 p.nowPlayingIdx = -1
if changed { if changed {
@@ -288,7 +308,7 @@ func (p *playbackEngine) StopAndClearPlayQueue() {
} }
func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem {
return p.deepCopyMediaItemSlice(p.playQueue) return deepCopyMediaItemSlice(p.playQueue)
} }
// Any time the user changes the favorite status of a track elsewhere in the app, // Any time the user changes the favorite status of a track elsewhere in the app,
@@ -315,7 +335,7 @@ func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) {
// Does not stop playback if the currently playing track is in the new queue, // Does not stop playback if the currently playing track is in the new queue,
// but updates the now playing index to point to the first instance of the track in the new queue. // but updates the now playing index to point to the first instance of the track in the new queue.
func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error { func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error {
newQueue := p.deepCopyMediaItemSlice(items) newQueue := deepCopyMediaItemSlice(items)
newNowPlayingIdx := -1 newNowPlayingIdx := -1
if p.nowPlayingIdx >= 0 { if p.nowPlayingIdx >= 0 {
nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID
@@ -433,12 +453,13 @@ func (p *playbackEngine) handleOnTrackChange() {
if p.player.GetStatus().State == player.Playing { if p.player.GetStatus().State == player.Playing {
p.playTimeStopwatch.Start() p.playTimeStopwatch.Start()
} }
if p.wasStopped || p.loopMode != LoopOne { if !p.noIncrementNextTrackChange && (p.wasStopped || p.loopMode != LoopOne) {
p.nowPlayingIdx++ p.nowPlayingIdx++
if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) { if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) {
p.nowPlayingIdx = 0 // wrapped around p.nowPlayingIdx = 0 // wrapped around
} }
} }
p.noIncrementNextTrackChange = false
nowPlaying := p.playQueue[p.nowPlayingIdx] nowPlaying := p.playQueue[p.nowPlayingIdx]
_, isRadio := nowPlaying.(*mediaprovider.RadioStation) _, isRadio := nowPlaying.(*mediaprovider.RadioStation)
p.isRadio = isRadio p.isRadio = isRadio
@@ -591,7 +612,7 @@ func (p *playbackEngine) sendNowPlayingScrobble() {
// creates a deep copy of the track info so that we can maintain our own state // creates a deep copy of the track info so that we can maintain our own state
// (play count increases, favorite, and rating) without messing up other views' track models // (play count increases, favorite, and rating) without messing up other views' track models
func (p *playbackEngine) deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.MediaItem { func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.MediaItem {
newTracks := make([]mediaprovider.MediaItem, len(tracks)) newTracks := make([]mediaprovider.MediaItem, len(tracks))
for i, tr := range tracks { for i, tr := range tracks {
newTracks[i] = tr.Copy() newTracks[i] = tr.Copy()
@@ -599,7 +620,7 @@ func (p *playbackEngine) deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem
return newTracks return newTracks
} }
func (p *playbackEngine) copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { func copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem {
newTracks := make([]mediaprovider.MediaItem, len(tracks)) newTracks := make([]mediaprovider.MediaItem, len(tracks))
for i, tr := range tracks { for i, tr := range tracks {
newTracks[i] = tr.Copy() newTracks[i] = tr.Copy()
@@ -668,3 +689,17 @@ func (p *playbackEngine) doUpdateTimePos(seeked bool) {
cb(s.TimePos, duration, seeked) cb(s.TimePos, duration, seeked)
} }
} }
func minInt(a, b int) int {
if a < b {
return a
}
return b
}
func maxInt(a, b int) int {
if a > b {
return a
}
return b
}
+108 -54
View File
@@ -2,7 +2,6 @@ package backend
import ( import (
"context" "context"
"errors"
"log" "log"
"math/rand" "math/rand"
@@ -13,7 +12,8 @@ import (
// A high-level MediaProvider-aware playback engine, serves as an // A high-level MediaProvider-aware playback engine, serves as an
// intermediary between the frontend and various Player backends. // intermediary between the frontend and various Player backends.
type PlaybackManager struct { type PlaybackManager struct {
engine *playbackEngine engine *playbackEngine
cmdQueue *playbackCommandQueue
} }
func NewPlaybackManager( func NewPlaybackManager(
@@ -23,9 +23,14 @@ func NewPlaybackManager(
scrobbleCfg *ScrobbleConfig, scrobbleCfg *ScrobbleConfig,
transcodeCfg *TranscodingConfig, transcodeCfg *TranscodingConfig,
) *PlaybackManager { ) *PlaybackManager {
return &PlaybackManager{ e := NewPlaybackEngine(ctx, s, p, scrobbleCfg, transcodeCfg)
engine: NewPlaybackEngine(ctx, s, p, scrobbleCfg, transcodeCfg), q := NewCommandQueue()
pm := &PlaybackManager{
engine: e,
cmdQueue: q,
} }
go pm.runCmdQueue(ctx)
return pm
} }
func (p *PlaybackManager) CurrentPlayer() player.BasePlayer { func (p *PlaybackManager) CurrentPlayer() player.BasePlayer {
@@ -106,7 +111,8 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM
if err != nil { if err != nil {
return err return err
} }
return p.LoadTracks(album.Tracks, insertQueueMode, shuffle) p.LoadTracks(album.Tracks, insertQueueMode, shuffle)
return nil
} }
// Loads the specified playlist into the play queue. // Loads the specified playlist into the play queue.
@@ -115,26 +121,28 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert
if err != nil { if err != nil {
return err return err
} }
return p.LoadTracks(playlist.Tracks, insertQueueMode, shuffle) p.LoadTracks(playlist.Tracks, insertQueueMode, shuffle)
return nil
} }
// Load tracks into the play queue. // Load tracks into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // If replacing the current queue (!appendToQueue), playback will be stopped.
func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) {
return p.engine.LoadTracks(tracks, insertQueueMode, shuffle) items := copyTrackSliceToMediaItemSlice(tracks)
p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
} }
// Load items into the play queue. // Load items into the play queue.
// If replacing the current queue (!appendToQueue), playback will be stopped. // If replacing the current queue (!appendToQueue), playback will be stopped.
func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) {
return p.engine.LoadItems(items, insertQueueMode, shuffle) p.cmdQueue.LoadItems(items, insertQueueMode, shuffle)
} }
// Replaces the play queue with the given set of tracks. // Replaces the play queue with the given set of tracks.
// Does not stop playback if the currently playing track is in the new queue, // Does not stop playback if the currently playing track is in the new queue,
// but updates the now playing index to point to the first instance of the track in the new queue. // but updates the now playing index to point to the first instance of the track in the new queue.
func (p *PlaybackManager) UpdatePlayQueue(items []mediaprovider.MediaItem) error { func (p *PlaybackManager) UpdatePlayQueue(items []mediaprovider.MediaItem) {
return p.engine.UpdatePlayQueue(items) p.cmdQueue.UpdatePlayQueue(items)
} }
func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool) error { func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool) error {
@@ -144,7 +152,8 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool
if p.engine.replayGainCfg.Mode == ReplayGainAuto { if p.engine.replayGainCfg.Mode == ReplayGainAuto {
p.SetReplayGainMode(player.ReplayGainAlbum) p.SetReplayGainMode(player.ReplayGainAlbum)
} }
return p.PlayTrackAt(firstTrack) p.PlayTrackAt(firstTrack)
return nil
} }
func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffle bool) error { func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffle bool) error {
@@ -154,7 +163,8 @@ func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffl
if p.engine.replayGainCfg.Mode == ReplayGainAuto { if p.engine.replayGainCfg.Mode == ReplayGainAuto {
p.SetReplayGainMode(player.ReplayGainTrack) p.SetReplayGainMode(player.ReplayGainTrack)
} }
return p.PlayTrackAt(firstTrack) p.PlayTrackAt(firstTrack)
return nil
} }
func (p *PlaybackManager) PlayTrack(trackID string) error { func (p *PlaybackManager) PlayTrack(trackID string) error {
@@ -166,13 +176,14 @@ func (p *PlaybackManager) PlayTrack(trackID string) error {
if p.engine.replayGainCfg.Mode == ReplayGainAuto { if p.engine.replayGainCfg.Mode == ReplayGainAuto {
p.SetReplayGainMode(player.ReplayGainTrack) p.SetReplayGainMode(player.ReplayGainTrack)
} }
return p.PlayFromBeginning() p.PlayFromBeginning()
return nil
} }
func (p *PlaybackManager) ShuffleArtistAlbums(artistID string) { func (p *PlaybackManager) ShuffleArtistAlbums(artistID string) {
artist, err := p.engine.sm.Server.GetArtist(artistID) artist, err := p.engine.sm.Server.GetArtist(artistID)
if err != nil { if err != nil {
log.Printf(err.Error()) log.Printf("failed to get artist: %v\n", err)
return return
} }
if len(artist.Albums) == 0 { if len(artist.Albums) == 0 {
@@ -196,7 +207,7 @@ func (p *PlaybackManager) ShuffleArtistAlbums(artistID string) {
func (p *PlaybackManager) PlayArtistDiscography(artistID string, shuffleTracks bool) { func (p *PlaybackManager) PlayArtistDiscography(artistID string, shuffleTracks bool) {
tr, err := p.engine.sm.Server.GetArtistTracks(artistID) tr, err := p.engine.sm.Server.GetArtistTracks(artistID)
if err != nil { if err != nil {
log.Printf(err.Error()) log.Printf("failed to get artist tracks: %v\n", err)
return return
} }
p.LoadTracks(tr, Replace, shuffleTracks) p.LoadTracks(tr, Replace, shuffleTracks)
@@ -210,12 +221,12 @@ func (p *PlaybackManager) PlayArtistDiscography(artistID string, shuffleTracks b
p.PlayFromBeginning() p.PlayFromBeginning()
} }
func (p *PlaybackManager) PlayFromBeginning() error { func (p *PlaybackManager) PlayFromBeginning() {
return p.engine.PlayTrackAt(0) p.cmdQueue.PlayTrackAt(0)
} }
func (p *PlaybackManager) PlayTrackAt(idx int) error { func (p *PlaybackManager) PlayTrackAt(idx int) {
return p.engine.PlayTrackAt(idx) p.cmdQueue.PlayTrackAt(idx)
} }
func (p *PlaybackManager) PlayRandomSongs(genreName string) { func (p *PlaybackManager) PlayRandomSongs(genreName string) {
@@ -231,12 +242,12 @@ func (p *PlaybackManager) PlaySimilarSongs(id string) {
} }
func (p *PlaybackManager) LoadRadioStation(station *mediaprovider.RadioStation, queueMode InsertQueueMode) { func (p *PlaybackManager) LoadRadioStation(station *mediaprovider.RadioStation, queueMode InsertQueueMode) {
p.engine.LoadRadioStation(station, queueMode) p.cmdQueue.LoadRadioStation(station, queueMode)
} }
func (p *PlaybackManager) PlayRadioStation(station *mediaprovider.RadioStation) error { func (p *PlaybackManager) PlayRadioStation(station *mediaprovider.RadioStation) {
p.LoadRadioStation(station, Replace) p.LoadRadioStation(station, Replace)
return p.PlayFromBeginning() p.PlayFromBeginning()
} }
func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Track, error)) { func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Track, error)) {
@@ -268,12 +279,12 @@ func (p *PlaybackManager) OnTrackRatingChanged(id string, rating int) {
} }
func (p *PlaybackManager) RemoveTracksFromQueue(idxs []int) { func (p *PlaybackManager) RemoveTracksFromQueue(idxs []int) {
p.engine.RemoveTracksFromQueue(idxs) p.cmdQueue.RemoveItemsFromQueue(idxs)
} }
// Stop playback and clear the play queue. // Stop playback and clear the play queue.
func (p *PlaybackManager) StopAndClearPlayQueue() { func (p *PlaybackManager) StopAndClearPlayQueue() {
p.engine.StopAndClearPlayQueue() p.cmdQueue.StopAndClearPlayQueue()
} }
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) { func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
@@ -289,17 +300,16 @@ func (p *PlaybackManager) SetReplayGainMode(mode player.ReplayGainMode) {
func (p *PlaybackManager) SetNextLoopMode() { func (p *PlaybackManager) SetNextLoopMode() {
switch p.engine.loopMode { switch p.engine.loopMode {
case LoopNone: case LoopNone:
p.engine.SetLoopMode(LoopAll) p.cmdQueue.SetLoopMode(LoopAll)
case LoopAll: case LoopAll:
p.engine.SetLoopMode(LoopOne) p.cmdQueue.SetLoopMode(LoopOne)
case LoopOne: case LoopOne:
p.engine.SetLoopMode(LoopNone) p.cmdQueue.SetLoopMode(LoopNone)
} }
} }
func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) { func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) {
p.engine.SetLoopMode(loopMode) p.cmdQueue.SetLoopMode(loopMode)
} }
func (p *PlaybackManager) GetLoopMode() LoopMode { func (p *PlaybackManager) GetLoopMode() LoopMode {
@@ -310,29 +320,29 @@ func (p *PlaybackManager) PlayerStatus() player.Status {
return p.engine.PlayerStatus() return p.engine.PlayerStatus()
} }
func (p *PlaybackManager) SetVolume(vol int) error { func (p *PlaybackManager) SetVolume(vol int) {
return p.engine.SetVolume(vol) p.cmdQueue.SetVolume(vol)
} }
func (p *PlaybackManager) Volume() int { func (p *PlaybackManager) Volume() int {
return p.engine.CurrentPlayer().GetVolume() return p.engine.CurrentPlayer().GetVolume()
} }
func (p *PlaybackManager) SeekNext() error { func (p *PlaybackManager) SeekNext() {
return p.engine.SeekNext() p.cmdQueue.SeekNext()
} }
func (p *PlaybackManager) SeekBackOrPrevious() error { func (p *PlaybackManager) SeekBackOrPrevious() {
return p.engine.SeekBackOrPrevious() p.cmdQueue.SeekBackOrPrevious()
} }
// Seek to given absolute position in the current track by seconds. // Seek to given absolute position in the current track by seconds.
func (p *PlaybackManager) SeekSeconds(sec float64) error { func (p *PlaybackManager) SeekSeconds(sec float64) {
return p.engine.SeekSeconds(sec) p.cmdQueue.SeekSeconds(sec)
} }
// Seek by given relative position in the current track by seconds. // Seek by given relative position in the current track by seconds.
func (p *PlaybackManager) SeekBySeconds(sec float64) error { func (p *PlaybackManager) SeekBySeconds(sec float64) {
status := p.engine.PlayerStatus() status := p.engine.PlayerStatus()
target := status.TimePos + sec target := status.TimePos + sec
if target < 0 { if target < 0 {
@@ -340,40 +350,84 @@ func (p *PlaybackManager) SeekBySeconds(sec float64) error {
} else if target > status.Duration { } else if target > status.Duration {
target = status.Duration target = status.Duration
} }
return p.engine.SeekSeconds(target) p.cmdQueue.SeekSeconds(target)
} }
// Seek to a fractional position in the current track [0..1] // Seek to a fractional position in the current track [0..1]
func (p *PlaybackManager) SeekFraction(fraction float64) error { func (p *PlaybackManager) SeekFraction(fraction float64) {
if fraction < 0 { if fraction < 0 {
fraction = 0 fraction = 0
} else if fraction > 1 { } else if fraction > 1 {
fraction = 1 fraction = 1
} }
target := p.engine.curTrackDuration * fraction target := p.engine.curTrackDuration * fraction
return p.engine.SeekSeconds(target) p.cmdQueue.SeekSeconds(target)
} }
func (p *PlaybackManager) Stop() error { func (p *PlaybackManager) Stop() {
return p.engine.Stop() p.cmdQueue.Stop()
} }
func (p *PlaybackManager) Pause() error { func (p *PlaybackManager) Pause() {
return p.engine.Pause() p.cmdQueue.Pause()
} }
func (p *PlaybackManager) Continue() error { func (p *PlaybackManager) Continue() {
return p.engine.Continue() p.cmdQueue.Continue()
} }
func (p *PlaybackManager) PlayPause() error { func (p *PlaybackManager) PlayPause() {
switch p.engine.PlayerStatus().State { switch p.engine.PlayerStatus().State {
case player.Playing: case player.Playing:
return p.engine.Pause() p.Pause()
case player.Paused: case player.Paused:
return p.engine.Continue() p.Continue()
case player.Stopped: case player.Stopped:
return p.engine.PlayTrackAt(0) p.PlayTrackAt(0)
}
}
func (p *PlaybackManager) runCmdQueue(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
case c := <-p.cmdQueue.C():
switch c.Type {
case cmdStop:
p.engine.Stop()
case cmdContinue:
p.engine.Continue()
case cmdPause:
p.engine.Pause()
case cmdPlayTrackAt:
p.engine.PlayTrackAt(c.Arg.(int))
case cmdSeekSeconds:
p.engine.SeekSeconds(c.Arg.(float64))
case cmdSeekFwdBackN:
p.engine.SeekFwdBackN(c.Arg.(int))
case cmdVolume:
p.engine.SetVolume(c.Arg.(int))
case cmdLoopMode:
p.engine.SetLoopMode(c.Arg.(LoopMode))
case cmdStopAndClearPlayQueue:
p.engine.StopAndClearPlayQueue()
case cmdUpdatePlayQueue:
p.engine.UpdatePlayQueue(c.Arg.([]mediaprovider.MediaItem))
case cmdRemoveTracksFromQueue:
p.engine.RemoveTracksFromQueue(c.Arg.([]int))
case cmdLoadItems:
p.engine.LoadItems(
c.Arg.([]mediaprovider.MediaItem),
c.Arg2.(InsertQueueMode),
c.Arg3.(bool),
)
case cmdLoadRadioStation:
p.engine.LoadRadioStation(
c.Arg.(*mediaprovider.RadioStation),
c.Arg2.(InsertQueueMode),
)
}
}
} }
return errors.New("unreached - invalid player state")
} }
+82 -56
View File
@@ -1,6 +1,8 @@
package jukebox package jukebox
import ( import (
"time"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player"
) )
@@ -12,25 +14,27 @@ const (
) )
type JukeboxPlayer struct { type JukeboxPlayer struct {
player.BasePlayerCallbackImpl
provider mediaprovider.JukeboxProvider provider mediaprovider.JukeboxProvider
state int // stopped, playing, paused state int // stopped, playing, paused
volume int volume int
seeking bool seeking bool
numTracks int
curTrack int curTrack int
curTrackDuration float64 queueLength int
startTrackTime float64 curTrackDuration float64
startedAtUnixSecs float64 startTrackTime float64
startedAtUnixMilli int64
nextTrackTimer *time.Timer
} }
func (j *JukeboxPlayer) SetVolume(vol int) error { func (j *JukeboxPlayer) SetVolume(vol int) error {
go func() { if err := j.provider.JukeboxSetVolume(vol); err != nil {
if err := j.provider.JukeboxSetVolume(vol); err == nil { return err
j.volume = vol }
} j.volume = vol
}()
return nil return nil
} }
@@ -38,26 +42,16 @@ func (j *JukeboxPlayer) GetVolume() int {
return j.volume return j.volume
} }
func (j *JukeboxPlayer) PlayTrackAt(idx int) error {
go func() {
if err := j.provider.JukeboxSeek(idx, 0); err == nil {
j.curTrack = idx
j.Continue()
}
}()
return nil
}
func (j *JukeboxPlayer) Continue() error { func (j *JukeboxPlayer) Continue() error {
if j.state == playing { if j.state == playing {
return nil return nil
} }
go func() { if err := j.startAndUpdateTime(); err != nil {
if err := j.provider.JukeboxStart(); err != nil { return err
return }
}
j.state = playing j.state = playing
}() j.InvokeOnPlaying()
return nil return nil
} }
@@ -65,12 +59,12 @@ func (j *JukeboxPlayer) Pause() error {
if j.state != playing { if j.state != playing {
return nil return nil
} }
go func() { if err := j.provider.JukeboxStop(); err != nil {
if err := j.provider.JukeboxStop(); err != nil { return err
return }
} // TODO: calculate paused at time
j.state = paused j.state = paused
}() j.InvokeOnPaused()
return nil return nil
} }
@@ -78,38 +72,53 @@ func (j *JukeboxPlayer) Stop() error {
if j.state == stopped { if j.state == stopped {
return nil return nil
} }
go func() { if err := j.provider.JukeboxStop(); err != nil {
if err := j.provider.JukeboxStop(); err != nil { return err
return }
} j.state = stopped
j.state = stopped j.InvokeOnStopped()
}()
return nil return nil
} }
func (j *JukeboxPlayer) SeekPrevious() error { func (j *JukeboxPlayer) PlayTrack(track *mediaprovider.Track) error {
track := j.curTrack if err := j.provider.JukeboxSet(track.ID); err != nil {
if track > 0 { return err
track = j.curTrack - 1
} }
return j.PlayTrackAt(track) j.startTrackTime = 0
if err := j.startAndUpdateTime(); err != nil {
return err
}
j.curTrack = 0
j.queueLength = 1
j.curTrackDuration = float64(track.Duration)
return nil
} }
func (j *JukeboxPlayer) SeekNext() error { func (j *JukeboxPlayer) SetNextTrack(track *mediaprovider.Track) error {
track := j.curTrack // we need to replace the last track in the queue, remove it first
if track >= j.numTracks { if j.curTrack < j.queueLength-1 {
return nil if err := j.provider.JukeboxRemove(j.curTrack + 1); err != nil {
return err
}
j.queueLength -= 1
} }
return j.PlayTrackAt(track + 1) // append the new track to the queue
if err := j.provider.JukeboxAdd(track.ID); err != nil {
return err
}
j.queueLength += 1
return nil
} }
func (j *JukeboxPlayer) SeekSeconds(secs float64) error { func (j *JukeboxPlayer) SeekSeconds(secs float64) error {
j.seeking = true j.seeking = true
go func() { err := j.provider.JukeboxSeek(j.curTrack, int(secs))
j.provider.JukeboxSeek(j.curTrack, int(secs)) j.seeking = false
j.seeking = false j.InvokeOnSeek()
}() return err
return nil
} }
func (j *JukeboxPlayer) IsSeeking() bool { func (j *JukeboxPlayer) IsSeeking() bool {
@@ -130,3 +139,20 @@ func (j *JukeboxPlayer) GetStatus() player.Status {
State: state, State: state,
} }
} }
func (j *JukeboxPlayer) startAndUpdateTime() error {
beforeStart := time.Now()
if err := j.provider.JukeboxStart(); err != nil {
return err
}
afterStart := time.Now()
// assume track started playing at (ie has been playing for)
// half the round-trip latency
j.startedAtUnixMilli = time.Now().Add(-afterStart.Sub(beforeStart)).UnixMilli()
return nil
}
func (j *JukeboxPlayer) handleNextTrack() {
}
+9 -59
View File
@@ -7,8 +7,8 @@ import (
"math" "math"
"strconv" "strconv"
"github.com/supersonic-app/go-mpv"
"github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player"
"github.com/supersonic-app/go-mpv"
) )
// Error returned by many Player functions if called before the player has not been initialized. // Error returned by many Player functions if called before the player has not been initialized.
@@ -50,6 +50,8 @@ var _ player.URLPlayer = (*Player)(nil)
// Player encapsulates the mpv instance and provides functions // Player encapsulates the mpv instance and provides functions
// to control it and to check its status. // to control it and to check its status.
type Player struct { type Player struct {
player.BasePlayerCallbackImpl
mpv *mpv.Mpv mpv *mpv.Mpv
initialized bool initialized bool
vol int vol int
@@ -66,13 +68,6 @@ type Player struct {
peaksEnabled bool peaksEnabled bool
bgCancel context.CancelFunc bgCancel context.CancelFunc
// callbacks
onPaused []func()
onStopped []func()
onPlaying []func()
onSeek []func()
onTrackChange []func()
} }
// Returns a new player. // Returns a new player.
@@ -403,33 +398,6 @@ func (p *Player) IsSeeking() bool {
return p.seeking && p.status.State == player.Playing return p.seeking && p.status.State == player.Playing
} }
// Registers a callback which is invoked when the player transitions to the Paused state.
func (p *Player) OnPaused(cb func()) {
p.onPaused = append(p.onPaused, cb)
}
// Registers a callback which is invoked when the player transitions to the Stopped state.
func (p *Player) OnStopped(cb func()) {
p.onStopped = append(p.onStopped, cb)
}
// Registers a callback which is invoked when the player transitions to the Playing state.
func (p *Player) OnPlaying(cb func()) {
p.onPlaying = append(p.onPlaying, cb)
}
// Registers a callback which is invoked whenever a seek event occurs.
func (p *Player) OnSeek(cb func()) {
p.onSeek = append(p.onSeek, cb)
}
// 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()) {
p.onTrackChange = append(p.onTrackChange, cb)
}
// Destroy the player. // Destroy the player.
func (p *Player) Destroy() { func (p *Player) Destroy() {
if p.bgCancel != nil { if p.bgCancel != nil {
@@ -466,23 +434,11 @@ func (p *Player) GetPeaks() (float64, float64, float64, float64) {
func (p *Player) setState(s player.State) { func (p *Player) setState(s player.State) {
switch { switch {
case s == player.Playing && p.status.State != player.Playing: case s == player.Playing && p.status.State != player.Playing:
defer func() { defer p.InvokeOnPlaying()
for _, cb := range p.onPlaying {
cb()
}
}()
case s == player.Paused && p.status.State != player.Paused: case s == player.Paused && p.status.State != player.Paused:
defer func() { defer p.InvokeOnPaused()
for _, cb := range p.onPaused {
cb()
}
}()
case s == player.Stopped && p.status.State != player.Stopped: case s == player.Stopped && p.status.State != player.Stopped:
defer func() { defer p.InvokeOnStopped()
for _, cb := range p.onStopped {
cb()
}
}()
} }
p.status.State = s p.status.State = s
} }
@@ -523,21 +479,15 @@ func (p *Player) eventHandler(ctx context.Context) {
p.seeking = false p.seeking = false
} }
case mpv.EVENT_SEEK: case mpv.EVENT_SEEK:
for _, cb := range p.onSeek { p.InvokeOnSeek()
cb()
}
case mpv.EVENT_FILE_LOADED: case mpv.EVENT_FILE_LOADED:
p.curPlaylistPos, _ = p.getInt64Property("playlist-pos") 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
for _, cb := range p.onSeek { p.InvokeOnSeek()
cb()
}
}
for _, cb := range p.onTrackChange {
cb()
} }
p.InvokeOnTrackChange()
case mpv.EVENT_IDLE: case mpv.EVENT_IDLE:
p.status.Duration = 0 p.status.Duration = 0
p.status.TimePos = 0 p.status.TimePos = 0
+65
View File
@@ -82,3 +82,68 @@ func (r ReplayGainMode) String() string {
return "no" return "no"
} }
} }
type BasePlayerCallbackImpl struct {
onPaused []func()
onStopped []func()
onPlaying []func()
onSeek []func()
onTrackChange []func()
}
// Registers a callback which is invoked when the player transitions to the Paused state.
func (p *BasePlayerCallbackImpl) OnPaused(cb func()) {
p.onPaused = append(p.onPaused, cb)
}
// Registers a callback which is invoked when the player transitions to the Stopped state.
func (p *BasePlayerCallbackImpl) OnStopped(cb func()) {
p.onStopped = append(p.onStopped, cb)
}
// Registers a callback which is invoked when the player transitions to the Playing state.
func (p *BasePlayerCallbackImpl) OnPlaying(cb func()) {
p.onPlaying = append(p.onPlaying, cb)
}
// Registers a callback which is invoked whenever a seek event occurs.
func (p *BasePlayerCallbackImpl) OnSeek(cb func()) {
p.onSeek = append(p.onSeek, cb)
}
// 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 *BasePlayerCallbackImpl) OnTrackChange(cb func()) {
p.onTrackChange = append(p.onTrackChange, cb)
}
func (p *BasePlayerCallbackImpl) InvokeOnPaused() {
for _, cb := range p.onPaused {
cb()
}
}
func (p *BasePlayerCallbackImpl) InvokeOnPlaying() {
for _, cb := range p.onPlaying {
cb()
}
}
func (p *BasePlayerCallbackImpl) InvokeOnStopped() {
for _, cb := range p.onStopped {
cb()
}
}
func (p *BasePlayerCallbackImpl) InvokeOnSeek() {
for _, cb := range p.onSeek {
cb()
}
}
func (p *BasePlayerCallbackImpl) InvokeOnTrackChange() {
for _, cb := range p.onTrackChange {
cb()
}
}
+1 -1
View File
@@ -106,7 +106,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr
pm.OnLoopModeChange(bp.AuxControls.SetLoopMode) pm.OnLoopModeChange(bp.AuxControls.SetLoopMode)
pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume) pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume)
bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { bp.AuxControls.VolumeControl.OnSetVolume = func(v int) {
_ = pm.SetVolume(v) pm.SetVolume(v)
} }
bp.AuxControls.OnChangeLoopMode(func() { bp.AuxControls.OnChangeLoopMode(func() {
pm.SetNextLoopMode() pm.SetNextLoopMode()
+1 -1
View File
@@ -158,7 +158,7 @@ func (c *Controller) ConnectPlayQueuelistActions(list *widgets.PlayQueueList) {
} }
list.OnAddToPlaylist = c.DoAddTracksToPlaylistWorkflow list.OnAddToPlaylist = c.DoAddTracksToPlaylistWorkflow
list.OnPlayItemAt = func(tracknum int) { list.OnPlayItemAt = func(tracknum int) {
_ = c.App.PlaybackManager.PlayTrackAt(tracknum) c.App.PlaybackManager.PlayTrackAt(tracknum)
} }
list.OnShowArtistPage = func(artistID string) { list.OnShowArtistPage = func(artistID string) {
c.NavigateTo(ArtistRoute(artistID)) c.NavigateTo(ArtistRoute(artistID))
+3 -3
View File
@@ -241,13 +241,13 @@ func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
if desk, ok := fyneApp.(desktop.App); ok { if desk, ok := fyneApp.(desktop.App); ok {
menu := fyne.NewMenu(appName, menu := fyne.NewMenu(appName,
fyne.NewMenuItem(fmt.Sprintf("%s/%s", lang.L("Play"), lang.L("Pause")), func() { fyne.NewMenuItem(fmt.Sprintf("%s/%s", lang.L("Play"), lang.L("Pause")), func() {
_ = m.App.PlaybackManager.PlayPause() m.App.PlaybackManager.PlayPause()
}), }),
fyne.NewMenuItem(lang.L("Previous"), func() { fyne.NewMenuItem(lang.L("Previous"), func() {
_ = m.App.PlaybackManager.SeekBackOrPrevious() m.App.PlaybackManager.SeekBackOrPrevious()
}), }),
fyne.NewMenuItem(lang.L("Next"), func() { fyne.NewMenuItem(lang.L("Next"), func() {
_ = m.App.PlaybackManager.SeekNext() m.App.PlaybackManager.SeekNext()
}), }),
fyne.NewMenuItemSeparator(), fyne.NewMenuItemSeparator(),
fyne.NewMenuItem(lang.L("Volume")+" +10%", func() { fyne.NewMenuItem(lang.L("Volume")+" +10%", func() {