From 44216edbb765a553d2765c56acd2ad149dc694ea Mon Sep 17 00:00:00 2001 From: Stash Dev Date: Sun, 10 Feb 2019 01:44:12 -0800 Subject: [PATCH] Added scene marker generator --- ffmpeg/encoder_marker.go | 58 +++++++++++++++++++++++ manager/generator.go | 8 ++-- manager/generator_preview.go | 24 +++++----- manager/manager.go | 5 +- manager/task_generate_markers.go | 79 ++++++++++++++++++++++++++++++++ 5 files changed, 155 insertions(+), 19 deletions(-) create mode 100644 ffmpeg/encoder_marker.go create mode 100644 manager/task_generate_markers.go diff --git a/ffmpeg/encoder_marker.go b/ffmpeg/encoder_marker.go new file mode 100644 index 000000000..db23487f4 --- /dev/null +++ b/ffmpeg/encoder_marker.go @@ -0,0 +1,58 @@ +package ffmpeg + +import ( + "fmt" + "strconv" +) + +type SceneMarkerOptions struct { + ScenePath string + Seconds int + Width int + OutputPath string +} + +func (e *Encoder) SceneMarkerVideo(probeResult VideoFile, options SceneMarkerOptions) error { + args := []string{ + "-v", "quiet", + "-ss", strconv.Itoa(options.Seconds), + "-t", "20", + "-i", probeResult.Path, + "-c:v", "libx264", + "-profile:v", "high", + "-level", "4.2", + "-preset", "veryslow", + "-crf", "24", + "-movflags", "+faststart", + "-threads", "4", + "-vf", fmt.Sprintf("scale=%v:-2", options.Width), + "-sws_flags", "lanczos", + "-c:a", "aac", + "-b:a", "64k", + "-strict", "-2", + options.OutputPath, + } + _, err := e.run(probeResult, args) + return err +} + +func (e *Encoder) SceneMarkerImage(probeResult VideoFile, options SceneMarkerOptions) error { + args := []string{ + "-v", "quiet", + "-ss", strconv.Itoa(options.Seconds), + "-t", "5", + "-i", probeResult.Path, + "-c:v", "libwebp", + "-lossless", "1", + "-q:v", "70", + "-compression_level", "6", + "-preset", "default", + "-loop", "0", + "-threads", "4", + "-vf", fmt.Sprintf("scale=%v:-2,fps=12", options.Width), + "-an", + options.OutputPath, + } + _, err := e.run(probeResult, args) + return err +} \ No newline at end of file diff --git a/manager/generator.go b/manager/generator.go index 7f36d5e12..fd4147073 100644 --- a/manager/generator.go +++ b/manager/generator.go @@ -9,7 +9,7 @@ import ( "strconv" ) -type Generator struct { +type GeneratorInfo struct { ChunkCount int FrameRate float64 NumberOfFrames int @@ -18,18 +18,18 @@ type Generator struct { VideoFile ffmpeg.VideoFile } -func newGenerator(videoFile ffmpeg.VideoFile) (*Generator, error) { +func newGeneratorInfo(videoFile ffmpeg.VideoFile) (*GeneratorInfo, error) { exists, err := utils.FileExists(videoFile.Path) if !exists { logger.Errorf("video file not found") return nil, err } - generator := &Generator{VideoFile: videoFile} + generator := &GeneratorInfo{VideoFile: videoFile} return generator, nil } -func (g *Generator) configure() error { +func (g *GeneratorInfo) configure() error { videoStream := g.VideoFile.VideoStream if videoStream == nil { return fmt.Errorf("missing video stream") diff --git a/manager/generator_preview.go b/manager/generator_preview.go index 7c68e73a9..4837d72a4 100644 --- a/manager/generator_preview.go +++ b/manager/generator_preview.go @@ -11,7 +11,7 @@ import ( ) type PreviewGenerator struct { - generator *Generator + Info *GeneratorInfo VideoFilename string ImageFilename string @@ -23,7 +23,7 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image if !exists { return nil, err } - generator, err := newGenerator(videoFile) + generator, err := newGeneratorInfo(videoFile) if err != nil { return nil, err } @@ -33,16 +33,16 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image } return &PreviewGenerator{ - generator: generator, - VideoFilename: videoFilename, - ImageFilename: imageFilename, + Info: generator, + VideoFilename: videoFilename, + ImageFilename: imageFilename, OutputDirectory: outputDirectory, }, nil } func (g *PreviewGenerator) Generate() error { instance.Paths.Generated.EmptyTmpDir() - logger.Infof("[generator] generating scene preview for %s", g.generator.VideoFile.Path) + logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path) encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) if err := g.generateConcatFile(); err != nil { @@ -65,7 +65,7 @@ func (g *PreviewGenerator) generateConcatFile() error { defer f.Close() w := bufio.NewWriter(f) - for i := 0; i < g.generator.ChunkCount; i++ { + for i := 0; i < g.Info.ChunkCount; i++ { num := fmt.Sprintf("%.3d", i) filename := "preview"+num+".mp4" _, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename)) @@ -80,8 +80,8 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error { return nil } - stepSize := int(g.generator.VideoFile.Duration / float64(g.generator.ChunkCount)) - for i := 0; i < g.generator.ChunkCount; i++ { + stepSize := int(g.Info.VideoFile.Duration / float64(g.Info.ChunkCount)) + for i := 0; i < g.Info.ChunkCount; i++ { time := i * stepSize num := fmt.Sprintf("%.3d", i) filename := "preview"+num+".mp4" @@ -92,11 +92,11 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error { Width: 640, OutputPath: chunkOutputPath, } - encoder.ScenePreviewVideoChunk(g.generator.VideoFile, options) + encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options) } videoOutputPath := path.Join(g.OutputDirectory, g.VideoFilename) - encoder.ScenePreviewVideoChunkCombine(g.generator.VideoFile, g.getConcatFilePath(), videoOutputPath) + encoder.ScenePreviewVideoChunkCombine(g.Info.VideoFile, g.getConcatFilePath(), videoOutputPath) logger.Debug("created video preview: ", videoOutputPath) return nil } @@ -110,7 +110,7 @@ func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error { videoPreviewPath := path.Join(g.OutputDirectory, g.VideoFilename) tmpOutputPath := instance.Paths.Generated.GetTmpPath(g.ImageFilename) - if err := encoder.ScenePreviewVideoToImage(g.generator.VideoFile, 640, videoPreviewPath, tmpOutputPath); err != nil { + if err := encoder.ScenePreviewVideoToImage(g.Info.VideoFile, 640, videoPreviewPath, tmpOutputPath); err != nil { return err } else { if err := os.Rename(tmpOutputPath, outputPath); err != nil { diff --git a/manager/manager.go b/manager/manager.go index 2401716a1..218c7a17f 100644 --- a/manager/manager.go +++ b/manager/manager.go @@ -120,9 +120,8 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod } if markers { - go func() { - wg.Done() // TODO - }() + task := GenerateMarkersTask{Scene: scene} + go task.Start(&wg) } if transcodes { diff --git a/manager/task_generate_markers.go b/manager/task_generate_markers.go new file mode 100644 index 000000000..5670cc8dd --- /dev/null +++ b/manager/task_generate_markers.go @@ -0,0 +1,79 @@ +package manager + +import ( + "github.com/stashapp/stash/ffmpeg" + "github.com/stashapp/stash/logger" + "github.com/stashapp/stash/models" + "github.com/stashapp/stash/utils" + "os" + "path" + "strconv" + "sync" +) + +type GenerateMarkersTask struct { + Scene models.Scene +} + +func (t *GenerateMarkersTask) Start(wg *sync.WaitGroup) { + instance.Paths.Generated.EmptyTmpDir() + qb := models.NewSceneMarkerQueryBuilder() + sceneMarkers, _ := qb.FindBySceneID(t.Scene.ID, nil) + if len(sceneMarkers) == 0 { + wg.Done() + return + } + + videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.Scene.Path) + if err != nil { + logger.Errorf("error reading video file: %s", err.Error()) + wg.Done() + return + } + + // Make the folder for the scenes markers + markersFolder := path.Join(instance.Paths.Generated.Markers, t.Scene.Checksum) + _ = utils.EnsureDir(markersFolder) + + encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) + for i, sceneMarker := range sceneMarkers { + index := i + 1 + logger.Progressf("[generator] <%s> scene marker %d of %d", t.Scene.Checksum, index, len(sceneMarkers)) + + seconds := int(sceneMarker.Seconds) + baseFilename := strconv.Itoa(seconds) + videoFilename := baseFilename + ".mp4" + imageFilename := baseFilename + ".webp" + videoPath := instance.Paths.SceneMarkers.GetStreamPath(t.Scene.Checksum, seconds) + imagePath := instance.Paths.SceneMarkers.GetStreamPreviewImagePath(t.Scene.Checksum, seconds) + videoExists, _ := utils.FileExists(videoPath) + imageExists, _ := utils.FileExists(imagePath) + + options := ffmpeg.SceneMarkerOptions{ + ScenePath: t.Scene.Path, + Seconds: seconds, + Width: 640, + } + if !videoExists { + options.OutputPath = instance.Paths.Generated.GetTmpPath(videoFilename) // tmp output in case the process ends abruptly + if err := encoder.SceneMarkerVideo(*videoFile, options); err != nil { + logger.Errorf("[generator] failed to generate marker video: %s", err) + } else { + _ = os.Rename(options.OutputPath, videoPath) + logger.Debug("created marker video: ", videoPath) + } + } + + if !imageExists { + options.OutputPath = instance.Paths.Generated.GetTmpPath(imageFilename) // tmp output in case the process ends abruptly + if err := encoder.SceneMarkerImage(*videoFile, options); err != nil { + logger.Errorf("[generator] failed to generate marker image: %s", err) + } else { + _ = os.Rename(options.OutputPath, imagePath) + logger.Debug("created marker image: ", videoPath) + } + } + } + + wg.Done() +}