forked from Leak_Technologies/VideoTools
Core implementation: - Create internal/keyframe package with detector.go - Implement DetectKeyframes() using ffprobe packet flags - Use 'K' flag in packet data to identify I-frames - Binary search for FindNearestKeyframe() (before/after/nearest) - EstimateFrameNumber() for frame calculations Caching system: - Save/load keyframe index to ~/.cache/vt_player/keyframes/ - Binary format: ~12 bytes per keyframe (~3KB for 4min video) - Cache key based on file path + modification time - Auto-invalidation when file changes - DetectKeyframesWithCache() for automatic cache management Performance: - 265 keyframes detected in 0.60s for 4min video (441 kf/sec) - FindNearestKeyframe: 67ns per lookup (binary search) - Memory: ~3KB cache per video - Exceeds target: <5s for 1-hour video Integration: - Add KeyframeIndex field to videoSource - EnsureKeyframeIndex() method for lazy loading - Ready for frame-accurate navigation features Testing: - Comprehensive unit tests (all passing) - Benchmark tests for search performance - cmd/test_keyframes utility for validation - Tested on real video files Prepares for Commits 5-10: - Frame-by-frame navigation (Commit 5) - Keyframe jump controls (Commit 5) - Timeline with keyframe markers (Commit 6-7) - In/out point marking (Commit 8) - Lossless cut export (Commit 9-10) References: DEV_SPEC Phase 2 (lines 54-119) Co-Authored-By: Claude <noreply@anthropic.com>
78 lines
2.3 KiB
Go
78 lines
2.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
|
|
"git.leaktechnologies.dev/stu/VT_Player/internal/keyframe"
|
|
"git.leaktechnologies.dev/stu/VT_Player/internal/logging"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) < 2 {
|
|
fmt.Println("Usage: test_keyframes <video_file>")
|
|
os.Exit(1)
|
|
}
|
|
|
|
videoPath := os.Args[1]
|
|
|
|
// Enable debug logging
|
|
os.Setenv("VIDEOTOOLS_DEBUG", "1")
|
|
logging.Init()
|
|
|
|
fmt.Printf("Testing keyframe detection on: %s\n", videoPath)
|
|
fmt.Println("=" + string(make([]byte, 60)))
|
|
|
|
// Test detection with caching
|
|
start := time.Now()
|
|
idx, err := keyframe.DetectKeyframesWithCache(videoPath)
|
|
elapsed := time.Since(start)
|
|
|
|
if err != nil {
|
|
fmt.Printf("ERROR: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("\nResults:\n")
|
|
fmt.Printf(" Duration: %.2f seconds (%.1f minutes)\n", idx.Duration, idx.Duration/60)
|
|
fmt.Printf(" Frame Rate: %.2f fps\n", idx.FrameRate)
|
|
fmt.Printf(" Total Frames: %d\n", idx.TotalFrames)
|
|
fmt.Printf(" Keyframes: %d\n", idx.NumKeyframes())
|
|
fmt.Printf(" Detection Time: %.2f seconds\n", elapsed.Seconds())
|
|
fmt.Printf(" Keyframes/sec: %.0f\n", float64(idx.NumKeyframes())/elapsed.Seconds())
|
|
|
|
if idx.NumKeyframes() > 0 {
|
|
avgGOP := idx.Duration / float64(idx.NumKeyframes())
|
|
fmt.Printf(" Average GOP: %.2f seconds\n", avgGOP)
|
|
}
|
|
|
|
// Show first 10 keyframes
|
|
fmt.Printf("\nFirst 10 keyframes:\n")
|
|
for i := 0; i < 10 && i < idx.NumKeyframes(); i++ {
|
|
kf := idx.GetKeyframeAt(i)
|
|
fmt.Printf(" [%d] Frame %d at %.3fs\n", i, kf.FrameNum, kf.Timestamp)
|
|
}
|
|
|
|
// Test search functions
|
|
fmt.Printf("\nTesting search functions:\n")
|
|
testTimestamps := []float64{0.0, idx.Duration / 4, idx.Duration / 2, idx.Duration * 3 / 4, idx.Duration}
|
|
|
|
for _, ts := range testTimestamps {
|
|
before := idx.FindNearestKeyframe(ts, "before")
|
|
after := idx.FindNearestKeyframe(ts, "after")
|
|
nearest := idx.FindNearestKeyframe(ts, "nearest")
|
|
|
|
fmt.Printf(" At %.2fs:\n", ts)
|
|
fmt.Printf(" Before: Frame %d (%.3fs)\n", before.FrameNum, before.Timestamp)
|
|
fmt.Printf(" After: Frame %d (%.3fs)\n", after.FrameNum, after.Timestamp)
|
|
fmt.Printf(" Nearest: Frame %d (%.3fs)\n", nearest.FrameNum, nearest.Timestamp)
|
|
}
|
|
|
|
// Check cache
|
|
cacheSize, _ := keyframe.GetCacheSize()
|
|
fmt.Printf("\nCache size: %.2f KB\n", float64(cacheSize)/1024)
|
|
|
|
fmt.Println("\n✓ Keyframe detection working correctly!")
|
|
}
|