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"
|
||||||
"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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user