move player package into backend
This commit is contained in:
+2
-2
@@ -10,9 +10,9 @@ import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/dweymouth/supersonic/backend/player/mpv"
|
||||
"github.com/dweymouth/supersonic/backend/util"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
"github.com/dweymouth/supersonic/player/mpv"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/google/uuid"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/quarckster/go-mpris-server/pkg/events"
|
||||
"github.com/quarckster/go-mpris-server/pkg/server"
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
"github.com/dweymouth/supersonic/backend/util"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
"github.com/dweymouth/supersonic/sharedutil"
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package jukebox
|
||||
|
||||
import (
|
||||
"github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
"github.com/dweymouth/supersonic/player"
|
||||
)
|
||||
|
||||
const (
|
||||
stopped = 0
|
||||
playing = 1
|
||||
paused = 2
|
||||
)
|
||||
|
||||
type JukeboxPlayer struct {
|
||||
provider mediaprovider.JukeboxProvider
|
||||
|
||||
state int // stopped, playing, paused
|
||||
volume int
|
||||
seeking bool
|
||||
numTracks int
|
||||
|
||||
curTrack int
|
||||
curTrackDuration float64
|
||||
startTrackTime float64
|
||||
startedAtUnixSecs float64
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) SetVolume(vol int) error {
|
||||
go func() {
|
||||
if err := j.provider.JukeboxSetVolume(vol); err == nil {
|
||||
j.volume = vol
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) GetVolume() int {
|
||||
return j.volume
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) PlayTrackAt(idx int) error {
|
||||
go func() {
|
||||
if err := j.provider.JukeboxSeek(idx, 0); err == nil {
|
||||
j.curTrack = idx
|
||||
j.Continue()
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) Continue() error {
|
||||
if j.state == playing {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
if err := j.provider.JukeboxStart(); err != nil {
|
||||
return
|
||||
}
|
||||
j.state = playing
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) Pause() error {
|
||||
if j.state != playing {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
if err := j.provider.JukeboxStop(); err != nil {
|
||||
return
|
||||
}
|
||||
j.state = paused
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) Stop() error {
|
||||
if j.state == stopped {
|
||||
return nil
|
||||
}
|
||||
go func() {
|
||||
if err := j.provider.JukeboxStop(); err != nil {
|
||||
return
|
||||
}
|
||||
j.state = stopped
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) SeekPrevious() error {
|
||||
track := j.curTrack
|
||||
if track > 0 {
|
||||
track = j.curTrack - 1
|
||||
}
|
||||
return j.PlayTrackAt(track)
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) SeekNext() error {
|
||||
track := j.curTrack
|
||||
if track >= j.numTracks {
|
||||
return nil
|
||||
}
|
||||
return j.PlayTrackAt(track + 1)
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) SeekSeconds(secs float64) error {
|
||||
j.seeking = true
|
||||
go func() {
|
||||
j.provider.JukeboxSeek(j.curTrack, int(secs))
|
||||
j.seeking = false
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) IsSeeking() bool {
|
||||
return j.seeking
|
||||
}
|
||||
|
||||
func (j *JukeboxPlayer) GetStatus() player.Status {
|
||||
state := player.Stopped
|
||||
if j.state == playing {
|
||||
state = player.Playing
|
||||
} else if j.state == paused {
|
||||
state = player.Paused
|
||||
}
|
||||
|
||||
// TODO - the rest
|
||||
|
||||
return player.Status{
|
||||
State: state,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package mpv
|
||||
|
||||
// Equalizer implementations based on the ffmpeg 'equalizer' filter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Equalizer interface {
|
||||
IsEnabled() bool
|
||||
Preamp() float64
|
||||
Curve() EqualizerCurve
|
||||
Type() string
|
||||
// Returns the band frequencies as strings friendly for display
|
||||
BandFrequencies() []string
|
||||
}
|
||||
|
||||
type ISO15BandEqualizer struct {
|
||||
Disabled bool
|
||||
EQPreamp float64
|
||||
BandGains [15]float64
|
||||
}
|
||||
|
||||
var (
|
||||
iso15Bands = []string{"25", "40", "63", "100", "160", "250", "400", "630", "1k", "1.6k", "2.5k", "4k", "6.3k", "10k", "16k"}
|
||||
iso15FMult = math.Pow(2, 2./3)
|
||||
)
|
||||
|
||||
var _ Equalizer = (*ISO15BandEqualizer)(nil)
|
||||
|
||||
func (i *ISO15BandEqualizer) IsEnabled() bool {
|
||||
return !i.Disabled
|
||||
}
|
||||
|
||||
func (i *ISO15BandEqualizer) Preamp() float64 {
|
||||
return i.EQPreamp
|
||||
}
|
||||
|
||||
func (i *ISO15BandEqualizer) Curve() EqualizerCurve {
|
||||
fC := float64(25)
|
||||
curve := make([]EqualizerBand, 0, len(i.BandGains))
|
||||
for _, bandGain := range i.BandGains {
|
||||
curve = append(curve, EqualizerBand{
|
||||
Frequency: int(math.Round(fC)),
|
||||
Width: 2. / 3,
|
||||
WidthType: WidthTypeOctave,
|
||||
Gain: bandGain,
|
||||
})
|
||||
fC *= iso15FMult
|
||||
}
|
||||
return curve
|
||||
}
|
||||
|
||||
func (*ISO15BandEqualizer) BandFrequencies() []string {
|
||||
ret := make([]string, len(iso15Bands))
|
||||
copy(ret, iso15Bands)
|
||||
return ret
|
||||
}
|
||||
|
||||
func (*ISO15BandEqualizer) Type() string {
|
||||
return "ISO15Band"
|
||||
}
|
||||
|
||||
type WidthType int
|
||||
|
||||
const (
|
||||
WidthTypeHz WidthType = iota
|
||||
WidthTypeKhz
|
||||
WidthTypeQ
|
||||
WidthTypeOctave
|
||||
WidthTypeSlope
|
||||
)
|
||||
|
||||
type EqualizerBand struct {
|
||||
Frequency int
|
||||
Gain float64
|
||||
Width float64
|
||||
WidthType WidthType
|
||||
}
|
||||
|
||||
type EqualizerCurve []EqualizerBand
|
||||
|
||||
func (e EqualizerCurve) String() string {
|
||||
var sb strings.Builder
|
||||
first := true
|
||||
for _, band := range e {
|
||||
if s := band.String(); s != "" {
|
||||
if !first {
|
||||
sb.WriteString(",")
|
||||
}
|
||||
sb.WriteString(s)
|
||||
first = false
|
||||
}
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (e EqualizerBand) String() string {
|
||||
if math.Abs(e.Gain) < 0.02 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("equalizer=f=%d:g=%0.2f:t=%s:w=%0.2f",
|
||||
e.Frequency, e.Gain, e.WidthType.String(), e.Width)
|
||||
}
|
||||
|
||||
func (w WidthType) String() string {
|
||||
switch w {
|
||||
case WidthTypeHz:
|
||||
return "h"
|
||||
case WidthTypeKhz:
|
||||
return "k"
|
||||
case WidthTypeQ:
|
||||
return "q"
|
||||
case WidthTypeOctave:
|
||||
return "o"
|
||||
case WidthTypeSlope:
|
||||
return "s"
|
||||
}
|
||||
return "x" // not reached
|
||||
}
|
||||
@@ -0,0 +1,516 @@
|
||||
package mpv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
|
||||
"github.com/dweymouth/go-mpv"
|
||||
"github.com/dweymouth/supersonic/backend/player"
|
||||
)
|
||||
|
||||
// Error returned by many Player functions if called before the player has not been initialized.
|
||||
var ErrUnitialized error = errors.New("mpv player uninitialized")
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
var _ player.URLPlayer = (*Player)(nil)
|
||||
|
||||
// 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
|
||||
seeking bool
|
||||
curPlaylistPos int64
|
||||
lenPlaylist int64
|
||||
prePausedState player.State
|
||||
clientName string
|
||||
equalizer Equalizer
|
||||
|
||||
bgCancel context.CancelFunc
|
||||
|
||||
// callbacks
|
||||
onPaused []func()
|
||||
onStopped []func()
|
||||
onPlaying []func()
|
||||
onSeek []func()
|
||||
onTrackChange []func()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Plays the specified file, clearing the previous play queue, if any.
|
||||
func (p *Player) PlayFile(url string) error {
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
}
|
||||
err := p.mpv.Command([]string{"loadfile", url, "replace"})
|
||||
if err == nil {
|
||||
p.lenPlaylist = 1
|
||||
if p.status.State == player.Paused {
|
||||
return p.Continue()
|
||||
}
|
||||
p.setState(player.Playing)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 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.lenPlaylist = 0
|
||||
p.setState(player.Stopped)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *Player) SetNextFile(url string) error {
|
||||
if p.lenPlaylist > p.curPlaylistPos+1 {
|
||||
if err := p.mpv.Command([]string{"playlist-remove", strconv.Itoa(int(p.curPlaylistPos) + 1)}); err != nil {
|
||||
return err
|
||||
}
|
||||
p.lenPlaylist--
|
||||
}
|
||||
if url == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := p.mpv.Command([]string{"loadfile", url, "append"})
|
||||
if err == nil {
|
||||
p.lenPlaylist++
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Seeks within the currently playing track.
|
||||
// See MPV seek command documentation for more details.
|
||||
func (p *Player) SeekSeconds(secs float64) error {
|
||||
if !p.initialized {
|
||||
return ErrUnitialized
|
||||
}
|
||||
target := fmt.Sprintf("%0.1f", secs)
|
||||
p.seeking = true
|
||||
err := p.mpv.Command([]string{"seek", target, "absolute"})
|
||||
return err
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
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()) {
|
||||
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:
|
||||
p.curPlaylistPos, _ = p.getInt64Property("playlist-pos")
|
||||
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()
|
||||
}
|
||||
}
|
||||
for _, cb := range p.onTrackChange {
|
||||
cb()
|
||||
}
|
||||
case mpv.EVENT_IDLE:
|
||||
p.status.Duration = 0
|
||||
p.status.TimePos = 0
|
||||
p.setState(player.Stopped)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package player
|
||||
|
||||
import "github.com/dweymouth/supersonic/backend/mediaprovider"
|
||||
|
||||
type URLPlayer interface {
|
||||
BasePlayer
|
||||
PlayFile(url string) error
|
||||
SetNextFile(url string) error
|
||||
}
|
||||
|
||||
type TrackPlayer interface {
|
||||
BasePlayer
|
||||
PlayTrack(track *mediaprovider.Track) error
|
||||
SetNextTrack(track *mediaprovider.Track) error
|
||||
}
|
||||
|
||||
type BasePlayer interface {
|
||||
Continue() error
|
||||
Pause() error
|
||||
Stop() error
|
||||
|
||||
SeekSeconds(secs float64) error
|
||||
IsSeeking() bool
|
||||
|
||||
SetVolume(int) error
|
||||
GetVolume() int
|
||||
|
||||
GetStatus() Status
|
||||
|
||||
// Event API
|
||||
OnPaused(func())
|
||||
OnStopped(func())
|
||||
OnPlaying(func())
|
||||
OnSeek(func())
|
||||
OnTrackChange(func())
|
||||
}
|
||||
|
||||
type ReplayGainPlayer interface {
|
||||
SetReplayGainOptions(ReplayGainOptions) error
|
||||
}
|
||||
|
||||
// The playback state (Stopped, Paused, or Playing).
|
||||
type State int
|
||||
|
||||
const (
|
||||
Stopped State = iota
|
||||
Paused
|
||||
Playing
|
||||
)
|
||||
|
||||
// The current status of the player.
|
||||
// Includes playback state, current time, total track time, and playlist position.
|
||||
type Status struct {
|
||||
State State
|
||||
TimePos float64
|
||||
Duration float64
|
||||
}
|
||||
|
||||
type ReplayGainMode int
|
||||
|
||||
const (
|
||||
ReplayGainNone ReplayGainMode = iota
|
||||
ReplayGainTrack
|
||||
ReplayGainAlbum
|
||||
)
|
||||
|
||||
// Replay Gain options (argument to SetReplayGainOptions).
|
||||
type ReplayGainOptions struct {
|
||||
Mode ReplayGainMode
|
||||
PreampGain float64
|
||||
PreventClipping bool
|
||||
// Fallback gain intentionally omitted
|
||||
}
|
||||
|
||||
func (r ReplayGainMode) String() string {
|
||||
switch r {
|
||||
case ReplayGainTrack:
|
||||
return "track"
|
||||
case ReplayGainAlbum:
|
||||
return "album"
|
||||
default:
|
||||
return "no"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user