Restructure ffmpeg (#2392)

* Refactor transcode generation
* Move phash generation into separate package
* Refactor image thumbnail generation
* Move JSONTime to separate package
* Ffmpeg refactoring
* Refactor live transcoding
* Refactor scene marker preview generation
* Refactor preview generation
* Refactor screenshot generation
* Refactor sprite generation
* Change ffmpeg.IsStreamable to return error
* Move frame rate calculation into ffmpeg
* Refactor file locking
* Refactor title set during scan
* Add missing lockmanager instance
* Return error instead of logging in MatchContainer
This commit is contained in:
WithoutPants
2022-04-18 10:50:10 +10:00
committed by GitHub
parent cdaa191155
commit aacf07feef
89 changed files with 3208 additions and 2004 deletions

View File

@@ -0,0 +1,38 @@
package transcoder
import (
"errors"
"github.com/stashapp/stash/pkg/ffmpeg"
)
var ErrUnsupportedFormat = errors.New("unsupported image format")
type ImageThumbnailOptions struct {
InputFormat ffmpeg.ImageFormat
OutputPath string
MaxDimensions int
Quality int
}
func ImageThumbnail(input string, options ImageThumbnailOptions) ffmpeg.Args {
var videoFilter ffmpeg.VideoFilter
videoFilter = videoFilter.ScaleMaxSize(options.MaxDimensions)
var args ffmpeg.Args
args = args.Overwrite().
ImageFormat(options.InputFormat).
Input(input).
VideoFilter(videoFilter).
VideoCodec(ffmpeg.VideoCodecMJpeg)
if options.Quality > 0 {
args = args.FixedQualityScaleVideo(options.Quality)
}
args = args.ImageFormat(ffmpeg.ImageFormatImage2Pipe).
Output(options.OutputPath)
return args
}

View File

@@ -0,0 +1,109 @@
package transcoder
import "github.com/stashapp/stash/pkg/ffmpeg"
type ScreenshotOptions struct {
OutputPath string
OutputType ScreenshotOutputType
// Quality is the quality scale. See https://ffmpeg.org/ffmpeg.html#Main-options
Quality int
Width int
// Verbosity is the logging verbosity. Defaults to LogLevelError if not set.
Verbosity ffmpeg.LogLevel
UseSelectFilter bool
}
func (o *ScreenshotOptions) setDefaults() {
if o.Verbosity == "" {
o.Verbosity = ffmpeg.LogLevelError
}
}
type ScreenshotOutputType struct {
codec ffmpeg.VideoCodec
format ffmpeg.Format
}
func (t ScreenshotOutputType) Args() []string {
var ret []string
if t.codec != "" {
ret = append(ret, t.codec.Args()...)
}
if t.format != "" {
ret = append(ret, t.format.Args()...)
}
return ret
}
var (
ScreenshotOutputTypeImage2 = ScreenshotOutputType{
format: "image2",
}
ScreenshotOutputTypeBMP = ScreenshotOutputType{
codec: ffmpeg.VideoCodecBMP,
format: "rawvideo",
}
)
func ScreenshotTime(input string, t float64, options ScreenshotOptions) ffmpeg.Args {
options.setDefaults()
var args ffmpeg.Args
args = args.LogLevel(options.Verbosity)
args = args.Overwrite()
args = args.Seek(t)
args = args.Input(input)
args = args.VideoFrames(1)
if options.Quality > 0 {
args = args.FixedQualityScaleVideo(options.Quality)
}
var vf ffmpeg.VideoFilter
if options.Width > 0 {
vf = vf.ScaleWidth(options.Width)
args = args.VideoFilter(vf)
}
args = args.AppendArgs(options.OutputType)
args = args.Output(options.OutputPath)
return args
}
// ScreenshotFrame uses the select filter to get a single frame from the video.
// It is very slow and should only be used for files with very small duration in secs / frame count.
func ScreenshotFrame(input string, frame int, options ScreenshotOptions) ffmpeg.Args {
options.setDefaults()
var args ffmpeg.Args
args = args.LogLevel(options.Verbosity)
args = args.Overwrite()
args = args.Input(input)
args = args.VideoFrames(1)
args = args.VSync(ffmpeg.VSyncMethodPassthrough)
var vf ffmpeg.VideoFilter
// keep only frame number options.Frame)
vf = vf.Select(frame)
if options.Width > 0 {
vf = vf.ScaleWidth(options.Width)
}
args = args.VideoFilter(vf)
args = args.AppendArgs(options.OutputType)
args = args.Output(options.OutputPath)
return args
}

View File

@@ -0,0 +1,67 @@
package transcoder
import (
"runtime"
"strings"
"github.com/stashapp/stash/pkg/ffmpeg"
)
type SpliceOptions struct {
OutputPath string
Format ffmpeg.Format
VideoCodec ffmpeg.VideoCodec
VideoArgs ffmpeg.Args
AudioCodec ffmpeg.AudioCodec
AudioArgs ffmpeg.Args
// Verbosity is the logging verbosity. Defaults to LogLevelError if not set.
Verbosity ffmpeg.LogLevel
}
func (o *SpliceOptions) setDefaults() {
if o.Verbosity == "" {
o.Verbosity = ffmpeg.LogLevelError
}
}
// fixWindowsPath replaces \ with / in the given path because the \ isn't recognized as valid on windows ffmpeg
func fixWindowsPath(str string) string {
if runtime.GOOS == "windows" {
return strings.ReplaceAll(str, `\`, "/")
}
return str
}
func Splice(concatFile string, options SpliceOptions) ffmpeg.Args {
options.setDefaults()
var args ffmpeg.Args
args = args.LogLevel(options.Verbosity)
args = args.Format(ffmpeg.FormatConcat)
args = args.Input(fixWindowsPath(concatFile))
args = args.Overwrite()
// if video codec is not provided, then use copy
if options.VideoCodec == "" {
options.VideoCodec = ffmpeg.VideoCodecCopy
}
args = args.VideoCodec(options.VideoCodec)
args = args.AppendArgs(options.VideoArgs)
// if audio codec is not provided, then skip it
if options.AudioCodec == "" {
args = args.SkipAudio()
} else {
args = args.AudioCodec(options.AudioCodec)
}
args = args.AppendArgs(options.AudioArgs)
args = args.Format(options.Format)
args = args.Output(options.OutputPath)
return args
}

View File

@@ -0,0 +1,99 @@
package transcoder
import "github.com/stashapp/stash/pkg/ffmpeg"
type TranscodeOptions struct {
OutputPath string
Format ffmpeg.Format
VideoCodec ffmpeg.VideoCodec
VideoArgs ffmpeg.Args
AudioCodec ffmpeg.AudioCodec
AudioArgs ffmpeg.Args
// if XError is true, then ffmpeg will fail on warnings
XError bool
StartTime float64
SlowSeek bool
Duration float64
// Verbosity is the logging verbosity. Defaults to LogLevelError if not set.
Verbosity ffmpeg.LogLevel
}
func (o *TranscodeOptions) setDefaults() {
if o.Verbosity == "" {
o.Verbosity = ffmpeg.LogLevelError
}
}
func Transcode(input string, options TranscodeOptions) ffmpeg.Args {
options.setDefaults()
// TODO - this should probably be generalised and applied to all operations. Need to verify impact on phash algorithm.
const fallbackMinSlowSeek = 20.0
var fastSeek float64
var slowSeek float64
if !options.SlowSeek {
fastSeek = options.StartTime
slowSeek = 0
} else {
// In slowseek mode, 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
}
}
var args ffmpeg.Args
args = args.LogLevel(options.Verbosity).Overwrite()
if options.XError {
args = args.XError()
}
if fastSeek > 0 {
args = args.Seek(fastSeek)
}
args = args.Input(input)
if slowSeek > 0 {
args = args.Seek(slowSeek)
}
if options.Duration > 0 {
args = args.Duration(options.Duration)
}
// https://trac.ffmpeg.org/ticket/6375
args = args.MaxMuxingQueueSize(1024)
args = args.VideoCodec(options.VideoCodec)
args = args.AppendArgs(options.VideoArgs)
// if audio codec is not provided, then skip it
if options.AudioCodec == "" {
args = args.SkipAudio()
} else {
args = args.AudioCodec(options.AudioCodec)
}
args = args.AppendArgs(options.AudioArgs)
args = args.Format(options.Format)
args = args.Output(options.OutputPath)
return args
}