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:
@@ -17,6 +17,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -305,15 +306,49 @@ func buildAvifFromFFmpeg(body []byte, outPath string) error {
|
||||
outPNG.Close()
|
||||
defer os.Remove(pngPath)
|
||||
|
||||
if err := extractVideoFrame(inPath, pngPath, "0.5"); err != nil {
|
||||
// Some clips are shorter than the seek offset; retry from frame 0.
|
||||
// Aim ~5s in — past the typical fade-in / first-GOP blockiness while
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
// downscale it to at most thumbWidth wide before writing pngPath.
|
||||
func extractVideoFrame(inPath, pngPath, seekSec string) error {
|
||||
|
||||
Reference in New Issue
Block a user