diff --git a/cmd/gtkplayer/main.go b/cmd/gtkplayer/main.go new file mode 100644 index 0000000..2eb9027 --- /dev/null +++ b/cmd/gtkplayer/main.go @@ -0,0 +1,149 @@ +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/gtk" +) + +// simple GTK + mpv embedded player demo (single pane). + +func main() { + flag.Parse() + 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(1280, 720) + + 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) + + // 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) + + 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) + } + } + }() + + win.Connect("destroy", func() { + gtk.MainQuit() + }) + + win.ShowAll() + 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) + } + if d, err := mpv.GetPropertyDouble("duration"); err == nil { + *duration = d + } + _ = mpv.SetPropertyBool("pause", false) +} + +// 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 + } + // 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 { + type xidGetter interface { + GetXID() uint + } + if xw, ok := w.(xidGetter); ok { + return uint64(xw.GetXID()) + } + return 0 +} + diff --git a/go.mod b/go.mod index faeffe1..e424aab 100644 --- a/go.mod +++ b/go.mod @@ -4,9 +4,12 @@ go 1.25.1 require ( fyne.io/fyne/v2 v2.7.1 + github.com/gotk3/gotk3 v0.6.4 github.com/hajimehoshi/oto v0.7.1 ) +replace github.com/gotk3/gotk3 => ./third_party/gotk3 + require ( fyne.io/systray v1.11.1-0.20250603113521-ca66a66d8b58 // indirect github.com/BurntSushi/toml v1.5.0 // indirect diff --git a/go.sum b/go.sum index 67cedb2..4d2c77a 100644 --- a/go.sum +++ b/go.sum @@ -35,6 +35,8 @@ github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y= github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= +github.com/gotk3/gotk3 v0.6.4 h1:5ur/PRr86PwCG8eSj98D1eXvhrNNK6GILS2zq779dCg= +github.com/gotk3/gotk3 v0.6.4/go.mod h1:/hqFpkNa9T3JgNAE2fLvCdov7c5bw//FHNZrZ3Uv9/Q= github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A= github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0= github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8= diff --git a/player/mpvembed/mpv.go b/player/mpvembed/mpv.go new file mode 100644 index 0000000..9b2a48c --- /dev/null +++ b/player/mpvembed/mpv.go @@ -0,0 +1,146 @@ +package mpvembed + +/* +#cgo pkg-config: mpv +#include +#include + +static inline const char* mpv_errstr(int err) { return mpv_error_string(err); } +*/ +import "C" +import ( + "errors" + "fmt" + "unsafe" +) + +// Client wraps a libmpv handle. +type Client struct { + handle *C.mpv_handle +} + +// New creates a new mpv client. +func New() (*Client, error) { + h := C.mpv_create() + if h == nil { + return nil, errors.New("mpv_create returned nil") + } + return &Client{handle: h}, nil +} + +// Initialize must be called before issuing commands. +func (c *Client) Initialize() error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + if res := C.mpv_initialize(c.handle); res < 0 { + return fmt.Errorf("mpv_initialize failed: %s", C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// Destroy terminates and frees the client. +func (c *Client) Destroy() { + if c.handle != nil { + C.mpv_terminate_destroy(c.handle) + c.handle = nil + } +} + +// SetOptionString sets an option before initialize. +func (c *Client) SetOptionString(name, value string) error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + cval := C.CString(value) + defer C.free(unsafe.Pointer(cval)) + if res := C.mpv_set_option_string(c.handle, cname, cval); res < 0 { + return fmt.Errorf("mpv_set_option_string %s failed: %s", name, C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// SetOptionInt sets an integer option. +func (c *Client) SetOptionInt(name string, val int64) error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + cval := C.longlong(val) + if res := C.mpv_set_option(c.handle, cname, C.mpv_format(C.MPV_FORMAT_INT64), unsafe.Pointer(&cval)); res < 0 { + return fmt.Errorf("mpv_set_option %s failed: %s", name, C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// SetWID binds a native window ID to mpv (for embedding). +func (c *Client) SetWID(wid uint64) error { + return c.SetOptionInt("wid", int64(wid)) +} + +// Command issues an mpv command with arguments. +func (c *Client) Command(args ...string) error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + // Build a NULL-terminated array of *char + cargs := make([]*C.char, len(args)+1) + for i, a := range args { + cstr := C.CString(a) + defer C.free(unsafe.Pointer(cstr)) + cargs[i] = cstr + } + cargs[len(args)] = nil + if res := C.mpv_command(c.handle, &cargs[0]); res < 0 { + return fmt.Errorf("mpv_command %v failed: %s", args, C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// SetPropertyBool sets a boolean property. +func (c *Client) SetPropertyBool(name string, v bool) error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + cval := C.int(0) + if v { + cval = 1 + } + if res := C.mpv_set_property(c.handle, cname, C.mpv_format(C.MPV_FORMAT_FLAG), unsafe.Pointer(&cval)); res < 0 { + return fmt.Errorf("mpv_set_property %s failed: %s", name, C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// SetPropertyDouble sets a double property. +func (c *Client) SetPropertyDouble(name string, v float64) error { + if c.handle == nil { + return errors.New("mpv handle is nil") + } + cname := C.CString(name) + defer C.free(unsafe.Pointer(cname)) + cval := C.double(v) + if res := C.mpv_set_property(c.handle, cname, C.mpv_format(C.MPV_FORMAT_DOUBLE), unsafe.Pointer(&cval)); res < 0 { + return fmt.Errorf("mpv_set_property %s failed: %s", name, C.GoString(C.mpv_errstr(res))) + } + return nil +} + +// GetPropertyDouble gets a double property. +func (c *Client) GetPropertyDouble(name string) (float64, 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.double + if res := C.mpv_get_property(c.handle, cname, C.mpv_format(C.MPV_FORMAT_DOUBLE), unsafe.Pointer(&out)); res < 0 { + return 0, fmt.Errorf("mpv_get_property %s failed: %s", name, C.GoString(C.mpv_errstr(res))) + } + return float64(out), nil +} diff --git a/third_party/gotk3/gdk/gdk_since_3_22.go b/third_party/gotk3/gdk/gdk_since_3_22.go new file mode 100644 index 0000000..ca27b5b --- /dev/null +++ b/third_party/gotk3/gdk/gdk_since_3_22.go @@ -0,0 +1,305 @@ +// +build !gtk_3_6,!gtk_3_8,!gtk_3_10,!gtk_3_12,!gtk_3_14,!gtk_3_16,!gtk_3_18,!gtk_3_20 +// Supports building with gtk 3.22+ + +// Copyright (c) 2013-2014 Conformal Systems +// +// This file originated from: http://opensource.conformal.com/ +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package gdk + +// #include +// #include "gdk_since_3_22.go.h" +import "C" +import ( + "unsafe" + + "github.com/gotk3/gotk3/glib" + "github.com/gotk3/gotk3/internal/callback" +) + +func init() { + + tm := []glib.TypeMarshaler{ + {glib.Type(C.gdk_subpixel_layout_get_type()), marshalSubpixelLayout}, + } + + glib.RegisterGValueMarshalers(tm) +} + +/* + * Constants + */ + +// SeatCapabilities is a representation of GDK's GdkSeatCapabilities +type SeatCapabilities int + +const ( + SEAT_CAPABILITY_NONE SeatCapabilities = C.GDK_SEAT_CAPABILITY_NONE + SEAT_CAPABILITY_POINTER SeatCapabilities = C.GDK_SEAT_CAPABILITY_POINTER + SEAT_CAPABILITY_TOUCH SeatCapabilities = C.GDK_SEAT_CAPABILITY_TOUCH + SEAT_CAPABILITY_TABLET_STYLUS SeatCapabilities = C.GDK_SEAT_CAPABILITY_TABLET_STYLUS + SEAT_CAPABILITY_KEYBOARD SeatCapabilities = C.GDK_SEAT_CAPABILITY_KEYBOARD + SEAT_CAPABILITY_ALL_POINTING SeatCapabilities = C.GDK_SEAT_CAPABILITY_ALL_POINTING + SEAT_CAPABILITY_ALL SeatCapabilities = C.GDK_SEAT_CAPABILITY_ALL +) + +func marshalSeatCapabilitiesLayout(p uintptr) (interface{}, error) { + c := C.g_value_get_enum((*C.GValue)(unsafe.Pointer(p))) + return SeatCapabilities(c), nil +} + +// SubpixelLayout is a representation of GDK's GdkSubpixelLayout. +type SubpixelLayout int + +const ( + SUBPIXEL_LAYOUT_UNKNOWN SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_UNKNOWN + SUBPIXEL_LAYOUT_NONE SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_NONE + SUBPIXEL_LAYOUT_HORIZONTAL_RGB SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_HORIZONTAL_RGB + SUBPIXEL_LAYOUT_HORIZONTAL_BGR SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_HORIZONTAL_BGR + SUBPIXEL_LAYOUT_VERTICAL_RGB SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_VERTICAL_RGB + SUBPIXEL_LAYOUT_VERTICAL_BGR SubpixelLayout = C.GDK_SUBPIXEL_LAYOUT_VERTICAL_BGR +) + +func marshalSubpixelLayout(p uintptr) (interface{}, error) { + c := C.g_value_get_enum((*C.GValue)(unsafe.Pointer(p))) + return SubpixelLayout(c), nil +} + +/* + * GdkDisplay + */ + +// GetNMonitors is a wrapper around gdk_display_get_n_monitors(). +func (v *Display) GetNMonitors() int { + c := C.gdk_display_get_n_monitors(v.native()) + return int(c) +} + +// GetPrimaryMonitor is a wrapper around gdk_display_get_primary_monitor(). +func (v *Display) GetPrimaryMonitor() (*Monitor, error) { + c := C.gdk_display_get_primary_monitor(v.native()) + if c == nil { + return nil, nilPtrErr + } + + return &Monitor{glib.Take(unsafe.Pointer(c))}, nil +} + +// GetMonitor is a wrapper around gdk_display_get_monitor(). +func (v *Display) GetMonitor(num int) (*Monitor, error) { + c := C.gdk_display_get_monitor(v.native(), C.int(num)) + if c == nil { + return nil, nilPtrErr + } + return &Monitor{glib.Take(unsafe.Pointer(c))}, nil +} + +// GetMonitorAtWindow is a wrapper around gdk_display_get_monitor_at_window(). +func (v *Display) GetMonitorAtWindow(w *Window) (*Monitor, error) { + c := C.gdk_display_get_monitor_at_window(v.native(), w.native()) + if c == nil { + return nil, nilPtrErr + } + return &Monitor{glib.Take(unsafe.Pointer(c))}, nil +} + +// GetMonitorAtPoint is a wrapper around gdk_display_get_monitor_at_point(). +func (v *Display) GetMonitorAtPoint(x int, y int) (*Monitor, error) { + c := C.gdk_display_get_monitor_at_point(v.native(), C.int(x), C.int(y)) + if c == nil { + return nil, nilPtrErr + } + return &Monitor{glib.Take(unsafe.Pointer(c))}, nil +} + +/* + * GdkSeat + */ + +// GdkSeatGrabPrepareFunc +type GrabPrepareFunc func(seat *Seat, window *Window, user_data C.gpointer) + +// GetDisplay is a wrapper around gdk_seat_get_display(). +func (v *Seat) GetDisplay() (*Display, error) { + return toDisplay(C.gdk_seat_get_display(v.native())) +} + +// Grab is a wrapper around gdk_seat_grab(). +func (v *Seat) Grab(window *Window, capabilities SeatCapabilities, owner_events bool, cursor *Cursor, event *Event, prepare_func GrabPrepareFunc, prepare_func_data C.gpointer) GrabStatus { + return GrabStatus(C.gdk_seat_grab(v.native(), window.native(), C.GdkSeatCapabilities(capabilities), gbool(owner_events), cursor.native(), event.native(), (*[0]byte)(C.gpointer(callback.Assign(prepare_func))), prepare_func_data)) +} + +// UnGrab is a wrapper around gdk_seat_ungrab(). +func (v *Seat) UnGrab() { + C.gdk_seat_ungrab(v.native()) +} + +// GetCapabilities is a wrapper around gdk_seat_get_capabilities(). +func (v *Seat) GetCapabilities() SeatCapabilities { + return SeatCapabilities(C.gdk_seat_get_capabilities(v.native())) +} + +// GetKeyboard is a wrapper around gdk_seat_get_keyboard(). +func (v *Seat) GetKeyboard() (*Device, error) { + return toDevice(C.gdk_seat_get_keyboard(v.native())) +} + +// GetSlaves is a wrapper around gdk_seat_get_slaves(). +func (v *Seat) GetSlaves(capabilities SeatCapabilities) *[]Device { + + clist := C.gdk_seat_get_slaves(v.native(), C.GdkSeatCapabilities(capabilities)) + if clist == nil { + return nil + } + dlist := glib.WrapSList(uintptr(unsafe.Pointer(clist))) + defer dlist.Free() + + var slaves = make([]Device, 0, dlist.Length()) + for ; dlist.DataRaw() != nil; dlist = dlist.Next() { + + d := (*C.GdkDevice)(dlist.DataRaw()) + obj := &glib.Object{glib.ToGObject(unsafe.Pointer(d))} + + slaves = append(slaves, Device{obj}) + } + return &slaves +} + + + +/* + * GdkMonitor + */ + +// Monitor is a representation of GDK's GdkMonitor. +type Monitor struct { + *glib.Object +} + +// native returns a pointer to the underlying GdkMonitor. +func (v *Monitor) native() *C.GdkMonitor { + if v == nil || v.GObject == nil { + return nil + } + p := unsafe.Pointer(v.GObject) + return C.toGdkMonitor(p) +} + +// Native returns a pointer to the underlying GdkMonitor. +func (v *Monitor) Native() uintptr { + return uintptr(unsafe.Pointer(v.native())) +} + +func marshalMonitor(p uintptr) (interface{}, error) { + c := C.g_value_get_object((*C.GValue)(unsafe.Pointer(p))) + obj := &glib.Object{glib.ToGObject(unsafe.Pointer(c))} + return &Monitor{obj}, nil +} + +func toMonitor(s *C.GdkMonitor) (*Monitor, error) { + if s == nil { + return nil, nilPtrErr + } + obj := &glib.Object{glib.ToGObject(unsafe.Pointer(s))} + return &Monitor{obj}, nil +} + +// GetDisplay is a wrapper around gdk_monitor_get_display(). +func (v *Monitor) GetDisplay() (*Display, error) { + return toDisplay(C.gdk_monitor_get_display(v.native())) +} + +// GetGeometry is a wrapper around gdk_monitor_get_geometry(). +func (v *Monitor) GetGeometry() *Rectangle { + var rect C.GdkRectangle + + C.gdk_monitor_get_geometry(v.native(), &rect) + + return wrapRectangle(&rect) +} + +// GetWorkarea is a wrapper around gdk_monitor_get_workarea(). +func (v *Monitor) GetWorkarea() *Rectangle { + var rect C.GdkRectangle + + C.gdk_monitor_get_workarea(v.native(), &rect) + + return wrapRectangle(&rect) +} + +// GetWidthMM is a wrapper around gdk_monitor_get_width_mm(). +func (v *Monitor) GetWidthMM() int { + return int(C.gdk_monitor_get_width_mm(v.native())) +} + +// GetHeightMM is a wrapper around gdk_monitor_get_height_mm(). +func (v *Monitor) GetHeightMM() int { + return int(C.gdk_monitor_get_height_mm(v.native())) +} + +// GetManufacturer is a wrapper around gdk_monitor_get_manufacturer(). +func (v *Monitor) GetManufacturer() string { + // transfer none: don't free data after the code is done. + return C.GoString(C.gdk_monitor_get_manufacturer(v.native())) +} + +// GetModel is a wrapper around gdk_monitor_get_model(). +func (v *Monitor) GetModel() string { + // transfer none: don't free data after the code is done. + return C.GoString(C.gdk_monitor_get_model(v.native())) +} + +// GetScaleFactor is a wrapper around gdk_monitor_get_scale_factor(). +func (v *Monitor) GetScaleFactor() int { + return int(C.gdk_monitor_get_scale_factor(v.native())) +} + +// GetRefreshRate is a wrapper around gdk_monitor_get_refresh_rate(). +func (v *Monitor) GetRefreshRate() int { + return int(C.gdk_monitor_get_refresh_rate(v.native())) +} + +// GetSubpixelLayout is a wrapper around gdk_monitor_get_subpixel_layout(). +func (v *Monitor) GetSubpixelLayout() SubpixelLayout { + return SubpixelLayout(C.gdk_monitor_get_subpixel_layout(v.native())) +} + +// IsPrimary is a wrapper around gdk_monitor_is_primary(). +func (v *Monitor) IsPrimary() bool { + return gobool(C.gdk_monitor_is_primary(v.native())) +} + +/* + * GdkDevice + */ + +// TODO: +// gdk_device_get_axes(). +// gdk_device_tool_get_serial(). +// gdk_device_tool_get_tool_type(). + +/* + * GdkGLContext + */ + +// GetUseES is a wrapper around gdk_gl_context_get_use_es(). +func (v *GLContext) GetUseES() bool { + return gobool(C.gdk_gl_context_get_use_es(v.native())) +} + +// SetUseES is a wrapper around gdk_gl_context_set_use_es(). +func (v *GLContext) SetUseES(es int) { + C.gdk_gl_context_set_use_es(v.native(), (C.int)(es)) +}