Fix GTK/mpv player build issues (imports, window ID, polling)

This commit is contained in:
Stu 2025-12-13 21:39:04 -05:00
parent 26c48ab981
commit ba1db9e16f
2 changed files with 199 additions and 88 deletions

View File

@ -1,23 +1,25 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"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"
)
// simple GTK + mpv embedded player demo (single pane).
type pane struct {
area *gtk.DrawingArea
mpv *mpvembed.Client
path string
}
func main() {
flag.Parse()
gtk.Init(nil)
win, err := gtk.WindowNew(gtk.WINDOW_TOPLEVEL)
@ -25,88 +27,30 @@ func main() {
log.Fatalf("window: %v", err)
}
win.SetTitle("VT Player (GTK/mpv)")
win.SetDefaultSize(1280, 720)
win.SetDefaultSize(1400, 800)
grid, _ := gtk.GridNew()
grid.SetColumnHomogeneous(true)
grid.SetRowHomogeneous(false)
win.Add(grid)
// Drawing area for mpv
da, _ := gtk.DrawingAreaNew()
da.SetHexpand(true)
da.SetVexpand(true)
grid.Attach(da, 0, 1, 4, 1)
// Two panes for compare; left/right
left := newPane()
right := newPane()
// Controls
openBtn, _ := gtk.ButtonNewWithLabel("Open...")
playBtn, _ := gtk.ButtonNewWithLabel("Play")
pauseBtn, _ := gtk.ButtonNewWithLabel("Pause")
seekStartBtn, _ := gtk.ButtonNewWithLabel("<< Start")
grid.Attach(openBtn, 0, 0, 1, 1)
grid.Attach(playBtn, 1, 0, 1, 1)
grid.Attach(pauseBtn, 2, 0, 1, 1)
grid.Attach(seekStartBtn, 3, 0, 1, 1)
controls := buildControls(win, left, right)
grid.Attach(controls, 0, 0, 2, 1)
mpv, err := mpvembed.New()
if err != nil {
log.Fatalf("mpv: %v", err)
}
defer mpv.Destroy()
var duration float64
da.Connect("realize", func() {
w := da.GetWindow()
if w == nil {
log.Println("no window yet")
return
}
// Bind native window ID
if xid := getWindowID(w); xid != 0 {
_ = mpv.SetWID(xid)
}
// Set options and init
_ = mpv.SetOptionString("pause", "yes")
if err := mpv.Initialize(); err != nil {
log.Fatalf("mpv init: %v", err)
}
})
openBtn.Connect("clicked", func() {
dlg, _ := gtk.FileChooserDialogNewWith1Button("Open Video", win, gtk.FILE_CHOOSER_ACTION_OPEN, "Open", gtk.RESPONSE_ACCEPT)
if resp := dlg.Run(); resp == int(gtk.RESPONSE_ACCEPT) {
filename := dlg.GetFilename()
if filename != "" {
loadFile(mpv, filename, &duration)
}
}
dlg.Destroy()
})
playBtn.Connect("clicked", func() {
_ = mpv.SetPropertyBool("pause", false)
})
pauseBtn.Connect("clicked", func() {
_ = mpv.SetPropertyBool("pause", true)
})
seekStartBtn.Connect("clicked", func() {
_ = mpv.Command("seek", "0", "absolute", "exact")
})
// Progress poll
go func() {
t := time.NewTicker(250 * time.Millisecond)
defer t.Stop()
for range t.C {
pos, err := mpv.GetPropertyDouble("time-pos")
if err == nil && duration > 0 {
fmt.Printf("\r%.1f / %.1f", pos, duration)
}
}
}()
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()
})
@ -114,36 +58,172 @@ func main() {
gtk.Main()
}
func loadFile(mpv *mpvembed.Client, path string, duration *float64) {
base := filepath.Base(path)
fmt.Printf("\nLoading: %s\n", base)
if err := mpv.Command("loadfile", path, "replace"); err != nil {
log.Printf("loadfile: %v", err)
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)
}
}
if d, err := mpv.GetPropertyDouble("duration"); err == nil {
*duration = d
dlg.Destroy()
}
func loadIntoPane(p *pane, filename string) {
if p.mpv == nil {
return
}
_ = mpv.SetPropertyBool("pause", false)
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 xid
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) uint64 {
func gdkWindowGetXID(w *gdk.Window) uint {
type xidGetter interface {
GetXID() uint
}
if xw, ok := w.(xidGetter); ok {
return uint64(xw.GetXID())
return xw.GetXID()
}
return 0
}

View File

@ -144,3 +144,34 @@ func (c *Client) GetPropertyDouble(name string) (float64, error) {
}
return float64(out), nil
}
// GetPropertyInt64 gets an int64 property.
func (c *Client) GetPropertyInt64(name string) (int64, error) {
if c.handle == nil {
return 0, errors.New("mpv handle is nil")
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
var out C.longlong
if res := C.mpv_get_property(c.handle, cname, C.mpv_format(C.MPV_FORMAT_INT64), unsafe.Pointer(&out)); res < 0 {
return 0, fmt.Errorf("mpv_get_property %s failed: %s", name, C.GoString(C.mpv_errstr(res)))
}
return int64(out), nil
}
// GetPropertyString gets a string property.
func (c *Client) GetPropertyString(name string) (string, error) {
if c.handle == nil {
return "", errors.New("mpv handle is nil")
}
cname := C.CString(name)
defer C.free(unsafe.Pointer(cname))
var out *C.char
if res := C.mpv_get_property(c.handle, cname, C.mpv_format(C.MPV_FORMAT_STRING), unsafe.Pointer(&out)); res < 0 {
return "", fmt.Errorf("mpv_get_property %s failed: %s", name, C.GoString(C.mpv_errstr(res)))
}
if out == nil {
return "", nil
}
return C.GoString(out), nil
}