mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Preview generation fallback (#725)
* Added preview generation fallback feature. When a preview generation fails (often for wmv/avi files), the new code tries with less stricted (no xerror) and more time consuming options (slow+fast seek). Fix a minor issue when stash downloads ffmpeg/ffprobe, but doesn't re-detect their paths.
This commit is contained in:
parent
44c32a91d3
commit
ecc42e4e24
6 changed files with 72 additions and 14 deletions
4
.gitignore
vendored
4
.gitignore
vendored
|
|
@ -48,6 +48,9 @@ ui/v2.5/src/core/generated-*.tsx
|
||||||
.idea/**/uiDesigner.xml
|
.idea/**/uiDesigner.xml
|
||||||
.idea/**/dbnavigator.xml
|
.idea/**/dbnavigator.xml
|
||||||
|
|
||||||
|
# Goland Junk
|
||||||
|
pkg/pkg
|
||||||
|
|
||||||
####
|
####
|
||||||
# Random
|
# Random
|
||||||
####
|
####
|
||||||
|
|
@ -58,3 +61,4 @@ node_modules
|
||||||
|
|
||||||
stash
|
stash
|
||||||
dist
|
dist
|
||||||
|
.DS_Store
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,51 @@ type ScenePreviewChunkOptions struct {
|
||||||
OutputPath string
|
OutputPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions, preset string) {
|
func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePreviewChunkOptions, preset string, fallback bool) error {
|
||||||
|
var fastSeek float64
|
||||||
|
var slowSeek float64
|
||||||
|
fallbackMinSlowSeek := 20.0
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-v", "error",
|
"-v", "error",
|
||||||
"-xerror",
|
}
|
||||||
"-ss", strconv.FormatFloat(options.StartTime, 'f', 2, 64),
|
|
||||||
"-i", probeResult.Path,
|
// Non-fallback: enable xerror.
|
||||||
|
// "-xerror" causes ffmpeg to fail on warnings, often the preview is fine but could be broken.
|
||||||
|
if !fallback {
|
||||||
|
args = append(args, "-xerror")
|
||||||
|
fastSeek = options.StartTime
|
||||||
|
slowSeek = 0
|
||||||
|
} else {
|
||||||
|
// In fallback mode, disable "-xerror" and try a combination of fast/slow seek instead of just fastseek
|
||||||
|
// Commonly with avi/wmv ffmpeg doesn't seem to always predict the right start point to begin decoding when
|
||||||
|
// using fast seek. If you force ffmpeg to decode more, it avoids the "blocky green artifact" issue.
|
||||||
|
if options.StartTime > fallbackMinSlowSeek {
|
||||||
|
// Handle seeks longer than fallbackMinSlowSeek with fast/slow seeks
|
||||||
|
// Allow for at least fallbackMinSlowSeek seconds of slow seek
|
||||||
|
fastSeek = options.StartTime - fallbackMinSlowSeek
|
||||||
|
slowSeek = fallbackMinSlowSeek
|
||||||
|
} else {
|
||||||
|
// Handle seeks shorter than fallbackMinSlowSeek with only slow seeks.
|
||||||
|
slowSeek = options.StartTime
|
||||||
|
fastSeek = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fastSeek > 0 {
|
||||||
|
args = append(args, "-ss")
|
||||||
|
args = append(args, strconv.FormatFloat(fastSeek, 'f', 2, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
args = append(args, "-i")
|
||||||
|
args = append(args, probeResult.Path)
|
||||||
|
|
||||||
|
if slowSeek > 0 {
|
||||||
|
args = append(args, "-ss")
|
||||||
|
args = append(args, strconv.FormatFloat(slowSeek, 'f', 2, 64))
|
||||||
|
}
|
||||||
|
|
||||||
|
args2 := []string{
|
||||||
"-t", strconv.FormatFloat(options.Duration, 'f', 2, 64),
|
"-t", strconv.FormatFloat(options.Duration, 'f', 2, 64),
|
||||||
"-max_muxing_queue_size", "1024", // https://trac.ffmpeg.org/ticket/6375
|
"-max_muxing_queue_size", "1024", // https://trac.ffmpeg.org/ticket/6375
|
||||||
"-y",
|
"-y",
|
||||||
|
|
@ -36,10 +75,14 @@ func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePre
|
||||||
"-strict", "-2",
|
"-strict", "-2",
|
||||||
options.OutputPath,
|
options.OutputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
|
||||||
|
finalArgs := append(args, args2...)
|
||||||
|
|
||||||
|
_, err := e.run(probeResult, finalArgs)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFilePath string, outputPath string) {
|
func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFilePath string, outputPath string) error {
|
||||||
args := []string{
|
args := []string{
|
||||||
"-v", "error",
|
"-v", "error",
|
||||||
"-f", "concat",
|
"-f", "concat",
|
||||||
|
|
@ -48,7 +91,8 @@ func (e *Encoder) ScenePreviewVideoChunkCombine(probeResult VideoFile, concatFil
|
||||||
"-c", "copy",
|
"-c", "copy",
|
||||||
outputPath,
|
outputPath,
|
||||||
}
|
}
|
||||||
_, _ = e.run(probeResult, args)
|
_, err := e.run(probeResult, args)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Encoder) ScenePreviewVideoToImage(probeResult VideoFile, width int, videoPreviewPath string, outputPath string) error {
|
func (e *Encoder) ScenePreviewVideoToImage(probeResult VideoFile, width int, videoPreviewPath string, outputPath string) error {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ func (e *Encoder) Screenshot(probeResult VideoFile, options ScreenshotOptions) e
|
||||||
"-v", options.Verbosity,
|
"-v", options.Verbosity,
|
||||||
"-ss", fmt.Sprintf("%v", options.Time),
|
"-ss", fmt.Sprintf("%v", options.Time),
|
||||||
"-y",
|
"-y",
|
||||||
"-i", probeResult.Path, // TODO: Wrap in quotes?
|
"-i", probeResult.Path,
|
||||||
"-vframes", "1",
|
"-vframes", "1",
|
||||||
"-q:v", fmt.Sprintf("%v", options.Quality),
|
"-q:v", fmt.Sprintf("%v", options.Quality),
|
||||||
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
|
"-vf", fmt.Sprintf("scale=%v:-1", options.Width),
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,13 @@ func (g *PreviewGenerator) Generate() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if g.GenerateVideo {
|
if g.GenerateVideo {
|
||||||
if err := g.generateVideo(&encoder); err != nil {
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if g.GenerateImage {
|
if g.GenerateImage {
|
||||||
if err := g.generateImage(&encoder); err != nil {
|
if err := g.generateImage(&encoder); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -90,7 +93,7 @@ func (g *PreviewGenerator) generateConcatFile() error {
|
||||||
return w.Flush()
|
return w.Flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder, fallback bool) error {
|
||||||
outputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
outputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
||||||
outputExists, _ := utils.FileExists(outputPath)
|
outputExists, _ := utils.FileExists(outputPath)
|
||||||
if !g.Overwrite && outputExists {
|
if !g.Overwrite && outputExists {
|
||||||
|
|
@ -111,11 +114,15 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
|
||||||
Width: 640,
|
Width: 640,
|
||||||
OutputPath: chunkOutputPath,
|
OutputPath: chunkOutputPath,
|
||||||
}
|
}
|
||||||
encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options, g.PreviewPreset)
|
if err := encoder.ScenePreviewVideoChunk(g.Info.VideoFile, options, g.PreviewPreset, fallback); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
videoOutputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
videoOutputPath := filepath.Join(g.OutputDirectory, g.VideoFilename)
|
||||||
encoder.ScenePreviewVideoChunkCombine(g.Info.VideoFile, g.getConcatFilePath(), videoOutputPath)
|
if err := encoder.ScenePreviewVideoChunkCombine(g.Info.VideoFile, g.getConcatFilePath(), videoOutputPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
logger.Debug("created video preview: ", videoOutputPath)
|
logger.Debug("created video preview: ", videoOutputPath)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -145,10 +145,12 @@ The FFMPEG and FFProbe binaries should be placed in %s
|
||||||
The error was: %s
|
The error was: %s
|
||||||
`
|
`
|
||||||
logger.Fatalf(msg, configDirectory, err)
|
logger.Fatalf(msg, configDirectory, err)
|
||||||
|
} else {
|
||||||
|
// After download get new paths for ffmpeg and ffprobe
|
||||||
|
ffmpegPath, ffprobePath = ffmpeg.GetPaths(configDirectory)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: is this valid after download?
|
|
||||||
instance.FFMPEGPath = ffmpegPath
|
instance.FFMPEGPath = ffmpegPath
|
||||||
instance.FFProbePath = ffprobePath
|
instance.FFProbePath = ffprobePath
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ const markup = `
|
||||||
* Add support for parent/child studios.
|
* Add support for parent/child studios.
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Make preview generation more fault-tolerant.
|
||||||
* Allow clearing of images and querying on missing images.
|
* Allow clearing of images and querying on missing images.
|
||||||
* Allow free-editing of scene movie number.
|
* Allow free-editing of scene movie number.
|
||||||
* Allow adding performers and studios from selectors.
|
* Allow adding performers and studios from selectors.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue