From b67abb89ff5231b12551c2d7983a360f39a1a5e7 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 27 Jan 2023 11:31:11 +1100 Subject: [PATCH] Allow configuration of ffmpeg args (#3216) * Allow configuration of ffmpeg args * Add UI settings for ffmpeg config * Add changelog entry * Add documentation in manual --- graphql/documents/data/config.graphql | 4 +++ graphql/schema/types/config.graphql | 30 ++++++++++++++++++ internal/api/resolver_mutation_configure.go | 13 ++++++++ internal/api/resolver_query_configuration.go | 4 +++ internal/api/routes_scene.go | 6 +++- internal/manager/config/config.go | 22 +++++++++++++ internal/manager/generator_sprite.go | 7 +++-- internal/manager/manager.go | 7 +++-- internal/manager/task_generate.go | 11 ++++--- internal/manager/task_generate_screenshot.go | 9 +++--- internal/manager/task_scan.go | 11 ++++--- pkg/ffmpeg/stream.go | 8 +++++ pkg/ffmpeg/transcoder/transcode.go | 8 +++++ pkg/scene/generate/generator.go | 16 +++++++--- pkg/scene/generate/preview.go | 6 ++++ pkg/scene/generate/transcode.go | 6 ++++ .../Settings/SettingsSystemPanel.tsx | 31 +++++++++++++++++++ ui/v2.5/src/docs/en/Changelog/v0190.md | 1 + ui/v2.5/src/docs/en/Manual/Configuration.md | 8 +++++ ui/v2.5/src/locales/en-GB.json | 22 +++++++++++++ 20 files changed, 204 insertions(+), 26 deletions(-) diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 57f08eb41..b5e377ec5 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -46,6 +46,10 @@ fragment ConfigGeneralData on ConfigGeneralResult { api_key } pythonPath + transcodeInputArgs + transcodeOutputArgs + liveTranscodeInputArgs + liveTranscodeOutputArgs } fragment ConfigInterfaceData on ConfigInterfaceResult { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 7cd1fea5f..0b17c1c01 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -69,6 +69,21 @@ input ConfigGeneralInput { maxTranscodeSize: StreamingResolutionEnum """Max streaming transcode size""" maxStreamingTranscodeSize: StreamingResolutionEnum + + """ffmpeg transcode input args - injected before input file + These are applied to generated transcodes (previews and transcodes)""" + transcodeInputArgs: [String!] + """ffmpeg transcode output args - injected before output file + These are applied to generated transcodes (previews and transcodes)""" + transcodeOutputArgs: [String!] + + """ffmpeg stream input args - injected before input file + These are applied when live transcoding""" + liveTranscodeInputArgs: [String!] + """ffmpeg stream output args - injected before output file + These are applied when live transcoding""" + liveTranscodeOutputArgs: [String!] + """Write image thumbnails to disk when generating on the fly""" writeImageThumbnails: Boolean """Username""" @@ -152,6 +167,21 @@ type ConfigGeneralResult { maxTranscodeSize: StreamingResolutionEnum """Max streaming transcode size""" maxStreamingTranscodeSize: StreamingResolutionEnum + + """ffmpeg transcode input args - injected before input file + These are applied to generated transcodes (previews and transcodes)""" + transcodeInputArgs: [String!]! + """ffmpeg transcode output args - injected before output file + These are applied to generated transcodes (previews and transcodes)""" + transcodeOutputArgs: [String!]! + + """ffmpeg stream input args - injected before input file + These are applied when live transcoding""" + liveTranscodeInputArgs: [String!]! + """ffmpeg stream output args - injected before output file + These are applied when live transcoding""" + liveTranscodeOutputArgs: [String!]! + """Write image thumbnails to disk when generating on the fly""" writeImageThumbnails: Boolean! """API Key""" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 96891d69d..29dec27e0 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -280,6 +280,19 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen c.Set(config.PythonPath, input.PythonPath) } + if input.TranscodeInputArgs != nil { + c.Set(config.TranscodeInputArgs, input.TranscodeInputArgs) + } + if input.TranscodeOutputArgs != nil { + c.Set(config.TranscodeOutputArgs, input.TranscodeOutputArgs) + } + if input.LiveTranscodeInputArgs != nil { + c.Set(config.LiveTranscodeInputArgs, input.LiveTranscodeInputArgs) + } + if input.LiveTranscodeOutputArgs != nil { + c.Set(config.LiveTranscodeOutputArgs, input.LiveTranscodeOutputArgs) + } + if err := c.Write(); err != nil { return makeConfigGeneralResult(), err } diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index 941fb9a49..ebc51c64f 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -123,6 +123,10 @@ func makeConfigGeneralResult() *ConfigGeneralResult { ScraperCDPPath: &scraperCDPPath, StashBoxes: config.GetStashBoxes(), PythonPath: config.GetPythonPath(), + TranscodeInputArgs: config.GetTranscodeInputArgs(), + TranscodeOutputArgs: config.GetTranscodeOutputArgs(), + LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(), + LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(), } } diff --git a/internal/api/routes_scene.go b/internal/api/routes_scene.go index d1b1b02c8..62c693d61 100644 --- a/internal/api/routes_scene.go +++ b/internal/api/routes_scene.go @@ -185,6 +185,8 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st width := f.Width height := f.Height + config := config.GetInstance() + options := ffmpeg.TranscodeStreamOptions{ Input: f.Path, Codec: streamFormat, @@ -194,7 +196,9 @@ func (rs sceneRoutes) streamTranscode(w http.ResponseWriter, r *http.Request, st VideoHeight: height, StartTime: ss, - MaxTranscodeSize: config.GetInstance().GetMaxStreamingTranscodeSize().GetMaxResolution(), + MaxTranscodeSize: config.GetMaxStreamingTranscodeSize().GetMaxResolution(), + ExtraInputArgs: config.GetLiveTranscodeInputArgs(), + ExtraOutputArgs: config.GetLiveTranscodeOutputArgs(), } if requestedSize != "" { diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index 9ba6efbd2..c422b2670 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -60,6 +60,12 @@ const ( MaxTranscodeSize = "max_transcode_size" MaxStreamingTranscodeSize = "max_streaming_transcode_size" + // ffmpeg extra args options + TranscodeInputArgs = "ffmpeg.transcode.input_args" + TranscodeOutputArgs = "ffmpeg.transcode.output_args" + LiveTranscodeInputArgs = "ffmpeg.live_transcode.input_args" + LiveTranscodeOutputArgs = "ffmpeg.live_transcode.output_args" + ParallelTasks = "parallel_tasks" parallelTasksDefault = 1 @@ -786,6 +792,22 @@ func (i *Instance) GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum return models.StreamingResolutionEnum(ret) } +func (i *Instance) GetTranscodeInputArgs() []string { + return i.getStringSlice(TranscodeInputArgs) +} + +func (i *Instance) GetTranscodeOutputArgs() []string { + return i.getStringSlice(TranscodeOutputArgs) +} + +func (i *Instance) GetLiveTranscodeInputArgs() []string { + return i.getStringSlice(LiveTranscodeInputArgs) +} + +func (i *Instance) GetLiveTranscodeOutputArgs() []string { + return i.getStringSlice(LiveTranscodeOutputArgs) +} + // IsWriteImageThumbnails returns true if image thumbnails should be written // to disk after generating on the fly. func (i *Instance) IsWriteImageThumbnails() bool { diff --git a/internal/manager/generator_sprite.go b/internal/manager/generator_sprite.go index 47110462d..fa265cd56 100644 --- a/internal/manager/generator_sprite.go +++ b/internal/manager/generator_sprite.go @@ -75,9 +75,10 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO SlowSeek: slowSeek, Columns: cols, g: &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + ScenePaths: instance.Paths.Scene, }, }, nil } diff --git a/internal/manager/manager.go b/internal/manager/manager.go index 9f2492ea7..8ca32385f 100644 --- a/internal/manager/manager.go +++ b/internal/manager/manager.go @@ -292,9 +292,10 @@ type coverGenerator struct { func (g *coverGenerator) GenerateCover(ctx context.Context, scene *models.Scene, f *file.VideoFile) error { gg := generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + ScenePaths: instance.Paths.Scene, } return gg.Screenshot(ctx, f.Path, scene.GetHash(instance.Config.GetVideoFileNamingAlgorithm()), f.Width, f.Duration, generate.ScreenshotOptions{}) diff --git a/internal/manager/task_generate.go b/internal/manager/task_generate.go index 01cb5dac0..70ae85a9c 100644 --- a/internal/manager/task_generate.go +++ b/internal/manager/task_generate.go @@ -102,11 +102,12 @@ func (j *GenerateJob) Execute(ctx context.Context, progress *job.Progress) { } g := &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - MarkerPaths: instance.Paths.SceneMarkers, - ScenePaths: instance.Paths.Scene, - Overwrite: j.overwrite, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + MarkerPaths: instance.Paths.SceneMarkers, + ScenePaths: instance.Paths.Scene, + Overwrite: j.overwrite, } if err := j.txnManager.WithReadTxn(ctx, func(ctx context.Context) error { diff --git a/internal/manager/task_generate_screenshot.go b/internal/manager/task_generate_screenshot.go index c235d00b1..8b38e282a 100644 --- a/internal/manager/task_generate_screenshot.go +++ b/internal/manager/task_generate_screenshot.go @@ -44,10 +44,11 @@ func (t *GenerateScreenshotTask) Start(ctx context.Context) { logger.Debugf("Creating screenshot for %s", scenePath) g := generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - ScenePaths: instance.Paths.Scene, - Overwrite: true, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + ScenePaths: instance.Paths.Scene, + Overwrite: true, } if err := g.Screenshot(context.TODO(), videoFile.Path, checksum, videoFile.Width, videoFile.Duration, generate.ScreenshotOptions{ diff --git a/internal/manager/task_scan.go b/internal/manager/task_scan.go index 60140f9e7..d4209aa44 100644 --- a/internal/manager/task_scan.go +++ b/internal/manager/task_scan.go @@ -445,11 +445,12 @@ func (g *sceneGenerators) Generate(ctx context.Context, s *models.Scene, f *file options := getGeneratePreviewOptions(GeneratePreviewOptionsInput{}) g := &generate.Generator{ - Encoder: instance.FFMPEG, - LockManager: instance.ReadLockManager, - MarkerPaths: instance.Paths.SceneMarkers, - ScenePaths: instance.Paths.Scene, - Overwrite: overwrite, + Encoder: instance.FFMPEG, + FFMpegConfig: instance.Config, + LockManager: instance.ReadLockManager, + MarkerPaths: instance.Paths.SceneMarkers, + ScenePaths: instance.Paths.Scene, + Overwrite: overwrite, } taskPreview := GeneratePreviewTask{ diff --git a/pkg/ffmpeg/stream.go b/pkg/ffmpeg/stream.go index 1088778d1..5b248b664 100644 --- a/pkg/ffmpeg/stream.go +++ b/pkg/ffmpeg/stream.go @@ -144,11 +144,17 @@ type TranscodeStreamOptions struct { // in some videos where the audio codec is not supported by ffmpeg // ffmpeg fails if you try to transcode the audio VideoOnly bool + + // arguments added before the input argument + ExtraInputArgs []string + // arguments added before the output argument + ExtraOutputArgs []string } func (o TranscodeStreamOptions) getStreamArgs() Args { var args Args args = append(args, "-hide_banner") + args = append(args, o.ExtraInputArgs...) args = args.LogLevel(LogLevelError) if o.StartTime != 0 { @@ -184,6 +190,8 @@ func (o TranscodeStreamOptions) getStreamArgs() Args { "-ac", "2", ) + args = append(args, o.ExtraOutputArgs...) + args = args.Format(o.Codec.format) args = args.Output("pipe:") diff --git a/pkg/ffmpeg/transcoder/transcode.go b/pkg/ffmpeg/transcoder/transcode.go index 8be24c540..3a2d6a554 100644 --- a/pkg/ffmpeg/transcoder/transcode.go +++ b/pkg/ffmpeg/transcoder/transcode.go @@ -21,6 +21,11 @@ type TranscodeOptions struct { // Verbosity is the logging verbosity. Defaults to LogLevelError if not set. Verbosity ffmpeg.LogLevel + + // arguments added before the input argument + ExtraInputArgs []string + // arguments added before the output argument + ExtraOutputArgs []string } func (o *TranscodeOptions) setDefaults() { @@ -59,6 +64,7 @@ func Transcode(input string, options TranscodeOptions) ffmpeg.Args { var args ffmpeg.Args args = args.LogLevel(options.Verbosity).Overwrite() + args = append(args, options.ExtraInputArgs...) if options.XError { args = args.XError() @@ -92,6 +98,8 @@ func Transcode(input string, options TranscodeOptions) ffmpeg.Args { } args = args.AppendArgs(options.AudioArgs) + args = append(args, options.ExtraOutputArgs...) + args = args.Format(options.Format) args = args.Output(options.OutputPath) diff --git a/pkg/scene/generate/generator.go b/pkg/scene/generate/generator.go index a76c7ce84..7243ca0ff 100644 --- a/pkg/scene/generate/generator.go +++ b/pkg/scene/generate/generator.go @@ -47,12 +47,18 @@ type ScenePaths interface { GetTranscodePath(checksum string) string } +type FFMpegConfig interface { + GetTranscodeInputArgs() []string + GetTranscodeOutputArgs() []string +} + type Generator struct { - Encoder ffmpeg.FFMpeg - LockManager *fsutil.ReadLockManager - MarkerPaths MarkerPaths - ScenePaths ScenePaths - Overwrite bool + Encoder ffmpeg.FFMpeg + FFMpegConfig FFMpegConfig + LockManager *fsutil.ReadLockManager + MarkerPaths MarkerPaths + ScenePaths ScenePaths + Overwrite bool } type generateFn func(lockCtx *fsutil.LockContext, tmpFn string) error diff --git a/pkg/scene/generate/preview.go b/pkg/scene/generate/preview.go index ff7167f1d..fc82ebdc5 100644 --- a/pkg/scene/generate/preview.go +++ b/pkg/scene/generate/preview.go @@ -199,6 +199,9 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), } if options.Audio { @@ -299,6 +302,9 @@ func (g Generator) previewVideoToImage(input string) generateFn { VideoCodec: ffmpeg.VideoCodecLibWebP, VideoArgs: videoArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), } args := transcoder.Transcode(input, encodeOptions) diff --git a/pkg/scene/generate/transcode.go b/pkg/scene/generate/transcode.go index b772a735b..6528f91da 100644 --- a/pkg/scene/generate/transcode.go +++ b/pkg/scene/generate/transcode.go @@ -86,6 +86,9 @@ func (g Generator) transcode(input string, options TranscodeOptions) generateFn VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, AudioCodec: ffmpeg.AudioCodecAAC, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), }) return g.generate(lockCtx, args) @@ -117,6 +120,9 @@ func (g Generator) transcodeVideo(input string, options TranscodeOptions) genera VideoCodec: ffmpeg.VideoCodecLibX264, VideoArgs: videoArgs, AudioArgs: audioArgs, + + ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(), + ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(), }) return g.generate(lockCtx, args) diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index 46d02ba80..32c164c5b 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -7,6 +7,7 @@ import { ModalSetting, NumberSetting, SelectSetting, + StringListSetting, StringSetting, } from "./Inputs"; import { SettingStateContext } from "./context"; @@ -227,6 +228,36 @@ export const SettingsConfigurationPanel: React.FC = () => { ))} + + saveGeneral({ transcodeInputArgs: v })} + value={general.transcodeInputArgs ?? []} + /> + saveGeneral({ transcodeOutputArgs: v })} + value={general.transcodeOutputArgs ?? []} + /> + + saveGeneral({ liveTranscodeInputArgs: v })} + value={general.liveTranscodeInputArgs ?? []} + /> + saveGeneral({ liveTranscodeOutputArgs: v })} + value={general.liveTranscodeOutputArgs ?? []} + /> diff --git a/ui/v2.5/src/docs/en/Changelog/v0190.md b/ui/v2.5/src/docs/en/Changelog/v0190.md index 83b261e1c..7d708b3b8 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0190.md +++ b/ui/v2.5/src/docs/en/Changelog/v0190.md @@ -2,6 +2,7 @@ * Performer autotagging does not currently match on performer aliases. This will be addressed when finer control over the matching is implemented. ### ✨ New Features +* Added support for injecting arguments into `ffmpeg` during generation and live-transcoding. ([#3216](https://github.com/stashapp/stash/pull/3216)) * Added URL and Date fields to Images. ([#3015](https://github.com/stashapp/stash/pull/3015)) * Added support for plugins to add injected CSS and Javascript to the UI. ([#3195](https://github.com/stashapp/stash/pull/3195)) * Added disambiguation field to Performers, to differentiate between performers with the same name. ([#3113](https://github.com/stashapp/stash/pull/3113)) diff --git a/ui/v2.5/src/docs/en/Manual/Configuration.md b/ui/v2.5/src/docs/en/Manual/Configuration.md index 2d993cf9c..68037fa83 100644 --- a/ui/v2.5/src/docs/en/Manual/Configuration.md +++ b/ui/v2.5/src/docs/en/Manual/Configuration.md @@ -77,6 +77,14 @@ This setting can be used to increase/decrease overall CPU utilisation in two sce Note: If this is set too high it will decrease overall performance and causes failures (out of memory). +## ffmpeg arguments + +Additional arguments can be injected into ffmpeg when generating previews and sprites, and when live-transcoding videos. + +The ffmpeg arguments configuration is split into `Input` and `Output` arguments. Input arguments are injected before the input file argument, and output arguments are injected before the output file argument. + +Arguments are accepted as a list of strings. Each string is a separate argument. For example, a single argument of `-foo bar` would be treated as a single argument `"-foo bar"`. The correct way to pass this argument would be to split it into two separate arguments: `"-foo", "bar"`. + ## Scraping ### User Agent string diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 38ab19287..dc41b2e11 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -271,6 +271,28 @@ "excluded_image_gallery_patterns_head": "Excluded Image/Gallery Patterns", "excluded_video_patterns_desc": "Regexps of video files/paths to exclude from Scan and add to Clean", "excluded_video_patterns_head": "Excluded Video Patterns", + "ffmpeg": { + "transcode": { + "input_args": { + "heading": "FFmpeg Transcode Input Args", + "desc": "Advanced: Additional arguments to pass to ffmpeg before the input field when generating video." + }, + "output_args": { + "heading": "FFmpeg Transcode Output Args", + "desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when generating video." + } + }, + "live_transcode": { + "input_args": { + "heading": "FFmpeg LiveTranscode Input Args", + "desc": "Advanced: Additional arguments to pass to ffmpeg before the input field when live transcoding video." + }, + "output_args": { + "heading": "FFmpeg Live Transcode Output Args", + "desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when live transcoding video." + } + } + }, "gallery_ext_desc": "Comma-delimited list of file extensions that will be identified as gallery zip files.", "gallery_ext_head": "Gallery zip Extensions", "generated_file_naming_hash_desc": "Use MD5 or oshash for generated file naming. Changing this requires that all scenes have the applicable MD5/oshash value populated. After changing this value, existing generated files will need to be migrated or regenerated. See Tasks page for migration.",