You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

97 lines
2.0 KiB

package video
import (
"bytes"
"fmt"
"os/exec"
"sync"
)
const (
defaultCacheEntries = 200
thumbnailWidth = 160
thumbnailHeight = 90
defaultFFmpegPath = "ffmpeg"
)
type Cache struct {
mu sync.Mutex
max int
ffmpegPath string
entries map[string][]byte
order []string
}
func NewCache(max int, ffmpegPath string) *Cache {
if max <= 0 {
max = defaultCacheEntries
}
if ffmpegPath == "" {
ffmpegPath = defaultFFmpegPath
}
return &Cache{
max: max,
ffmpegPath: ffmpegPath,
entries: make(map[string][]byte),
}
}
func (c *Cache) Thumbnail(absPath string) ([]byte, error) {
if cached, ok := c.get(absPath); ok {
return cached, nil
}
cmd := exec.Command(
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",
"-vframes", "1",
"-vf", fmt.Sprintf("scale=%d:%d:force_original_aspect_ratio=decrease", thumbnailWidth, thumbnailHeight),
"-f", "image2",
"-vcodec", "mjpeg",
"pipe:1",
)
var stderr bytes.Buffer
cmd.Stderr = &stderr
output, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("generate video thumbnail: %w%s", err, stderrSuffix(stderr.String()))
}
c.add(absPath, output)
return append([]byte(nil), output...), nil
}
func (c *Cache) get(key string) ([]byte, bool) {
c.mu.Lock()
defer c.mu.Unlock()
value, ok := c.entries[key]
if !ok {
return nil, false
}
return append([]byte(nil), value...), true
}
func (c *Cache) add(key string, value []byte) {
c.mu.Lock()
defer c.mu.Unlock()
if _, ok := c.entries[key]; !ok {
c.order = append(c.order, key)
}
c.entries[key] = append([]byte(nil), value...)
for len(c.order) > c.max {
oldest := c.order[0]
c.order = c.order[1:]
delete(c.entries, oldest)
}
}