Kyoo/transcoder/src/thumbnails.go
2024-01-28 19:55:18 +01:00

144 lines
3.0 KiB
Go

package src
import (
"fmt"
"image"
"image/color"
"log"
"math"
"os"
"sync"
"github.com/disintegration/imaging"
"gitlab.com/opennota/screengen"
)
// We want to have a thumbnail every ${interval} seconds.
var default_interval = 10
type ThumbnailsCreator struct {
created map[string]string
running map[string]chan struct{}
lock sync.Mutex
}
func NewThumbnailsCreator() *ThumbnailsCreator {
return &ThumbnailsCreator{
created: make(map[string]string),
running: make(map[string]chan struct{}),
}
}
func (t *ThumbnailsCreator) ExtractThumbnail(path string, name string) (string, error) {
t.lock.Lock()
defer t.lock.Unlock()
out, ok := t.created[path]
if ok {
return out, nil
}
wait, ok := t.running[path]
if ok {
t.lock.Unlock()
<-wait
t.lock.Lock()
out = t.created[path]
return out, nil
}
wait = make(chan struct{})
t.running[path] = wait
t.lock.Unlock()
out, err := extractThumbnail(path, name)
t.lock.Lock()
// this will be unlocked by the defer at the top of the function.
delete(t.running, path)
if err == nil {
t.created[path] = out
}
return out, err
}
func extractThumbnail(path string, name string) (string, error) {
ret, err := GetInfo(path)
if err != nil {
return "", err
}
out := fmt.Sprintf("%s/%s", GetMetadataPath(), ret.Sha)
sprite_path := fmt.Sprintf("%s/sprite.png", out)
vtt_path := fmt.Sprintf("%s/sprite.vtt", out)
if _, err := os.Stat(sprite_path); err != nil {
return out, nil
}
gen, err := screengen.NewGenerator(path)
if err != nil {
log.Printf("Error reading video file: %v", err)
return "", err
}
defer gen.Close()
gen.Fast = true
// truncate duration to full seconds
// this prevents empty/black images when the movie is some milliseconds longer
// ffmpeg then sometimes takes a black screenshot AFTER the movie finished for some reason
duration := 1000 * (int(gen.Duration) / 1000)
var numcaps, interval int
if default_interval < duration {
numcaps = duration / default_interval * 1000
interval = default_interval
} else {
numcaps = duration / 10
interval = duration / numcaps
}
columns := int(math.Sqrt(float64(numcaps)))
rows := int(math.Ceil(float64(numcaps) / float64(columns)))
width := gen.Width()
height := gen.Height()
sprite := imaging.New(width*columns, height*rows, color.Black)
vtt := "WEBVTT\n\n"
ts := 0
for i := 0; i < numcaps; i++ {
img, err := gen.Image(int64(ts))
if err != nil {
log.Printf("Could not generate screenshot %s", err)
return "", err
}
// TODO: resize image
x := i % width
y := i / width
sprite = imaging.Paste(sprite, img, image.Pt(x, y))
timestamps := ts
ts += interval
vtt += fmt.Sprintf(
"%s --> %s\n%s#xywh=%d,%d,%d,%d\n\n",
tsToVttTime(timestamps),
tsToVttTime(ts),
name,
x,
y,
width,
height,
)
}
os.WriteFile(vtt_path, []byte(vtt), 0o644)
imaging.Save(sprite, sprite_path)
return out, nil
}
func tsToVttTime(ts int) string {
return fmt.Sprintf("%02d:%02d:%02d.000", ts/3600, ts/60, ts%60)
}