forked from Leak_Technologies/VideoTools
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
|
muted bool
|
||||||
paused bool
|
paused bool
|
||||||
current float64
|
current float64
|
||||||
mu sync.Mutex
|
mpv *mpvClient
|
||||||
cmd *exec.Cmd
|
prog func(float64)
|
||||||
stdin io.WriteCloser
|
|
||||||
done chan struct{}
|
done chan struct{}
|
||||||
}
|
mu sync.Mutex
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, prog func(float64), img *canvas.Image) *playSession {
|
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("═══════════════════════════════════════════════════════\n")
|
||||||
fmt.Printf("📁 Video: %s\n", filepath.Base(path))
|
fmt.Printf("📁 Video: %s\n", filepath.Base(path))
|
||||||
fmt.Printf("📐 Source: %dx%d @ %.2f fps\n", w, h, fps)
|
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")
|
fmt.Printf("═══════════════════════════════════════════════════════\n\n")
|
||||||
|
|
||||||
// Check if video file exists
|
|
||||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||||
fmt.Printf("❌ ERROR: Video file does not exist: %s\n", path)
|
fmt.Printf("❌ ERROR: Video file does not exist: %s\n", path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if _, err := exec.LookPath("mpv"); err != nil {
|
||||||
if _, err := exec.LookPath("ffplay"); err != nil {
|
fmt.Printf("❌ ERROR: mpv not found in PATH: %v\n", err)
|
||||||
fmt.Printf("❌ ERROR: ffplay not found in PATH: %v\n", err)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("✅ Play session created successfully\n")
|
ps := &playSession{
|
||||||
return &playSession{
|
|
||||||
path: path,
|
path: path,
|
||||||
fps: fps,
|
fps: fps,
|
||||||
volume: 100,
|
volume: 100,
|
||||||
done: make(chan struct{}),
|
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() {
|
func (p *playSession) Play() {
|
||||||
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
if p.cmd == nil {
|
if p.mpv != nil {
|
||||||
fmt.Printf("⚡ Starting ffplay subprocess...\n")
|
_ = p.mpv.Play()
|
||||||
p.startFFplayLocked(p.current)
|
p.paused = false
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) Pause() {
|
func (p *playSession) Pause() {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
if p.stdin != nil {
|
if p.mpv != nil {
|
||||||
_, _ = p.stdin.Write([]byte("p"))
|
_ = p.mpv.Pause()
|
||||||
p.paused = !p.paused
|
p.paused = true
|
||||||
fmt.Printf("⏸️ Pause toggle -> %v\n", p.paused)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3810,32 +3800,16 @@ func (p *playSession) Seek(offset float64) {
|
||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
p.current = offset
|
p.current = offset
|
||||||
p.stopLocked()
|
if p.mpv != nil {
|
||||||
p.startFFplayLocked(p.current)
|
_ = p.mpv.Seek(p.current)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) SetVolume(v float64) {
|
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.
|
|
||||||
}
|
|
||||||
|
|
||||||
// StepFrame steps forward or backward by one frame
|
// StepFrame steps forward or backward by one frame
|
||||||
// direction: 1 for forward, -1 for backward
|
// direction: 1 for forward, -1 for backward
|
||||||
func (p *playSession) StepFrame(direction int) {
|
func (p *playSession) StepFrame(direction int) {
|
||||||
// Approximate by seeking one frame forward/backward
|
|
||||||
step := 1.0 / math.Max(p.fps, 24)
|
step := 1.0 / math.Max(p.fps, 24)
|
||||||
p.Seek(p.current + float64(direction)*step)
|
p.Seek(p.current + float64(direction)*step)
|
||||||
}
|
}
|
||||||
|
|
@ -3844,6 +3818,9 @@ func (p *playSession) StepFrame(direction int) {
|
||||||
func (p *playSession) GetCurrentPosition() float64 {
|
func (p *playSession) GetCurrentPosition() float64 {
|
||||||
p.mu.Lock()
|
p.mu.Lock()
|
||||||
defer p.mu.Unlock()
|
defer p.mu.Unlock()
|
||||||
|
if p.mpv != nil {
|
||||||
|
return p.mpv.Position()
|
||||||
|
}
|
||||||
return p.current
|
return p.current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3854,49 +3831,33 @@ func (p *playSession) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) stopLocked() {
|
func (p *playSession) stopLocked() {
|
||||||
if p.cmd != nil && p.cmd.Process != nil {
|
if p.mpv != nil {
|
||||||
_ = p.cmd.Process.Kill()
|
_ = p.mpv.Quit()
|
||||||
_, _ = p.cmd.Process.Wait()
|
p.mpv = nil
|
||||||
}
|
}
|
||||||
p.cmd = nil
|
close(p.done)
|
||||||
p.stdin = nil
|
|
||||||
p.done = make(chan struct{})
|
p.done = make(chan struct{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *playSession) startFFplayLocked(offset float64) {
|
func (p *playSession) startProgressPoll() {
|
||||||
var stderr bytes.Buffer
|
if p.prog == nil {
|
||||||
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)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cmd.Stdout = os.Stdout
|
ticker := time.NewTicker(250 * time.Millisecond)
|
||||||
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
|
|
||||||
go func() {
|
go func() {
|
||||||
_ = cmd.Wait()
|
defer ticker.Stop()
|
||||||
close(p.done)
|
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