more work for preparing for casting

This commit is contained in:
Drew Weymouth
2025-03-29 15:15:13 -07:00
parent 4f731833c0
commit 8bd1c8183d
6 changed files with 261 additions and 151 deletions
+18 -11
View File
@@ -11,18 +11,16 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"slices"
"strings" "strings"
"time" "time"
"github.com/dweymouth/supersonic/backend/ipc" "github.com/dweymouth/supersonic/backend/ipc"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/dlna"
"github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/supersonic-app/go-upnpcast/device"
"github.com/supersonic-app/go-upnpcast/services"
"github.com/20after4/configdir" "github.com/20after4/configdir"
"github.com/zalando/go-keyring" "github.com/zalando/go-keyring"
@@ -45,7 +43,7 @@ type App struct {
ServerManager *ServerManager ServerManager *ServerManager
ImageManager *ImageManager ImageManager *ImageManager
PlaybackManager *PlaybackManager PlaybackManager *PlaybackManager
LocalPlayer player.BasePlayer LocalPlayer *mpv.Player
UpdateChecker UpdateChecker UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler MPRISHandler *MPRISHandler
WinSMTC *SMTC WinSMTC *SMTC
@@ -154,6 +152,21 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas
a.LrcLibFetcher = NewLrcLibFetcher(a.cacheDir, a.Config.Application.CustomLrcLibUrl, timeout) a.LrcLibFetcher = NewLrcLibFetcher(a.cacheDir, a.Config.Application.CustomLrcLibUrl, timeout)
} }
// Periodically scan for remote players
go func() {
t := time.NewTicker(5 * time.Minute)
for {
a.PlaybackManager.ScanRemotePlayers(a.bgrndCtx)
select {
case <-a.bgrndCtx.Done():
t.Stop()
return
case <-t.C:
continue
}
}
}()
a.PlaybackManager.OnPlaying(func() { a.PlaybackManager.OnPlaying(func() {
SetSystemSleepDisabled(true) SetSystemSleepDisabled(true)
}) })
@@ -275,11 +288,7 @@ func (a *App) initMPV() error {
if err := p.Init(c.InMemoryCacheSizeMB); err != nil { if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
return fmt.Errorf("failed to initialize mpv player: %s", err.Error()) return fmt.Errorf("failed to initialize mpv player: %s", err.Error())
} }
// a.LocalPlayer = p a.LocalPlayer = p
devices, _ := device.SearchMediaRenderers(context.Background(), 10, services.AVTransport)
if len(devices) > 0 {
a.LocalPlayer, _ = dlna.NewDLNAPlayer(devices[0])
}
return nil return nil
} }
@@ -287,7 +296,6 @@ func (a *App) setupMPV() error {
a.Config.LocalPlayback.Volume = clamp(a.Config.LocalPlayback.Volume, 0, 100) a.Config.LocalPlayback.Volume = clamp(a.Config.LocalPlayback.Volume, 0, 100)
a.LocalPlayer.SetVolume(a.Config.LocalPlayback.Volume) a.LocalPlayer.SetVolume(a.Config.LocalPlayback.Volume)
/*
devs, err := a.LocalPlayer.ListAudioDevices() devs, err := a.LocalPlayer.ListAudioDevices()
if err != nil { if err != nil {
return err return err
@@ -337,7 +345,6 @@ func (a *App) setupMPV() error {
} }
copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands) copy(eq.BandGains[:], a.Config.LocalPlayback.GraphicEqualizerBands)
a.LocalPlayer.SetEqualizer(eq) a.LocalPlayer.SetEqualizer(eq)
*/
return nil return nil
} }
+58 -16
View File
@@ -58,6 +58,9 @@ type playbackEngine struct {
noIncrementNextTrackChange bool // true iff the nowPlayingIndex should not be incremented on the next onTrackChange noIncrementNextTrackChange bool // true iff the nowPlayingIndex should not be incremented on the next onTrackChange
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
pendingPlayerChange bool
pendingPlayerChangeTimePos float64
// 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
scrobbleCfg *ScrobbleConfig scrobbleCfg *ScrobbleConfig
@@ -102,23 +105,8 @@ func NewPlaybackEngine(
case "One": case "One":
pm.loopMode = LoopOne pm.loopMode = LoopOne
} }
p.OnTrackChange(pm.handleOnTrackChange)
p.OnSeek(func() {
pm.doUpdateTimePos(true)
pm.invokeNoArgCallbacks(pm.onSeek)
})
p.OnStopped(pm.handleOnStopped)
p.OnPaused(func() {
pm.playTimeStopwatch.Stop()
pm.stopPollTimePos()
pm.invokeNoArgCallbacks(pm.onPaused)
})
p.OnPlaying(func() {
pm.playTimeStopwatch.Start()
pm.startPollTimePos()
pm.invokeNoArgCallbacks(pm.onPlaying)
})
pm.registerPlayerCallbacks(p)
s.OnLogout(func() { s.OnLogout(func() {
pm.StopAndClearPlayQueue() pm.StopAndClearPlayQueue()
}) })
@@ -126,6 +114,51 @@ func NewPlaybackEngine(
return pm return pm
} }
func (p *playbackEngine) registerPlayerCallbacks(pl player.BasePlayer) {
pl.OnTrackChange(p.handleOnTrackChange)
pl.OnSeek(func() {
p.doUpdateTimePos(true)
p.invokeNoArgCallbacks(p.onSeek)
})
pl.OnStopped(p.handleOnStopped)
pl.OnPaused(func() {
p.playTimeStopwatch.Stop()
p.stopPollTimePos()
p.invokeNoArgCallbacks(p.onPaused)
})
pl.OnPlaying(func() {
p.playTimeStopwatch.Start()
p.startPollTimePos()
p.invokeNoArgCallbacks(p.onPlaying)
})
}
func (p *playbackEngine) SetPlayer(pl player.BasePlayer) {
needToUnpause := false
stat := p.player.GetStatus()
switch stat.State {
case player.Stopped:
// nothing
case player.Playing:
p.Pause()
fallthrough
case player.Paused:
p.pendingPlayerChangeTimePos = stat.TimePos
p.pendingPlayerChange = true
needToUnpause = true
}
p.player = pl
p.registerPlayerCallbacks(pl)
if needToUnpause {
p.PlayTrackAt(p.nowPlayingIdx)
p.SeekSeconds(p.pendingPlayerChangeTimePos)
p.pendingPlayerChange = false
}
}
func (p *playbackEngine) PlayTrackAt(idx int) error { 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")
@@ -238,6 +271,15 @@ func (p *playbackEngine) Pause() error {
} }
func (p *playbackEngine) Continue() error { func (p *playbackEngine) Continue() error {
if p.pendingPlayerChange {
err := p.PlayTrackAt(p.nowPlayingIdx)
if p.pendingPlayerChangeTimePos != 0 {
p.SeekSeconds(p.pendingPlayerChangeTimePos)
}
p.pendingPlayerChange = false
return err
}
if p.PlayerStatus().State == player.Stopped { if p.PlayerStatus().State == player.Stopped {
return p.PlayTrackAt(0) return p.PlayTrackAt(0)
} }
+56
View File
@@ -7,12 +7,16 @@ import (
"math/rand" "math/rand"
"runtime" "runtime"
"slices" "slices"
"sync"
"time" "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"
"github.com/dweymouth/supersonic/backend/player/dlna"
"github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/supersonic-app/go-upnpcast/device"
"github.com/supersonic-app/go-upnpcast/services"
) )
// A high-level MediaProvider-aware playback engine, serves as an // A high-level MediaProvider-aware playback engine, serves as an
@@ -22,11 +26,21 @@ type PlaybackManager struct {
cmdQueue *playbackCommandQueue cmdQueue *playbackCommandQueue
cfg *AppConfig cfg *AppConfig
localPlayer player.BasePlayer
remotePlayersLock sync.Mutex
remotePlayers []remotePlayer
autoplay bool autoplay bool
lastPlayTime float64 lastPlayTime float64
} }
type remotePlayer struct {
Name string
Protocol string
new func() (player.BasePlayer, error)
}
func NewPlaybackManager( func NewPlaybackManager(
ctx context.Context, ctx context.Context,
s *ServerManager, s *ServerManager,
@@ -43,6 +57,7 @@ func NewPlaybackManager(
cmdQueue: q, cmdQueue: q,
cfg: appCfg, cfg: appCfg,
autoplay: playbackCfg.Autoplay, autoplay: playbackCfg.Autoplay,
localPlayer: p,
} }
pm.addOnTrackChangeHook() pm.addOnTrackChangeHook()
go pm.runCmdQueue(ctx) go pm.runCmdQueue(ctx)
@@ -81,6 +96,47 @@ func (p *PlaybackManager) addOnTrackChangeHook() {
}) })
} }
func (p *PlaybackManager) ScanRemotePlayers(ctx context.Context) {
devices, _ := device.SearchMediaRenderers(ctx, 10, services.AVTransport, services.RenderingControl)
var discovered []remotePlayer
for _, d := range devices {
p := remotePlayer{
Name: d.FriendlyName,
Protocol: "DLNA",
new: func() (player.BasePlayer, error) {
return dlna.NewDLNAPlayer(d)
},
}
discovered = append(discovered, p)
}
p.remotePlayersLock.Lock()
p.remotePlayers = discovered
p.remotePlayersLock.Unlock()
}
func (p *PlaybackManager) RemotePlayers() []remotePlayer {
p.remotePlayersLock.Lock()
players := p.remotePlayers
p.remotePlayersLock.Unlock()
return players
}
func (p *PlaybackManager) SetRemotePlayer(rp *remotePlayer) error {
if rp == nil {
p.engine.SetPlayer(p.localPlayer)
return nil
}
player, err := rp.new()
if err != nil {
return err
}
p.engine.SetPlayer(player)
return nil
}
func (p *PlaybackManager) CurrentPlayer() player.BasePlayer { func (p *PlaybackManager) CurrentPlayer() player.BasePlayer {
return p.engine.CurrentPlayer() return p.engine.CurrentPlayer()
} }
+23 -23
View File
@@ -84,66 +84,66 @@ func (r ReplayGainMode) String() string {
} }
type BasePlayerCallbackImpl struct { type BasePlayerCallbackImpl struct {
onPaused []func() onPaused func()
onStopped []func() onStopped func()
onPlaying []func() onPlaying func()
onSeek []func() onSeek func()
onTrackChange []func() onTrackChange func()
} }
// Registers a callback which is invoked when the player transitions to the Paused state. // Sets a callback which is invoked when the player transitions to the Paused state.
func (p *BasePlayerCallbackImpl) OnPaused(cb func()) { func (p *BasePlayerCallbackImpl) OnPaused(cb func()) {
p.onPaused = append(p.onPaused, cb) p.onPaused = cb
} }
// Registers a callback which is invoked when the player transitions to the Stopped state. // Sets a callback which is invoked when the player transitions to the Stopped state.
func (p *BasePlayerCallbackImpl) OnStopped(cb func()) { func (p *BasePlayerCallbackImpl) OnStopped(cb func()) {
p.onStopped = append(p.onStopped, cb) p.onStopped = cb
} }
// Registers a callback which is invoked when the player transitions to the Playing state. // Sets a callback which is invoked when the player transitions to the Playing state.
func (p *BasePlayerCallbackImpl) OnPlaying(cb func()) { func (p *BasePlayerCallbackImpl) OnPlaying(cb func()) {
p.onPlaying = append(p.onPlaying, cb) p.onPlaying = cb
} }
// Registers a callback which is invoked whenever a seek event occurs. // Registers a callback which is invoked whenever a seek event occurs.
func (p *BasePlayerCallbackImpl) OnSeek(cb func()) { func (p *BasePlayerCallbackImpl) OnSeek(cb func()) {
p.onSeek = append(p.onSeek, cb) p.onSeek = cb
} }
// 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 *BasePlayerCallbackImpl) OnTrackChange(cb func()) { func (p *BasePlayerCallbackImpl) OnTrackChange(cb func()) {
p.onTrackChange = append(p.onTrackChange, cb) p.onTrackChange = cb
} }
func (p *BasePlayerCallbackImpl) InvokeOnPaused() { func (p *BasePlayerCallbackImpl) InvokeOnPaused() {
for _, cb := range p.onPaused { if p.onPaused != nil {
cb() p.onPaused()
} }
} }
func (p *BasePlayerCallbackImpl) InvokeOnPlaying() { func (p *BasePlayerCallbackImpl) InvokeOnPlaying() {
for _, cb := range p.onPlaying { if p.onPlaying != nil {
cb() p.onPlaying()
} }
} }
func (p *BasePlayerCallbackImpl) InvokeOnStopped() { func (p *BasePlayerCallbackImpl) InvokeOnStopped() {
for _, cb := range p.onStopped { if p.onStopped != nil {
cb() p.onStopped()
} }
} }
func (p *BasePlayerCallbackImpl) InvokeOnSeek() { func (p *BasePlayerCallbackImpl) InvokeOnSeek() {
for _, cb := range p.onSeek { if p.onSeek != nil {
cb() p.onSeek()
} }
} }
func (p *BasePlayerCallbackImpl) InvokeOnTrackChange() { func (p *BasePlayerCallbackImpl) InvokeOnTrackChange() {
for _, cb := range p.onTrackChange { if p.onTrackChange != nil {
cb() p.onTrackChange()
} }
} }
+4 -2
View File
@@ -16,6 +16,8 @@ import (
fynetooltip "github.com/dweymouth/fyne-tooltip" fynetooltip "github.com/dweymouth/fyne-tooltip"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/dialogs"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
@@ -268,7 +270,7 @@ func (c *Controller) ShowAboutDialog() {
} }
func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) { func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map[string]string) {
/*
devs, err := c.App.LocalPlayer.ListAudioDevices() devs, err := c.App.LocalPlayer.ListAudioDevices()
if err != nil { if err != nil {
log.Printf("error listing audio devices: %v", err) log.Printf("error listing audio devices: %v", err)
@@ -317,7 +319,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
c.ClosePopUpOnEscape(pop) c.ClosePopUpOnEscape(pop)
c.haveModal = true c.haveModal = true
pop.Show() pop.Show()
*/
} }
func (c *Controller) doModalClosed() { func (c *Controller) doModalClosed() {
+6 -3
View File
@@ -7,6 +7,7 @@ import (
"fyne.io/fyne/v2" "fyne.io/fyne/v2"
"fyne.io/fyne/v2/lang" "fyne.io/fyne/v2/lang"
"github.com/dweymouth/supersonic/backend/player" "github.com/dweymouth/supersonic/backend/player"
"github.com/dweymouth/supersonic/backend/player/mpv"
"github.com/dweymouth/supersonic/ui/shortcuts" "github.com/dweymouth/supersonic/ui/shortcuts"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
"github.com/dweymouth/supersonic/ui/visualizations" "github.com/dweymouth/supersonic/ui/visualizations"
@@ -21,12 +22,14 @@ type visualizationData struct {
} }
func (c *Controller) initVisualizations() { func (c *Controller) initVisualizations() {
c.App.LocalPlayer.OnStopped(c.stopVisualizationAnim) c.App.PlaybackManager.OnStopped(c.stopVisualizationAnim)
c.App.LocalPlayer.OnPaused(c.stopVisualizationAnim) c.App.PlaybackManager.OnPaused(c.stopVisualizationAnim)
c.App.LocalPlayer.OnPlaying(func() { c.App.PlaybackManager.OnPlaying(func() {
if _, ok := c.App.PlaybackManager.CurrentPlayer().(*mpv.Player); ok {
if c.peakMeter != nil { if c.peakMeter != nil {
c.startVisualizationAnim() c.startVisualizationAnim()
} }
}
}) })
} }