176 lines
3.7 KiB
Go
176 lines
3.7 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// mpvClient manages a single mpv process via IPC.
|
|
type mpvClient struct {
|
|
cmd *exec.Cmd
|
|
sockPath string
|
|
conn net.Conn
|
|
enc *json.Encoder
|
|
mu sync.Mutex
|
|
quitOnce sync.Once
|
|
}
|
|
|
|
func newMPVClient() *mpvClient {
|
|
sock := filepath.Join(os.TempDir(), fmt.Sprintf("vtplayer-mpv-%d.sock", time.Now().UnixNano()))
|
|
return &mpvClient{sockPath: sock}
|
|
}
|
|
|
|
func (m *mpvClient) EnsureRunning() error {
|
|
m.mu.Lock()
|
|
running := m.cmd != nil && m.conn != nil
|
|
m.mu.Unlock()
|
|
if running {
|
|
return nil
|
|
}
|
|
if _, err := exec.LookPath("mpv"); err != nil {
|
|
return fmt.Errorf("mpv not found in PATH: %w", err)
|
|
}
|
|
|
|
// Clean old socket if exists
|
|
_ = os.Remove(m.sockPath)
|
|
|
|
args := []string{
|
|
"--input-ipc-server=" + m.sockPath,
|
|
"--idle=yes",
|
|
"--force-window=yes",
|
|
"--keep-open=yes",
|
|
"--no-terminal",
|
|
"--pause",
|
|
}
|
|
cmd := exec.Command("mpv", args...)
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
if err := cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start mpv: %w", err)
|
|
}
|
|
|
|
// Wait for socket to appear and connect
|
|
deadline := time.Now().Add(3 * time.Second)
|
|
var conn net.Conn
|
|
for time.Now().Before(deadline) {
|
|
c, err := net.Dial("unix", m.sockPath)
|
|
if err == nil {
|
|
conn = c
|
|
break
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
if conn == nil {
|
|
_ = cmd.Process.Kill()
|
|
return fmt.Errorf("mpv IPC socket not available")
|
|
}
|
|
|
|
m.mu.Lock()
|
|
m.cmd = cmd
|
|
m.conn = conn
|
|
m.enc = json.NewEncoder(conn)
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (m *mpvClient) sendCommand(cmd []interface{}) error {
|
|
m.mu.Lock()
|
|
enc := m.enc
|
|
conn := m.conn
|
|
m.mu.Unlock()
|
|
if enc == nil || conn == nil {
|
|
return fmt.Errorf("mpv not connected")
|
|
}
|
|
payload := map[string]interface{}{"command": cmd}
|
|
return enc.Encode(payload)
|
|
}
|
|
|
|
func (m *mpvClient) LoadFile(path string) error {
|
|
if err := m.EnsureRunning(); err != nil {
|
|
return err
|
|
}
|
|
return m.sendCommand([]interface{}{"loadfile", path, "replace"})
|
|
}
|
|
|
|
func (m *mpvClient) Play() error {
|
|
if err := m.EnsureRunning(); err != nil {
|
|
return err
|
|
}
|
|
return m.sendCommand([]interface{}{"set_property", "pause", false})
|
|
}
|
|
|
|
func (m *mpvClient) Pause() error {
|
|
if err := m.EnsureRunning(); err != nil {
|
|
return err
|
|
}
|
|
return m.sendCommand([]interface{}{"set_property", "pause", true})
|
|
}
|
|
|
|
func (m *mpvClient) Seek(seconds float64) error {
|
|
if err := m.EnsureRunning(); err != nil {
|
|
return err
|
|
}
|
|
return m.sendCommand([]interface{}{"seek", seconds, "absolute"})
|
|
}
|
|
|
|
func (m *mpvClient) SetVolume(vol float64) error {
|
|
if err := m.EnsureRunning(); err != nil {
|
|
return err
|
|
}
|
|
return m.sendCommand([]interface{}{"set_property", "volume", vol})
|
|
}
|
|
|
|
func (m *mpvClient) Position() float64 {
|
|
// Query synchronously by opening a short connection; mpv IPC replies on same socket.
|
|
// For simplicity here, we return 0 if it fails.
|
|
m.mu.Lock()
|
|
conn := m.conn
|
|
m.mu.Unlock()
|
|
if conn == nil {
|
|
return 0
|
|
}
|
|
// Make a temporary connection to avoid racing on the encoder
|
|
c, err := net.Dial("unix", m.sockPath)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
defer c.Close()
|
|
dec := json.NewDecoder(c)
|
|
enc := json.NewEncoder(c)
|
|
_ = enc.Encode(map[string]interface{}{"command": []interface{}{"get_property", "time-pos"}})
|
|
var resp map[string]interface{}
|
|
if err := dec.Decode(&resp); err != nil {
|
|
return 0
|
|
}
|
|
if v, ok := resp["data"].(float64); ok {
|
|
return v
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (m *mpvClient) Quit() error {
|
|
var err error
|
|
m.quitOnce.Do(func() {
|
|
_ = m.sendCommand([]interface{}{"quit"})
|
|
m.mu.Lock()
|
|
if m.conn != nil {
|
|
_ = m.conn.Close()
|
|
m.conn = nil
|
|
}
|
|
if m.cmd != nil && m.cmd.Process != nil {
|
|
_ = m.cmd.Process.Kill()
|
|
}
|
|
m.cmd = nil
|
|
m.enc = nil
|
|
m.mu.Unlock()
|
|
_ = os.Remove(m.sockPath)
|
|
})
|
|
return err
|
|
}
|