2019-02-10 05:30:54 +00:00
|
|
|
package manager
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bufio"
|
|
|
|
"fmt"
|
2020-07-19 01:59:18 +00:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
|
2019-02-14 23:42:52 +00:00
|
|
|
"github.com/stashapp/stash/pkg/ffmpeg"
|
|
|
|
"github.com/stashapp/stash/pkg/logger"
|
|
|
|
"github.com/stashapp/stash/pkg/utils"
|
2019-02-10 05:30:54 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type PreviewGenerator struct {
|
2019-02-10 09:44:12 +00:00
|
|
|
Info *GeneratorInfo
|
2019-02-10 05:30:54 +00:00
|
|
|
|
2020-11-25 01:45:10 +00:00
|
|
|
VideoChecksum string
|
2019-02-14 22:53:32 +00:00
|
|
|
VideoFilename string
|
|
|
|
ImageFilename string
|
2019-02-10 05:30:54 +00:00
|
|
|
OutputDirectory string
|
2020-05-26 23:33:49 +00:00
|
|
|
|
|
|
|
GenerateVideo bool
|
|
|
|
GenerateImage bool
|
|
|
|
|
|
|
|
PreviewPreset string
|
2020-07-19 01:59:18 +00:00
|
|
|
|
|
|
|
Overwrite bool
|
2019-02-10 05:30:54 +00:00
|
|
|
}
|
|
|
|
|
2020-11-25 01:45:10 +00:00
|
|
|
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, videoFilename string, imageFilename string, outputDirectory string, generateVideo bool, generateImage bool, previewPreset string) (*PreviewGenerator, error) {
|
2019-02-10 05:30:54 +00:00
|
|
|
exists, err := utils.FileExists(videoFile.Path)
|
|
|
|
if !exists {
|
|
|
|
return nil, err
|
|
|
|
}
|
2019-02-10 09:44:12 +00:00
|
|
|
generator, err := newGeneratorInfo(videoFile)
|
2019-02-10 05:30:54 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
generator.ChunkCount = 12 // 12 segments to the preview
|
|
|
|
|
|
|
|
return &PreviewGenerator{
|
2019-02-10 09:44:12 +00:00
|
|
|
Info: generator,
|
2020-11-25 01:45:10 +00:00
|
|
|
VideoChecksum: videoChecksum,
|
2019-02-10 09:44:12 +00:00
|
|
|
VideoFilename: videoFilename,
|
|
|
|
ImageFilename: imageFilename,
|
2019-02-10 05:30:54 +00:00
|
|
|
OutputDirectory: outputDirectory,
|
2020-05-26 23:33:49 +00:00
|
|
|
GenerateVideo: generateVideo,
|
|
|
|
GenerateImage: generateImage,
|
|
|
|
PreviewPreset: previewPreset,
|
2019-02-10 05:30:54 +00:00
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *PreviewGenerator) Generate() error {
|
2019-02-10 09:44:12 +00:00
|
|
|
logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path)
|
2020-07-23 02:51:35 +00:00
|
|
|
|
|
|
|
if err := g.Info.configure(); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
2019-03-23 14:56:59 +00:00
|
|
|
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
|
2020-05-26 23:33:49 +00:00
|
|
|
if g.GenerateVideo {
|
2020-08-16 23:21:58 +00:00
|
|
|
if err := g.generateVideo(&encoder, false); err != nil {
|
|
|
|
logger.Warnf("[generator] failed generating scene preview, trying fallback")
|
|
|
|
if err := g.generateVideo(&encoder, true); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2020-05-26 23:33:49 +00:00
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
}
|
2020-05-26 23:33:49 +00:00
|
|
|
if g.GenerateImage {
|
|
|
|
if err := g.generateImage(&encoder); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *PreviewGenerator) generateConcatFile() error {
|
|
|
|
f, err := os.Create(g.getConcatFilePath())
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
defer f.Close()
|
|
|
|
|
|
|
|
w := bufio.NewWriter(f)
|
2019-02-10 09:44:12 +00:00
|
|
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
2019-02-10 05:30:54 +00:00
|
|
|
num := fmt.Sprintf("%.3d", i)
|
2020-11-25 01:45:10 +00:00
|
|
|
filename := "preview_" + g.VideoChecksum + "_" + num + ".mp4"
|
2019-02-10 05:30:54 +00:00
|
|
|
_, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename))
|
|
|
|
}
|
|
|
|
return w.Flush()
|
|
|
|
}
|
|
|
|
|
2020-08-16 23:21:58 +00:00
|
|
|
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder, fallback bool) error {
|
2019-02-10 17:33:18 +00:00
|
|
|
outputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
2019-02-10 05:30:54 +00:00
|
|
|
outputExists, _ := utils.FileExists(outputPath)
|
2020-07-19 01:59:18 +00:00
|
|
|
if !g.Overwrite && outputExists {
|
2019-02-10 05:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
2021-04-22 03:51:51 +00:00
|
|
|
err := g.generateConcatFile()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var tmpFiles []string // a list of tmp files used during the preview generation
|
|
|
|
tmpFiles = append(tmpFiles, g.getConcatFilePath()) // add concat filename to tmpFiles
|
|
|
|
defer func() { removeFiles(tmpFiles) }() // remove tmpFiles when done
|
2019-02-10 05:30:54 +00:00
|
|
|
|
2020-07-23 02:51:35 +00:00
|
|
|
stepSize, offset := g.Info.getStepSizeAndOffset()
|
|
|
|
|
2021-04-22 03:51:51 +00:00
|
|
|
durationSegment := g.Info.ChunkDuration
|
|
|
|
if durationSegment < 0.75 { // a very short duration can create files without a video stream
|
|
|
|
durationSegment = 0.75 // use 0.75 in that case
|
|
|
|
logger.Warnf("[generator] Segment duration (%f) too short.Using 0.75 instead.", g.Info.ChunkDuration)
|
|
|
|
}
|
|
|
|
|
2019-02-10 09:44:12 +00:00
|
|
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
2020-07-23 02:51:35 +00:00
|
|
|
time := offset + (float64(i) * stepSize)
|
2019-02-10 05:30:54 +00:00
|
|
|
num := fmt.Sprintf("%.3d", i)
|
2020-11-25 01:45:10 +00:00
|
|
|
filename := "preview_" + g.VideoChecksum + "_" + num + ".mp4"
|
2019-02-10 05:30:54 +00:00
|
|
|
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
|
2021-04-22 03:51:51 +00:00
|
|
|
tmpFiles = append(tmpFiles, chunkOutputPath) // add chunk filename to tmpFiles
|
2019-02-10 05:30:54 +00:00
|
|
|
options := ffmpeg.ScenePreviewChunkOptions{
|
2020-07-23 02:51:35 +00:00
|
|
|
StartTime: time,
|
2021-04-22 03:51:51 +00:00
|
|
|
Duration: durationSegment,
|
2019-02-14 22:53:32 +00:00
|
|
|
Width: 640,
|
2019-02-10 05:30:54 +00:00
|
|
|
OutputPath: chunkOutputPath,
|
|
|
|
}
|
2020-08-16 23:21:58 +00:00
|
|
|
if err := encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options, g.PreviewPreset, fallback); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
}
|
|
|
|
|
2019-02-10 17:33:18 +00:00
|
|
|
videoOutputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
2020-08-16 23:21:58 +00:00
|
|
|
if err := encoder.ScenePreviewVideoChunkCombine(g.Info.VideoFile, g.getConcatFilePath(), videoOutputPath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
2019-02-10 05:30:54 +00:00
|
|
|
logger.Debug("created video preview: ", videoOutputPath)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error {
|
2019-02-10 17:33:18 +00:00
|
|
|
outputPath := filepath.Join(g.OutputDirectory, g.ImageFilename)
|
2019-02-10 05:30:54 +00:00
|
|
|
outputExists, _ := utils.FileExists(outputPath)
|
2020-07-19 01:59:18 +00:00
|
|
|
if !g.Overwrite && outputExists {
|
2019-02-10 05:30:54 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-02-10 17:33:18 +00:00
|
|
|
videoPreviewPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
2019-02-10 05:30:54 +00:00
|
|
|
tmpOutputPath := instance.Paths.Generated.GetTmpPath(g.ImageFilename)
|
2019-02-10 09:44:12 +00:00
|
|
|
if err := encoder.ScenePreviewVideoToImage(g.Info.VideoFile, 640, videoPreviewPath, tmpOutputPath); err != nil {
|
2019-02-10 05:30:54 +00:00
|
|
|
return err
|
|
|
|
}
|
2020-08-21 07:57:07 +00:00
|
|
|
if err := utils.SafeMove(tmpOutputPath, outputPath); err != nil {
|
2019-02-14 22:53:32 +00:00
|
|
|
return err
|
|
|
|
}
|
|
|
|
logger.Debug("created video preview image: ", outputPath)
|
2019-02-10 05:30:54 +00:00
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (g *PreviewGenerator) getConcatFilePath() string {
|
2020-11-25 01:45:10 +00:00
|
|
|
return instance.Paths.Generated.GetTmpPath(fmt.Sprintf("files_%s.txt", g.VideoChecksum))
|
2019-02-14 22:53:32 +00:00
|
|
|
}
|
2021-04-22 03:51:51 +00:00
|
|
|
|
|
|
|
func removeFiles(list []string) {
|
|
|
|
for _, f := range list {
|
|
|
|
if err := os.Remove(f); err != nil {
|
|
|
|
logger.Warnf("[generator] Delete error: %s", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|