mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
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:
38
pkg/ffmpeg/transcoder/image.go
Normal file
38
pkg/ffmpeg/transcoder/image.go
Normal 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
|
||||
}
|
||||
109
pkg/ffmpeg/transcoder/screenshot.go
Normal file
109
pkg/ffmpeg/transcoder/screenshot.go
Normal 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
|
||||
}
|
||||
67
pkg/ffmpeg/transcoder/splice.go
Normal file
67
pkg/ffmpeg/transcoder/splice.go
Normal 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
|
||||
}
|
||||
99
pkg/ffmpeg/transcoder/transcode.go
Normal file
99
pkg/ffmpeg/transcoder/transcode.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user