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 }