add interface to decouple MPV player from PlaybackManager

This commit is contained in:
Drew Weymouth
2023-12-18 20:22:24 -08:00
parent 1665281ad7
commit df6c53a3f5
6 changed files with 135 additions and 107 deletions
+7 -7
View File
@@ -207,12 +207,12 @@ func (m *MPRISHandler) PlaybackStatus() (types.PlaybackStatus, error) {
}
func (m *MPRISHandler) LoopStatus() (types.LoopStatus, error) {
switch m.pm.LoopMode() {
case LoopModeAll:
switch m.pm.GetLoopMode() {
case player.LoopAll:
return types.LoopStatusPlaylist, nil
case LoopModeOne:
case player.LoopOne:
return types.LoopStatusTrack, nil
case LoopModeNone:
case player.LoopNone:
return types.LoopStatusNone, nil
}
return "", errors.New("unknown loop status")
@@ -221,11 +221,11 @@ func (m *MPRISHandler) LoopStatus() (types.LoopStatus, error) {
func (m *MPRISHandler) SetLoopStatus(status types.LoopStatus) error {
switch status {
case types.LoopStatusPlaylist:
return m.pm.SetLoopMode(LoopModeAll)
return m.pm.SetLoopMode(player.LoopAll)
case types.LoopStatusTrack:
return m.pm.SetLoopMode(LoopModeOne)
return m.pm.SetLoopMode(player.LoopOne)
case types.LoopStatusNone:
return m.pm.SetLoopMode(LoopModeNone)
return m.pm.SetLoopMode(player.LoopNone)
}
return errors.New("unknown loop status")
}
+58 -35
View File
@@ -2,14 +2,12 @@ package backend
import (
"context"
"fmt"
"log"
"time"
"github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/sharedutil"
)
@@ -20,14 +18,6 @@ var (
ReplayGainAuto = "Auto"
)
type LoopMode int
const (
LoopModeNone LoopMode = LoopMode(player.LoopNone)
LoopModeAll LoopMode = LoopMode(player.LoopAll)
LoopModeOne LoopMode = LoopMode(player.LoopOne)
)
// A high-level Subsonic-aware playback backend.
// Manages loading tracks into the Player queue,
// sending callbacks on play time updates and track changes.
@@ -36,7 +26,7 @@ type PlaybackManager struct {
cancelPollPos context.CancelFunc
pollingTick *time.Ticker
sm *ServerManager
player *mpv.Player
player player.BasePlayer
playTimeStopwatch util.Stopwatch
curTrackTime float64
@@ -44,7 +34,7 @@ type PlaybackManager struct {
callbacksDisabled bool
playQueue []*mediaprovider.Track
nowPlayingIdx int64
nowPlayingIdx int
// to pass to onSongChange listeners; clear once listeners have been called
lastScrobbled *mediaprovider.Track
@@ -55,7 +45,7 @@ type PlaybackManager struct {
// registered callbacks
onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
onPlayTimeUpdate []func(float64, float64)
onLoopModeChange []func(LoopMode)
onLoopModeChange []func(player.LoopMode)
onVolumeChange []func(int)
onSeek []func()
onPaused []func()
@@ -66,7 +56,7 @@ type PlaybackManager struct {
func NewPlaybackManager(
ctx context.Context,
s *ServerManager,
p *mpv.Player,
p player.BasePlayer,
scrobbleCfg *ScrobbleConfig,
transcodeCfg *TranscodingConfig,
) *PlaybackManager {
@@ -79,8 +69,8 @@ func NewPlaybackManager(
scrobbleCfg: scrobbleCfg,
transcodeCfg: transcodeCfg,
}
p.OnTrackChange(func(tracknum int64) {
if tracknum >= int64(len(pm.playQueue)) {
p.OnTrackChange(func(tracknum int) {
if tracknum >= len(pm.playQueue) {
return
}
pm.checkScrobble() // scrobble the previous song if needed
@@ -165,7 +155,7 @@ func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) {
}
// Registers a callback that is notified whenever the loop mode changes.
func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) {
func (p *PlaybackManager) OnLoopModeChange(cb func(player.LoopMode)) {
p.onLoopModeChange = append(p.onLoopModeChange, cb)
}
@@ -223,11 +213,18 @@ func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, appendToQueu
util.ShuffleSlice(nums)
}
for _, i := range nums {
url, err := p.sm.Server.GetStreamURL(tracks[i].ID, p.transcodeCfg.ForceRawFile)
if err != nil {
return err
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.TrackIDPlayer); ok {
trP.AppendTrack(tracks[i].ID)
} else {
panic("unsupported player type")
}
p.player.AppendFile(url)
// 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
@@ -245,7 +242,7 @@ func (p *PlaybackManager) PlayAlbum(albumID string, firstTrack int, shuffle bool
p.SetReplayGainMode(player.ReplayGainAlbum)
}
if firstTrack <= 0 {
return p.player.PlayFromBeginning()
return p.player.PlayTrackAt(0)
}
return p.player.PlayTrackAt(firstTrack)
}
@@ -258,7 +255,7 @@ func (p *PlaybackManager) PlayPlaylist(playlistID string, firstTrack int, shuffl
p.SetReplayGainMode(player.ReplayGainTrack)
}
if firstTrack <= 0 {
return p.player.PlayFromBeginning()
return p.player.PlayTrackAt(0)
}
return p.player.PlayTrackAt(firstTrack)
}
@@ -276,7 +273,7 @@ func (p *PlaybackManager) PlayTrack(trackID string) error {
}
func (p *PlaybackManager) PlayFromBeginning() error {
return p.player.PlayFromBeginning()
return p.player.PlayTrackAt(0)
}
func (p *PlaybackManager) PlayTrackAt(idx int) error {
@@ -374,6 +371,12 @@ func (p *PlaybackManager) StopAndClearPlayQueue() {
}
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
rGainPlayer, ok := p.player.(player.ReplayGainPlayer)
if !ok {
log.Println("Error: player doesn't support ReplayGain")
return
}
p.replayGainCfg = config
mode := player.ReplayGainNone
switch config.Mode {
@@ -388,7 +391,7 @@ func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
if config.Mode == ReplayGainAuto {
mode = player.ReplayGainTrack
}
p.player.SetReplayGainOptions(player.ReplayGainOptions{
rGainPlayer.SetReplayGainOptions(player.ReplayGainOptions{
Mode: mode,
PreventClipping: config.PreventClipping,
PreampGain: config.PreampGainDB,
@@ -396,7 +399,12 @@ func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
}
func (p *PlaybackManager) SetReplayGainMode(mode player.ReplayGainMode) {
p.player.SetReplayGainOptions(player.ReplayGainOptions{
rGainPlayer, ok := p.player.(player.ReplayGainPlayer)
if !ok {
log.Println("Error: player doesn't support ReplayGain")
return
}
rGainPlayer.SetReplayGainOptions(player.ReplayGainOptions{
PreventClipping: p.replayGainCfg.PreventClipping,
PreampGain: p.replayGainCfg.PreampGainDB,
Mode: mode,
@@ -406,18 +414,29 @@ func (p *PlaybackManager) SetReplayGainMode(mode player.ReplayGainMode) {
// Changes the loop mode of the player to the next one.
// Useful for toggling UI elements, to change modes without knowing the current player mode.
func (p *PlaybackManager) SetNextLoopMode() error {
if err := p.player.SetNextLoopMode(); err != nil {
return err
var err error
switch p.GetLoopMode() {
case player.LoopNone:
err = p.SetLoopMode(player.LoopAll)
case player.LoopAll:
err = p.SetLoopMode(player.LoopOne)
case player.LoopOne:
err = p.SetLoopMode(player.LoopNone)
default:
return nil
}
if err != nil {
return err
}
for _, cb := range p.onLoopModeChange {
cb(LoopMode(p.player.GetLoopMode()))
cb(p.player.GetLoopMode())
}
return nil
}
func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) error {
func (p *PlaybackManager) SetLoopMode(loopMode player.LoopMode) error {
if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil {
return err
}
@@ -429,8 +448,8 @@ func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) error {
return nil
}
func (p *PlaybackManager) LoopMode() LoopMode {
return LoopMode(p.player.GetLoopMode())
func (p *PlaybackManager) GetLoopMode() player.LoopMode {
return p.player.GetLoopMode()
}
func (p *PlaybackManager) PlayerStatus() player.Status {
@@ -457,12 +476,15 @@ func (p *PlaybackManager) SeekNext() error {
}
func (p *PlaybackManager) SeekBackOrPrevious() error {
return p.player.SeekBackOrPrevious()
if p.player.GetStatus().TimePos > 3 {
return p.player.SeekSeconds(0)
}
return p.player.SeekPrevious()
}
// 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)
return p.player.SeekSeconds(sec)
}
// Seek to a fractional position in the current track [0..1]
@@ -472,7 +494,8 @@ func (p *PlaybackManager) SeekFraction(fraction float64) error {
} else if fraction > 1 {
fraction = 1
}
return p.player.Seek(fmt.Sprintf("%0.1f", fraction*100), mpv.SeekAbsolutePercent)
target := p.curTrackTime * fraction
return p.player.SeekSeconds(target)
}
func (p *PlaybackManager) Stop() error {
+13 -55
View File
@@ -15,16 +15,6 @@ import (
// Error returned by many Player functions if called before the player has not been initialized.
var ErrUnitialized error = errors.New("mpv player uninitialized")
// Argument to Seek function (SeekAbsolute, SeekRelative, SeekAbsolutePercent, SeekRelativePercent).
type SeekMode int
const (
SeekAbsolute SeekMode = iota
SeekRelative
SeekAbsolutePercent
SeekRelativePercent
)
// Information about a specific audio device.
// Returned by ListAudioDevices.
type AudioDevice struct {
@@ -56,6 +46,8 @@ type MediaInfo struct {
Bitrate int
}
var _ player.URLPlayer = (*Player)(nil)
// Player encapsulates the mpv instance and provides functions
// to control it and to check its status.
type Player struct {
@@ -80,7 +72,7 @@ type Player struct {
onStopped []func()
onPlaying []func()
onSeek []func()
onTrackChange []func(int64)
onTrackChange []func(int)
}
// Returns a new player.
@@ -203,30 +195,25 @@ func (p *Player) ClearPlayQueue() error {
// Seeks within the currently playing track.
// See MPV seek command documentation for more details.
func (p *Player) Seek(target string, mode SeekMode) error {
func (p *Player) SeekSeconds(secs float64) error {
if !p.initialized {
return ErrUnitialized
}
target := fmt.Sprintf("%0.1f", secs)
p.seeking = true
err := p.mpv.Command([]string{"seek", target, mode.String()})
err := p.mpv.Command([]string{"seek", target, "absolute"})
return err
}
// Seeks to the beginning of the current track if:
// - The current track is the first track in the play queue, or
// - The current time is more than 3 seconds past the beginning of the track.
//
// Else seeks to the beginning of the previous track.
func (p *Player) SeekBackOrPrevious() error {
// 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("time-pos"); err == nil && pos > 3 {
return p.Seek("0", SeekAbsolutePercent)
}
if pos, err := p.getInt64Property("playlist-pos"); err == nil && pos == 0 {
return p.Seek("0", SeekAbsolutePercent)
return p.SeekSeconds(0)
}
return p.mpv.Command([]string{"playlist-prev"})
}
@@ -445,21 +432,6 @@ func (p *Player) SetLoopMode(mode player.LoopMode) error {
return nil
}
// Change the loop mode of the player to the next one.
// Useful for toggling UI elements, to change modes without knowing the current player mode.
func (p *Player) SetNextLoopMode() error {
switch p.loopMode {
case player.LoopNone:
return p.SetLoopMode(player.LoopAll)
case player.LoopAll:
return p.SetLoopMode(player.LoopOne)
case player.LoopOne:
return p.SetLoopMode(player.LoopNone)
default:
return nil
}
}
// Get the current status of the player.
func (p *Player) GetStatus() player.Status {
if !p.initialized {
@@ -475,7 +447,7 @@ func (p *Player) GetStatus() player.Status {
p.status.Duration = dur.(float64)
}
if playpos, err := p.getInt64Property("playlist-pos"); err == nil {
p.status.PlaylistPos = playpos
p.status.PlaylistPos = int(playpos)
}
return p.status
}
@@ -586,7 +558,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(int64)) {
func (p *Player) OnTrackChange(cb func(int)) {
p.onTrackChange = append(p.onTrackChange, cb)
}
@@ -657,7 +629,7 @@ func (p *Player) eventHandler(ctx context.Context) {
if pos, err := p.getInt64Property("playlist-pos"); err == nil {
p.curPlaylistPos = pos
for _, cb := range p.onTrackChange {
cb(pos)
cb(int(pos))
}
}
case mpv.EVENT_IDLE:
@@ -668,17 +640,3 @@ func (p *Player) eventHandler(ctx context.Context) {
}
}
}
func (s SeekMode) String() string {
switch s {
case SeekAbsolute:
return "absolute"
case SeekRelative:
return "relative"
case SeekAbsolutePercent:
return "absolute-percent"
case SeekRelativePercent:
return "relative-percent"
}
return "UNKNOWN_SEEK_MODE"
}
+48 -1
View File
@@ -1,5 +1,52 @@
package player
type URLPlayer interface {
BasePlayer
AppendFile(url string) error
PlayFile(url string) error
}
type TrackIDPlayer interface {
BasePlayer
AppendTrack(trackID string) error
PlayTrack(trackID string) error
}
type BasePlayer interface {
// Transport
PlayTrackAt(idx int) error
Continue() error
Pause() error
PlayPause() 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 ReplayGainPlayer interface {
SetReplayGainOptions(ReplayGainOptions) error
}
// The playback state (Stopped, Paused, or Playing).
type State int
@@ -15,7 +62,7 @@ type Status struct {
State State
TimePos float64
Duration float64
PlaylistPos int64
PlaylistPos int
}
// The playback loop mode (LoopNone, LoopAll, LoopOne).
+4 -4
View File
@@ -162,13 +162,13 @@ func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
if desk, ok := fyneApp.(desktop.App); ok {
menu := fyne.NewMenu(appName,
fyne.NewMenuItem("Play/Pause", func() {
_ = m.App.Player.PlayPause()
_ = m.App.PlaybackManager.PlayPause()
}),
fyne.NewMenuItem("Previous", func() {
_ = m.App.Player.SeekBackOrPrevious()
_ = m.App.PlaybackManager.SeekBackOrPrevious()
}),
fyne.NewMenuItem("Next", func() {
_ = m.App.Player.SeekNext()
_ = m.App.PlaybackManager.SeekNext()
}),
fyne.NewMenuItemSeparator(),
fyne.NewMenuItem("Volume +10%", func() {
@@ -298,7 +298,7 @@ func (m *MainWindow) addShortcuts() {
case fyne.KeyEscape:
m.Controller.CloseEscapablePopUp()
case fyne.KeySpace:
m.App.Player.PlayPause()
m.App.PlaybackManager.PlayPause()
}
})
}
+5 -5
View File
@@ -7,7 +7,7 @@ import (
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
"github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/player"
myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util"
)
@@ -67,15 +67,15 @@ func (a *AuxControls) OnChangeLoopMode(f func()) {
a.loop.OnTapped = f
}
func (a *AuxControls) SetLoopMode(mode backend.LoopMode) {
func (a *AuxControls) SetLoopMode(mode player.LoopMode) {
switch mode {
case backend.LoopModeAll:
case player.LoopAll:
a.loop.Importance = widget.HighImportance
a.loop.Icon = myTheme.RepeatIcon
case backend.LoopModeOne:
case player.LoopOne:
a.loop.Importance = widget.HighImportance
a.loop.Icon = myTheme.RepeatOneIcon
case backend.LoopModeNone:
case player.LoopNone:
a.loop.Importance = widget.MediumImportance
a.loop.Icon = myTheme.RepeatIcon
}