169 lines
3.5 KiB
Go
169 lines
3.5 KiB
Go
// backend\assets_sprite.go
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
/*
|
|
const (
|
|
previewSpriteCols = 10
|
|
previewSpriteRows = 8
|
|
previewSpriteFrameCount = previewSpriteCols * previewSpriteRows
|
|
previewSpriteCellW = 160
|
|
previewSpriteCellH = 90
|
|
)
|
|
*/
|
|
|
|
const (
|
|
previewSpriteCols = 6
|
|
previewSpriteRows = 5
|
|
previewSpriteFrameCount = previewSpriteCols * previewSpriteRows
|
|
previewSpriteCellW = 120
|
|
previewSpriteCellH = 68
|
|
)
|
|
|
|
func fixedPreviewSpriteLayout() (cols, rows, count, cellW, cellH int) {
|
|
return previewSpriteCols, previewSpriteRows, previewSpriteFrameCount, previewSpriteCellW, previewSpriteCellH
|
|
}
|
|
|
|
func previewSpriteStepSeconds(durationSec float64) float64 {
|
|
if durationSec <= 0 {
|
|
return 5
|
|
}
|
|
|
|
step := durationSec / float64(previewSpriteFrameCount)
|
|
if step < 0.5 {
|
|
step = 0.5
|
|
}
|
|
|
|
return step
|
|
}
|
|
|
|
// generatePreviewSpriteJPG erzeugt ein statisches JPG-Spritesheet aus einem Video.
|
|
// ffmpeg muss im PATH verfügbar sein.
|
|
func generatePreviewSpriteJPG(
|
|
ctx context.Context,
|
|
videoPath string,
|
|
outPath string,
|
|
cols int,
|
|
rows int,
|
|
stepSec float64,
|
|
cellW int,
|
|
cellH int,
|
|
) error {
|
|
videoPath = strings.TrimSpace(videoPath)
|
|
outPath = strings.TrimSpace(outPath)
|
|
|
|
if videoPath == "" {
|
|
return fmt.Errorf("generatePreviewSpriteJPG: empty videoPath")
|
|
}
|
|
if outPath == "" {
|
|
return fmt.Errorf("generatePreviewSpriteJPG: empty outPath")
|
|
}
|
|
if cols <= 0 || rows <= 0 {
|
|
return fmt.Errorf("generatePreviewSpriteJPG: invalid grid %dx%d", cols, rows)
|
|
}
|
|
if stepSec <= 0 {
|
|
return fmt.Errorf("generatePreviewSpriteJPG: invalid stepSec %.3f", stepSec)
|
|
}
|
|
if cellW <= 0 || cellH <= 0 {
|
|
return fmt.Errorf("generatePreviewSpriteJPG: invalid cell size %dx%d", cellW, cellH)
|
|
}
|
|
|
|
if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil {
|
|
return fmt.Errorf("mkdir sprite dir: %w", err)
|
|
}
|
|
|
|
ext := filepath.Ext(outPath)
|
|
if ext == "" {
|
|
ext = ".jpg"
|
|
}
|
|
base := strings.TrimSuffix(outPath, ext)
|
|
tmpPath := base + ".tmp" + ext
|
|
|
|
// alte temp-Datei vorsichtshalber vorher entfernen
|
|
_ = os.Remove(tmpPath)
|
|
|
|
// bei JEDEM Fehler temp wieder aufräumen
|
|
renameOK := false
|
|
defer func() {
|
|
if !renameOK {
|
|
_ = os.Remove(tmpPath)
|
|
}
|
|
}()
|
|
|
|
ffmpegPath := strings.TrimSpace(getSettings().FFmpegPath)
|
|
if ffmpegPath == "" {
|
|
ffmpegPath = "ffmpeg"
|
|
}
|
|
|
|
// robustere Filterkette
|
|
vf := fmt.Sprintf(
|
|
"fps=1/%f,"+
|
|
"scale=%d:%d:force_original_aspect_ratio=decrease:flags=bilinear,"+
|
|
"pad=%d:%d:(ow-iw)/2:(oh-ih)/2:black,"+
|
|
"setsar=1,"+
|
|
"tile=%dx%d:margin=0:padding=0",
|
|
stepSec,
|
|
cellW, cellH,
|
|
cellW, cellH,
|
|
cols, rows,
|
|
)
|
|
|
|
cmd := exec.CommandContext(
|
|
ctx,
|
|
ffmpegPath,
|
|
"-hide_banner",
|
|
"-y",
|
|
"-i", videoPath,
|
|
"-an",
|
|
"-sn",
|
|
"-threads", "0",
|
|
"-vf", vf,
|
|
"-frames:v", "1",
|
|
"-c:v", "mjpeg",
|
|
"-q:v", "4",
|
|
"-f", "image2",
|
|
tmpPath,
|
|
)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
msg := strings.TrimSpace(string(out))
|
|
if msg == "" {
|
|
msg = "(keine ffmpeg-ausgabe)"
|
|
}
|
|
return fmt.Errorf(
|
|
"ffmpeg sprite failed for %q -> %q: %w | args=%q | output=%s",
|
|
videoPath,
|
|
tmpPath,
|
|
err,
|
|
strings.Join(cmd.Args, " "),
|
|
msg,
|
|
)
|
|
}
|
|
|
|
fi, err := os.Stat(tmpPath)
|
|
if err != nil {
|
|
return fmt.Errorf("sprite temp stat failed: %w", err)
|
|
}
|
|
if fi.IsDir() || fi.Size() <= 0 {
|
|
return fmt.Errorf("sprite temp file invalid/empty")
|
|
}
|
|
|
|
_ = os.Remove(outPath)
|
|
if err := os.Rename(tmpPath, outPath); err != nil {
|
|
return fmt.Errorf("sprite rename failed: %w", err)
|
|
}
|
|
|
|
renameOK = true
|
|
return nil
|
|
}
|