mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Added scene marker generator
This commit is contained in:
58
ffmpeg/encoder_marker.go
Normal file
58
ffmpeg/encoder_marker.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Generator struct {
|
type GeneratorInfo struct {
|
||||||
ChunkCount int
|
ChunkCount int
|
||||||
FrameRate float64
|
FrameRate float64
|
||||||
NumberOfFrames int
|
NumberOfFrames int
|
||||||
@@ -18,18 +18,18 @@ type Generator struct {
|
|||||||
VideoFile ffmpeg.VideoFile
|
VideoFile ffmpeg.VideoFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func newGenerator(videoFile ffmpeg.VideoFile) (*Generator, error) {
|
func newGeneratorInfo(videoFile ffmpeg.VideoFile) (*GeneratorInfo, error) {
|
||||||
exists, err := utils.FileExists(videoFile.Path)
|
exists, err := utils.FileExists(videoFile.Path)
|
||||||
if !exists {
|
if !exists {
|
||||||
logger.Errorf("video file not found")
|
logger.Errorf("video file not found")
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
generator := &Generator{VideoFile: videoFile}
|
generator := &GeneratorInfo{VideoFile: videoFile}
|
||||||
return generator, nil
|
return generator, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Generator) configure() error {
|
func (g *GeneratorInfo) configure() error {
|
||||||
videoStream := g.VideoFile.VideoStream
|
videoStream := g.VideoFile.VideoStream
|
||||||
if videoStream == nil {
|
if videoStream == nil {
|
||||||
return fmt.Errorf("missing video stream")
|
return fmt.Errorf("missing video stream")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PreviewGenerator struct {
|
type PreviewGenerator struct {
|
||||||
generator *Generator
|
Info *GeneratorInfo
|
||||||
|
|
||||||
VideoFilename string
|
VideoFilename string
|
||||||
ImageFilename string
|
ImageFilename string
|
||||||
@@ -23,7 +23,7 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||||||
if !exists {
|
if !exists {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
generator, err := newGenerator(videoFile)
|
generator, err := newGeneratorInfo(videoFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,7 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||||||
}
|
}
|
||||||
|
|
||||||
return &PreviewGenerator{
|
return &PreviewGenerator{
|
||||||
generator: generator,
|
Info: generator,
|
||||||
VideoFilename: videoFilename,
|
VideoFilename: videoFilename,
|
||||||
ImageFilename: imageFilename,
|
ImageFilename: imageFilename,
|
||||||
OutputDirectory: outputDirectory,
|
OutputDirectory: outputDirectory,
|
||||||
@@ -42,7 +42,7 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
|
|||||||
|
|
||||||
func (g *PreviewGenerator) Generate() error {
|
func (g *PreviewGenerator) Generate() error {
|
||||||
instance.Paths.Generated.EmptyTmpDir()
|
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)
|
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG)
|
||||||
|
|
||||||
if err := g.generateConcatFile(); err != nil {
|
if err := g.generateConcatFile(); err != nil {
|
||||||
@@ -65,7 +65,7 @@ func (g *PreviewGenerator) generateConcatFile() error {
|
|||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
w := bufio.NewWriter(f)
|
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)
|
num := fmt.Sprintf("%.3d", i)
|
||||||
filename := "preview"+num+".mp4"
|
filename := "preview"+num+".mp4"
|
||||||
_, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename))
|
_, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename))
|
||||||
@@ -80,8 +80,8 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
stepSize := int(g.generator.VideoFile.Duration / float64(g.generator.ChunkCount))
|
stepSize := int(g.Info.VideoFile.Duration / float64(g.Info.ChunkCount))
|
||||||
for i := 0; i < g.generator.ChunkCount; i++ {
|
for i := 0; i < g.Info.ChunkCount; i++ {
|
||||||
time := i * stepSize
|
time := i * stepSize
|
||||||
num := fmt.Sprintf("%.3d", i)
|
num := fmt.Sprintf("%.3d", i)
|
||||||
filename := "preview"+num+".mp4"
|
filename := "preview"+num+".mp4"
|
||||||
@@ -92,11 +92,11 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
|||||||
Width: 640,
|
Width: 640,
|
||||||
OutputPath: chunkOutputPath,
|
OutputPath: chunkOutputPath,
|
||||||
}
|
}
|
||||||
encoder.ScenePreviewVideoChunk(g.generator.VideoFile, options)
|
encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
videoOutputPath := path.Join(g.OutputDirectory, g.VideoFilename)
|
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)
|
logger.Debug("created video preview: ", videoOutputPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error {
|
|||||||
|
|
||||||
videoPreviewPath := path.Join(g.OutputDirectory, g.VideoFilename)
|
videoPreviewPath := path.Join(g.OutputDirectory, g.VideoFilename)
|
||||||
tmpOutputPath := instance.Paths.Generated.GetTmpPath(g.ImageFilename)
|
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
|
return err
|
||||||
} else {
|
} else {
|
||||||
if err := os.Rename(tmpOutputPath, outputPath); err != nil {
|
if err := os.Rename(tmpOutputPath, outputPath); err != nil {
|
||||||
|
|||||||
@@ -120,9 +120,8 @@ func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcod
|
|||||||
}
|
}
|
||||||
|
|
||||||
if markers {
|
if markers {
|
||||||
go func() {
|
task := GenerateMarkersTask{Scene: scene}
|
||||||
wg.Done() // TODO
|
go task.Start(&wg)
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if transcodes {
|
if transcodes {
|
||||||
|
|||||||
79
manager/task_generate_markers.go
Normal file
79
manager/task_generate_markers.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user