From 8815f69fe8564032faa5797e99f5bb8661d4a146 Mon Sep 17 00:00:00 2001 From: Stu Date: Tue, 9 Dec 2025 18:20:36 -0500 Subject: [PATCH] Show preview frame when loading videos --- main.go | 193 ++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 152 insertions(+), 41 deletions(-) diff --git a/main.go b/main.go index 40b5ce0..acf79ef 100644 --- a/main.go +++ b/main.go @@ -379,18 +379,22 @@ func (s *appState) applyInverseDefaults(src *videoSource) { } func (s *appState) setContent(body fyne.CanvasObject) { + fmt.Printf("šŸŽØ setContent called with body: %v\n", body != nil) update := func() { bg := canvas.NewRectangle(backgroundColor) // Don't set a minimum size - let content determine layout naturally if body == nil { + fmt.Printf("šŸŽØ Setting empty background\n") s.window.SetContent(bg) return } + fmt.Printf("šŸŽØ Setting window content with body\n") s.window.SetContent(container.NewMax(bg, body)) } // Use async Do() instead of DoAndWait() to avoid deadlock when called from main goroutine fyne.Do(update) + fmt.Printf("šŸŽØ setContent completed\n") } // showErrorWithCopy displays an error dialog with a "Copy Error" button @@ -422,6 +426,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.showPlayerView() } @@ -464,6 +469,17 @@ 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) + // Populate a preview frame if available + if len(src.PreviewFrames) == 0 { + if frames, err := capturePreviewFrames(src.Path, src.Duration); err == nil && len(frames) > 0 { + src.PreviewFrames = frames + } + } + if len(src.PreviewFrames) > 0 { + s.currentFrame = src.PreviewFrames[0] + videoImg.File = s.currentFrame + videoImg.Refresh() + } videoImg.FillMode = canvas.ImageFillContain stage := container.NewMax(stageBG, videoImg) @@ -561,10 +577,12 @@ 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.stopPreview() s.stopPlayer() s.stopCompareSessions() s.active = "player" + fmt.Printf("šŸ“ŗ s.source is nil: %v\n", s.source == nil) // Helper to refresh the view after selection/loads. refresh := func() { @@ -606,21 +624,20 @@ func (s *appState) showPlayerView() { }), ) - viewMenu := fyne.NewMenu("View", - fyne.NewMenuItem("Playlist", func() { - // Will be implemented with playlist toggle - }), - ) + // Simplified View menu - only essential items + viewMenu := fyne.NewMenu("View") + // Only show Compare Videos if we have multiple videos loaded if len(s.loadedVideos) >= 2 { viewMenu.Items = append(viewMenu.Items, fyne.NewMenuItem("Compare Videos", func() { s.showCompareView() })) } + // Tools menu with essential features toolsMenu := fyne.NewMenu("Tools") - // Keyframing mode toggle + // Keyframing mode toggle - main feature keyframeModeItem := fyne.NewMenuItem("Frame-Accurate Mode", func() { s.keyframingMode = !s.keyframingMode @@ -692,9 +709,12 @@ func (s *appState) showPlayerView() { container.NewCenter(hint), ) - playerArea = container.NewMax(bg, container.NewCenter(centerStack)) + playerArea := container.NewMax(bg, container.NewCenter(centerStack)) + fmt.Printf("šŸ“ŗ Empty player view created\n") + s.setContent(playerArea) } else { src := s.source + fmt.Printf("šŸŽ¬ Creating player view for loaded video\n") // Image surface stageBG := canvas.NewRectangle(utils.MustHex("#0F1529")) @@ -739,14 +759,18 @@ func (s *appState) showPlayerView() { playlistContainer.Resize(fyne.NewSize(250, 540)) // Playlist starts hidden by default - user can toggle with menu button - var playlistVisible bool = false + playlistVisible := false var mainContent *fyne.Container - if playlistVisible { - mainContent = container.NewBorder(nil, nil, nil, playlistContainer, container.NewPadded(stage)) - } else { - mainContent = container.NewPadded(stage) + // Function to update main content layout + updateMainContent := func() { + if playlistVisible { + mainContent = container.NewBorder(nil, nil, nil, playlistContainer, container.NewPadded(stage)) + } else { + mainContent = container.NewPadded(stage) + } } + updateMainContent() currentTime := widget.NewLabel("0:00") totalTime := widget.NewLabel(src.DurationString()) @@ -798,6 +822,10 @@ func (s *appState) showPlayerView() { ensureSession = func() bool { if s.playSess == nil { s.playSess = newPlaySession(src.Path, src.Width, src.Height, src.FrameRate, 960, 540, updateProgress, videoImg) + if s.playSess == nil { + fmt.Printf("āŒ ERROR: Failed to create play session\n") + return false + } s.playSess.SetVolume(s.playerVolume) s.playerPaused = true } @@ -825,14 +853,18 @@ func (s *appState) showPlayerView() { var playBtn *widget.Button playBtn = ui.NewIconButton(ui.IconPlayArrow, "Play/Pause", func() { + fmt.Printf("šŸŽ® Play button clicked (paused: %v)\n", s.playerPaused) if !ensureSession() { + fmt.Printf("āŒ ERROR: Failed to ensure play session\n") return } if s.playerPaused { + fmt.Printf("ā–¶ļø Starting playback...\n") s.playSess.Play() s.playerPaused = false playBtn.SetText(ui.IconPause) } else { + fmt.Printf("āøļø Pausing playback...\n") s.playSess.Pause() s.playerPaused = true playBtn.SetText(ui.IconPlayArrow) @@ -848,10 +880,13 @@ func (s *appState) showPlayerView() { var volIcon *widget.Button volIcon = ui.NewIconButton(ui.IconVolumeUp, "Mute/Unmute", func() { + fmt.Printf("šŸ”Š Volume button clicked (muted: %v, volume: %.1f)\n", s.playerMuted, s.playerVolume) if !ensureSession() { + fmt.Printf("āŒ ERROR: Failed to ensure play session for volume control\n") return } if s.playerMuted { + fmt.Printf("šŸ”Š Unmuting volume to %.1f\n", s.lastVolume) target := s.lastVolume if target <= 0 { target = 50 @@ -860,6 +895,7 @@ func (s *appState) showPlayerView() { s.playerMuted = false s.playSess.SetVolume(target) } else { + fmt.Printf("šŸ”‡ Muting volume\n") s.lastVolume = s.playerVolume s.playerVolume = 0 s.playerMuted = true @@ -888,15 +924,10 @@ func (s *appState) showPlayerView() { // Playlist toggle button playlistToggleBtn := ui.NewIconButton(ui.IconMenu, "Toggle Playlist", func() { playlistVisible = !playlistVisible - if playlistVisible { - mainContent.Objects = []fyne.CanvasObject{container.NewPadded(stage)} - mainContent.Objects = []fyne.CanvasObject{} - *mainContent = *container.NewBorder(nil, nil, nil, playlistContainer, container.NewPadded(stage)) - } else { - mainContent.Objects = []fyne.CanvasObject{} - *mainContent = *container.NewPadded(stage) - } - mainContent.Refresh() + fmt.Printf("šŸ“‹ Playlist toggle: %v\n", playlistVisible) + updateMainContent() + // Refresh the entire player view to apply layout changes + s.setContent(playerArea) }) // Progress bar - use timeline in keyframing mode, slider otherwise @@ -970,10 +1001,11 @@ func (s *appState) showPlayerView() { } }) - // Center the playback controls - playbackControls := container.NewHBox(prevBtn, playBtn, nextBtn) + // Create basic playback controls (always centered) + basicControls := container.NewHBox(prevBtn, playBtn, nextBtn) // Add frame navigation if keyframing mode is enabled + var playbackControls fyne.CanvasObject if s.keyframingMode { playbackControls = container.NewHBox( prevBtn, @@ -984,23 +1016,25 @@ func (s *appState) showPlayerView() { keyframeNextBtn, nextBtn, ) + } else { + playbackControls = basicControls } - // Create control row with centered playback controls - var controlRow *fyne.Container + // Create control row with properly centered playback controls + var controlRow fyne.CanvasObject if s.keyframingMode { // Show frame counter in keyframing mode controlRow = container.NewBorder( nil, nil, - volContainer, // Volume on left + volContainer, // Volume on left container.NewHBox(frameCounter, playlistToggleBtn), // Frame counter and playlist toggle on right - container.NewCenter(playbackControls), // Playback controls centered + container.NewCenter(playbackControls), // Playback controls centered ) } else { controlRow = container.NewBorder( nil, nil, - volContainer, // Volume on left - container.NewHBox(playlistToggleBtn), // Playlist toggle on right + volContainer, // Volume on left + container.NewHBox(playlistToggleBtn), // Playlist toggle on right container.NewCenter(playbackControls), // Playback controls centered ) } @@ -1073,18 +1107,17 @@ func (s *appState) showPlayerView() { // Previously had TappableOverlay here but it blocked all button clicks // Need to redesign so controls overlay the video without blocking interaction - playerArea = container.NewBorder( + playerArea := container.NewBorder( nil, container.NewVBox(container.NewPadded(progressBar), container.NewPadded(controlRow)), nil, nil, mainContent, ) + + fmt.Printf("šŸŽ¬ Player view layout created\n") + s.setContent(playerArea) } - - mainPanel := playerArea - - s.setContent(mainPanel) } // Legacy queue view left in place but not used in player-only mode. @@ -1178,8 +1211,10 @@ func (s *appState) showModule(id string) { } func (s *appState) handleModuleDrop(moduleID string, items []fyne.URI) { + fmt.Printf("šŸ“¦ handleModuleDrop called: moduleID=%s itemCount=%d\n", moduleID, len(items)) logging.Debug(logging.CatModule, "handleModuleDrop called: moduleID=%s itemCount=%d", moduleID, len(items)) if len(items) == 0 { + fmt.Printf("āŒ No items to process\n") logging.Debug(logging.CatModule, "handleModuleDrop: no items to process") return } @@ -2059,7 +2094,9 @@ func runGUI() { } }) + fmt.Printf("šŸš€ About to call showMainMenu\n") state.showMainMenu() + fmt.Printf("āœ… showMainMenu completed\n") logging.Debug(logging.CatUI, "main menu rendered with %d modules", len(modulesList)) // Start stats bar update loop on a timer @@ -3670,15 +3707,27 @@ func newPlaySession(path string, w, h int, fps float64, targetW, targetH int, pr 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 + } + + fmt.Printf("āœ… Play session created successfully\n") return &playSession{ path: path, fps: fps, @@ -3842,10 +3891,11 @@ func (p *playSession) runVideo(offset float64) { "-hide_banner", "-loglevel", "error", "-ss", fmt.Sprintf("%.3f", offset), "-i", p.path, - "-vf", fmt.Sprintf("scale=%d:%d", p.targetW, p.targetH), + "-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, " ")) @@ -3867,10 +3917,14 @@ func (p *playSession) runVideo(offset float64) { 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 in %.3fs\n", time.Since(startTime).Seconds()) + 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 { @@ -3880,7 +3934,7 @@ func (p *playSession) runVideo(offset float64) { p.videoCmd = cmd frameSize := p.targetW * p.targetH * 3 buf := make([]byte, frameSize) - fmt.Printf("šŸ“¦ Frame buffer allocated: %.2f MB\n", float64(frameSize)/(1024*1024)) + fmt.Printf("šŸ“¦ Frame buffer allocated: %d bytes (%.2f MB)\n", frameSize, float64(frameSize)/(1024*1024)) go func() { defer cmd.Process.Kill() @@ -3930,8 +3984,10 @@ func (p *playSession) runVideo(offset float64) { fmt.Printf("šŸŽžļø FIRST FRAME decoded in %.3fs (read: %.3fs)\n", elapsed.Seconds(), readDuration.Seconds()) } - if delay := time.Until(nextFrameAt); delay > 0 { - time.Sleep(delay) + // 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) @@ -4000,9 +4056,15 @@ func (p *playSession) runAudio(offset float64) { return } if err := cmd.Start(); err != nil { - logging.Debug(logging.CatFFMPEG, "audio start failed: %v (%s)", err, strings.TrimSpace(stderr.String())) + 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 { @@ -4205,7 +4267,9 @@ func (s *appState) importCoverImage(path string) (string, error) { // handleDropPlayer accepts dropped files/folders anywhere and loads them into the playlist/player. func (s *appState) handleDropPlayer(items []fyne.URI) { + fmt.Printf("šŸ“¦ handleDropPlayer called with %d items\n", len(items)) if len(items) == 0 { + fmt.Printf("āŒ No items in drop\n") return } @@ -4332,26 +4396,59 @@ func (s *appState) detectModuleTileAtPosition(pos fyne.Position) string { } func (s *appState) loadVideo(path string) { + fmt.Printf("\n═══════════════════════════════════════════════════════\n") + fmt.Printf("šŸŽ¬ LOADING VIDEO\n") + fmt.Printf("═══════════════════════════════════════════════════════\n") + fmt.Printf("šŸ“ Path: %s\n", path) + + // Check if file exists first + if _, err := os.Stat(path); os.IsNotExist(err) { + fmt.Printf("āŒ ERROR: File does not exist: %s\n", path) + fyne.CurrentApp().Driver().DoFromGoroutine(func() { + s.showErrorWithCopy("File Not Found", fmt.Errorf("video file not found: %s", path)) + }, false) + return + } + + fmt.Printf("āœ… File exists, starting analysis...\n") + fmt.Printf("═══════════════════════════════════════════════════════\n\n") + if s.playSess != nil { + fmt.Printf("ā¹ļø Stopping current playback session\n") s.playSess.Stop() s.playSess = nil } s.stopProgressLoop() + + fmt.Printf("šŸ” Probing video metadata...\n") src, err := probeVideo(path) if err != nil { + fmt.Printf("āŒ ERROR: Video probe failed: %v\n", err) logging.Debug(logging.CatFFMPEG, "ffprobe failed for %s: %v", path, err) fyne.CurrentApp().Driver().DoFromGoroutine(func() { s.showErrorWithCopy("Failed to Analyze Video", fmt.Errorf("failed to analyze %s: %w", filepath.Base(path), err)) }, false) return } - if frames, err := capturePreviewFrames(src.Path, src.Duration); err == nil { + + fmt.Printf("āœ… Video probe successful:\n") + fmt.Printf(" - Resolution: %dx%d\n", src.Width, src.Height) + fmt.Printf(" - Duration: %.2fs\n", src.Duration) + fmt.Printf(" - Frame Rate: %.2f fps\n", src.FrameRate) + fmt.Printf(" - Codec: %s\n", src.VideoCodec) + + fmt.Printf("šŸ–¼ļø Generating preview frames...\n") + frames, err := capturePreviewFrames(src.Path, src.Duration) + if err == nil { src.PreviewFrames = frames + fmt.Printf("āœ… Generated %d preview frames\n", len(frames)) } else { + fmt.Printf("āš ļø Preview generation failed: %v\n", err) logging.Debug(logging.CatFFMPEG, "preview generation failed: %v", err) } // Maintain/extend loaded video list for navigation + fmt.Printf("šŸ“‹ Managing video playlist...\n") found := -1 for i, v := range s.loadedVideos { if v.Path == src.Path { @@ -4363,15 +4460,22 @@ func (s *appState) loadVideo(path string) { if found >= 0 { s.loadedVideos[found] = src s.currentIndex = found + fmt.Printf("šŸ”„ Updated existing video at index %d\n", found) } else if len(s.loadedVideos) > 0 { s.loadedVideos = append(s.loadedVideos, src) s.currentIndex = len(s.loadedVideos) - 1 + fmt.Printf("āž• Added new video at index %d\n", s.currentIndex) } else { s.loadedVideos = []*videoSource{src} s.currentIndex = 0 + fmt.Printf("šŸŽÆ Created new playlist with video at index 0\n") } + fmt.Printf("šŸ“ŗ Total videos in playlist: %d\n", len(s.loadedVideos)) + fmt.Printf("šŸŽÆ Current video index: %d\n", s.currentIndex) + logging.Debug(logging.CatModule, "video loaded %+v", src) + fmt.Printf("šŸ”„ Switching to player view...\n") fyne.CurrentApp().Driver().DoFromGoroutine(func() { s.switchToVideo(s.currentIndex) }, false) @@ -4502,7 +4606,9 @@ func (s *appState) loadVideos(paths []string) { // switchToVideo switches to a specific video by index func (s *appState) switchToVideo(index int) { + fmt.Printf("šŸ”„ switchToVideo called with index %d (total: %d)\n", index, len(s.loadedVideos)) if index < 0 || index >= len(s.loadedVideos) { + fmt.Printf("āŒ Invalid index %d (range: 0-%d)\n", index, len(s.loadedVideos)-1) return } @@ -4510,10 +4616,15 @@ func (s *appState) switchToVideo(index int) { src := s.loadedVideos[index] s.source = src + fmt.Printf("šŸŽ¬ Switched to: %s\n", filepath.Base(src.Path)) + fmt.Printf("šŸ“ Resolution: %dx%d, Duration: %.2fs\n", src.Width, src.Height, src.Duration) + if len(src.PreviewFrames) > 0 { s.currentFrame = src.PreviewFrames[0] + fmt.Printf("šŸ–¼ļø Set preview frame: %s\n", src.PreviewFrames[0]) } else { s.currentFrame = "" + fmt.Printf("āš ļø No preview frames available\n") } s.applyInverseDefaults(src)