VT_Player/mpv_client.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
}