Refactor pt. 1: move MPV player to own package and extract some types

This commit is contained in:
Drew Weymouth
2023-12-17 13:50:30 -08:00
parent a16e687c02
commit 55f9461410
11 changed files with 760 additions and 711 deletions
+12 -5
View File
@@ -12,6 +12,7 @@ import (
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/google/uuid" "github.com/google/uuid"
@@ -36,7 +37,7 @@ type App struct {
ServerManager *ServerManager ServerManager *ServerManager
ImageManager *ImageManager ImageManager *ImageManager
PlaybackManager *PlaybackManager PlaybackManager *PlaybackManager
Player *player.Player Player *mpv.Player
UpdateChecker UpdateChecker UpdateChecker UpdateChecker
MPRISHandler *MPRISHandler MPRISHandler *MPRISHandler
@@ -194,7 +195,7 @@ func (a *App) callOnReactivate() {
} }
func (a *App) initMPV() error { func (a *App) initMPV() error {
p := player.NewWithClientName(a.appName) p := mpv.NewWithClientName(a.appName)
c := a.Config.LocalPlayback c := a.Config.LocalPlayback
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500) c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
if err := p.Init(c.InMemoryCacheSizeMB); err != nil { if err := p.Init(c.InMemoryCacheSizeMB); err != nil {
@@ -234,10 +235,16 @@ func (a *App) setupMPV() error {
if !sharedutil.SliceContains(rgainOpts, a.Config.ReplayGain.Mode) { if !sharedutil.SliceContains(rgainOpts, a.Config.ReplayGain.Mode) {
a.Config.ReplayGain.Mode = ReplayGainNone a.Config.ReplayGain.Mode = ReplayGainNone
} }
mode := player.ReplayGainMode(a.Config.ReplayGain.Mode) mode := player.ReplayGainNone
if a.Config.ReplayGain.Mode == ReplayGainAuto { switch a.Config.ReplayGain.Mode {
case ReplayGainAlbum:
mode = player.ReplayGainAlbum
case ReplayGainTrack:
mode = player.ReplayGainTrack
case ReplayGainAuto:
mode = player.ReplayGainTrack mode = player.ReplayGainTrack
} }
a.Player.SetReplayGainOptions(player.ReplayGainOptions{ a.Player.SetReplayGainOptions(player.ReplayGainOptions{
Mode: mode, Mode: mode,
PreventClipping: a.Config.ReplayGain.PreventClipping, PreventClipping: a.Config.ReplayGain.PreventClipping,
@@ -245,7 +252,7 @@ func (a *App) setupMPV() error {
}) })
a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive) a.Player.SetAudioExclusive(a.Config.LocalPlayback.AudioExclusive)
eq := &player.ISO15BandEqualizer{ eq := &mpv.ISO15BandEqualizer{
EQPreamp: a.Config.LocalPlayback.EqualizerPreamp, EQPreamp: a.Config.LocalPlayback.EqualizerPreamp,
Disabled: !a.Config.LocalPlayback.EqualizerEnabled, Disabled: !a.Config.LocalPlayback.EqualizerEnabled,
} }
+4 -3
View File
@@ -23,6 +23,7 @@ import (
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/player" "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. // os_remote_command_callback is called by Objective-C when incoming OS media commands are received.
@@ -51,7 +52,7 @@ func os_remote_command_callback(command C.Command, value C.double) {
// MPMediaHandler is the handler for MacOS media controls and system events. // MPMediaHandler is the handler for MacOS media controls and system events.
type MPMediaHandler struct { type MPMediaHandler struct {
player *player.Player player *mpv.Player
playbackManager *PlaybackManager playbackManager *PlaybackManager
artURLLookup func(string) (string, error) artURLLookup func(string) (string, error)
} }
@@ -62,7 +63,7 @@ var mpMediaEventRecipient *MPMediaHandler
// NewMPMediaHandler creates a new MPMediaHandler instances and sets it as the current recipient // NewMPMediaHandler creates a new MPMediaHandler instances and sets it as the current recipient
// for incoming system events. // for incoming system events.
func InitMPMediaHandler(player *player.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error { func InitMPMediaHandler(player *mpv.Player, playbackManager *PlaybackManager, artURLLookup func(trackID string) (string, error)) error {
mp := &MPMediaHandler{ mp := &MPMediaHandler{
player: player, player: player,
playbackManager: playbackManager, playbackManager: playbackManager,
@@ -187,5 +188,5 @@ func (mp *MPMediaHandler) OnCommandSeek(positionSeconds float64) {
if mp == nil || mp.player == nil { if mp == nil || mp.player == nil {
return return
} }
mp.player.Seek(fmt.Sprintf("%0.2f", positionSeconds), player.SeekAbsolute) mp.player.Seek(fmt.Sprintf("%0.2f", positionSeconds), mpv.SeekAbsolute)
} }
+5 -5
View File
@@ -8,6 +8,7 @@ import (
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/godbus/dbus/v5" "github.com/godbus/dbus/v5"
"github.com/quarckster/go-mpris-server/pkg/events" "github.com/quarckster/go-mpris-server/pkg/events"
"github.com/quarckster/go-mpris-server/pkg/server" "github.com/quarckster/go-mpris-server/pkg/server"
@@ -43,13 +44,13 @@ type MPRISHandler struct {
connErr error connErr error
playerName string playerName string
curTrackPath string // empty for no track curTrackPath string // empty for no track
p *player.Player p *mpv.Player
pm *PlaybackManager pm *PlaybackManager
s *server.Server s *server.Server
evt *events.EventHandler evt *events.EventHandler
} }
func NewMPRISHandler(playerName string, p *player.Player, pm *PlaybackManager) *MPRISHandler { func NewMPRISHandler(playerName string, p *mpv.Player, pm *PlaybackManager) *MPRISHandler {
m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")} m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")}
m.s = server.NewServer(playerName, m, m) m.s = server.NewServer(playerName, m, m)
m.evt = events.NewEventHandler(m.s) m.evt = events.NewEventHandler(m.s)
@@ -180,12 +181,12 @@ func (m *MPRISHandler) Play() error {
} }
func (m *MPRISHandler) Seek(offset types.Microseconds) error { func (m *MPRISHandler) Seek(offset types.Microseconds) error {
return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(offset)), player.SeekRelative) return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(offset)), mpv.SeekRelative)
} }
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.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(position)), player.SeekAbsolute) return m.p.Seek(fmt.Sprintf("%0.2f", microsecondsToSeconds(position)), mpv.SeekAbsolute)
} }
return nil return nil
} }
@@ -326,4 +327,3 @@ func encodeTrackId(id string) string {
data := []byte(id) data := []byte(id)
return base32.StdEncoding.WithPadding('0').EncodeToString(data) return base32.StdEncoding.WithPadding('0').EncodeToString(data)
} }
+17 -7
View File
@@ -8,13 +8,14 @@ import (
"github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/backend/mediaprovider"
"github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/util"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
) )
const ( var (
ReplayGainNone = string(player.ReplayGainNone) ReplayGainNone = player.ReplayGainNone.String()
ReplayGainAlbum = string(player.ReplayGainAlbum) ReplayGainAlbum = player.ReplayGainAlbum.String()
ReplayGainTrack = string(player.ReplayGainTrack) ReplayGainTrack = player.ReplayGainTrack.String()
ReplayGainAuto = "Auto" ReplayGainAuto = "Auto"
) )
@@ -34,7 +35,7 @@ type PlaybackManager struct {
cancelPollPos context.CancelFunc cancelPollPos context.CancelFunc
pollingTick *time.Ticker pollingTick *time.Ticker
sm *ServerManager sm *ServerManager
player *player.Player player *mpv.Player
playTimeStopwatch util.Stopwatch playTimeStopwatch util.Stopwatch
curTrackTime float64 curTrackTime float64
@@ -59,7 +60,7 @@ type PlaybackManager struct {
func NewPlaybackManager( func NewPlaybackManager(
ctx context.Context, ctx context.Context,
s *ServerManager, s *ServerManager,
p *player.Player, p *mpv.Player,
scrobbleCfg *ScrobbleConfig, scrobbleCfg *ScrobbleConfig,
transcodeCfg *TranscodingConfig, transcodeCfg *TranscodingConfig,
) *PlaybackManager { ) *PlaybackManager {
@@ -335,7 +336,16 @@ func (p *PlaybackManager) StopAndClearPlayQueue() {
func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) { func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
p.replayGainCfg = config p.replayGainCfg = config
mode := player.ReplayGainMode(config.Mode) mode := player.ReplayGainNone
switch config.Mode {
case ReplayGainAuto:
mode = player.ReplayGainTrack
case ReplayGainTrack:
mode = player.ReplayGainTrack
case ReplayGainAlbum:
mode = player.ReplayGainAlbum
}
if config.Mode == ReplayGainAuto { if config.Mode == ReplayGainAuto {
mode = player.ReplayGainTrack mode = player.ReplayGainTrack
} }
@@ -1,4 +1,4 @@
package player package mpv
// Equalizer implementations based on the ffmpeg 'equalizer' filter // Equalizer implementations based on the ffmpeg 'equalizer' filter
+693
View File
@@ -0,0 +1,693 @@
package mpv
import (
"context"
"errors"
"fmt"
"log"
"math"
"strconv"
"github.com/dweymouth/go-mpv"
"github.com/dweymouth/supersonic/player"
)
// 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
)
// 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 {
// The name of the audio device.
// This is the string to pass to SetAudioDevice.
Name string
// The description of the audio device.
// This is the friendly string that should be used in UIs.
Description string
}
// Media information about the currently playing media.
type MediaInfo struct {
// The sample format as string. This uses the same names as used in other places of mpv.
// NOTE: this is the format that the decoder outputs, NOT necessarily the format of the file.
Format string
// Audio samplerate.
Samplerate int
// The number of channels.
ChannelCount int
// The audio codec.
Codec string
// The average bit rate in bits per second.
Bitrate int
}
// Player encapsulates the mpv instance and provides functions
// to control it and to check its status.
type Player struct {
mpv *mpv.Mpv
initialized bool
vol int
replayGainOpts player.ReplayGainOptions
haveRGainOpts bool
audioExclusive bool
status player.Status
loopMode player.LoopMode
seeking bool
curPlaylistPos int64
prePausedState player.State
clientName string
equalizer Equalizer
bgCancel context.CancelFunc
// callbacks
onPaused []func()
onStopped []func()
onPlaying []func()
onSeek []func()
onTrackChange []func(int64)
}
// Returns a new player.
// Must call Init on the player before it is ready for playback.
func New() *Player {
return NewWithClientName("")
}
// Same as New, but sets the application name that mpv
// reports to the system audio API.
func NewWithClientName(c string) *Player {
return &Player{
vol: -1, // use 100 in Init
clientName: c,
}
}
// Initializes the Player and makes it ready for playback.
// Most Player functions will return ErrUnitialized if called before Init.
func (p *Player) Init(maxCacheMB int) error {
if !p.initialized {
m := mpv.Create()
m.SetOptionString("idle", "yes")
m.SetOptionString("video", "no")
m.SetOptionString("audio-display", "no")
m.SetOptionString("gapless-audio", "weak")
m.SetOptionString("prefetch-playlist", "yes")
m.SetOptionString("force-seekable", "yes")
m.SetOptionString("terminal", "no")
// limit in-memory cache size
m.SetOptionString("demuxer-max-bytes", fmt.Sprintf("%dMiB", maxCacheMB))
if p.vol < 0 {
p.vol = 100
}
m.SetOption("volume", mpv.FORMAT_INT64, p.vol)
p.SetAudioExclusive(p.audioExclusive)
if p.haveRGainOpts {
p.SetReplayGainOptions(p.replayGainOpts)
}
if p.clientName != "" {
m.SetOptionString("audio-client-name", p.clientName)
}
if err := m.Initialize(); err != nil {
return fmt.Errorf("error initializing mpv: %s", err.Error())
}
p.mpv = m
}
ctx, cancel := context.WithCancel(context.Background())
go p.eventHandler(ctx)
p.bgCancel = cancel
p.initialized = true
return nil
}
// Appends the given file to the play queue.
// Note that the Player API does not provide methods to read
// the play queue. Clients are expected to maintain their own play queue model.
func (p *Player) AppendFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"loadfile", url, "append"})
}
// Plays the specified file, clearing the previous play queue, if any.
func (p *Player) PlayFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized {
return ErrUnitialized
}
err := p.mpv.Command([]string{"loadfile", url, "replace"})
if err == nil {
p.setState(player.Playing)
}
return err
}
// Removes the item at the given index from the internal playqueue.
func (p *Player) RemoveTrackAt(idx int) error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)})
}
// Stops playback and clears the play queue.
func (p *Player) Stop() error {
if !p.initialized {
return ErrUnitialized
}
var err error
if p.status.State == player.Stopped {
err = p.mpv.Command([]string{"playlist-clear"})
} else {
if err = p.mpv.Command([]string{"stop"}); err == nil {
// if player was paused, stop command actually doesn't clear pause state
err = p.setPaused(false)
}
}
if err == nil {
p.setState(player.Stopped)
}
return err
}
// Clears the play queue, except for the currently playing file.
func (p *Player) ClearPlayQueue() error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-clear"})
}
// Seeks within the currently playing track.
// See MPV seek command documentation for more details.
func (p *Player) Seek(target string, mode SeekMode) error {
if !p.initialized {
return ErrUnitialized
}
p.seeking = true
err := p.mpv.Command([]string{"seek", target, mode.String()})
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 {
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.mpv.Command([]string{"playlist-prev"})
}
// Seeks to the next track in the play queue, if any.
func (p *Player) SeekNext() error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-next"})
}
// Sets the volume of the player (0-100).
// Unlike most Player functions, SetVolume can be called before Init,
// to set the initial volume of the player on startup.
func (p *Player) SetVolume(vol int) error {
if vol > 100 {
vol = 100
} else if vol < 0 {
vol = 0
}
if p.initialized {
err := p.mpv.SetProperty("volume", mpv.FORMAT_INT64, vol)
if err == nil {
p.vol = vol
}
return err
}
p.vol = vol
return nil
}
// Sets the ReplayGain options of the player.
// Unlike most Player functions, SetReplayGainOptions can be called
// before Init, to set the initial replaygain options of the player on startup.
func (p *Player) SetReplayGainOptions(options player.ReplayGainOptions) error {
p.replayGainOpts = options
p.haveRGainOpts = true
mode := "no"
switch options.Mode {
case player.ReplayGainAlbum:
mode = "album"
case player.ReplayGainTrack:
mode = "track"
}
if p.initialized {
if err := p.mpv.SetPropertyString("replaygain", mode); err != nil {
return err
}
if err := p.mpv.SetProperty("replaygain-preamp", mpv.FORMAT_DOUBLE, options.PreampGain); err != nil {
return err
}
clip := "yes"
if options.PreventClipping {
clip = "no"
}
if err := p.mpv.SetPropertyString("replaygain-clip", clip); err != nil {
return err
}
}
return nil
}
// Sets the audio exclusive option of the player.
// Unlike most Player functions, SetAudioExclusive can be called
// before Init, to set the initial option of the player on startup.
func (p *Player) SetAudioExclusive(tf bool) {
p.audioExclusive = tf
if p.initialized {
val := "no"
if tf {
val = "yes"
}
p.mpv.SetOptionString("audio-exclusive", val)
}
}
// Gets the current volume of the player.
func (p *Player) GetVolume() int {
return p.vol
}
// sets paused status and ensures that audio exlusive is false while paused
// (releases audio device to other players)
func (p *Player) setPaused(paused bool) error {
if !paused && p.audioExclusive {
if err := p.mpv.SetOptionString("audio-exclusive", "yes"); err != nil {
return err
}
}
err := p.mpv.SetProperty("pause", mpv.FORMAT_FLAG, paused)
if err == nil && paused && p.audioExclusive {
err = p.mpv.SetOptionString("audio-exclusive", "no")
}
return err
}
// Start playback from the first track in the play queue.
func (p *Player) PlayFromBeginning() error {
return p.PlayTrackAt(0)
}
// Start playback from the specified track index in the play queue.
func (p *Player) PlayTrackAt(idx int) error {
// check if we have anything to play
if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) {
return nil
}
err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)})
if p.GetStatus().State == player.Paused {
err = p.setPaused(false)
}
if err == nil {
p.setState(player.Playing)
}
return err
}
// Begins playback if there is anything in the play queue and player is stopped or paused.
// If player is playing, pauses playback.
func (p *Player) PlayPause() error {
if !p.initialized {
return ErrUnitialized
}
switch p.status.State {
case player.Stopped:
// check if we have anything to play
if c, err := p.getInt64Property("playlist-count"); err == nil && c > 0 {
err := p.mpv.Command([]string{"playlist-play-index", "0"})
if err == nil {
p.setState(player.Playing)
}
return err
}
return nil
case player.Playing:
return p.Pause()
case player.Paused:
return p.Continue()
default:
return errors.New("Unknown player state")
}
}
// Pause playback and update the player state
func (p *Player) Pause() error {
if p.status.State != player.Playing {
return nil
}
err := p.setPaused(true)
if err == nil {
p.prePausedState = p.status.State
p.setState(player.Paused)
}
return err
}
// Continue playback and update the player state
func (p *Player) Continue() error {
if p.status.State == player.Paused {
err := p.setPaused(false)
if err == nil {
p.setState(p.prePausedState)
}
return err
} else if p.status.State == player.Stopped {
return p.PlayFromBeginning()
}
return nil
}
// Get the loop mode of the player.
func (p *Player) GetLoopMode() player.LoopMode {
return p.loopMode
}
// Set the loop mode of the player.
func (p *Player) SetLoopMode(mode player.LoopMode) error {
if !p.initialized {
return ErrUnitialized
}
// Return early if player is already in specified mode
if mode == p.loopMode {
return nil
}
switch mode {
case player.LoopNone:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case player.LoopAll:
if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case player.LoopOne:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil {
return err
}
}
p.loopMode = mode
return nil
}
// 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 {
return p.status
}
pos, _ := p.mpv.GetProperty("playback-time", mpv.FORMAT_DOUBLE)
dur, _ := p.mpv.GetProperty("duration", mpv.FORMAT_DOUBLE)
if pos != nil {
p.status.TimePos = pos.(float64)
}
if dur != nil {
p.status.Duration = dur.(float64)
}
if playpos, err := p.getInt64Property("playlist-pos"); err == nil {
p.status.PlaylistPos = playpos
}
return p.status
}
// List available audio devices.
func (p *Player) ListAudioDevices() ([]AudioDevice, error) {
n, err := p.mpv.GetProperty("audio-device-list", mpv.FORMAT_NODE)
if err != nil {
return nil, err
}
nodeArr := n.(*mpv.Node).Data.([]*mpv.Node)
devices := make([]AudioDevice, len(nodeArr))
for i, node := range nodeArr {
dev := node.Data.(map[string]*mpv.Node)
name := dev["name"].Data.(string)
desc := dev["description"].Data.(string)
devices[i] = AudioDevice{Name: name, Description: desc}
}
return devices, nil
}
func (p *Player) SetAudioDevice(deviceName string) error {
return p.mpv.SetPropertyString("audio-device", deviceName)
}
func (p *Player) SetEqualizer(eq Equalizer) error {
p.equalizer = eq
if eq == nil || !eq.IsEnabled() {
return p.mpv.SetPropertyString("af", "")
}
af := ""
if math.Abs(eq.Preamp()) > 0.01 {
af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp())
}
eqAF := eq.Curve().String()
if af == "" {
af = eqAF
} else if eqAF != "" {
af = fmt.Sprintf("%s,%s", af, eqAF)
}
return p.mpv.SetPropertyString("af", af)
}
func (p *Player) Equalizer() Equalizer {
return p.equalizer
}
func (p *Player) GetMediaInfo() (MediaInfo, error) {
var info MediaInfo
n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE)
if err != nil {
return info, err
}
nodeMap := n.(*mpv.Node).Data.(map[string]*mpv.Node)
info.Format = nodeMap["format"].Data.(string)
info.Samplerate = int(nodeMap["samplerate"].Data.(int64))
info.ChannelCount = int(nodeMap["channel-count"].Data.(int64))
br, err := p.mpv.GetProperty("audio-bitrate", mpv.FORMAT_INT64)
if err == nil {
info.Bitrate = int(br.(int64))
}
codec, err := p.mpv.GetProperty("track-list/0/codec", mpv.FORMAT_STRING)
if err == nil {
info.Codec = codec.(string)
}
return info, nil
}
func (p *Player) getInt64Property(propName string) (int64, error) {
playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64)
if err != nil {
return -1, err
}
if playpos != nil {
return playpos.(int64), nil
}
return -1, errors.New("mpv did not report playlist pos")
}
// Returns true if a seek is currently in progress.
func (p *Player) IsSeeking() bool {
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(int64)) {
p.onTrackChange = append(p.onTrackChange, cb)
}
// Destroy the player.
func (p *Player) Destroy() {
if p.bgCancel != nil {
p.bgCancel()
}
if p.initialized {
p.mpv.Command([]string{"stop"})
p.mpv.TerminateDestroy()
p.initialized = false
}
}
// sets the state and invokes callbacks, if triggered
func (p *Player) setState(s player.State) {
switch {
case s == player.Playing && p.status.State != player.Playing:
defer func() {
for _, cb := range p.onPlaying {
cb()
}
}()
case s == player.Paused && p.status.State != player.Paused:
defer func() {
for _, cb := range p.onPaused {
cb()
}
}()
case s == player.Stopped && p.status.State != player.Stopped:
defer func() {
for _, cb := range p.onStopped {
cb()
}
}()
}
p.status.State = s
}
func (p *Player) eventHandler(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
e := p.mpv.WaitEvent(1 /*timeout seconds*/)
if e.Event_Id != mpv.EVENT_NONE {
//log.Printf("mpv event: %+v\n", e)
}
switch e.Event_Id {
case mpv.EVENT_PLAYBACK_RESTART:
if p.seeking {
p.seeking = false
}
case mpv.EVENT_SEEK:
for _, cb := range p.onSeek {
cb()
}
case mpv.EVENT_FILE_LOADED:
if p.status.State == player.Paused {
// seek while paused switches to a new file
// mpv does not fire seek event in this case
for _, cb := range p.onSeek {
cb()
}
}
if pos, err := p.getInt64Property("playlist-pos"); err == nil {
p.curPlaylistPos = pos
for _, cb := range p.onTrackChange {
cb(pos)
}
}
case mpv.EVENT_IDLE:
p.status.Duration = 0
p.status.TimePos = 0
p.setState(player.Stopped)
}
}
}
}
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"
}
+16 -680
View File
@@ -1,19 +1,5 @@
package player package player
import (
"context"
"errors"
"fmt"
"log"
"math"
"strconv"
"github.com/dweymouth/go-mpv"
)
// Error returned by many Player functions if called before the player has not been initialized.
var ErrUnitialized error = errors.New("mpv player uninitialized")
// The playback state (Stopped, Paused, or Playing). // The playback state (Stopped, Paused, or Playing).
type State int type State int
@@ -32,23 +18,21 @@ type Status struct {
PlaylistPos int64 PlaylistPos int64
} }
// Argument to Seek function (SeekAbsolute, SeekRelative, SeekAbsolutePercent, SeekRelativePercent). // The playback loop mode (LoopNone, LoopAll, LoopOne).
type SeekMode int type LoopMode int
const ( const (
SeekAbsolute SeekMode = iota LoopNone LoopMode = iota
SeekRelative LoopAll
SeekAbsolutePercent LoopOne
SeekRelativePercent
) )
// One of "no", "track", or "album" type ReplayGainMode int
type ReplayGainMode string
const ( const (
ReplayGainNone ReplayGainMode = "no" ReplayGainNone ReplayGainMode = iota
ReplayGainTrack ReplayGainMode = "track" ReplayGainTrack
ReplayGainAlbum ReplayGainMode = "album" ReplayGainAlbum
) )
// Replay Gain options (argument to SetReplayGainOptions). // Replay Gain options (argument to SetReplayGainOptions).
@@ -59,665 +43,17 @@ type ReplayGainOptions struct {
// Fallback gain intentionally omitted // Fallback gain intentionally omitted
} }
// The playback loop mode (LoopNone, LoopAll, LoopOne). func (r ReplayGainMode) String() string {
type LoopMode int switch r {
case ReplayGainTrack:
const ( return "track"
LoopNone LoopMode = iota case ReplayGainAlbum:
LoopAll return "album"
LoopOne
)
// Information about a specific audio device.
// Returned by ListAudioDevices.
type AudioDevice struct {
// The name of the audio device.
// This is the string to pass to SetAudioDevice.
Name string
// The description of the audio device.
// This is the friendly string that should be used in UIs.
Description string
}
// Media information about the currently playing media.
type MediaInfo struct {
// The sample format as string. This uses the same names as used in other places of mpv.
// NOTE: this is the format that the decoder outputs, NOT necessarily the format of the file.
Format string
// Audio samplerate.
Samplerate int
// The number of channels.
ChannelCount int
// The audio codec.
Codec string
// The average bit rate in bits per second.
Bitrate int
}
// Player encapsulates the mpv instance and provides functions
// to control it and to check its status.
type Player struct {
mpv *mpv.Mpv
initialized bool
vol int
replayGainOpts ReplayGainOptions
haveRGainOpts bool
audioExclusive bool
status Status
loopMode LoopMode
seeking bool
curPlaylistPos int64
prePausedState State
clientName string
equalizer Equalizer
bgCancel context.CancelFunc
// callbacks
onPaused []func()
onStopped []func()
onPlaying []func()
onSeek []func()
onTrackChange []func(int64)
}
// Returns a new player.
// Must call Init on the player before it is ready for playback.
func New() *Player {
return NewWithClientName("")
}
// Same as New, but sets the application name that mpv
// reports to the system audio API.
func NewWithClientName(c string) *Player {
return &Player{
vol: -1, // use 100 in Init
clientName: c,
}
}
// Initializes the Player and makes it ready for playback.
// Most Player functions will return ErrUnitialized if called before Init.
func (p *Player) Init(maxCacheMB int) error {
if !p.initialized {
m := mpv.Create()
m.SetOptionString("idle", "yes")
m.SetOptionString("video", "no")
m.SetOptionString("audio-display", "no")
m.SetOptionString("gapless-audio", "weak")
m.SetOptionString("prefetch-playlist", "yes")
m.SetOptionString("force-seekable", "yes")
m.SetOptionString("terminal", "no")
// limit in-memory cache size
m.SetOptionString("demuxer-max-bytes", fmt.Sprintf("%dMiB", maxCacheMB))
if p.vol < 0 {
p.vol = 100
}
m.SetOption("volume", mpv.FORMAT_INT64, p.vol)
p.SetAudioExclusive(p.audioExclusive)
if p.haveRGainOpts {
p.SetReplayGainOptions(p.replayGainOpts)
}
if p.clientName != "" {
m.SetOptionString("audio-client-name", p.clientName)
}
if err := m.Initialize(); err != nil {
return fmt.Errorf("error initializing mpv: %s", err.Error())
}
p.mpv = m
}
ctx, cancel := context.WithCancel(context.Background())
go p.eventHandler(ctx)
p.bgCancel = cancel
p.initialized = true
return nil
}
// Appends the given file to the play queue.
// Note that the Player API does not provide methods to read
// the play queue. Clients are expected to maintain their own play queue model.
func (p *Player) AppendFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"loadfile", url, "append"})
}
// Plays the specified file, clearing the previous play queue, if any.
func (p *Player) PlayFile(url string) error {
log.Printf("Adding playback URL: %s", url)
if !p.initialized {
return ErrUnitialized
}
err := p.mpv.Command([]string{"loadfile", url, "replace"})
if err == nil {
p.setState(Playing)
}
return err
}
// Removes the item at the given index from the internal playqueue.
func (p *Player) RemoveTrackAt(idx int) error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-remove", strconv.Itoa(idx)})
}
// Stops playback and clears the play queue.
func (p *Player) Stop() error {
if !p.initialized {
return ErrUnitialized
}
var err error
if p.status.State == Stopped {
err = p.mpv.Command([]string{"playlist-clear"})
} else {
if err = p.mpv.Command([]string{"stop"}); err == nil {
// if player was paused, stop command actually doesn't clear pause state
err = p.setPaused(false)
}
}
if err == nil {
p.setState(Stopped)
}
return err
}
// Clears the play queue, except for the currently playing file.
func (p *Player) ClearPlayQueue() error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-clear"})
}
// Seeks within the currently playing track.
// See MPV seek command documentation for more details.
func (p *Player) Seek(target string, mode SeekMode) error {
if !p.initialized {
return ErrUnitialized
}
p.seeking = true
err := p.mpv.Command([]string{"seek", target, mode.String()})
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 {
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.mpv.Command([]string{"playlist-prev"})
}
// Seeks to the next track in the play queue, if any.
func (p *Player) SeekNext() error {
if !p.initialized {
return ErrUnitialized
}
return p.mpv.Command([]string{"playlist-next"})
}
// Sets the volume of the player (0-100).
// Unlike most Player functions, SetVolume can be called before Init,
// to set the initial volume of the player on startup.
func (p *Player) SetVolume(vol int) error {
if vol > 100 {
vol = 100
} else if vol < 0 {
vol = 0
}
if p.initialized {
err := p.mpv.SetProperty("volume", mpv.FORMAT_INT64, vol)
if err == nil {
p.vol = vol
}
return err
}
p.vol = vol
return nil
}
// Sets the ReplayGain options of the player.
// Unlike most Player functions, SetReplayGainOptions can be called
// before Init, to set the initial replaygain options of the player on startup.
func (p *Player) SetReplayGainOptions(options ReplayGainOptions) error {
p.replayGainOpts = options
p.haveRGainOpts = true
if p.initialized {
if err := p.mpv.SetPropertyString("replaygain", string(options.Mode)); err != nil {
return err
}
if err := p.mpv.SetProperty("replaygain-preamp", mpv.FORMAT_DOUBLE, options.PreampGain); err != nil {
return err
}
clip := "yes"
if options.PreventClipping {
clip = "no"
}
if err := p.mpv.SetPropertyString("replaygain-clip", clip); err != nil {
return err
}
}
return nil
}
// Sets the audio exclusive option of the player.
// Unlike most Player functions, SetAudioExclusive can be called
// before Init, to set the initial option of the player on startup.
func (p *Player) SetAudioExclusive(tf bool) {
p.audioExclusive = tf
if p.initialized {
val := "no"
if tf {
val = "yes"
}
p.mpv.SetOptionString("audio-exclusive", val)
}
}
// Gets the current volume of the player.
func (p *Player) GetVolume() int {
return p.vol
}
// sets paused status and ensures that audio exlusive is false while paused
// (releases audio device to other players)
func (p *Player) setPaused(paused bool) error {
if !paused && p.audioExclusive {
if err := p.mpv.SetOptionString("audio-exclusive", "yes"); err != nil {
return err
}
}
err := p.mpv.SetProperty("pause", mpv.FORMAT_FLAG, paused)
if err == nil && paused && p.audioExclusive {
err = p.mpv.SetOptionString("audio-exclusive", "no")
}
return err
}
// Start playback from the first track in the play queue.
func (p *Player) PlayFromBeginning() error {
return p.PlayTrackAt(0)
}
// Start playback from the specified track index in the play queue.
func (p *Player) PlayTrackAt(idx int) error {
// check if we have anything to play
if c, err := p.getInt64Property("playlist-count"); err == nil && c <= int64(idx) {
return nil
}
err := p.mpv.Command([]string{"playlist-play-index", strconv.Itoa(idx)})
if p.GetStatus().State == Paused {
err = p.setPaused(false)
}
if err == nil {
p.setState(Playing)
}
return err
}
// Begins playback if there is anything in the play queue and player is stopped or paused.
// If player is playing, pauses playback.
func (p *Player) PlayPause() error {
if !p.initialized {
return ErrUnitialized
}
switch p.status.State {
case Stopped:
// check if we have anything to play
if c, err := p.getInt64Property("playlist-count"); err == nil && c > 0 {
err := p.mpv.Command([]string{"playlist-play-index", "0"})
if err == nil {
p.setState(Playing)
}
return err
}
return nil
case Playing:
return p.Pause()
case Paused:
return p.Continue()
default: default:
return errors.New("Unknown player state") return "no"
} }
} }
// Pause playback and update the player state
func (p *Player) Pause() error {
if p.status.State != Playing {
return nil
}
err := p.setPaused(true)
if err == nil {
p.prePausedState = p.status.State
p.setState(Paused)
}
return err
}
// Continue playback and update the player state
func (p *Player) Continue() error {
if p.status.State == Paused {
err := p.setPaused(false)
if err == nil {
p.setState(p.prePausedState)
}
return err
} else if p.status.State == Stopped {
return p.PlayFromBeginning()
}
return nil
}
// Get the loop mode of the player.
func (p *Player) GetLoopMode() LoopMode {
return p.loopMode
}
// Set the loop mode of the player.
func (p *Player) SetLoopMode(mode LoopMode) error {
if !p.initialized {
return ErrUnitialized
}
// Return early if player is already in specified mode
if mode == p.loopMode {
return nil
}
switch mode {
case LoopNone:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case LoopAll:
if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "no"); err != nil {
return err
}
case LoopOne:
if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
return err
}
if err := p.mpv.SetOptionString("loop-file", "inf"); err != nil {
return err
}
}
p.loopMode = mode
return nil
}
// 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 LoopNone:
return p.SetLoopMode(LoopAll)
case LoopAll:
return p.SetLoopMode(LoopOne)
case LoopOne:
return p.SetLoopMode(LoopNone)
default:
return nil
}
}
// Get the current status of the player.
func (p *Player) GetStatus() Status {
if !p.initialized {
return p.status
}
pos, _ := p.mpv.GetProperty("playback-time", mpv.FORMAT_DOUBLE)
dur, _ := p.mpv.GetProperty("duration", mpv.FORMAT_DOUBLE)
if pos != nil {
p.status.TimePos = pos.(float64)
}
if dur != nil {
p.status.Duration = dur.(float64)
}
if playpos, err := p.getInt64Property("playlist-pos"); err == nil {
p.status.PlaylistPos = playpos
}
return p.status
}
// List available audio devices.
func (p *Player) ListAudioDevices() ([]AudioDevice, error) {
n, err := p.mpv.GetProperty("audio-device-list", mpv.FORMAT_NODE)
if err != nil {
return nil, err
}
nodeArr := n.(*mpv.Node).Data.([]*mpv.Node)
devices := make([]AudioDevice, len(nodeArr))
for i, node := range nodeArr {
dev := node.Data.(map[string]*mpv.Node)
name := dev["name"].Data.(string)
desc := dev["description"].Data.(string)
devices[i] = AudioDevice{Name: name, Description: desc}
}
return devices, nil
}
func (p *Player) SetAudioDevice(deviceName string) error {
return p.mpv.SetPropertyString("audio-device", deviceName)
}
func (p *Player) SetEqualizer(eq Equalizer) error {
p.equalizer = eq
if eq == nil || !eq.IsEnabled() {
return p.mpv.SetPropertyString("af", "")
}
af := ""
if math.Abs(eq.Preamp()) > 0.01 {
af = fmt.Sprintf("volume=volume=%0.1fdB", eq.Preamp())
}
eqAF := eq.Curve().String()
if af == "" {
af = eqAF
} else if eqAF != "" {
af = fmt.Sprintf("%s,%s", af, eqAF)
}
return p.mpv.SetPropertyString("af", af)
}
func (p *Player) Equalizer() Equalizer {
return p.equalizer
}
func (p *Player) GetMediaInfo() (MediaInfo, error) {
var info MediaInfo
n, err := p.mpv.GetProperty("audio-params", mpv.FORMAT_NODE)
if err != nil {
return info, err
}
nodeMap := n.(*mpv.Node).Data.(map[string]*mpv.Node)
info.Format = nodeMap["format"].Data.(string)
info.Samplerate = int(nodeMap["samplerate"].Data.(int64))
info.ChannelCount = int(nodeMap["channel-count"].Data.(int64))
br, err := p.mpv.GetProperty("audio-bitrate", mpv.FORMAT_INT64)
if err == nil {
info.Bitrate = int(br.(int64))
}
codec, err := p.mpv.GetProperty("track-list/0/codec", mpv.FORMAT_STRING)
if err == nil {
info.Codec = codec.(string)
}
return info, nil
}
func (p *Player) getInt64Property(propName string) (int64, error) {
playpos, err := p.mpv.GetProperty(propName, mpv.FORMAT_INT64)
if err != nil {
return -1, err
}
if playpos != nil {
return playpos.(int64), nil
}
return -1, errors.New("mpv did not report playlist pos")
}
// Returns true if a seek is currently in progress.
func (p *Player) IsSeeking() bool {
return p.seeking && p.status.State == 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(int64)) {
p.onTrackChange = append(p.onTrackChange, cb)
}
// Destroy the player.
func (p *Player) Destroy() {
if p.bgCancel != nil {
p.bgCancel()
}
if p.initialized {
p.mpv.Command([]string{"stop"})
p.mpv.TerminateDestroy()
p.initialized = false
}
}
// sets the state and invokes callbacks, if triggered
func (p *Player) setState(s State) {
switch {
case s == Playing && p.status.State != Playing:
defer func() {
for _, cb := range p.onPlaying {
cb()
}
}()
case s == Paused && p.status.State != Paused:
defer func() {
for _, cb := range p.onPaused {
cb()
}
}()
case s == Stopped && p.status.State != Stopped:
defer func() {
for _, cb := range p.onStopped {
cb()
}
}()
}
p.status.State = s
}
func (p *Player) eventHandler(ctx context.Context) {
for {
select {
case <-ctx.Done():
return
default:
e := p.mpv.WaitEvent(1 /*timeout seconds*/)
if e.Event_Id != mpv.EVENT_NONE {
//log.Printf("mpv event: %+v\n", e)
}
switch e.Event_Id {
case mpv.EVENT_PLAYBACK_RESTART:
if p.seeking {
p.seeking = false
}
case mpv.EVENT_SEEK:
for _, cb := range p.onSeek {
cb()
}
case mpv.EVENT_FILE_LOADED:
if p.status.State == Paused {
// seek while paused switches to a new file
// mpv does not fire seek event in this case
for _, cb := range p.onSeek {
cb()
}
}
if pos, err := p.getInt64Property("playlist-pos"); err == nil {
p.curPlaylistPos = pos
for _, cb := range p.onTrackChange {
cb(pos)
}
}
case mpv.EVENT_IDLE:
p.status.Duration = 0
p.status.TimePos = 0
p.setState(Stopped)
}
}
}
}
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"
}
func (l LoopMode) String() string { func (l LoopMode) String() string {
switch l { switch l {
case LoopNone: case LoopNone:
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"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/player" "github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
"github.com/dweymouth/supersonic/ui/widgets" "github.com/dweymouth/supersonic/ui/widgets"
@@ -34,7 +34,7 @@ type BottomPanel struct {
var _ fyne.Widget = (*BottomPanel)(nil) var _ fyne.Widget = (*BottomPanel)(nil)
func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel { func NewBottomPanel(p *mpv.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel {
bp := &BottomPanel{playbackManager: pm} bp := &BottomPanel{playbackManager: pm}
bp.ExtendBaseWidget(bp) bp.ExtendBaseWidget(bp)
@@ -93,7 +93,7 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro
p.SeekBackOrPrevious() p.SeekBackOrPrevious()
}) })
bp.Controls.OnSeek(func(f float64) { bp.Controls.OnSeek(func(f float64) {
p.Seek(fmt.Sprintf("%d", int(f*100)), player.SeekAbsolutePercent) p.Seek(fmt.Sprintf("%d", int(f*100)), mpv.SeekAbsolutePercent)
}) })
bp.AuxControls = widgets.NewAuxControls(p.GetVolume()) bp.AuxControls = widgets.NewAuxControls(p.GetVolume())
+3 -2
View File
@@ -8,6 +8,7 @@ import (
"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/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/controller" "github.com/dweymouth/supersonic/ui/controller"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
@@ -41,7 +42,7 @@ type nowPlayingPageState struct {
pool *util.WidgetPool pool *util.WidgetPool
conf *backend.NowPlayingPageConfig conf *backend.NowPlayingPageConfig
pm *backend.PlaybackManager pm *backend.PlaybackManager
p *player.Player p *mpv.Player
canRate bool canRate bool
} }
@@ -51,7 +52,7 @@ func NewNowPlayingPage(
pool *util.WidgetPool, pool *util.WidgetPool,
conf *backend.NowPlayingPageConfig, conf *backend.NowPlayingPageConfig,
pm *backend.PlaybackManager, pm *backend.PlaybackManager,
p *player.Player, // TODO: once other player backends are supported (eg uPnP), refactor p *mpv.Player, // TODO: once other player backends are supported (eg uPnP), refactor
canRate bool, canRate bool,
) *NowPlayingPage { ) *NowPlayingPage {
a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{ a := &NowPlayingPage{nowPlayingPageState: nowPlayingPageState{
+3 -2
View File
@@ -13,6 +13,7 @@ import (
"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/player" "github.com/dweymouth/supersonic/player"
"github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/sharedutil" "github.com/dweymouth/supersonic/sharedutil"
"github.com/dweymouth/supersonic/ui/dialogs" "github.com/dweymouth/supersonic/ui/dialogs"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -474,7 +475,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
devs, err := c.App.Player.ListAudioDevices() devs, err := c.App.Player.ListAudioDevices()
if err != nil { if err != nil {
log.Printf("error listing audio devices: %v", err) log.Printf("error listing audio devices: %v", err)
devs = []player.AudioDevice{{Name: "auto", Description: "Autoselect device"}} devs = []mpv.AudioDevice{{Name: "auto", Description: "Autoselect device"}}
} }
bands := c.App.Player.Equalizer().BandFrequencies() bands := c.App.Player.Equalizer().BandFrequencies()
@@ -491,7 +492,7 @@ func (c *Controller) ShowSettingsDialog(themeUpdateCallbk func(), themeFiles map
dlg.OnThemeSettingChanged = themeUpdateCallbk dlg.OnThemeSettingChanged = themeUpdateCallbk
dlg.OnEqualizerSettingsChanged = func() { dlg.OnEqualizerSettingsChanged = func() {
// currently we only have one equalizer type // currently we only have one equalizer type
eq := c.App.Player.Equalizer().(*player.ISO15BandEqualizer) eq := c.App.Player.Equalizer().(*mpv.ISO15BandEqualizer)
eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled eq.Disabled = !c.App.Config.LocalPlayback.EqualizerEnabled
eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp eq.EQPreamp = c.App.Config.LocalPlayback.EqualizerPreamp
copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands) copy(eq.BandGains[:], c.App.Config.LocalPlayback.GraphicEqualizerBands)
+3 -3
View File
@@ -10,7 +10,7 @@ import (
"unicode" "unicode"
"github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend"
"github.com/dweymouth/supersonic/player" "github.com/dweymouth/supersonic/player/mpv"
"github.com/dweymouth/supersonic/ui/layouts" "github.com/dweymouth/supersonic/ui/layouts"
myTheme "github.com/dweymouth/supersonic/ui/theme" myTheme "github.com/dweymouth/supersonic/ui/theme"
"github.com/dweymouth/supersonic/ui/util" "github.com/dweymouth/supersonic/ui/util"
@@ -37,7 +37,7 @@ type SettingsDialog struct {
OnEqualizerSettingsChanged func() OnEqualizerSettingsChanged func()
config *backend.Config config *backend.Config
audioDevices []player.AudioDevice audioDevices []mpv.AudioDevice
themeFiles map[string]string // filename -> displayName themeFiles map[string]string // filename -> displayName
promptText *widget.RichText promptText *widget.RichText
@@ -49,7 +49,7 @@ type SettingsDialog struct {
// TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor. // TODO: having this depend on the player package for the AudioDevice type is kinda gross. Refactor.
func NewSettingsDialog( func NewSettingsDialog(
config *backend.Config, config *backend.Config,
audioDeviceList []player.AudioDevice, audioDeviceList []mpv.AudioDevice,
themeFileList map[string]string, themeFileList map[string]string,
equalizerBands []string, equalizerBands []string,
clientDecidesScrobble bool, clientDecidesScrobble bool,