first commit
This commit is contained in:
@@ -0,0 +1,428 @@
|
||||
package player
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/yourok/go-mpv/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).
|
||||
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
|
||||
PlaylistPos int64
|
||||
}
|
||||
|
||||
// Argument to Seek function (SeekAbsolute, SeekRelative, SeekAbsolutePercent, SeekRelativePercent).
|
||||
type SeekMode int
|
||||
|
||||
const (
|
||||
SeekAbsolute SeekMode = iota
|
||||
SeekRelative
|
||||
SeekAbsolutePercent
|
||||
SeekRelativePercent
|
||||
)
|
||||
|
||||
// Player encapsulates the mpv instance and provides functions
|
||||
// to control it and to check its status.
|
||||
type Player struct {
|
||||
mpv *mpv.Mpv
|
||||
vol int
|
||||
status Status
|
||||
seeking bool
|
||||
curPlaylistPos int64
|
||||
prePausedState State
|
||||
clientName string
|
||||
|
||||
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() error {
|
||||
if p.mpv == nil {
|
||||
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")
|
||||
|
||||
if p.vol < 0 {
|
||||
p.vol = 100
|
||||
}
|
||||
m.SetOption("volume", mpv.FORMAT_INT64, p.vol)
|
||||
|
||||
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
|
||||
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 {
|
||||
if p.mpv == nil {
|
||||
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 {
|
||||
if p.mpv == nil {
|
||||
return ErrUnitialized
|
||||
}
|
||||
err := p.mpv.Command([]string{"loadfile", url, "replace"})
|
||||
if err == nil {
|
||||
p.setState(Playing)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Stops playback and clears the play queue.
|
||||
func (p *Player) Stop() error {
|
||||
if p.mpv == nil {
|
||||
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
|
||||
}
|
||||
|
||||
// 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.mpv == nil {
|
||||
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.mpv == nil {
|
||||
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.mpv == nil {
|
||||
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.mpv != nil {
|
||||
err := p.mpv.SetProperty("volume", mpv.FORMAT_INT64, vol)
|
||||
if err == nil {
|
||||
p.vol = vol
|
||||
}
|
||||
return err
|
||||
}
|
||||
p.vol = vol
|
||||
return nil
|
||||
}
|
||||
|
||||
// Gets the current volume of the player.
|
||||
func (p *Player) GetVolume() int {
|
||||
return p.vol
|
||||
}
|
||||
|
||||
func (p *Player) setPaused(paused bool) error {
|
||||
return p.mpv.SetProperty("pause", mpv.FORMAT_FLAG, paused)
|
||||
}
|
||||
|
||||
// Start playback from the first track in the play queue.
|
||||
func (p *Player) PlayFromBeginning() error {
|
||||
err := p.mpv.Command([]string{"playlist-play-index", "0"})
|
||||
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.mpv == nil {
|
||||
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:
|
||||
err := p.setPaused(true)
|
||||
if err == nil {
|
||||
p.prePausedState = p.status.State
|
||||
p.setState(Paused)
|
||||
}
|
||||
return err
|
||||
case Paused:
|
||||
err := p.setPaused(false)
|
||||
if err == nil {
|
||||
p.setState(p.prePausedState)
|
||||
}
|
||||
return err
|
||||
default:
|
||||
return errors.New("Unknown player state")
|
||||
}
|
||||
}
|
||||
|
||||
// Get the current status of the player.
|
||||
func (p *Player) GetStatus() Status {
|
||||
if p.mpv == nil {
|
||||
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
|
||||
}
|
||||
|
||||
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.mpv != nil {
|
||||
p.mpv.Command([]string{"stop"})
|
||||
p.mpv.DetachDestroy()
|
||||
p.mpv = nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}()
|
||||
if p.status.State == Stopped {
|
||||
defer func() {
|
||||
for _, cb := range p.onTrackChange {
|
||||
cb(p.curPlaylistPos)
|
||||
}
|
||||
}()
|
||||
}
|
||||
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(0.2 /*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 {
|
||||
if p.curPlaylistPos != pos {
|
||||
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"
|
||||
}
|
||||
Reference in New Issue
Block a user