forked from Leak_Technologies/VideoTools
Compare commits
No commits in common. "2e4b433f01098dd36f4ebf1927f3cc33c7e22b60" and "142d2f1383efc9e7cb60ef018fb027af366fc110" have entirely different histories.
2e4b433f01
...
142d2f1383
|
|
@ -127,7 +127,6 @@ VideoToolsClean # Clean build artifacts and cache
|
|||
### Essential
|
||||
- **Go 1.21 or later** - https://go.dev/dl/
|
||||
- **Bash or Zsh** shell
|
||||
- **mpv** (runtime playback backend)
|
||||
|
||||
### Optional
|
||||
- **FFmpeg** (for actual video encoding)
|
||||
|
|
@ -143,20 +142,6 @@ VideoToolsClean # Clean build artifacts and cache
|
|||
---
|
||||
|
||||
## Troubleshooting
|
||||
### "mpv not found"
|
||||
|
||||
**Solution:** Install mpv from your package manager:
|
||||
|
||||
```bash
|
||||
# Debian/Ubuntu
|
||||
sudo apt-get install -y mpv
|
||||
|
||||
# Fedora/RHEL
|
||||
sudo dnf install -y mpv
|
||||
|
||||
# Arch
|
||||
sudo pacman -S --needed mpv
|
||||
```
|
||||
|
||||
### "Go is not installed"
|
||||
|
||||
|
|
@ -371,3 +356,4 @@ Installation works in WSL environment. Ensure you have WSL with Linux distro ins
|
|||
---
|
||||
|
||||
Enjoy using VideoTools! 🎬
|
||||
|
||||
|
|
|
|||
|
|
@ -1,218 +0,0 @@
|
|||
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)
|
||||
|
||||
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)
|
||||
|
||||
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")
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
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 {
|
||||
if w == nil {
|
||||
return 0
|
||||
}
|
||||
return uint64(gdkWindowGetXID(w))
|
||||
}
|
||||
|
||||
// gdkWindowGetXID extracts the XID from a GDK window when running on X11.
|
||||
func gdkWindowGetXID(w *gdk.Window) uint {
|
||||
return uint(w.GetXID())
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
|
|
@ -4,12 +4,9 @@ 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
|
||||
|
|
|
|||
2
go.sum
2
go.sum
|
|
@ -35,8 +35,6 @@ 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=
|
||||
|
|
|
|||
689
main.go
689
main.go
|
|
@ -162,66 +162,44 @@ func (c convertConfig) CoverLabel() string {
|
|||
return filepath.Base(c.CoverArtPath)
|
||||
}
|
||||
|
||||
// requestPlayerView schedules a single player view render, coalescing rapid requests.
|
||||
func (s *appState) requestPlayerView() {
|
||||
s.playerViewPendingMu.Lock()
|
||||
if s.playerViewPending {
|
||||
s.playerViewPendingMu.Unlock()
|
||||
return
|
||||
}
|
||||
s.playerViewPending = true
|
||||
s.playerViewRefreshNeeded = true
|
||||
s.playerViewPendingMu.Unlock()
|
||||
fyne.Do(func() {
|
||||
s.showPlayerView()
|
||||
})
|
||||
}
|
||||
|
||||
type appState struct {
|
||||
window fyne.Window
|
||||
active string
|
||||
lastModule string
|
||||
source *videoSource
|
||||
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
||||
currentIndex int // Current video index in loadedVideos
|
||||
anim *previewAnimator
|
||||
convert convertConfig
|
||||
currentFrame string
|
||||
player player.Controller
|
||||
playerReady bool
|
||||
playerVolume float64
|
||||
playerMuted bool
|
||||
lastVolume float64
|
||||
playerPaused bool
|
||||
playerPos float64
|
||||
playerLast time.Time
|
||||
compareSess1 *playSession
|
||||
compareSess2 *playSession
|
||||
lastSourcePath string
|
||||
playerViewSource string
|
||||
renderingPlayerView bool
|
||||
playerViewRefreshNeeded bool
|
||||
lastPlayerViewRender time.Time
|
||||
playerViewPendingMu sync.Mutex
|
||||
playerViewPending bool
|
||||
progressQuit chan struct{}
|
||||
convertCancel context.CancelFunc
|
||||
playerSurf *playerSurface
|
||||
convertBusy bool
|
||||
convertStatus string
|
||||
convertActiveIn string
|
||||
convertActiveOut string
|
||||
convertProgress float64
|
||||
playSess *playSession
|
||||
jobQueue *queue.Queue
|
||||
statsBar *ui.ConversionStatsBar
|
||||
queueBtn *widget.Button
|
||||
queueScroll *container.Scroll
|
||||
queueOffset fyne.Position
|
||||
compareFile1 *videoSource
|
||||
compareFile2 *videoSource
|
||||
keyframingMode bool // Toggle for frame-accurate editing features
|
||||
isFullscreen bool // Fullscreen mode state
|
||||
window fyne.Window
|
||||
active string
|
||||
lastModule string
|
||||
source *videoSource
|
||||
loadedVideos []*videoSource // Multiple loaded videos for navigation
|
||||
currentIndex int // Current video index in loadedVideos
|
||||
anim *previewAnimator
|
||||
convert convertConfig
|
||||
currentFrame string
|
||||
player player.Controller
|
||||
playerReady bool
|
||||
playerVolume float64
|
||||
playerMuted bool
|
||||
lastVolume float64
|
||||
playerPaused bool
|
||||
playerPos float64
|
||||
playerLast time.Time
|
||||
compareSess1 *playSession
|
||||
compareSess2 *playSession
|
||||
progressQuit chan struct{}
|
||||
convertCancel context.CancelFunc
|
||||
playerSurf *playerSurface
|
||||
convertBusy bool
|
||||
convertStatus string
|
||||
convertActiveIn string
|
||||
convertActiveOut string
|
||||
convertProgress float64
|
||||
playSess *playSession
|
||||
jobQueue *queue.Queue
|
||||
statsBar *ui.ConversionStatsBar
|
||||
queueBtn *widget.Button
|
||||
queueScroll *container.Scroll
|
||||
queueOffset fyne.Position
|
||||
compareFile1 *videoSource
|
||||
compareFile2 *videoSource
|
||||
keyframingMode bool // Toggle for frame-accurate editing features
|
||||
isFullscreen bool // Fullscreen mode state
|
||||
}
|
||||
|
||||
func (s *appState) stopPreview() {
|
||||
|
|
@ -288,7 +266,19 @@ func (s *appState) queueProgressCounts() (completed, total int) {
|
|||
}
|
||||
|
||||
func (s *appState) updateQueueButtonLabel() {
|
||||
// Queue UI hidden in player-only mode.
|
||||
if s.queueBtn == nil {
|
||||
return
|
||||
}
|
||||
completed, total := s.queueProgressCounts()
|
||||
// Include active direct conversion in totals
|
||||
if s.convertBusy {
|
||||
total++
|
||||
}
|
||||
label := "View Queue"
|
||||
if total > 0 {
|
||||
label = fmt.Sprintf("View Queue %d/%d", completed, total)
|
||||
}
|
||||
s.queueBtn.SetText(label)
|
||||
}
|
||||
|
||||
type playerSurface struct {
|
||||
|
|
@ -438,17 +428,7 @@ func (s *appState) showErrorWithCopy(title string, err error) {
|
|||
func (s *appState) showMainMenu() {
|
||||
fmt.Printf("🎬 showMainMenu called - going to player view\n")
|
||||
// Minimal entry point: go straight to the player view.
|
||||
s.requestPlayerView()
|
||||
}
|
||||
|
||||
func (s *appState) onClose() {
|
||||
s.stopPreview()
|
||||
s.stopPlayer()
|
||||
s.stopCompareSessions()
|
||||
if s.jobQueue != nil {
|
||||
s.jobQueue.Stop()
|
||||
}
|
||||
logging.Close()
|
||||
s.showPlayerView()
|
||||
}
|
||||
|
||||
// showCompareView renders a simple side-by-side player for the first two loaded videos.
|
||||
|
|
@ -489,7 +469,6 @@ func (s *appState) buildComparePane(src *videoSource, onStop func(), setSess fun
|
|||
stageBG := canvas.NewRectangle(utils.MustHex("#0F1529"))
|
||||
stageBG.SetMinSize(fyne.NewSize(640, 360))
|
||||
videoImg := canvas.NewImageFromResource(nil)
|
||||
videoImg.SetMinSize(fyne.NewSize(960, 540))
|
||||
// Populate a preview frame if available
|
||||
if len(src.PreviewFrames) == 0 {
|
||||
if frames, err := capturePreviewFrames(src.Path, src.Duration); err == nil && len(frames) > 0 {
|
||||
|
|
@ -599,38 +578,11 @@ func (s *appState) buildComparePane(src *videoSource, onStop func(), setSess fun
|
|||
// showPlayerView renders the player-focused UI with a lightweight playlist.
|
||||
func (s *appState) showPlayerView() {
|
||||
fmt.Printf("🎬 showPlayerView called\n")
|
||||
s.playerViewPendingMu.Lock()
|
||||
s.playerViewPending = false
|
||||
s.playerViewPendingMu.Unlock()
|
||||
if s.renderingPlayerView {
|
||||
fmt.Printf("⏳ player view render already in progress, skipping\n")
|
||||
return
|
||||
}
|
||||
currentPath := ""
|
||||
if s.source != nil {
|
||||
currentPath = s.source.Path
|
||||
}
|
||||
// Hard throttle: if we rendered recently for the same source, skip.
|
||||
if !s.playerViewRefreshNeeded && s.active == "player" && s.playerViewSource == currentPath && time.Since(s.lastPlayerViewRender) < time.Second {
|
||||
fmt.Printf("⏳ throttle: rendered recently for %s\n", currentPath)
|
||||
return
|
||||
}
|
||||
s.renderingPlayerView = true
|
||||
defer func() { s.renderingPlayerView = false }()
|
||||
|
||||
if !s.playerViewRefreshNeeded && s.active == "player" && s.source != nil && s.playerViewSource == currentPath {
|
||||
fmt.Printf("📺 already in player view for this source; skipping rebuild\n")
|
||||
return
|
||||
}
|
||||
s.playerViewRefreshNeeded = false
|
||||
// Do not stop the player; only clear compare previews.
|
||||
s.stopPreview()
|
||||
s.stopPlayer()
|
||||
s.stopCompareSessions()
|
||||
s.active = "player"
|
||||
if s.source != nil {
|
||||
s.lastSourcePath = s.source.Path
|
||||
}
|
||||
fmt.Printf("📺 s.source is nil: %v\n", s.source == nil)
|
||||
s.lastPlayerViewRender = time.Now()
|
||||
|
||||
// Helper to refresh the view after selection/loads.
|
||||
refresh := func() {
|
||||
|
|
@ -644,7 +596,7 @@ func (s *appState) showPlayerView() {
|
|||
if err != nil || r == nil {
|
||||
return
|
||||
}
|
||||
path := uriPath(r.URI())
|
||||
path := r.URI().Path()
|
||||
r.Close()
|
||||
go s.loadVideo(path)
|
||||
}, s.window)
|
||||
|
|
@ -701,6 +653,8 @@ func (s *appState) showPlayerView() {
|
|||
}
|
||||
}()
|
||||
}
|
||||
|
||||
refresh()
|
||||
})
|
||||
keyframeModeItem.Checked = s.keyframingMode
|
||||
toolsMenu.Items = append(toolsMenu.Items, keyframeModeItem)
|
||||
|
|
@ -711,7 +665,6 @@ func (s *appState) showPlayerView() {
|
|||
// Player area
|
||||
var playerArea fyne.CanvasObject
|
||||
if s.source == nil {
|
||||
s.playerViewSource = ""
|
||||
bg := canvas.NewRectangle(utils.MustHex("#05070C"))
|
||||
bg.SetMinSize(fyne.NewSize(960, 540))
|
||||
|
||||
|
|
@ -761,7 +714,6 @@ func (s *appState) showPlayerView() {
|
|||
s.setContent(playerArea)
|
||||
} else {
|
||||
src := s.source
|
||||
s.playerViewSource = src.Path
|
||||
fmt.Printf("🎬 Creating player view for loaded video\n")
|
||||
|
||||
// Image surface
|
||||
|
|
@ -1447,7 +1399,7 @@ func (s *appState) batchAddToQueue(paths []string) {
|
|||
}
|
||||
}
|
||||
s.loadVideos(combined)
|
||||
s.requestPlayerView()
|
||||
s.showPlayerView()
|
||||
}
|
||||
}, false)
|
||||
}
|
||||
|
|
@ -2034,7 +1986,6 @@ func runGUI() {
|
|||
a.Settings().SetTheme(&ui.MonoTheme{})
|
||||
logging.Debug(logging.CatUI, "created fyne app: %#v", a)
|
||||
w := a.NewWindow("VT Player")
|
||||
state.window = w
|
||||
if icon := utils.LoadAppIcon(); icon != nil {
|
||||
a.SetIcon(icon)
|
||||
w.SetIcon(icon)
|
||||
|
|
@ -2096,7 +2047,7 @@ func runGUI() {
|
|||
// Initialize conversion stats bar
|
||||
state.statsBar = ui.NewConversionStatsBar(func() {
|
||||
// Clicking the stats bar opens the queue view
|
||||
// Queue hidden in player-only mode
|
||||
state.showQueue()
|
||||
})
|
||||
|
||||
// Initialize job queue
|
||||
|
|
@ -2109,6 +2060,9 @@ func runGUI() {
|
|||
app.Driver().DoFromGoroutine(func() {
|
||||
state.updateStatsBar()
|
||||
state.updateQueueButtonLabel()
|
||||
if state.active == "queue" {
|
||||
state.refreshQueueView()
|
||||
}
|
||||
}, false)
|
||||
})
|
||||
|
||||
|
|
@ -2145,12 +2099,19 @@ func runGUI() {
|
|||
fmt.Printf("✅ showMainMenu completed\n")
|
||||
logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList))
|
||||
|
||||
// Stats update loop disabled in player-only mode to avoid view churn.
|
||||
|
||||
w.SetOnClosed(func() {
|
||||
state.onClose()
|
||||
a.Quit()
|
||||
})
|
||||
// Start stats bar update loop on a timer
|
||||
go func() {
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for range ticker.C {
|
||||
app := fyne.CurrentApp()
|
||||
if app != nil && app.Driver() != nil {
|
||||
app.Driver().DoFromGoroutine(func() {
|
||||
state.updateStatsBar()
|
||||
}, false)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
w.ShowAndRun()
|
||||
}
|
||||
|
|
@ -3485,9 +3446,11 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
state.currentFrame = sourceFrame
|
||||
}
|
||||
|
||||
img := canvas.NewImageFromResource(nil)
|
||||
var img *canvas.Image
|
||||
if sourceFrame != "" {
|
||||
img = canvas.NewImageFromFile(sourceFrame)
|
||||
} else {
|
||||
img = canvas.NewImageFromResource(nil)
|
||||
}
|
||||
img.FillMode = canvas.ImageFillContain
|
||||
// Don't set rigid MinSize on image - it will scale to container
|
||||
|
|
@ -3551,22 +3514,14 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
}
|
||||
|
||||
var controls fyne.CanvasObject
|
||||
autoStarted := false
|
||||
if usePlayer {
|
||||
var volIcon *widget.Button
|
||||
var updatingVolume bool
|
||||
ensureSession := func() bool {
|
||||
if state.playSess == nil {
|
||||
fmt.Printf("🎥 Creating play session for %s\n", src.Path)
|
||||
state.playSess = newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, int(targetWidth-28), int(targetHeight-40), updateProgress, img)
|
||||
if state.playSess == nil {
|
||||
fmt.Printf("❌ ERROR: Failed to create play session\n")
|
||||
return false
|
||||
}
|
||||
state.playSess.SetVolume(state.playerVolume)
|
||||
state.playerPaused = false
|
||||
state.playSess.Play()
|
||||
autoStarted = true
|
||||
state.playerPaused = true
|
||||
}
|
||||
return state.playSess != nil
|
||||
}
|
||||
|
|
@ -3631,18 +3586,15 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
updateVolIcon()
|
||||
volSlider.Refresh()
|
||||
playBtn := utils.MakeIconButton("▶/⏸", "Play/Pause", func() {
|
||||
fmt.Printf("▶ Play/Pause pressed\n")
|
||||
if !ensureSession() {
|
||||
return
|
||||
}
|
||||
if state.playerPaused {
|
||||
state.playSess.Play()
|
||||
state.playerPaused = false
|
||||
fmt.Printf("▶ Playback started\n")
|
||||
} else {
|
||||
state.playSess.Pause()
|
||||
state.playerPaused = true
|
||||
fmt.Printf("⏸ Playback paused\n")
|
||||
}
|
||||
})
|
||||
fullBtn := utils.MakeIconButton("⛶", "Toggle fullscreen", func() {
|
||||
|
|
@ -3654,15 +3606,6 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
container.NewHBox(playBtn, fullBtn, coverBtn, importBtn, layout.NewSpacer(), volBox),
|
||||
progress,
|
||||
)
|
||||
|
||||
// Auto-start playback once when view is built
|
||||
if !autoStarted {
|
||||
if ensureSession() {
|
||||
state.playSess.Play()
|
||||
state.playerPaused = false
|
||||
fmt.Printf("▶ Auto-start playback\n")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
slider := widget.NewSlider(0, math.Max(1, float64(len(src.PreviewFrames)-1)))
|
||||
slider.Step = 1
|
||||
|
|
@ -3722,16 +3665,37 @@ func buildVideoPane(state *appState, min fyne.Size, src *videoSource, onCover fu
|
|||
}
|
||||
|
||||
type playSession struct {
|
||||
path string
|
||||
fps float64
|
||||
volume float64
|
||||
muted bool
|
||||
paused bool
|
||||
current float64
|
||||
mpv *mpvClient
|
||||
prog func(float64)
|
||||
done chan struct{}
|
||||
mu sync.Mutex
|
||||
path string
|
||||
fps float64
|
||||
width int
|
||||
height int
|
||||
targetW int
|
||||
targetH int
|
||||
volume float64
|
||||
muted bool
|
||||
paused bool
|
||||
current float64
|
||||
stop chan struct{}
|
||||
done chan struct{}
|
||||
prog func(float64)
|
||||
img *canvas.Image
|
||||
mu sync.Mutex
|
||||
videoCmd *exec.Cmd
|
||||
audioCmd *exec.Cmd
|
||||
frameN int
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -3740,57 +3704,62 @@ 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 mpv subprocess\n")
|
||||
fmt.Printf("🎯 Target: %dx%d\n", targetW, targetH)
|
||||
fmt.Printf("═══════════════════════════════════════════════════════\n\n")
|
||||
|
||||
// Validate input parameters
|
||||
if fps <= 0 {
|
||||
fps = 24
|
||||
fmt.Printf("⚠️ Invalid FPS (%.2f), defaulting to 24\n", fps)
|
||||
}
|
||||
if targetW <= 0 {
|
||||
targetW = 640
|
||||
fmt.Printf("⚠️ Invalid target width (%d), defaulting to 640\n", targetW)
|
||||
}
|
||||
if targetH <= 0 {
|
||||
targetH = int(float64(targetW) * (float64(h) / float64(utils.MaxInt(w, 1))))
|
||||
fmt.Printf("⚠️ Invalid target height (%d), calculating to %d\n", targetH, targetH)
|
||||
}
|
||||
|
||||
// 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("mpv"); err != nil {
|
||||
fmt.Printf("❌ ERROR: mpv not found in PATH: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
return &playSession{
|
||||
path: path,
|
||||
fps: fps,
|
||||
width: w,
|
||||
height: h,
|
||||
targetW: targetW,
|
||||
targetH: targetH,
|
||||
volume: 100,
|
||||
stop: make(chan struct{}),
|
||||
done: make(chan struct{}),
|
||||
prog: prog,
|
||||
img: img,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *playSession) Play() {
|
||||
fmt.Printf("▶️ PLAY called (current position: %.2fs)\n", p.current)
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Play()
|
||||
p.paused = false
|
||||
if p.videoCmd == nil && p.audioCmd == nil {
|
||||
fmt.Printf("⚡ Starting playback from scratch...\n")
|
||||
p.startLocked(p.current)
|
||||
return
|
||||
}
|
||||
fmt.Printf("▶️ Resuming playback\n")
|
||||
p.paused = false
|
||||
}
|
||||
|
||||
func (p *playSession) Pause() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Pause()
|
||||
p.paused = true
|
||||
}
|
||||
p.paused = true
|
||||
}
|
||||
|
||||
func (p *playSession) Seek(offset float64) {
|
||||
|
|
@ -3799,28 +3768,80 @@ func (p *playSession) Seek(offset float64) {
|
|||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
paused := p.paused
|
||||
p.current = offset
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Seek(p.current)
|
||||
p.stopLocked()
|
||||
p.startLocked(p.current)
|
||||
p.paused = paused
|
||||
if p.paused {
|
||||
// Ensure loops honor paused right after restart.
|
||||
time.AfterFunc(30*time.Millisecond, func() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.paused = true
|
||||
})
|
||||
}
|
||||
if p.prog != nil {
|
||||
p.prog(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
|
||||
}
|
||||
}
|
||||
|
||||
// StepFrame steps forward or backward by one frame
|
||||
// direction: 1 for forward, -1 for backward
|
||||
func (p *playSession) StepFrame(direction int) {
|
||||
step := 1.0 / math.Max(p.fps, 24)
|
||||
p.Seek(p.current + float64(direction)*step)
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
// Ensure we're paused for frame stepping
|
||||
if !p.paused {
|
||||
p.paused = true
|
||||
}
|
||||
|
||||
// Calculate new position (1 frame = 1/fps seconds)
|
||||
frameDuration := 1.0 / p.fps
|
||||
newPos := p.current + (float64(direction) * frameDuration)
|
||||
if newPos < 0 {
|
||||
newPos = 0
|
||||
}
|
||||
|
||||
p.current = newPos
|
||||
p.stopLocked()
|
||||
p.startLocked(p.current)
|
||||
p.paused = true
|
||||
|
||||
// Ensure paused state sticks
|
||||
time.AfterFunc(30*time.Millisecond, func() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
p.paused = true
|
||||
})
|
||||
|
||||
if p.prog != nil {
|
||||
p.prog(p.current)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentPosition returns the current playback position
|
||||
func (p *playSession) GetCurrentPosition() float64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
if p.mpv != nil {
|
||||
return p.mpv.Position()
|
||||
}
|
||||
return p.current
|
||||
}
|
||||
|
||||
|
|
@ -3831,30 +3852,285 @@ func (p *playSession) Stop() {
|
|||
}
|
||||
|
||||
func (p *playSession) stopLocked() {
|
||||
if p.mpv != nil {
|
||||
_ = p.mpv.Quit()
|
||||
p.mpv = nil
|
||||
select {
|
||||
case <-p.stop:
|
||||
default:
|
||||
close(p.stop)
|
||||
}
|
||||
close(p.done)
|
||||
if p.videoCmd != nil && p.videoCmd.Process != nil {
|
||||
_ = p.videoCmd.Process.Kill()
|
||||
_ = p.videoCmd.Wait()
|
||||
}
|
||||
if p.audioCmd != nil && p.audioCmd.Process != nil {
|
||||
_ = p.audioCmd.Process.Kill()
|
||||
_ = p.audioCmd.Wait()
|
||||
}
|
||||
p.videoCmd = nil
|
||||
p.audioCmd = nil
|
||||
p.stop = make(chan struct{})
|
||||
p.done = make(chan struct{})
|
||||
}
|
||||
|
||||
func (p *playSession) startProgressPoll() {
|
||||
if p.prog == nil {
|
||||
func (p *playSession) startLocked(offset float64) {
|
||||
p.paused = false
|
||||
p.current = offset
|
||||
p.frameN = 0
|
||||
logging.Debug(logging.CatFFMPEG, "playSession start path=%s offset=%.3f fps=%.3f target=%dx%d", p.path, offset, p.fps, p.targetW, p.targetH)
|
||||
p.runVideo(offset)
|
||||
p.runAudio(offset)
|
||||
}
|
||||
|
||||
func (p *playSession) runVideo(offset float64) {
|
||||
fmt.Printf("📹 Starting video decode pipeline...\n")
|
||||
fmt.Printf(" - Resolution: %dx%d\n", p.targetW, p.targetH)
|
||||
fmt.Printf(" - Frame rate: %.2f fps\n", p.fps)
|
||||
fmt.Printf(" - Offset: %.2fs\n", offset)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
args := []string{
|
||||
"-hide_banner", "-loglevel", "error",
|
||||
"-ss", fmt.Sprintf("%.3f", offset),
|
||||
"-i", p.path,
|
||||
"-vf", fmt.Sprintf("scale=%d:%d:flags=bilinear", p.targetW, p.targetH),
|
||||
"-f", "rawvideo",
|
||||
"-pix_fmt", "rgb24",
|
||||
"-r", fmt.Sprintf("%.3f", p.fps),
|
||||
"-vsync", "0", // Avoid frame duplication
|
||||
"-",
|
||||
}
|
||||
fmt.Printf("🔧 FFmpeg command: ffmpeg %s\n", strings.Join(args, " "))
|
||||
|
||||
cmd := exec.Command("ffmpeg", args...)
|
||||
cmd.Stderr = &stderr
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
fmt.Printf("❌ ERROR: Failed to create video pipe: %v\n", err)
|
||||
logging.Debug(logging.CatFFMPEG, "video pipe error: %v", err)
|
||||
return
|
||||
}
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
|
||||
fmt.Printf("⚡ Starting FFmpeg process...\n")
|
||||
startTime := time.Now()
|
||||
if err := cmd.Start(); err != nil {
|
||||
errMsg := strings.TrimSpace(stderr.String())
|
||||
fmt.Printf("❌ ERROR: FFmpeg failed to start: %v\n", err)
|
||||
if errMsg != "" {
|
||||
fmt.Printf(" FFmpeg error: %s\n", errMsg)
|
||||
}
|
||||
// Check if ffmpeg is available
|
||||
if _, pathErr := exec.LookPath("ffmpeg"); pathErr != nil {
|
||||
fmt.Printf("❌ FATAL: ffmpeg not found in PATH: %v\n", pathErr)
|
||||
}
|
||||
logging.Debug(logging.CatFFMPEG, "video start failed: %v (%s)", err, errMsg)
|
||||
return
|
||||
}
|
||||
fmt.Printf("✅ FFmpeg started (PID: %d) in %.3fs\n", cmd.Process.Pid, time.Since(startTime).Seconds())
|
||||
// Pace frames to the source frame rate instead of hammering refreshes as fast as possible.
|
||||
frameDur := time.Second
|
||||
if p.fps > 0 {
|
||||
frameDur = time.Duration(float64(time.Second) / math.Max(p.fps, 0.1))
|
||||
}
|
||||
nextFrameAt := time.Now()
|
||||
p.videoCmd = cmd
|
||||
frameSize := p.targetW * p.targetH * 3
|
||||
buf := make([]byte, frameSize)
|
||||
fmt.Printf("📦 Frame buffer allocated: %d bytes (%.2f MB)\n", frameSize, float64(frameSize)/(1024*1024))
|
||||
|
||||
go func() {
|
||||
defer ticker.Stop()
|
||||
defer cmd.Process.Kill()
|
||||
|
||||
// Performance monitoring variables
|
||||
var lastFPSReport time.Time
|
||||
var fpsCounter int
|
||||
loopStart := time.Now()
|
||||
|
||||
fmt.Printf("🔄 Frame decode loop started\n")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
pos := p.GetCurrentPosition()
|
||||
p.mu.Lock()
|
||||
p.current = pos
|
||||
p.mu.Unlock()
|
||||
p.prog(pos)
|
||||
case <-p.done:
|
||||
case <-p.stop:
|
||||
fmt.Printf("⏹️ Video decode loop stopped\n")
|
||||
logging.Debug(logging.CatFFMPEG, "video loop stop")
|
||||
return
|
||||
default:
|
||||
}
|
||||
if p.paused {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
nextFrameAt = time.Now().Add(frameDur)
|
||||
continue
|
||||
}
|
||||
|
||||
readStart := time.Now()
|
||||
_, err := io.ReadFull(stdout, buf)
|
||||
readDuration := time.Since(readStart)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
fmt.Printf("📺 Video playback completed (reached end)\n")
|
||||
return
|
||||
}
|
||||
msg := strings.TrimSpace(stderr.String())
|
||||
fmt.Printf("❌ ERROR: Frame read failed: %v\n", err)
|
||||
if msg != "" {
|
||||
fmt.Printf(" FFmpeg error: %s\n", msg)
|
||||
}
|
||||
logging.Debug(logging.CatFFMPEG, "video read failed: %v (%s)", err, msg)
|
||||
return
|
||||
}
|
||||
|
||||
// Track first frame timing
|
||||
if p.frameN == 0 {
|
||||
elapsed := time.Since(loopStart)
|
||||
fmt.Printf("🎞️ FIRST FRAME decoded in %.3fs (read: %.3fs)\n", elapsed.Seconds(), readDuration.Seconds())
|
||||
}
|
||||
|
||||
// Improved frame pacing - use a more stable timing approach
|
||||
now := time.Now()
|
||||
if now.Before(nextFrameAt) {
|
||||
time.Sleep(nextFrameAt.Sub(now))
|
||||
}
|
||||
nextFrameAt = nextFrameAt.Add(frameDur)
|
||||
|
||||
// Allocate a fresh frame to avoid concurrent texture reuse issues.
|
||||
frame := image.NewRGBA(image.Rect(0, 0, p.targetW, p.targetH))
|
||||
utils.CopyRGBToRGBA(frame.Pix, buf)
|
||||
|
||||
renderStart := time.Now()
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
if p.img != nil {
|
||||
// Ensure we render the live frame, not a stale resource preview.
|
||||
p.img.Resource = nil
|
||||
p.img.File = ""
|
||||
p.img.Image = frame
|
||||
p.img.Refresh()
|
||||
}
|
||||
}, false)
|
||||
renderDuration := time.Since(renderStart)
|
||||
|
||||
// Log first few frames in detail
|
||||
if p.frameN < 3 {
|
||||
fmt.Printf("🖼️ Frame %d: decode=%.3fs render=%.3fs\n", p.frameN+1, readDuration.Seconds(), renderDuration.Seconds())
|
||||
logging.Debug(logging.CatFFMPEG, "video frame %d drawn (%.2fs)", p.frameN+1, p.current)
|
||||
}
|
||||
|
||||
p.frameN++
|
||||
fpsCounter++
|
||||
|
||||
// FPS report every 2 seconds
|
||||
if time.Since(lastFPSReport) >= 2*time.Second {
|
||||
fps := float64(fpsCounter) / time.Since(lastFPSReport).Seconds()
|
||||
fmt.Printf("📊 Performance: %.1f FPS (target: %.1f FPS) | Frames: %d\n", fps, p.fps, p.frameN)
|
||||
fpsCounter = 0
|
||||
lastFPSReport = time.Now()
|
||||
}
|
||||
|
||||
if p.fps > 0 {
|
||||
p.current = offset + (float64(p.frameN) / p.fps)
|
||||
}
|
||||
if p.prog != nil {
|
||||
p.prog(p.current)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (p *playSession) runAudio(offset float64) {
|
||||
const sampleRate = 48000
|
||||
const channels = 2
|
||||
const bytesPerSample = 2
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command("ffmpeg",
|
||||
"-hide_banner", "-loglevel", "error",
|
||||
"-ss", fmt.Sprintf("%.3f", offset),
|
||||
"-i", p.path,
|
||||
"-vn",
|
||||
"-ac", fmt.Sprintf("%d", channels),
|
||||
"-ar", fmt.Sprintf("%d", sampleRate),
|
||||
"-f", "s16le",
|
||||
"-",
|
||||
)
|
||||
cmd.Stderr = &stderr
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatFFMPEG, "audio pipe error: %v", err)
|
||||
return
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
errMsg := strings.TrimSpace(stderr.String())
|
||||
fmt.Printf("❌ ERROR: Audio FFmpeg failed to start: %v\n", err)
|
||||
if errMsg != "" {
|
||||
fmt.Printf(" Audio FFmpeg error: %s\n", errMsg)
|
||||
}
|
||||
logging.Debug(logging.CatFFMPEG, "audio start failed: %v (%s)", err, errMsg)
|
||||
return
|
||||
}
|
||||
fmt.Printf("✅ Audio FFmpeg started (PID: %d)\n", cmd.Process.Pid)
|
||||
p.audioCmd = cmd
|
||||
ctx, err := getAudioContext(sampleRate, channels, bytesPerSample)
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatFFMPEG, "audio context error: %v", err)
|
||||
return
|
||||
}
|
||||
player := ctx.NewPlayer()
|
||||
if player == nil {
|
||||
logging.Debug(logging.CatFFMPEG, "audio player creation failed")
|
||||
return
|
||||
}
|
||||
localPlayer := player
|
||||
go func() {
|
||||
defer cmd.Process.Kill()
|
||||
defer localPlayer.Close()
|
||||
chunk := make([]byte, 4096)
|
||||
tmp := make([]byte, 4096)
|
||||
loggedFirst := false
|
||||
for {
|
||||
select {
|
||||
case <-p.stop:
|
||||
logging.Debug(logging.CatFFMPEG, "audio loop stop")
|
||||
return
|
||||
default:
|
||||
}
|
||||
if p.paused {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
n, err := stdout.Read(chunk)
|
||||
if n > 0 {
|
||||
if !loggedFirst {
|
||||
logging.Debug(logging.CatFFMPEG, "audio stream delivering bytes")
|
||||
loggedFirst = true
|
||||
}
|
||||
gain := p.volume / 100.0
|
||||
if gain < 0 {
|
||||
gain = 0
|
||||
}
|
||||
if gain > 2 {
|
||||
gain = 2
|
||||
}
|
||||
copy(tmp, chunk[:n])
|
||||
if p.muted || gain <= 0 {
|
||||
for i := 0; i < n; i++ {
|
||||
tmp[i] = 0
|
||||
}
|
||||
} else if math.Abs(1-gain) > 0.001 {
|
||||
for i := 0; i+1 < n; i += 2 {
|
||||
sample := int16(binary.LittleEndian.Uint16(tmp[i:]))
|
||||
amp := int(float64(sample) * gain)
|
||||
if amp > math.MaxInt16 {
|
||||
amp = math.MaxInt16
|
||||
}
|
||||
if amp < math.MinInt16 {
|
||||
amp = math.MinInt16
|
||||
}
|
||||
binary.LittleEndian.PutUint16(tmp[i:], uint16(int16(amp)))
|
||||
}
|
||||
}
|
||||
localPlayer.Write(tmp[:n])
|
||||
}
|
||||
if err != nil {
|
||||
if !errors.Is(err, io.EOF) {
|
||||
logging.Debug(logging.CatFFMPEG, "audio read failed: %v (%s)", err, strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -4024,10 +4300,8 @@ func (s *appState) handleDropPlayer(items []fyne.URI) {
|
|||
|
||||
if len(videoPaths) > 1 {
|
||||
go s.loadVideos(videoPaths)
|
||||
s.requestPlayerView()
|
||||
} else {
|
||||
go s.loadVideo(videoPaths[0])
|
||||
s.requestPlayerView()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -4204,7 +4478,6 @@ func (s *appState) loadVideo(path string) {
|
|||
fmt.Printf("🔄 Switching to player view...\n")
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.switchToVideo(s.currentIndex)
|
||||
s.requestPlayerView()
|
||||
}, false)
|
||||
}
|
||||
|
||||
|
|
@ -4221,11 +4494,8 @@ func (s *appState) clearVideo() {
|
|||
s.convert.CoverArtPath = ""
|
||||
s.convert.AspectHandling = "Auto"
|
||||
s.convert.OutputAspect = "Source"
|
||||
s.lastSourcePath = ""
|
||||
s.playerViewSource = ""
|
||||
s.playerViewRefreshNeeded = true
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.requestPlayerView()
|
||||
s.showPlayerView()
|
||||
}, false)
|
||||
}
|
||||
|
||||
|
|
@ -4329,9 +4599,7 @@ func (s *appState) loadVideos(paths []string) {
|
|||
s.loadedVideos = loaded
|
||||
s.currentIndex = 0
|
||||
fyne.Do(func() {
|
||||
s.playerViewRefreshNeeded = true
|
||||
s.switchToVideo(0)
|
||||
s.requestPlayerView()
|
||||
})
|
||||
}()
|
||||
}
|
||||
|
|
@ -4374,8 +4642,9 @@ func (s *appState) switchToVideo(index int) {
|
|||
s.playerPos = 0
|
||||
s.playerPaused = true
|
||||
|
||||
// Do not rebuild the view; player session stays active.
|
||||
s.lastSourcePath = src.Path
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.showPlayerView()
|
||||
}, false)
|
||||
}
|
||||
|
||||
// nextVideo switches to the next loaded video
|
||||
|
|
|
|||
175
mpv_client.go
175
mpv_client.go
|
|
@ -1,175 +0,0 @@
|
|||
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
|
||||
}
|
||||
|
|
@ -1,177 +0,0 @@
|
|||
package mpvembed
|
||||
|
||||
/*
|
||||
#cgo pkg-config: mpv
|
||||
#include <mpv/client.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
|
@ -42,10 +42,6 @@ fi
|
|||
if ! pkg-config --exists gl 2>/dev/null; then
|
||||
MISSING_DEPS+=("mesa-libGL-devel")
|
||||
fi
|
||||
# Runtime dependency: mpv
|
||||
if ! command -v mpv >/dev/null 2>&1; then
|
||||
MISSING_DEPS+=("mpv")
|
||||
fi
|
||||
|
||||
if [ ${#MISSING_DEPS[@]} -gt 0 ]; then
|
||||
echo "⚠️ Missing system dependencies: ${MISSING_DEPS[*]}"
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ install_fedora() {
|
|||
libXxf86vm-devel \
|
||||
mesa-libGL-devel \
|
||||
alsa-lib-devel \
|
||||
mpv \
|
||||
ffmpeg-free \
|
||||
golang
|
||||
echo "✓ Fedora dependencies installed"
|
||||
|
|
@ -56,7 +55,6 @@ install_ubuntu() {
|
|||
libxi-dev \
|
||||
libxxf86vm-dev \
|
||||
libasound2-dev \
|
||||
mpv \
|
||||
ffmpeg \
|
||||
golang-go
|
||||
echo "✓ Ubuntu/Debian dependencies installed"
|
||||
|
|
@ -76,7 +74,6 @@ install_arch() {
|
|||
libxi \
|
||||
libxxf86vm \
|
||||
alsa-lib \
|
||||
mpv \
|
||||
ffmpeg \
|
||||
go
|
||||
echo "✓ Arch Linux dependencies installed"
|
||||
|
|
@ -96,7 +93,6 @@ install_opensuse() {
|
|||
libXi-devel \
|
||||
libXxf86vm-devel \
|
||||
alsa-devel \
|
||||
mpv \
|
||||
ffmpeg \
|
||||
go
|
||||
echo "✓ openSUSE dependencies installed"
|
||||
|
|
@ -162,13 +158,6 @@ else
|
|||
echo "⚠️ ffmpeg not found in PATH"
|
||||
fi
|
||||
|
||||
# Check mpv
|
||||
if command -v mpv &> /dev/null; then
|
||||
echo "✓ mpv: $(mpv --version | head -1)"
|
||||
else
|
||||
echo "⚠️ mpv not found in PATH"
|
||||
fi
|
||||
|
||||
# Check pkg-config
|
||||
if command -v pkg-config &> /dev/null; then
|
||||
echo "✓ pkg-config: $(pkg-config --version)"
|
||||
|
|
|
|||
|
|
@ -3,8 +3,7 @@
|
|||
|
||||
param(
|
||||
[switch]$UseScoop = $false,
|
||||
[switch]$SkipFFmpeg = $false,
|
||||
[switch]$SkipMPV = $false
|
||||
[switch]$SkipFFmpeg = $false
|
||||
)
|
||||
|
||||
Write-Host "════════════════════════════════════════════════════════════════" -ForegroundColor Cyan
|
||||
|
|
@ -86,20 +85,8 @@ function Install-ViaChocolatey {
|
|||
Write-Host "Installing ffmpeg..." -ForegroundColor Yellow
|
||||
choco install -y ffmpeg
|
||||
} else {
|
||||
Write-Host "✓ ffmpeg already installed" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# Install mpv
|
||||
if (-not $SkipMPV) {
|
||||
if (-not (Test-Command mpv)) {
|
||||
Write-Host "Installing mpv..." -ForegroundColor Yellow
|
||||
choco install -y mpv
|
||||
} else {
|
||||
Write-Host "✓ mpv already installed" -ForegroundColor Green
|
||||
Write-Host "✓ ffmpeg already installed" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "ℹ️ mpv skipped (use -SkipMPV:$false to install)" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
Write-Host "✓ Chocolatey installation complete" -ForegroundColor Green
|
||||
|
|
@ -156,21 +143,9 @@ function Install-ViaScoop {
|
|||
if (-not (Test-Command ffmpeg)) {
|
||||
Write-Host "Installing ffmpeg..." -ForegroundColor Yellow
|
||||
scoop install ffmpeg
|
||||
} else {
|
||||
Write-Host "✓ ffmpeg already installed" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
|
||||
# Install mpv
|
||||
if (-not $SkipMPV) {
|
||||
if (-not (Test-Command mpv)) {
|
||||
Write-Host "Installing mpv..." -ForegroundColor Yellow
|
||||
scoop install mpv
|
||||
} else {
|
||||
Write-Host "✓ mpv already installed" -ForegroundColor Green
|
||||
Write-Host "✓ ffmpeg already installed" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "ℹ️ mpv skipped (use -SkipMPV:$false to install)" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
Write-Host "✓ Scoop installation complete" -ForegroundColor Green
|
||||
|
|
@ -254,17 +229,6 @@ if (Test-Command ffmpeg) {
|
|||
}
|
||||
}
|
||||
|
||||
if (Test-Command mpv) {
|
||||
$mpvVersion = mpv --version | Select-Object -First 1
|
||||
Write-Host "✓ mpv: $mpvVersion" -ForegroundColor Green
|
||||
} else {
|
||||
if ($SkipMPV) {
|
||||
Write-Host "ℹ️ mpv skipped (use -SkipMPV:$false to install)" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "⚠️ mpv not found in PATH (restart terminal or install with -SkipMPV:$false)" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Command git) {
|
||||
$gitVersion = git --version
|
||||
Write-Host "✓ Git: $gitVersion" -ForegroundColor Green
|
||||
|
|
|
|||
305
third_party/gotk3/gdk/gdk_since_3_22.go
vendored
305
third_party/gotk3/gdk/gdk_since_3_22.go
vendored
|
|
@ -1,305 +0,0 @@
|
|||
// +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 <info@conformal.com>
|
||||
//
|
||||
// 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 <gdk/gdk.h>
|
||||
// #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))
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user