mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-05-24 02:02:36 -04:00
144 lines
3.0 KiB
Go
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)
|
|
}
|