Refactor pt. 1: move MPV player to own package and extract some types
This commit is contained in:
+12
-5
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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
@@ -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
@@ -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())
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user