Switch playback to mpv IPC and poll progress
This commit is contained in:
parent
0ba248af4e
commit
1dfab7000b
147
main.go
147
main.go
|
|
@ -3728,23 +3728,10 @@ type playSession struct {
|
|||
muted bool
|
||||
paused bool
|
||||
current float64
|
||||
mu sync.Mutex
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
mpv *mpvClient
|
||||
prog func(float64)
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
var audioCtxGlobal struct {
|
||||
once sync.Once
|
||||
ctx *oto.Context
|
||||
err error
|
||||
}
|
||||
|
||||
func getAudioContext(sampleRate, channels, bytesPerSample int) (*oto.Context, error) {
|
||||
audioCtxGlobal.once.Do(func() {
|
||||
audioCtxGlobal.ctx, audioCtxGlobal.err = oto.NewContext(sampleRate, channels, bytesPerSample, 2048)
|
||||
})
|
||||
return audioCtxGlobal.ctx, audioCtxGlobal.err
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, prog func(float64), img *canvas.Image) *playSession {
|
||||
|
|
@ -3753,53 +3740,56 @@ func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, pr
|
|||
fmt.Printf("═══════════════════════════════════════════════════════\n")
|
||||
fmt.Printf("📁 Video: %s\n", filepath.Base(path))
|
||||
fmt.Printf("📐 Source: %dx%d @ %.2f fps\n", w, h, fps)
|
||||
fmt.Printf("🎯 Playback via ffplay subprocess\n")
|
||||
fmt.Printf("🎯 Playback via mpv subprocess\n")
|
||||
fmt.Printf("═══════════════════════════════════════════════════════\n\n")
|
||||
|
||||
// Check if video file exists
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
fmt.Printf("❌ ERROR: Video file does not exist: %s\n", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, err := exec.LookPath("ffplay"); err != nil {
|
||||
fmt.Printf("❌ ERROR: ffplay not found in PATH: %v\n", err)
|
||||
if _, err := exec.LookPath("mpv"); err != nil {
|
||||
fmt.Printf("❌ ERROR: mpv not found in PATH: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("✅ Play session created successfully\n")
|
||||
return &playSession{
|
||||
ps := &playSession{
|
||||
path: path,
|
||||
fps: fps,
|
||||
volume: 100,
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
mpv: newMPVClient(),
|
||||
}
|
||||
if err := ps.mpv.EnsureRunning(); err != nil {
|
||||
fmt.Printf("❌ ERROR: failed to start mpv: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
if err := ps.mpv.LoadFile(path); err != nil {
|
||||
fmt.Printf("❌ ERROR: mpv load failed: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
_ = ps.mpv.SetVolume(ps.volume)
|
||||
ps.startProgressPoll()
|
||||
fmt.Printf("✅ Play session created successfully\n")
|
||||
return ps
|
||||
}
|
||||
|
||||
func (p *playSession) Play() {
|
||||
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.cmd == nil {
|
||||
fmt.Printf("⚡ Starting ffplay subprocess...\n")
|
||||
p.startFFplayLocked(p.current)
|
||||
return
|
||||
}
|
||||
// Toggle pause/resume by sending "p" to stdin
|
||||
if p.stdin != nil {
|
||||
_, _ = p.stdin.Write([]byte("p"))
|
||||
p.paused = !p.paused
|
||||
fmt.Printf("⏯️ Toggled pause -> %v\n", p.paused)
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Play()
|
||||
p.paused = false
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playSession) Pause() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.stdin != nil {
|
||||
_, _ = p.stdin.Write([]byte("p"))
|
||||
p.paused = !p.paused
|
||||
fmt.Printf("⏸️ Pause toggle -> %v\n", p.paused)
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Pause()
|
||||
p.paused = true
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3810,32 +3800,16 @@ func (p *playSession) Seek(offset float64) {
|
|||
offset = 0
|
||||
}
|
||||
p.current = offset
|
||||
p.stopLocked()
|
||||
p.startFFplayLocked(p.current)
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Seek(p.current)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playSession) SetVolume(v float64) {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if v < 0 {
|
||||
v = 0
|
||||
}
|
||||
if v > 100 {
|
||||
v = 100
|
||||
}
|
||||
p.volume = v
|
||||
if v > 0 {
|
||||
p.muted = false
|
||||
} else {
|
||||
p.muted = true
|
||||
}
|
||||
// Runtime volume changes are not pushed to ffplay; a restart or pause toggle will pick it up.
|
||||
}
|
||||
func (p *playSession) SetVolume(v float64) {}
|
||||
|
||||
// StepFrame steps forward or backward by one frame
|
||||
// direction: 1 for forward, -1 for backward
|
||||
func (p *playSession) StepFrame(direction int) {
|
||||
// Approximate by seeking one frame forward/backward
|
||||
step := 1.0 / math.Max(p.fps, 24)
|
||||
p.Seek(p.current + float64(direction)*step)
|
||||
}
|
||||
|
|
@ -3844,6 +3818,9 @@ func (p *playSession) StepFrame(direction int) {
|
|||
func (p *playSession) GetCurrentPosition() float64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.mpv != nil {
|
||||
return p.mpv.Position()
|
||||
}
|
||||
return p.current
|
||||
}
|
||||
|
||||
|
|
@ -3854,49 +3831,33 @@ func (p *playSession) Stop() {
|
|||
}
|
||||
|
||||
func (p *playSession) stopLocked() {
|
||||
if p.cmd != nil && p.cmd.Process != nil {
|
||||
_ = p.cmd.Process.Kill()
|
||||
_, _ = p.cmd.Process.Wait()
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Quit()
|
||||
p.mpv = nil
|
||||
}
|
||||
p.cmd = nil
|
||||
p.stdin = nil
|
||||
close(p.done)
|
||||
p.done = make(chan struct{})
|
||||
}
|
||||
|
||||
func (p *playSession) startFFplayLocked(offset float64) {
|
||||
var stderr bytes.Buffer
|
||||
args := []string{
|
||||
"-hide_banner",
|
||||
"-loglevel", "warning",
|
||||
"-autoexit",
|
||||
"-ss", fmt.Sprintf("%.3f", offset),
|
||||
"-i", p.path,
|
||||
"-window_title", "VT Player",
|
||||
}
|
||||
if p.volume > 0 {
|
||||
args = append([]string{"-volume", fmt.Sprintf("%d", int(p.volume))}, args...)
|
||||
}
|
||||
cmd := exec.Command("ffplay", args...)
|
||||
cmd.Stderr = &stderr
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ ERROR: failed to create ffplay stdin: %v\n", err)
|
||||
func (p *playSession) startProgressPoll() {
|
||||
if p.prog == nil {
|
||||
return
|
||||
}
|
||||
cmd.Stdout = os.Stdout
|
||||
if err := cmd.Start(); err != nil {
|
||||
fmt.Printf("❌ ERROR: ffplay failed to start: %v\n", err)
|
||||
if msg := strings.TrimSpace(stderr.String()); msg != "" {
|
||||
fmt.Printf(" ffplay stderr: %s\n", msg)
|
||||
}
|
||||
return
|
||||
}
|
||||
p.cmd = cmd
|
||||
p.stdin = stdin
|
||||
p.paused = false
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
close(p.done)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pos := p.GetCurrentPosition()
|
||||
p.mu.Lock()
|
||||
p.current = pos
|
||||
p.mu.Unlock()
|
||||
p.prog(pos)
|
||||
case <-p.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
|
|
|||
175
mpv_client.go
Normal file
175
mpv_client.go
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user