mirror of
https://github.com/navidrome/navidrome.git
synced 2025-04-14 11:17:19 +03:00
* Fixing typo FFmpegPath -> MPVPath * Fixing panic by applying afontenot patch * Using mpv audio-device flag and naming for config and playback
224 lines
5.4 KiB
Go
224 lines
5.4 KiB
Go
package mpv
|
|
|
|
// Audio-playback using mpv media-server. See mpv.io
|
|
// https://github.com/dexterlb/mpvipc
|
|
// https://mpv.io/manual/master/#json-ipc
|
|
// https://mpv.io/manual/master/#properties
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/DexterLB/mpvipc"
|
|
"github.com/navidrome/navidrome/log"
|
|
"github.com/navidrome/navidrome/model"
|
|
)
|
|
|
|
type MpvTrack struct {
|
|
MediaFile model.MediaFile
|
|
PlaybackDone chan bool
|
|
Conn *mpvipc.Connection
|
|
IPCSocketName string
|
|
Exe *Executor
|
|
CloseCalled bool
|
|
}
|
|
|
|
func NewTrack(playbackDoneChannel chan bool, deviceName string, mf model.MediaFile) (*MpvTrack, error) {
|
|
log.Debug("loading track", "trackname", mf.Path, "mediatype", mf.ContentType())
|
|
|
|
if _, err := mpvCommand(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tmpSocketName := TempFileName("mpv-ctrl-", ".socket")
|
|
|
|
args := createMPVCommand(mpvComdTemplate, deviceName, mf.Path, tmpSocketName)
|
|
exe, err := start(args)
|
|
if err != nil {
|
|
log.Error("error starting mpv process", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
// wait for socket to show up
|
|
err = waitForFile(tmpSocketName, 3*time.Second, 100*time.Millisecond)
|
|
if err != nil {
|
|
log.Error("error or timeout waiting for control socket", "socketname", tmpSocketName, "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
conn := mpvipc.NewConnection(tmpSocketName)
|
|
err = conn.Open()
|
|
|
|
if err != nil {
|
|
log.Error("error opening new connection", "error", err)
|
|
return nil, err
|
|
}
|
|
|
|
theTrack := &MpvTrack{MediaFile: mf, PlaybackDone: playbackDoneChannel, Conn: conn, IPCSocketName: tmpSocketName, Exe: &exe, CloseCalled: false}
|
|
|
|
go func() {
|
|
conn.WaitUntilClosed()
|
|
log.Info("Hitting end-of-stream, signalling on channel")
|
|
if !theTrack.CloseCalled {
|
|
playbackDoneChannel <- true
|
|
}
|
|
}()
|
|
|
|
return theTrack, nil
|
|
}
|
|
|
|
func (t *MpvTrack) String() string {
|
|
return fmt.Sprintf("Name: %s, Socket: %s", t.MediaFile.Path, t.IPCSocketName)
|
|
}
|
|
|
|
// Used to control the playback volume. A float value between 0.0 and 1.0.
|
|
func (t *MpvTrack) SetVolume(value float32) {
|
|
// mpv's volume as described in the --volume parameter:
|
|
// Set the startup volume. 0 means silence, 100 means no volume reduction or amplification.
|
|
// Negative values can be passed for compatibility, but are treated as 0.
|
|
log.Debug("request for gain", "gain", value)
|
|
vol := int(value * 100)
|
|
|
|
err := t.Conn.Set("volume", vol)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Debug("set volume", "volume", vol)
|
|
}
|
|
|
|
func (t *MpvTrack) Unpause() {
|
|
err := t.Conn.Set("pause", false)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Info("unpaused track")
|
|
}
|
|
|
|
func (t *MpvTrack) Pause() {
|
|
err := t.Conn.Set("pause", true)
|
|
if err != nil {
|
|
log.Error(err)
|
|
}
|
|
log.Info("paused track")
|
|
}
|
|
|
|
func (t *MpvTrack) Close() {
|
|
log.Debug("closing resources")
|
|
t.CloseCalled = true
|
|
// trying to shutdown mpv process using socket
|
|
if t.isSocketfilePresent() {
|
|
log.Debug("sending shutdown command")
|
|
_, err := t.Conn.Call("quit")
|
|
if err != nil {
|
|
log.Error("error sending quit command to mpv-ipc socket", "error", err)
|
|
|
|
if t.Exe != nil {
|
|
log.Debug("cancelling executor")
|
|
err = t.Exe.Cancel()
|
|
if err != nil {
|
|
log.Error("error canceling executor")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if t.isSocketfilePresent() {
|
|
log.Debug("Removing socketfile", "socketfile", t.IPCSocketName)
|
|
err := os.Remove(t.IPCSocketName)
|
|
if err != nil {
|
|
log.Error("error cleaning up socketfile: ", t.IPCSocketName)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (t *MpvTrack) isSocketfilePresent() bool {
|
|
if len(t.IPCSocketName) < 1 {
|
|
return false
|
|
}
|
|
|
|
fileInfo, err := os.Stat(t.IPCSocketName)
|
|
return err == nil && fileInfo != nil && !fileInfo.IsDir()
|
|
}
|
|
|
|
// Position returns the playback position in seconds
|
|
// every now and then the mpv IPC interface returns "mpv error: property unavailable"
|
|
// in this case we have to retry
|
|
func (t *MpvTrack) Position() int {
|
|
retryCount := 0
|
|
for {
|
|
position, err := t.Conn.Get("time-pos")
|
|
if err != nil && err.Error() == "mpv error: property unavailable" {
|
|
log.Debug("got the mpv error: property unavailable error, retry ...")
|
|
retryCount += 1
|
|
if retryCount > 5 {
|
|
return 0
|
|
}
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error("error getting position in track", "error", err)
|
|
return 0
|
|
}
|
|
|
|
pos, ok := position.(float64)
|
|
if !ok {
|
|
log.Error("could not cast position from mpv into float64")
|
|
return 0
|
|
} else {
|
|
return int(pos)
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (t *MpvTrack) SetPosition(offset int) error {
|
|
pos := t.Position()
|
|
if pos == offset {
|
|
log.Debug("no position difference, skipping operation")
|
|
return nil
|
|
}
|
|
err := t.Conn.Set("time-pos", float64(offset))
|
|
if err != nil {
|
|
log.Error("could not set the position in track", "offset", offset, "error", err)
|
|
return err
|
|
}
|
|
log.Info("set position", "offset", offset)
|
|
return nil
|
|
}
|
|
|
|
func (t *MpvTrack) IsPlaying() bool {
|
|
pausing, err := t.Conn.Get("pause")
|
|
if err != nil {
|
|
log.Error("problem getting paused status", "error", err)
|
|
return false
|
|
}
|
|
|
|
pause, ok := pausing.(bool)
|
|
if !ok {
|
|
log.Error("could not cast pausing to boolean")
|
|
return false
|
|
}
|
|
return !pause
|
|
}
|
|
|
|
func waitForFile(path string, timeout time.Duration, pause time.Duration) error {
|
|
start := time.Now()
|
|
end := start.Add(timeout)
|
|
var retries int = 0
|
|
|
|
for {
|
|
fileInfo, err := os.Stat(path)
|
|
if err == nil && fileInfo != nil && !fileInfo.IsDir() {
|
|
log.Debug("file found", "retries", retries, "waittime", time.Since(start).Microseconds())
|
|
return nil
|
|
}
|
|
if time.Now().After(end) {
|
|
return fmt.Errorf("timeout reached: %s", timeout)
|
|
}
|
|
time.Sleep(pause)
|
|
retries += 1
|
|
}
|
|
}
|