Added scene preview generator

This commit is contained in:
Stash Dev
2019-02-09 21:30:54 -08:00
parent 16ead91a05
commit 1aa7557432
12 changed files with 575 additions and 167 deletions

View File

@@ -21,7 +21,8 @@ func (r *queryResolver) MetadataExport(ctx context.Context) (string, error) {
} }
func (r *queryResolver) MetadataGenerate(ctx context.Context) (string, error) { func (r *queryResolver) MetadataGenerate(ctx context.Context) (string, error) {
panic("not implemented") manager.GetInstance().Generate(true, true, true, true)
return "todo", nil
} }
func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) { func (r *queryResolver) MetadataClean(ctx context.Context) (string, error) {

View File

@@ -1,7 +1,6 @@
package ffmpeg package ffmpeg
import ( import (
"fmt"
"github.com/stashapp/stash/logger" "github.com/stashapp/stash/logger"
"io/ioutil" "io/ioutil"
"os/exec" "os/exec"
@@ -11,65 +10,17 @@ import (
var progressRegex = regexp.MustCompile(`time=(\d+):(\d+):(\d+.\d+)`) var progressRegex = regexp.MustCompile(`time=(\d+):(\d+):(\d+.\d+)`)
type encoder struct { type Encoder struct {
Path string Path string
} }
func NewEncoder(ffmpegPath string) encoder { func NewEncoder(ffmpegPath string) Encoder {
return encoder{ return Encoder{
Path: ffmpegPath, Path: ffmpegPath,
} }
} }
type ScreenshotOptions struct { func (e *Encoder) run(probeResult VideoFile, args []string) (string, error) {
OutputPath string
Quality int
Time float64
Width int
Verbosity string
}
type TranscodeOptions struct {
OutputPath string
}
func (e *encoder) Screenshot(probeResult FFProbeResult, options ScreenshotOptions) {
if options.Verbosity == "" {
options.Verbosity = "quiet"
}
if options.Quality == 0 {
options.Quality = 1
}
args := []string{
"-v", options.Verbosity,
"-ss", fmt.Sprintf("%v", options.Time),
"-y",
"-i", probeResult.Path, // TODO: Wrap in quotes?
"-vframes", "1",
"-q:v", fmt.Sprintf("%v", options.Quality),
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
"-f", "image2",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *encoder) Transcode(probeResult FFProbeResult, options TranscodeOptions) {
args := []string{
"-i", probeResult.Path,
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "superfast",
"-crf", "23",
"-vf", "scale=iw:-2",
"-c:a", "aac",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *encoder) run(probeResult FFProbeResult, args []string) (string, error) {
cmd := exec.Command(e.Path, args...) cmd := exec.Command(e.Path, args...)
stderr, err := cmd.StderrPipe() stderr, err := cmd.StderrPipe()

View File

@@ -0,0 +1,65 @@
package ffmpeg
import (
"fmt"
"strconv"
)
type ScenePreviewChunkOptions struct {
Time int
Width int
OutputPath string
}
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions) {
args := []string{
"-v", "quiet",
"-ss", strconv.Itoa(options.Time),
"-t", "0.75",
"-i", probeResult.Path,
"-y",
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "veryslow",
"-crf", "21",
"-threads", "4",
"-vf", fmt.Sprintf("scale=%v:-2", options.Width),
"-c:a", "aac",
"-b:a", "128k",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFilePath string, outputPath string) {
args := []string{
"-v", "quiet",
"-f", "concat",
"-i", concatFilePath,
"-y",
"-c", "copy",
outputPath,
}
_, _ = e.run(probeResult, args)
}
func (e *Encoder) ScenePreviewVideoToImage(probeResult VideoFile, width int, videoPreviewPath string, outputPath string) error {
args := []string{
"-v", "quiet",
"-i", videoPreviewPath,
"-y",
"-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", width),
"-an",
outputPath,
}
_, err := e.run(probeResult, args)
return err
}

View File

@@ -0,0 +1,32 @@
package ffmpeg
import "fmt"
type ScreenshotOptions struct {
OutputPath string
Quality int
Time float64
Width int
Verbosity string
}
func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) {
if options.Verbosity == "" {
options.Verbosity = "quiet"
}
if options.Quality == 0 {
options.Quality = 1
}
args := []string{
"-v", options.Verbosity,
"-ss", fmt.Sprintf("%v", options.Time),
"-y",
"-i", probeResult.Path, // TODO: Wrap in quotes?
"-vframes", "1",
"-q:v", fmt.Sprintf("%v", options.Quality),
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
"-f", "image2",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}

View File

@@ -0,0 +1,20 @@
package ffmpeg
type TranscodeOptions struct {
OutputPath string
}
func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
args := []string{
"-i", probeResult.Path,
"-c:v", "libx264",
"-profile:v", "high",
"-level", "4.2",
"-preset", "superfast",
"-crf", "23",
"-vf", "scale=iw:-2",
"-c:a", "aac",
options.OutputPath,
}
_, _ = e.run(probeResult, args)
}

View File

@@ -6,18 +6,26 @@ import (
"math" "math"
"os" "os"
"os/exec" "os/exec"
"runtime"
"strconv" "strconv"
"strings" "strings"
"time" "time"
) )
type ffprobeExecutable struct { var ValidCodecs = []string{"h264", "h265", "vp8", "vp9"}
Path string
func IsValidCodec(codecName string) bool {
for _, c := range ValidCodecs {
if c == codecName {
return true
}
}
return false
} }
type FFProbeResult struct { type VideoFile struct {
JSON ffprobeJSON JSON FFProbeJSON
AudioStream *FFProbeStream
VideoStream *FFProbeStream
Path string Path string
Container string Container string
@@ -37,52 +45,62 @@ type FFProbeResult struct {
AudioCodec string AudioCodec string
} }
func NewFFProbe(ffprobePath string) ffprobeExecutable {
return ffprobeExecutable{
Path: ffprobePath,
}
}
// Execute exec command and bind result to struct. // Execute exec command and bind result to struct.
func (ffp *ffprobeExecutable) ProbeVideo(filePath string) (*FFProbeResult, error) { func NewVideoFile(ffprobePath string, videoPath string) (*VideoFile, error) {
args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", filePath} args := []string{"-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", "-show_error", videoPath}
// Extremely slow on windows for some reason //// Extremely slow on windows for some reason
if runtime.GOOS != "windows" { //if runtime.GOOS != "windows" {
args = append(args, "-count_frames") // args = append(args, "-count_frames")
} //}
out, err := exec.Command(ffp.Path, args...).Output() out, err := exec.Command(ffprobePath, args...).Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", filePath, string(out), err.Error()) return nil, fmt.Errorf("FFProbe encountered an error with <%s>.\nError JSON:\n%s\nError: %s", videoPath, string(out), err.Error())
} }
probeJSON := &ffprobeJSON{} probeJSON := &FFProbeJSON{}
if err := json.Unmarshal(out, probeJSON); err != nil { if err := json.Unmarshal(out, probeJSON); err != nil {
return nil, err return nil, err
} }
result := ffp.newProbeResult(filePath, *probeJSON) return parse(videoPath, probeJSON)
return result, nil
} }
func (ffp *ffprobeExecutable) newProbeResult(filePath string, probeJson ffprobeJSON) *FFProbeResult { func parse(filePath string, probeJSON *FFProbeJSON) (*VideoFile, error) {
videoStreamIndex := ffp.getStreamIndex("video", probeJson) if probeJSON == nil {
audioStreamIndex := ffp.getStreamIndex("audio", probeJson) return nil, fmt.Errorf("failed to get ffprobe json")
}
result := &VideoFile{}
result.JSON = *probeJSON
if result.JSON.Error.Code != 0 {
return nil, fmt.Errorf("ffprobe error code %d: %s", result.JSON.Error.Code, result.JSON.Error.String)
}
//} else if (ffprobeResult.stderr.includes("could not find codec parameters")) {
// throw new Error(`FFProbe [${filePath}] -> Could not find codec parameters`);
//} // TODO nil_or_unsupported.(video_stream) && nil_or_unsupported.(audio_stream)
result := &FFProbeResult{}
result.JSON = probeJson
result.Path = filePath result.Path = filePath
result.Container = probeJson.Format.FormatName
duration, _ := strconv.ParseFloat(probeJson.Format.Duration, 64) result.Bitrate, _ = strconv.ParseInt(probeJSON.Format.BitRate, 10, 64)
result.Container = probeJSON.Format.FormatName
duration, _ := strconv.ParseFloat(probeJSON.Format.Duration, 64)
result.Duration = math.Round(duration*100)/100 result.Duration = math.Round(duration*100)/100
result.StartTime, _ = strconv.ParseFloat(probeJson.Format.StartTime, 64)
result.Bitrate, _ = strconv.ParseInt(probeJson.Format.BitRate, 10, 64)
fileStat, _ := os.Stat(filePath) fileStat, _ := os.Stat(filePath)
result.Size = fileStat.Size() result.Size = fileStat.Size()
result.CreationTime = probeJson.Format.Tags.CreationTime result.StartTime, _ = strconv.ParseFloat(probeJSON.Format.StartTime, 64)
result.CreationTime = probeJSON.Format.Tags.CreationTime
if videoStreamIndex != -1 { audioStream := result.GetAudioStream()
videoStream := probeJson.Streams[videoStreamIndex] if audioStream != nil {
result.AudioCodec = audioStream.CodecName
result.AudioStream = audioStream
}
videoStream := result.GetVideoStream()
if videoStream != nil {
result.VideoStream = videoStream
result.VideoCodec = videoStream.CodecName result.VideoCodec = videoStream.CodecName
result.VideoBitrate, _ = strconv.ParseInt(videoStream.BitRate, 10, 64) result.VideoBitrate, _ = strconv.ParseInt(videoStream.BitRate, 10, 64)
var framerate float64 var framerate float64
@@ -104,14 +122,26 @@ func (ffp *ffprobeExecutable) newProbeResult(filePath string, probeJson ffprobeJ
} }
} }
if audioStreamIndex != -1 { return result, nil
result.AudioCodec = probeJson.Streams[audioStreamIndex].CodecName
}
return result
} }
func (ffp *ffprobeExecutable) getStreamIndex(fileType string, probeJson ffprobeJSON) int { func (v *VideoFile) GetAudioStream() *FFProbeStream {
index := v.getStreamIndex("audio", v.JSON)
if index != -1 {
return &v.JSON.Streams[index]
}
return nil
}
func (v *VideoFile) GetVideoStream() *FFProbeStream {
index := v.getStreamIndex("video", v.JSON)
if index != -1 {
return &v.JSON.Streams[index]
}
return nil
}
func (v *VideoFile) getStreamIndex(fileType string, probeJson FFProbeJSON) int {
for i, stream := range probeJson.Streams { for i, stream := range probeJson.Streams {
if stream.CodecType == fileType { if stream.CodecType == fileType {
return i return i

View File

@@ -4,7 +4,7 @@ import (
"time" "time"
) )
type ffprobeJSON struct { type FFProbeJSON struct {
Format struct { Format struct {
BitRate string `json:"bit_rate"` BitRate string `json:"bit_rate"`
Duration string `json:"duration"` Duration string `json:"duration"`
@@ -24,67 +24,69 @@ type ffprobeJSON struct {
MinorVersion string `json:"minor_version"` MinorVersion string `json:"minor_version"`
} `json:"tags"` } `json:"tags"`
} `json:"format"` } `json:"format"`
Streams []struct { Streams []FFProbeStream `json:"streams"`
AvgFrameRate string `json:"avg_frame_rate"`
BitRate string `json:"bit_rate"`
BitsPerRawSample string `json:"bits_per_raw_sample,omitempty"`
ChromaLocation string `json:"chroma_location,omitempty"`
CodecLongName string `json:"codec_long_name"`
CodecName string `json:"codec_name"`
CodecTag string `json:"codec_tag"`
CodecTagString string `json:"codec_tag_string"`
CodecTimeBase string `json:"codec_time_base"`
CodecType string `json:"codec_type"`
CodedHeight int `json:"coded_height,omitempty"`
CodedWidth int `json:"coded_width,omitempty"`
DisplayAspectRatio string `json:"display_aspect_ratio,omitempty"`
Disposition struct {
AttachedPic int `json:"attached_pic"`
CleanEffects int `json:"clean_effects"`
Comment int `json:"comment"`
Default int `json:"default"`
Dub int `json:"dub"`
Forced int `json:"forced"`
HearingImpaired int `json:"hearing_impaired"`
Karaoke int `json:"karaoke"`
Lyrics int `json:"lyrics"`
Original int `json:"original"`
TimedThumbnails int `json:"timed_thumbnails"`
VisualImpaired int `json:"visual_impaired"`
} `json:"disposition"`
Duration string `json:"duration"`
DurationTs int `json:"duration_ts"`
HasBFrames int `json:"has_b_frames,omitempty"`
Height int `json:"height,omitempty"`
Index int `json:"index"`
IsAvc string `json:"is_avc,omitempty"`
Level int `json:"level,omitempty"`
NalLengthSize string `json:"nal_length_size,omitempty"`
NbFrames string `json:"nb_frames"`
PixFmt string `json:"pix_fmt,omitempty"`
Profile string `json:"profile"`
RFrameRate string `json:"r_frame_rate"`
Refs int `json:"refs,omitempty"`
SampleAspectRatio string `json:"sample_aspect_ratio,omitempty"`
StartPts int `json:"start_pts"`
StartTime string `json:"start_time"`
Tags struct {
CreationTime time.Time `json:"creation_time"`
HandlerName string `json:"handler_name"`
Language string `json:"language"`
Rotate string `json:"rotate"`
} `json:"tags"`
TimeBase string `json:"time_base"`
Width int `json:"width,omitempty"`
BitsPerSample int `json:"bits_per_sample,omitempty"`
ChannelLayout string `json:"channel_layout,omitempty"`
Channels int `json:"channels,omitempty"`
MaxBitRate string `json:"max_bit_rate,omitempty"`
SampleFmt string `json:"sample_fmt,omitempty"`
SampleRate string `json:"sample_rate,omitempty"`
} `json:"streams"`
Error struct { Error struct {
Code int `json:"code"` Code int `json:"code"`
String string `json:"string"` String string `json:"string"`
} `json:"error"` } `json:"error"`
}
type FFProbeStream struct {
AvgFrameRate string `json:"avg_frame_rate"`
BitRate string `json:"bit_rate"`
BitsPerRawSample string `json:"bits_per_raw_sample,omitempty"`
ChromaLocation string `json:"chroma_location,omitempty"`
CodecLongName string `json:"codec_long_name"`
CodecName string `json:"codec_name"`
CodecTag string `json:"codec_tag"`
CodecTagString string `json:"codec_tag_string"`
CodecTimeBase string `json:"codec_time_base"`
CodecType string `json:"codec_type"`
CodedHeight int `json:"coded_height,omitempty"`
CodedWidth int `json:"coded_width,omitempty"`
DisplayAspectRatio string `json:"display_aspect_ratio,omitempty"`
Disposition struct {
AttachedPic int `json:"attached_pic"`
CleanEffects int `json:"clean_effects"`
Comment int `json:"comment"`
Default int `json:"default"`
Dub int `json:"dub"`
Forced int `json:"forced"`
HearingImpaired int `json:"hearing_impaired"`
Karaoke int `json:"karaoke"`
Lyrics int `json:"lyrics"`
Original int `json:"original"`
TimedThumbnails int `json:"timed_thumbnails"`
VisualImpaired int `json:"visual_impaired"`
} `json:"disposition"`
Duration string `json:"duration"`
DurationTs int `json:"duration_ts"`
HasBFrames int `json:"has_b_frames,omitempty"`
Height int `json:"height,omitempty"`
Index int `json:"index"`
IsAvc string `json:"is_avc,omitempty"`
Level int `json:"level,omitempty"`
NalLengthSize string `json:"nal_length_size,omitempty"`
NbFrames string `json:"nb_frames"`
PixFmt string `json:"pix_fmt,omitempty"`
Profile string `json:"profile"`
RFrameRate string `json:"r_frame_rate"`
Refs int `json:"refs,omitempty"`
SampleAspectRatio string `json:"sample_aspect_ratio,omitempty"`
StartPts int `json:"start_pts"`
StartTime string `json:"start_time"`
Tags struct {
CreationTime time.Time `json:"creation_time"`
HandlerName string `json:"handler_name"`
Language string `json:"language"`
Rotate string `json:"rotate"`
} `json:"tags"`
TimeBase string `json:"time_base"`
Width int `json:"width,omitempty"`
BitsPerSample int `json:"bits_per_sample,omitempty"`
ChannelLayout string `json:"channel_layout,omitempty"`
Channels int `json:"channels,omitempty"`
MaxBitRate string `json:"max_bit_rate,omitempty"`
SampleFmt string `json:"sample_fmt,omitempty"`
SampleRate string `json:"sample_rate,omitempty"`
} }

62
manager/generator.go Normal file
View File

@@ -0,0 +1,62 @@
package manager
import (
"fmt"
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/utils"
"os/exec"
"strconv"
)
type Generator struct {
ChunkCount int
FrameRate float64
NumberOfFrames int
NthFrame int
VideoFile ffmpeg.VideoFile
}
func newGenerator(videoFile ffmpeg.VideoFile) (*Generator, error) {
exists, err := utils.FileExists(videoFile.Path)
if !exists {
logger.Errorf("video file not found")
return nil, err
}
generator := &Generator{VideoFile: videoFile}
return generator, nil
}
func (g *Generator) configure() error {
videoStream := g.VideoFile.VideoStream
if videoStream == nil {
return fmt.Errorf("missing video stream")
}
var framerate float64
if g.VideoFile.FrameRate == 0 {
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
} else {
framerate = g.VideoFile.FrameRate
}
g.FrameRate = framerate
numberOfFrames, _ := strconv.Atoi(videoStream.NbFrames)
if numberOfFrames == 0 {
command := `ffmpeg -nostats -i `+g.VideoFile.Path+` -vcodec copy -f rawvideo -y /dev/null 2>&1 | \
grep frame | \
awk '{split($0,a,"fps")}END{print a[1]}' | \
sed 's/.*= *//'`
commandResult, _ := exec.Command(command).Output()
numberOfFrames, _ := strconv.Atoi(string(commandResult))
if numberOfFrames == 0 { // TODO: test
numberOfFrames = int(framerate * g.VideoFile.Duration)
}
}
g.NumberOfFrames = numberOfFrames
g.NthFrame = g.NumberOfFrames / g.ChunkCount
return nil
}

View File

@@ -0,0 +1,127 @@
package manager
import (
"bufio"
"fmt"
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/utils"
"os"
"path"
)
type PreviewGenerator struct {
generator *Generator
VideoFilename string
ImageFilename string
OutputDirectory string
}
func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, imageFilename string, outputDirectory string) (*PreviewGenerator, error) {
exists, err := utils.FileExists(videoFile.Path)
if !exists {
return nil, err
}
generator, err := newGenerator(videoFile)
if err != nil {
return nil, err
}
generator.ChunkCount = 12 // 12 segments to the preview
if err := generator.configure(); err != nil {
return nil, err
}
return &PreviewGenerator{
generator: 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)
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG)
if err := g.generateConcatFile(); err != nil {
return err
}
if err := g.generateVideo(&encoder); err != nil {
return err
}
if err := g.generateImage(&encoder); err != nil {
return err
}
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)
for i := 0; i < g.generator.ChunkCount; i++ {
num := fmt.Sprintf("%.3d", i)
filename := "preview"+num+".mp4"
_, _ = w.WriteString(fmt.Sprintf("file '%s'\n", filename))
}
return w.Flush()
}
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
outputPath := path.Join(g.OutputDirectory, g.VideoFilename)
outputExists, _ := utils.FileExists(outputPath)
if outputExists {
return nil
}
stepSize := int(g.generator.VideoFile.Duration / float64(g.generator.ChunkCount))
for i := 0; i < g.generator.ChunkCount; i++ {
time := i * stepSize
num := fmt.Sprintf("%.3d", i)
filename := "preview"+num+".mp4"
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)
options := ffmpeg.ScenePreviewChunkOptions{
Time: time,
Width: 640,
OutputPath: chunkOutputPath,
}
encoder.ScenePreviewVideoChunk(g.generator.VideoFile, options)
}
videoOutputPath := path.Join(g.OutputDirectory, g.VideoFilename)
encoder.ScenePreviewVideoChunkCombine(g.generator.VideoFile, g.getConcatFilePath(), videoOutputPath)
logger.Debug("created video preview: ", videoOutputPath)
return nil
}
func (g *PreviewGenerator) generateImage(encoder *ffmpeg.Encoder) error {
outputPath := path.Join(g.OutputDirectory, g.ImageFilename)
outputExists, _ := utils.FileExists(outputPath)
if outputExists {
return nil
}
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 {
return err
} else {
if err := os.Rename(tmpOutputPath, outputPath); err != nil {
return err
}
logger.Debug("created video preview image: ", outputPath)
}
return nil
}
func (g *PreviewGenerator) getConcatFilePath() string {
return instance.Paths.Generated.GetTmpPath("files.txt")
}

View File

@@ -4,6 +4,7 @@ import (
"github.com/bmatcuk/doublestar" "github.com/bmatcuk/doublestar"
"github.com/stashapp/stash/logger" "github.com/stashapp/stash/logger"
"github.com/stashapp/stash/manager/paths" "github.com/stashapp/stash/manager/paths"
"github.com/stashapp/stash/models"
"path/filepath" "path/filepath"
"sync" "sync"
) )
@@ -85,10 +86,70 @@ func (s *singleton) Export() {
}() }()
} }
func (s *singleton) Generate(sprites bool, previews bool, markers bool, transcodes bool) {
if s.Status != Idle { return }
s.Status = Generate
qb := models.NewSceneQueryBuilder()
//this.job.total = await ObjectionUtils.getCount(Scene);
instance.Paths.Generated.EnsureTmpDir()
go func() {
defer s.returnToIdleState()
scenes, err := qb.All()
if err != nil {
logger.Errorf("failed to get scenes for generate")
return
}
delta := btoi(sprites) + btoi(previews) + btoi(markers) + btoi(transcodes)
var wg sync.WaitGroup
for _, scene := range scenes {
wg.Add(delta)
if sprites {
go func() {
wg.Done() // TODO
}()
}
if previews {
task := GeneratePreviewTask{Scene: scene}
go task.Start(&wg)
}
if markers {
go func() {
wg.Done() // TODO
}()
}
if transcodes {
go func() {
wg.Done() // TODO
}()
}
wg.Wait()
}
}()
}
func (s *singleton) returnToIdleState() { func (s *singleton) returnToIdleState() {
if r := recover(); r!= nil { if r := recover(); r!= nil {
logger.Info("recovered from ", r) logger.Info("recovered from ", r)
} }
if s.Status == Generate {
instance.Paths.Generated.RemoveTmpDir()
}
s.Status = Idle s.Status = Idle
} }
func btoi(b bool) int {
if b {
return 1
}
return 0
}

View File

@@ -0,0 +1,58 @@
package manager
import (
"github.com/stashapp/stash/ffmpeg"
"github.com/stashapp/stash/logger"
"github.com/stashapp/stash/models"
"github.com/stashapp/stash/utils"
"sync"
)
type GeneratePreviewTask struct {
Scene models.Scene
}
func (t *GeneratePreviewTask) Start(wg *sync.WaitGroup) {
videoFilename := t.videoFilename()
imageFilename := t.imageFilename()
if t.doesPreviewExist(videoFilename, imageFilename) {
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
}
generator, err := NewPreviewGenerator(*videoFile, videoFilename, imageFilename, instance.Paths.Generated.Screenshots)
if err != nil {
logger.Errorf("error creating preview generator: %s", err.Error())
wg.Done()
return
}
if err := generator.Generate(); err != nil {
logger.Errorf("error generating preview: %s", err.Error())
wg.Done()
return
}
wg.Done()
}
func (t *GeneratePreviewTask) doesPreviewExist(videoFilename string, imageFilename string) bool {
videoExists, _ := utils.FileExists(instance.Paths.Scene.GetStreamPreviewPath(videoFilename))
imageExists, _ := utils.FileExists(instance.Paths.Scene.GetStreamPreviewImagePath(imageFilename))
return videoExists && imageExists
}
func (t *GeneratePreviewTask) videoFilename() string {
return t.Scene.Checksum + ".mp4"
}
func (t *GeneratePreviewTask) imageFilename() string {
return t.Scene.Checksum + ".webp"
}

View File

@@ -70,8 +70,7 @@ func (t *ScanTask) scanGallery() {
} }
func (t *ScanTask) scanScene() { func (t *ScanTask) scanScene() {
ffprobe := ffmpeg.NewFFProbe(instance.Paths.FixedPaths.FFProbe) videoFile, err := ffmpeg.NewVideoFile(instance.Paths.FixedPaths.FFProbe, t.FilePath)
ffprobeResult, err := ffprobe.ProbeVideo(t.FilePath)
if err != nil { if err != nil {
logger.Error(err.Error()) logger.Error(err.Error())
return return
@@ -90,7 +89,7 @@ func (t *ScanTask) scanScene() {
return return
} }
t.makeScreenshots(*ffprobeResult, checksum) t.makeScreenshots(*videoFile, checksum)
scene, _ = qb.FindByChecksum(checksum) scene, _ = qb.FindByChecksum(checksum)
ctx := context.TODO() ctx := context.TODO()
@@ -105,14 +104,14 @@ func (t *ScanTask) scanScene() {
newScene := models.Scene{ newScene := models.Scene{
Checksum: checksum, Checksum: checksum,
Path: t.FilePath, Path: t.FilePath,
Duration: sql.NullFloat64{Float64: ffprobeResult.Duration, Valid: true }, Duration: sql.NullFloat64{Float64: videoFile.Duration, Valid: true },
VideoCodec: sql.NullString{ String: ffprobeResult.VideoCodec, Valid: true}, VideoCodec: sql.NullString{ String: videoFile.VideoCodec, Valid: true},
AudioCodec: sql.NullString{ String: ffprobeResult.AudioCodec, Valid: true}, AudioCodec: sql.NullString{ String: videoFile.AudioCodec, Valid: true},
Width: sql.NullInt64{ Int64: int64(ffprobeResult.Width), Valid: true }, Width: sql.NullInt64{ Int64: int64(videoFile.Width), Valid: true },
Height: sql.NullInt64{ Int64: int64(ffprobeResult.Height), Valid: true }, Height: sql.NullInt64{ Int64: int64(videoFile.Height), Valid: true },
Framerate: sql.NullFloat64{ Float64: ffprobeResult.FrameRate, Valid: true }, Framerate: sql.NullFloat64{ Float64: videoFile.FrameRate, Valid: true },
Bitrate: sql.NullInt64{ Int64: ffprobeResult.Bitrate, Valid: true }, Bitrate: sql.NullInt64{ Int64: videoFile.Bitrate, Valid: true },
Size: sql.NullString{ String: strconv.Itoa(int(ffprobeResult.Size)), Valid: true }, Size: sql.NullString{ String: strconv.Itoa(int(videoFile.Size)), Valid: true },
CreatedAt: models.SQLiteTimestamp{ Timestamp: currentTime }, CreatedAt: models.SQLiteTimestamp{ Timestamp: currentTime },
UpdatedAt: models.SQLiteTimestamp{ Timestamp: currentTime }, UpdatedAt: models.SQLiteTimestamp{ Timestamp: currentTime },
} }
@@ -127,7 +126,7 @@ func (t *ScanTask) scanScene() {
} }
} }
func (t *ScanTask) makeScreenshots(probeResult ffmpeg.FFProbeResult, checksum string) { func (t *ScanTask) makeScreenshots(probeResult ffmpeg.VideoFile, checksum string) {
thumbPath := instance.Paths.Scene.GetThumbnailScreenshotPath(checksum) thumbPath := instance.Paths.Scene.GetThumbnailScreenshotPath(checksum)
normalPath := instance.Paths.Scene.GetScreenshotPath(checksum) normalPath := instance.Paths.Scene.GetScreenshotPath(checksum)
@@ -142,7 +141,7 @@ func (t *ScanTask) makeScreenshots(probeResult ffmpeg.FFProbeResult, checksum st
t.makeScreenshot(probeResult, normalPath, 2, probeResult.Width) t.makeScreenshot(probeResult, normalPath, 2, probeResult.Width)
} }
func (t *ScanTask) makeScreenshot(probeResult ffmpeg.FFProbeResult, outputPath string, quality int, width int) { func (t *ScanTask) makeScreenshot(probeResult ffmpeg.VideoFile, outputPath string, quality int, width int) {
encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG) encoder := ffmpeg.NewEncoder(instance.Paths.FixedPaths.FFMPEG)
options := ffmpeg.ScreenshotOptions{ options := ffmpeg.ScreenshotOptions{
OutputPath: outputPath, OutputPath: outputPath,