Browse Source

create thumbs bug

main
Domagoj Zecevic 2 days ago
parent
commit
8759a6613e
  1. 42
      internal/video/thumb.go

42
internal/video/thumb.go

@ -2,9 +2,13 @@ package video
import ( import (
"bytes" "bytes"
"context"
"fmt" "fmt"
"log"
"os/exec" "os/exec"
"path/filepath"
"sync" "sync"
"time"
) )
const ( 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) { func (c *Cache) Thumbnail(absPath string) ([]byte, error) {
if cached, ok := c.get(absPath); ok { if cached, ok := c.get(absPath); ok {
return cached, nil return cached, nil
} }
cmd := exec.Command( ctx, cancel := context.WithTimeout(context.Background(), thumbnailTimeout)
defer cancel()
cmd := exec.CommandContext(
ctx,
c.ffmpegPath, c.ffmpegPath,
"-loglevel", "error", "-loglevel", "error",
"-i", absPath, "-i", absPath,
// -ss must come AFTER -i for raw H.265 bitstreams. // Do NOT use -ss here.
// Input seeking (-ss before -i) requires an index that raw streams lack, // -ss before -i: input seeking — raw H.265 has no index, causes
// causing "could not seek to position 0.000" and an empty output. // "could not seek to position 0.000" and empty output.
// Output seeking (-ss after -i) decodes from the start and is reliable. // -ss after -i: output seeking — decodes from start then skips,
"-ss", "0", // 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", "-vframes", "1",
"-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", thumbnailWidth, thumbnailHeight), "-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", thumbnailWidth, thumbnailHeight),
"-f", "image2", "-f", "image2",
@ -62,7 +78,19 @@ func (c *Cache) Thumbnail(absPath string) ([]byte, error) {
output, err := cmd.Output() output, err := cmd.Output()
if err != nil { 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) c.add(absPath, output)

Loading…
Cancel
Save