package main import ( "fmt" "log" "path/filepath" "time" "git.leaktechnologies.dev/stu/VT_Player/player/mpvembed" "github.com/gotk3/gotk3/gdk" "github.com/gotk3/gotk3/glib" "github.com/gotk3/gotk3/gtk" ) type pane struct { area *gtk.DrawingArea mpv *mpvembed.Client path string } func main() { gtk.Init(nil) win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL) if err != nil { log.Fatalf("window: %v", err) } win.SetTitle("VT Player (GTK/mpv)") win.SetDefaultSize(1400, 800) grid, _ := gtk.GridNew() grid.SetColumnHomogeneous(true) grid.SetRowHomogeneous(false) win.Add(grid) // Two panes for compare; left/right left := newPane() right := newPane() controls := buildControls(win, left, right) grid.Attach(controls, 0, 0, 2, 1) grid.Attach(left.area, 0, 1, 1, 1) grid.Attach(right.area, 1, 1, 1, 1) win.Connect("destroy", func() { if left.mpv != nil { left.mpv.Destroy() } if right.mpv != nil { right.mpv.Destroy() } gtk.MainQuit() }) win.ShowAll() gtk.Main() } func newPane() *pane { da, _ := gtk.DrawingAreaNew() da.SetHExpand(true) da.SetVExpand(true) p := &pane{area: da} da.Connect("realize", func() { if p.mpv != nil { return } mpv, err := mpvembed.New() if err != nil { log.Printf("mpv create: %v", err) return } p.mpv = mpv if w, ok := da.GetWindow(); ok && w != nil { if xid := getWindowID(w); xid != 0 { _ = mpv.SetWID(xid) } } _ = mpv.SetOptionString("pause", "yes") if err := mpv.Initialize(); err != nil { log.Printf("mpv init: %v", err) } }) return p } func buildControls(win *gtk.Window, left, right *pane) *gtk.Box { box, _ := gtk.BoxNew(gtk.ORIENTATION_VERTICAL, 6) row1, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) row2, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) row3, _ := gtk.BoxNew(gtk.ORIENTATION_HORIZONTAL, 4) openL, _ := gtk.ButtonNewWithLabel("Open Left") openR, _ := gtk.ButtonNewWithLabel("Open Right") play, _ := gtk.ButtonNewWithLabel("Play Both") pause, _ := gtk.ButtonNewWithLabel("Pause Both") seek0, _ := gtk.ButtonNewWithLabel("Seek 0") stepF, _ := gtk.ButtonNewWithLabel("Step +1f") stepB, _ := gtk.ButtonNewWithLabel("Step -1f") info, _ := gtk.LabelNew("Meta: -") openL.Connect("clicked", func() { chooseAndLoad(win, left) }) openR.Connect("clicked", func() { chooseAndLoad(win, right) }) play.Connect("clicked", func() { if left.mpv != nil { _ = left.mpv.SetPropertyBool("pause", false) } if right.mpv != nil { _ = right.mpv.SetPropertyBool("pause", false) } }) pause.Connect("clicked", func() { if left.mpv != nil { _ = left.mpv.SetPropertyBool("pause", true) } if right.mpv != nil { _ = right.mpv.SetPropertyBool("pause", true) } }) seek0.Connect("clicked", func() { if left.mpv != nil { _ = left.mpv.Command("seek", "0", "absolute", "exact") } if right.mpv != nil { _ = right.mpv.Command("seek", "0", "absolute", "exact") } }) stepF.Connect("clicked", func() { if left.mpv != nil { _ = left.mpv.Command("frame-step") } if right.mpv != nil { _ = right.mpv.Command("frame-step") } }) stepB.Connect("clicked", func() { if left.mpv != nil { _ = left.mpv.Command("frame-back-step") } if right.mpv != nil { _ = right.mpv.Command("frame-back-step") } }) // Poll meta/progress go func() { t := time.NewTicker(500 * time.Millisecond) defer t.Stop() for range t.C { text := metaSummary(left, right) glib.IdleAdd(func() { info.SetText(text) }) } }() row1.PackStart(openL, false, false, 0) row1.PackStart(openR, false, false, 0) row1.PackStart(play, false, false, 0) row1.PackStart(pause, false, false, 0) row1.PackStart(seek0, false, false, 0) row1.PackStart(stepB, false, false, 0) row1.PackStart(stepF, false, false, 0) row2.PackStart(info, false, false, 0) box.PackStart(row1, false, false, 0) box.PackStart(row2, false, false, 0) box.PackStart(row3, false, false, 0) return box } func chooseAndLoad(win *gtk.Window, p *pane) { dlg, _ := gtk.FileChooserDialogNewWith1Button("Open Video", win, gtk.FILE_CHOOSER_ACTION_OPEN, "Open", gtk.RESPONSE_ACCEPT) if resp := dlg.Run(); resp == gtk.RESPONSE_ACCEPT { filename := dlg.GetFilename() if filename != "" { loadIntoPane(p, filename) } } dlg.Destroy() } func loadIntoPane(p *pane, filename string) { if p.mpv == nil { return } p.path = filename if err := p.mpv.Command("loadfile", filename, "replace"); err != nil { log.Printf("loadfile %s: %v", filename, err) } _ = p.mpv.SetPropertyBool("pause", false) } func metaSummary(a, b *pane) string { parts := func(p *pane) string { if p == nil || p.mpv == nil || p.path == "" { return "[empty]" } dur, _ := p.mpv.GetPropertyDouble("duration") pos, _ := p.mpv.GetPropertyDouble("time-pos") w, _ := p.mpv.GetPropertyInt64("width") h, _ := p.mpv.GetPropertyInt64("height") return fmt.Sprintf("%s | %dx%d | %.1f/%.1fs", filepath.Base(p.path), w, h, pos, dur) } return fmt.Sprintf("L: %s | R: %s", parts(a), parts(b)) } // getWindowID returns the native window handle (XID on X11, HWND on Windows). func getWindowID(w *gdk.Window) uint64 { // X11 if xid := gdkWindowGetXID(w); xid != 0 { return uint64(xid) } // TODO: add Windows handle if needed. return 0 } // gdkWindowGetXID extracts the XID from a GDK window when running on X11. func gdkWindowGetXID(w *gdk.Window) uint { type xidGetter interface { GetXID() uint } if xw, ok := w.(xidGetter); ok { return xw.GetXID() } return 0 }