From 1665281ad7ccfed4f4237ee449c4aea6f79c0102 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 18 Dec 2023 09:06:17 -0800 Subject: [PATCH] Refactor pt. 2: route player transport controls+callbacks through PlaybackManager --- backend/app.go | 4 +- backend/mpmedia_mac.go | 53 ++++++++++-------------- backend/mpmedia_other.go | 4 +- backend/mpris.go | 53 ++++++++++++------------ backend/playbackmanager.go | 82 ++++++++++++++++++++++++++++++++++++++ player/mpv/player.go | 9 ----- ui/bottompanel.go | 45 ++++++++++----------- ui/mainwindow.go | 2 +- 8 files changed, 155 insertions(+), 97 deletions(-) diff --git a/backend/app.go b/backend/app.go index 539ebc8..216a418 100644 --- a/backend/app.go +++ b/backend/app.go @@ -118,7 +118,7 @@ func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleas // OS media center integrations a.setupMPRIS(displayAppName) - InitMPMediaHandler(a.Player, a.PlaybackManager, func(id string) (string, error) { + InitMPMediaHandler(a.PlaybackManager, func(id string) (string, error) { a.ImageManager.GetCoverThumbnail(id) // ensure image is cached locally return a.ImageManager.GetCoverArtUrl(id) }) @@ -263,7 +263,7 @@ func (a *App) setupMPV() error { } func (a *App) setupMPRIS(mprisAppName string) { - a.MPRISHandler = NewMPRISHandler(mprisAppName, a.Player, a.PlaybackManager) + a.MPRISHandler = NewMPRISHandler(mprisAppName, a.PlaybackManager) a.MPRISHandler.ArtURLLookup = a.ImageManager.GetCoverArtUrl a.MPRISHandler.OnRaise = func() error { a.callOnReactivate(); return nil } a.MPRISHandler.OnQuit = func() error { diff --git a/backend/mpmedia_mac.go b/backend/mpmedia_mac.go index 6abed67..9e59056 100644 --- a/backend/mpmedia_mac.go +++ b/backend/mpmedia_mac.go @@ -16,14 +16,11 @@ import ( ) import ( - "fmt" "log" "strings" "unsafe" "github.com/dweymouth/supersonic/backend/mediaprovider" - "github.com/dweymouth/supersonic/player" - "github.com/dweymouth/supersonic/player/mpv" ) // os_remote_command_callback is called by Objective-C when incoming OS media commands are received. @@ -52,7 +49,6 @@ func os_remote_command_callback(command C.Command, value C.double) { // MPMediaHandler is the handler for MacOS media controls and system events. type MPMediaHandler struct { - player *mpv.Player playbackManager *PlaybackManager artURLLookup func(string) (string, error) } @@ -63,9 +59,8 @@ var mpMediaEventRecipient *MPMediaHandler // NewMPMediaHandler creates a new MPMediaHandler instances and sets it as the current recipient // for incoming system events. -func InitMPMediaHandler(player *mpv.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { +func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { mp := &MPMediaHandler{ - player: player, playbackManager: playbackManager, artURLLookup: artURLLookup, } @@ -79,22 +74,22 @@ func InitMPMediaHandler(player *mpv.Player, playbackManager *PlaybackManager, ar go mp.updateMetadata(track) }) - mp.player.OnStopped(func() { + mp.playbackManager.OnStopped(func() { C.set_os_playback_state_stopped() }) - mp.player.OnSeek(func() { - C.update_os_now_playing_info_position(C.double(mp.player.GetStatus().TimePos)) + mp.playbackManager.OnSeek(func() { + C.update_os_now_playing_info_position(C.double(mp.playbackManager.PlayerStatus().TimePos)) }) - mp.player.OnPlaying(func() { + mp.playbackManager.OnPlaying(func() { C.set_os_playback_state_playing() - C.update_os_now_playing_info_position(C.double(mp.player.GetStatus().TimePos)) + C.update_os_now_playing_info_position(C.double(mp.playbackManager.PlayerStatus().TimePos)) }) - mp.player.OnPaused(func() { + mp.playbackManager.OnPaused(func() { C.set_os_playback_state_paused() - C.update_os_now_playing_info_position(C.double(mp.player.GetStatus().TimePos)) + C.update_os_now_playing_info_position(C.double(mp.playbackManager.PlayerStatus().TimePos)) }) return nil @@ -133,60 +128,56 @@ func (mp *MPMediaHandler) updateMetadata(track *mediaprovider.Track) { // MPMediaHandler instance received OS command 'pause' func (mp *MPMediaHandler) OnCommandPause() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.Pause() + mp.playbackManager.Pause() } // MPMediaHandler instance received OS command 'play' func (mp *MPMediaHandler) OnCommandPlay() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.Continue() + mp.playbackManager.Continue() } // MPMediaHandler instance received OS command 'stop' func (mp *MPMediaHandler) OnCommandStop() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.Stop() + mp.playbackManager.Stop() } // MPMediaHandler instance received OS command 'toggle' func (mp *MPMediaHandler) OnCommandTogglePlayPause() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - if mp.player.GetStatus().State == player.Playing { - mp.OnCommandPause() - } else { - mp.OnCommandPlay() - } + mp.playbackManager.PlayPause() } // MPMediaHandler instance received OS command 'next track' func (mp *MPMediaHandler) OnCommandNextTrack() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.SeekNext() + mp.playbackManager.SeekNext() } // MPMediaHandler instance received OS command 'previous track' func (mp *MPMediaHandler) OnCommandPreviousTrack() { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.SeekBackOrPrevious() + mp.playbackManager.SeekBackOrPrevious() } // MPMediaHandler instance received OS command to 'seek' func (mp *MPMediaHandler) OnCommandSeek(positionSeconds float64) { - if mp == nil || mp.player == nil { + if mp == nil || mp.playbackManager == nil { return } - mp.player.Seek(fmt.Sprintf("%0.2f", positionSeconds), mpv.SeekAbsolute) + mp.playbackManager.SeekSeconds(positionSeconds) } diff --git a/backend/mpmedia_other.go b/backend/mpmedia_other.go index 42025cc..44e0634 100644 --- a/backend/mpmedia_other.go +++ b/backend/mpmedia_other.go @@ -4,11 +4,9 @@ package backend import ( "errors" - - "github.com/dweymouth/supersonic/player/mpv" ) -func InitMPMediaHandler(player *mpv.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { +func InitMPMediaHandler(playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { // MPMediaHandler only supports macOS. return errors.New("unsupported platform") } diff --git a/backend/mpris.go b/backend/mpris.go index a425403..084a295 100644 --- a/backend/mpris.go +++ b/backend/mpris.go @@ -3,12 +3,10 @@ package backend import ( "encoding/base32" "errors" - "fmt" "strconv" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/player" - "github.com/dweymouth/supersonic/player/mpv" "github.com/godbus/dbus/v5" "github.com/quarckster/go-mpris-server/pkg/events" "github.com/quarckster/go-mpris-server/pkg/server" @@ -44,24 +42,23 @@ type MPRISHandler struct { connErr error playerName string curTrackPath string // empty for no track - p *mpv.Player pm *PlaybackManager s *server.Server evt *events.EventHandler } -func NewMPRISHandler(playerName string, p *mpv.Player, pm *PlaybackManager) *MPRISHandler { - m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")} +func NewMPRISHandler(playerName string, pm *PlaybackManager) *MPRISHandler { + m := &MPRISHandler{playerName: playerName, pm: pm, connErr: errors.New("not started")} m.s = server.NewServer(playerName, m, m) m.evt = events.NewEventHandler(m.s) - m.p.OnSeek(func() { + pm.OnSeek(func() { if m.connErr == nil { - pos := secondsToMicroseconds(m.p.GetStatus().TimePos) + pos := secondsToMicroseconds(pm.PlayerStatus().TimePos) m.evt.Player.OnSeek(pos) } }) - m.pm.OnSongChange(func(tr, _ *mediaprovider.Track) { + pm.OnSongChange(func(tr, _ *mediaprovider.Track) { if m.connErr == nil { m.evt.Player.OnTitle() } @@ -71,7 +68,7 @@ func NewMPRISHandler(playerName string, p *mpv.Player, pm *PlaybackManager) *MPR m.curTrackPath = dbusTrackIDPrefix + encodeTrackId(tr.ID) } }) - m.pm.OnVolumeChange(func(vol int) { + pm.OnVolumeChange(func(vol int) { if m.connErr == nil { m.evt.Player.OnVolume() } @@ -81,9 +78,9 @@ func NewMPRISHandler(playerName string, p *mpv.Player, pm *PlaybackManager) *MPR m.evt.Player.OnPlayPause() } } - m.p.OnStopped(emitPlayStatus) - m.p.OnPlaying(emitPlayStatus) - m.p.OnPaused(emitPlayStatus) + m.pm.OnStopped(emitPlayStatus) + m.pm.OnPlaying(emitPlayStatus) + m.pm.OnPaused(emitPlayStatus) return m } @@ -148,45 +145,47 @@ func (m *MPRISHandler) SupportedMimeTypes() ([]string, error) { // OrgMprisMediaPlayer2PlayerAdapter implementation func (m *MPRISHandler) Next() error { - return m.p.SeekNext() + return m.pm.SeekNext() } func (m *MPRISHandler) Previous() error { - return m.p.SeekBackOrPrevious() + return m.pm.SeekBackOrPrevious() } func (m *MPRISHandler) Pause() error { - if m.p.GetStatus().State == player.Playing { - return m.p.PlayPause() + if m.pm.PlayerStatus().State == player.Playing { + return m.pm.PlayPause() } return nil } func (m *MPRISHandler) PlayPause() error { - return m.p.PlayPause() + return m.pm.PlayPause() } func (m *MPRISHandler) Stop() error { - return m.p.Stop() + return m.pm.Stop() } func (m *MPRISHandler) Play() error { - switch m.p.GetStatus().State { + switch m.pm.PlayerStatus().State { case player.Paused: - return m.p.PlayPause() + return m.pm.PlayPause() case player.Stopped: - return m.p.PlayFromBeginning() + return m.pm.PlayFromBeginning() } return nil } func (m *MPRISHandler) Seek(offset types.Microseconds) error { - return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(offset)), mpv.SeekRelative) + // MPRIS seek command is relative to current position + pos := m.pm.PlayerStatus().TimePos + microsecondsToSeconds(offset) + return m.pm.SeekSeconds(pos) } func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error { if m.curTrackPath == trackId { - return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(position)), mpv.SeekAbsolute) + return m.pm.SeekSeconds(microsecondsToSeconds(position)) } return nil } @@ -196,7 +195,7 @@ func (m *MPRISHandler) OpenUri(uri string) error { } func (m *MPRISHandler) PlaybackStatus() (types.PlaybackStatus, error) { - switch m.p.GetStatus().State { + switch m.pm.PlayerStatus().State { case player.Playing: return types.PlaybackStatusPlaying, nil case player.Paused: @@ -244,7 +243,7 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) { if m.curTrackPath != "" { trackObjPath = m.curTrackPath } - status := m.p.GetStatus() + status := m.pm.PlayerStatus() var tr mediaprovider.Track if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped { tr = *np @@ -272,7 +271,7 @@ func (m *MPRISHandler) Metadata() (types.Metadata, error) { } func (m *MPRISHandler) Volume() (float64, error) { - return float64(m.p.GetVolume()) / 100, nil + return float64(m.pm.Volume()) / 100, nil } func (m *MPRISHandler) SetVolume(v float64) error { @@ -280,7 +279,7 @@ func (m *MPRISHandler) SetVolume(v float64) error { } func (m *MPRISHandler) Position() (int64, error) { - return int64(secondsToMicroseconds(m.p.GetStatus().TimePos)), nil + return int64(secondsToMicroseconds(m.pm.PlayerStatus().TimePos)), nil } func (m *MPRISHandler) MinimumRate() (float64, error) { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 5be630a..d9a7d9e 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -2,6 +2,7 @@ package backend import ( "context" + "fmt" "log" "time" @@ -51,10 +52,15 @@ type PlaybackManager struct { transcodeCfg *TranscodingConfig replayGainCfg ReplayGainConfig + // registered callbacks onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64) onLoopModeChange []func(LoopMode) onVolumeChange []func(int) + onSeek []func() + onPaused []func() + onStopped []func() + onPlaying []func() } func NewPlaybackManager( @@ -89,6 +95,7 @@ func NewPlaybackManager( }) p.OnSeek(func() { pm.doUpdateTimePos() + pm.invokeNoArgCallbacks(pm.onSeek) }) p.OnStopped(func() { pm.playTimeStopwatch.Stop() @@ -96,14 +103,17 @@ func NewPlaybackManager( pm.stopPollTimePos() pm.doUpdateTimePos() pm.invokeOnSongChangeCallbacks() + pm.invokeNoArgCallbacks(pm.onStopped) }) p.OnPaused(func() { pm.playTimeStopwatch.Stop() pm.stopPollTimePos() + pm.invokeNoArgCallbacks(pm.onPaused) }) p.OnPlaying(func() { pm.playTimeStopwatch.Start() pm.startPollTimePos() + pm.invokeNoArgCallbacks(pm.onPlaying) }) s.OnLogout(func() { @@ -113,6 +123,15 @@ func NewPlaybackManager( return pm } +func (pm *PlaybackManager) invokeNoArgCallbacks(cbs []func()) { + if pm.callbacksDisabled { + return + } + for _, cb := range cbs { + cb() + } +} + func (p *PlaybackManager) IsSeeking() bool { return p.player.IsSeeking() } @@ -155,6 +174,26 @@ func (p *PlaybackManager) OnVolumeChange(cb func(int)) { p.onVolumeChange = append(p.onVolumeChange, cb) } +// Registers a callback that is notified whenever the player has been seeked. +func (p *PlaybackManager) OnSeek(cb func()) { + p.onSeek = append(p.onSeek, cb) +} + +// Registers a callback that is notified whenever the player has been paused. +func (p *PlaybackManager) OnPaused(cb func()) { + p.onPaused = append(p.onPaused, cb) +} + +// Registers a callback that is notified whenever the player is stopped. +func (p *PlaybackManager) OnStopped(cb func()) { + p.onStopped = append(p.onStopped, cb) +} + +// Registers a callback that is notified whenever the player begins playing. +func (p *PlaybackManager) OnPlaying(cb func()) { + p.onPlaying = append(p.onPlaying, cb) +} + // Loads the specified album into the play queue. func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle bool) error { album, err := p.sm.Server.GetAlbum(albumID) @@ -394,6 +433,10 @@ func (p *PlaybackManager) LoopMode() LoopMode { return LoopMode(p.player.GetLoopMode()) } +func (p *PlaybackManager) PlayerStatus() player.Status { + return p.player.GetStatus() +} + func (p *PlaybackManager) SetVolume(vol int) error { vol = clamp(vol, 0, 100) if err := p.player.SetVolume(vol); err != nil { @@ -409,6 +452,45 @@ func (p *PlaybackManager) Volume() int { return p.player.GetVolume() } +func (p *PlaybackManager) SeekNext() error { + return p.player.SeekNext() +} + +func (p *PlaybackManager) SeekBackOrPrevious() error { + return p.player.SeekBackOrPrevious() +} + +// Seek to given absolute position in the current track by seconds. +func (p *PlaybackManager) SeekSeconds(sec float64) error { + return p.player.Seek(fmt.Sprintf("%0.2f", sec), mpv.SeekAbsolute) +} + +// Seek to a fractional position in the current track [0..1] +func (p *PlaybackManager) SeekFraction(fraction float64) error { + if fraction < 0 { + fraction = 0 + } else if fraction > 1 { + fraction = 1 + } + return p.player.Seek(fmt.Sprintf("%0.1f", fraction*100), mpv.SeekAbsolutePercent) +} + +func (p *PlaybackManager) Stop() error { + return p.player.Stop() +} + +func (p *PlaybackManager) Pause() error { + return p.player.Pause() +} + +func (p *PlaybackManager) Continue() error { + return p.player.Continue() +} + +func (p *PlaybackManager) PlayPause() error { + return p.player.PlayPause() +} + // call BEFORE updating p.nowPlayingIdx func (p *PlaybackManager) checkScrobble() { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { diff --git a/player/mpv/player.go b/player/mpv/player.go index afad809..7775906 100644 --- a/player/mpv/player.go +++ b/player/mpv/player.go @@ -25,15 +25,6 @@ const ( SeekRelativePercent ) -// One of "no", "track", or "album" -type ReplayGainMode string - -const ( - ReplayGainNone ReplayGainMode = "no" - ReplayGainTrack ReplayGainMode = "track" - ReplayGainAlbum ReplayGainMode = "album" -) - // Information about a specific audio device. // Returned by ListAudioDevices. type AudioDevice struct { diff --git a/ui/bottompanel.go b/ui/bottompanel.go index f77c92c..e80f504 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -1,14 +1,12 @@ package ui import ( - "fmt" "image" "log" "time" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" - "github.com/dweymouth/supersonic/player/mpv" "github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/widgets" @@ -21,8 +19,7 @@ import ( type BottomPanel struct { widget.BaseWidget - ImageManager *backend.ImageManager - playbackManager *backend.PlaybackManager + ImageManager *backend.ImageManager NowPlaying *widgets.NowPlayingCard Controls *widgets.PlayerControls @@ -34,24 +31,24 @@ type BottomPanel struct { var _ fyne.Widget = (*BottomPanel)(nil) -func NewBottomPanel(p *mpv.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel { - bp := &BottomPanel{playbackManager: pm} +func NewBottomPanel(pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel { + bp := &BottomPanel{} bp.ExtendBaseWidget(bp) - bp.playbackManager.OnSongChange(bp.onSongChange) - bp.playbackManager.OnPlayTimeUpdate(func(cur, total float64) { - if !bp.playbackManager.IsSeeking() { + pm.OnSongChange(bp.onSongChange) + pm.OnPlayTimeUpdate(func(cur, total float64) { + if !pm.IsSeeking() { bp.Controls.UpdatePlayTime(cur, total) } }) - p.OnPaused(func() { + pm.OnPaused(func() { bp.Controls.SetPlaying(false) }) - p.OnPlaying(func() { + pm.OnPlaying(func() { bp.Controls.SetPlaying(true) }) - p.OnStopped(func() { + pm.OnStopped(func() { bp.Controls.SetPlaying(false) }) @@ -65,45 +62,45 @@ func NewBottomPanel(p *mpv.Player, pm *backend.PlaybackManager, contr *controlle } } bp.NowPlaying.OnSetFavorite = func(fav bool) { - contr.SetTrackFavorites([]string{bp.playbackManager.NowPlaying().ID}, fav) + contr.SetTrackFavorites([]string{pm.NowPlaying().ID}, fav) } bp.NowPlaying.OnSetRating = func(rating int) { - contr.SetTrackRatings([]string{bp.playbackManager.NowPlaying().ID}, rating) + contr.SetTrackRatings([]string{pm.NowPlaying().ID}, rating) } bp.NowPlaying.OnAddToPlaylist = func() { - contr.DoAddTracksToPlaylistWorkflow([]string{bp.playbackManager.NowPlaying().ID}) + contr.DoAddTracksToPlaylistWorkflow([]string{pm.NowPlaying().ID}) } bp.NowPlaying.OnAlbumNameTapped = func() { - contr.NavigateTo(controller.AlbumRoute(bp.playbackManager.NowPlaying().AlbumID)) + contr.NavigateTo(controller.AlbumRoute(pm.NowPlaying().AlbumID)) } bp.NowPlaying.OnArtistNameTapped = func(artistID string) { contr.NavigateTo(controller.ArtistRoute(artistID)) } bp.NowPlaying.OnTrackNameTapped = func() { - contr.NavigateTo(controller.NowPlayingRoute(bp.playbackManager.NowPlaying().ID)) + contr.NavigateTo(controller.NowPlayingRoute(pm.NowPlaying().ID)) } bp.Controls = widgets.NewPlayerControls() bp.Controls.OnPlayPause(func() { - p.PlayPause() + pm.PlayPause() }) bp.Controls.OnSeekNext(func() { - p.SeekNext() + pm.SeekNext() }) bp.Controls.OnSeekPrevious(func() { - p.SeekBackOrPrevious() + pm.SeekBackOrPrevious() }) bp.Controls.OnSeek(func(f float64) { - p.Seek(fmt.Sprintf("%d", int(f*100)), mpv.SeekAbsolutePercent) + pm.SeekFraction(f) }) - bp.AuxControls = widgets.NewAuxControls(p.GetVolume()) + bp.AuxControls = widgets.NewAuxControls(pm.Volume()) pm.OnLoopModeChange(bp.AuxControls.SetLoopMode) pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume) bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { - _ = bp.playbackManager.SetVolume(v) + _ = pm.SetVolume(v) } bp.AuxControls.OnChangeLoopMode(func() { - bp.playbackManager.SetNextLoopMode() + pm.SetNextLoopMode() }) bp.container = container.New(layouts.NewLeftMiddleRightLayout(500), diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 56156bd..a410d86 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -79,7 +79,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m.Controller.ReloadFunc = m.BrowsingPane.Reload m.Controller.CurPageFunc = m.BrowsingPane.CurrentPage - m.BottomPanel = NewBottomPanel(app.Player, app.PlaybackManager, m.Controller) + m.BottomPanel = NewBottomPanel(app.PlaybackManager, m.Controller) m.BottomPanel.ImageManager = app.ImageManager m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane) m.Window.SetContent(m.container)