Seek 5s into videos for thumbnail extraction

The 0.5s seek often landed on the initial keyframe or a fade-in,
producing blocky thumbnails. Aim ~5s in instead, with an ffprobe
midpoint fallback for clips shorter than that and a 2s → 0
retry chain if the seek still overshoots.
This commit is contained in:
prosolis
2026-05-26 23:03:31 -07:00
parent 3e29acaa23
commit fc4cab3ad6

View File

@@ -17,6 +17,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@@ -305,15 +306,49 @@ func buildAvifFromFFmpeg(body []byte, outPath string) error {
outPNG.Close() outPNG.Close()
defer os.Remove(pngPath) defer os.Remove(pngPath)
if err := extractVideoFrame(inPath, pngPath, "0.5"); err != nil { // Aim ~5s in — past the typical fade-in / first-GOP blockiness while
// Some clips are shorter than the seek offset; retry from frame 0. // still likely within the clip. For shorter clips we fall back to the
// midpoint (when ffprobe is available) so we don't seek off the end.
seek := "5"
if d := probeVideoDuration(inPath); d > 0 && d < 5 {
seek = strconv.FormatFloat(d/2, 'f', 2, 64)
}
if err := extractVideoFrame(inPath, pngPath, seek); err != nil {
// Seek overshot or container's funky — try 2s, then frame 0.
if err := extractVideoFrame(inPath, pngPath, "2"); err != nil {
if err := extractVideoFrame(inPath, pngPath, "0"); err != nil { if err := extractVideoFrame(inPath, pngPath, "0"); err != nil {
return err return err
} }
} }
}
return avifEncodeFromPNG(pngPath, outPath) return avifEncodeFromPNG(pngPath, outPath)
} }
// probeVideoDuration returns the clip length in seconds, or 0 if ffprobe is
// missing or can't read it (image-as-video fallback, malformed containers).
func probeVideoDuration(inPath string) float64 {
if _, err := exec.LookPath("ffprobe"); err != nil {
return 0
}
ctx, cancel := context.WithTimeout(context.Background(), ffmpegTimeout)
defer cancel()
cmd := exec.CommandContext(ctx, "ffprobe",
"-v", "error",
"-show_entries", "format=duration",
"-of", "default=noprint_wrappers=1:nokey=1",
inPath,
)
out, err := cmd.Output()
if err != nil {
return 0
}
d, err := strconv.ParseFloat(strings.TrimSpace(string(out)), 64)
if err != nil || d <= 0 {
return 0
}
return d
}
// extractVideoFrame asks ffmpeg to seek to seekSec, grab one frame, and // extractVideoFrame asks ffmpeg to seek to seekSec, grab one frame, and
// downscale it to at most thumbWidth wide before writing pngPath. // downscale it to at most thumbWidth wide before writing pngPath.
func extractVideoFrame(inPath, pngPath, seekSec string) error { func extractVideoFrame(inPath, pngPath, seekSec string) error {