From 8759a6613eb425384798d6203d0e3294cc494aec Mon Sep 17 00:00:00 2001 From: Domagoj Zecevic Date: Fri, 19 Jun 2026 11:01:49 +0200 Subject: [PATCH] create thumbs bug --- internal/video/thumb.go | 42 ++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/internal/video/thumb.go b/internal/video/thumb.go index dc2c16b..f810a85 100644 --- a/internal/video/thumb.go +++ b/internal/video/thumb.go @@ -2,9 +2,13 @@ package video import ( "bytes" + "context" "fmt" + "log" "os/exec" + "path/filepath" "sync" + "time" ) const ( @@ -36,20 +40,32 @@ func NewCache(max int, ffmpegPath string) *Cache { } } +// thumbnailTimeout is the maximum time we allow ffmpeg to spend extracting +// a single frame. Most clips produce a frame in under a second; 30 s is a +// generous safety net for slow hardware or unexpectedly large files. +const thumbnailTimeout = 30 * time.Second + func (c *Cache) Thumbnail(absPath string) ([]byte, error) { if cached, ok := c.get(absPath); ok { return cached, nil } - cmd := exec.Command( + ctx, cancel := context.WithTimeout(context.Background(), thumbnailTimeout) + defer cancel() + + cmd := exec.CommandContext( + ctx, c.ffmpegPath, "-loglevel", "error", "-i", absPath, - // -ss must come AFTER -i for raw H.265 bitstreams. - // Input seeking (-ss before -i) requires an index that raw streams lack, - // causing "could not seek to position 0.000" and an empty output. - // Output seeking (-ss after -i) decodes from the start and is reliable. - "-ss", "0", + // Do NOT use -ss here. + // -ss before -i: input seeking — raw H.265 has no index, causes + // "could not seek to position 0.000" and empty output. + // -ss after -i: output seeking — decodes from start then skips, + // still trips on files whose first NAL units are invalid. + // Dropping -ss entirely and using only -vframes 1 lets ffmpeg decode + // until it finds the first valid frame, which is robust across all + // camera files regardless of their NAL unit layout. "-vframes", "1", "-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", thumbnailWidth, thumbnailHeight), "-f", "image2", @@ -62,7 +78,19 @@ func (c *Cache) Thumbnail(absPath string) ([]byte, error) { output, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("generate video thumbnail: %w%s", err, stderrSuffix(stderr.String())) + log.Printf("video thumbnail failed for %s: %v — stderr: %s", + filepath.Base(absPath), err, stderr.String()) + return nil, fmt.Errorf("generate video thumbnail: %w", err) + } + + // ffmpeg can exit 0 but write nothing when the file has no decodable + // frame (truncated clip, all-invalid NAL units, etc.). Treat empty + // output as an error so we never cache broken placeholder bytes and + // serve them as a "successful" thumbnail on every subsequent request. + if len(output) == 0 { + log.Printf("video thumbnail: empty output for %s — stderr: %s", + filepath.Base(absPath), stderr.String()) + return nil, fmt.Errorf("video thumbnail: no frame decoded from %s", filepath.Base(absPath)) } c.add(absPath, output)