diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 6990d9d95..5ab7fdfea 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -184,6 +184,18 @@ input ConfigGeneralInput { scraperPackageSources: [PackageSourceInput!] "Source of plugin packages" pluginPackageSources: [PackageSourceInput!] + + "Size of the longest dimension for each sprite in pixels" + spriteScreenshotSize: Int + + "True if sprite generation should use the sprite interval and min/max sprites settings instead of the default" + useCustomSpriteInterval: Boolean + "Time between two different scrubber sprites in seconds - only used if useCustomSpriteInterval is true" + spriteInterval: Float + "Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true" + minimumSprites: Int + "Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true" + maximumSprites: Int } type ConfigGeneralResult { @@ -287,6 +299,16 @@ type ConfigGeneralResult { logAccess: Boolean! "Maximum log size" logFileMaxSize: Int! + "True if sprite generation should use the sprite interval and min/max sprites settings instead of the default" + useCustomSpriteInterval: Boolean! + "Time between two different scrubber sprites in seconds - only used if useCustomSpriteInterval is true" + spriteInterval: Float! + "Minimum number of sprites to be generated - only used if useCustomSpriteInterval is true" + minimumSprites: Int! + "Maximum number of sprites to be generated - only used if useCustomSpriteInterval is true" + maximumSprites: Int! + "Size of the longest dimension for each sprite in pixels" + spriteScreenshotSize: Int! "Array of video file extensions" videoExtensions: [String!]! "Array of image file extensions" diff --git a/internal/api/resolver_mutation_configure.go b/internal/api/resolver_mutation_configure.go index 23b61c208..718d24998 100644 --- a/internal/api/resolver_mutation_configure.go +++ b/internal/api/resolver_mutation_configure.go @@ -287,6 +287,11 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen if input.PreviewPreset != nil { c.SetString(config.PreviewPreset, input.PreviewPreset.String()) } + r.setConfigBool(config.UseCustomSpriteInterval, input.UseCustomSpriteInterval) + r.setConfigFloat(config.SpriteInterval, input.SpriteInterval) + r.setConfigInt(config.MinimumSprites, input.MinimumSprites) + r.setConfigInt(config.MaximumSprites, input.MaximumSprites) + r.setConfigInt(config.SpriteScreenshotSize, input.SpriteScreenshotSize) r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration) if input.MaxTranscodeSize != nil { diff --git a/internal/api/resolver_query_configuration.go b/internal/api/resolver_query_configuration.go index bc76212eb..cf2c0e3cc 100644 --- a/internal/api/resolver_query_configuration.go +++ b/internal/api/resolver_query_configuration.go @@ -96,6 +96,11 @@ func makeConfigGeneralResult() *ConfigGeneralResult { CalculateMd5: config.IsCalculateMD5(), VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(), ParallelTasks: config.GetParallelTasks(), + UseCustomSpriteInterval: config.GetUseCustomSpriteInterval(), + SpriteInterval: config.GetSpriteInterval(), + SpriteScreenshotSize: config.GetSpriteScreenshotSize(), + MinimumSprites: config.GetMinimumSprites(), + MaximumSprites: config.GetMaximumSprites(), PreviewAudio: config.GetPreviewAudio(), PreviewSegments: config.GetPreviewSegments(), PreviewSegmentDuration: config.GetPreviewSegmentDuration(), diff --git a/internal/manager/config/config.go b/internal/manager/config/config.go index bb99bdcfc..19e263810 100644 --- a/internal/manager/config/config.go +++ b/internal/manager/config/config.go @@ -83,6 +83,21 @@ const ( ParallelTasks = "parallel_tasks" parallelTasksDefault = 1 + UseCustomSpriteInterval = "use_custom_sprite_interval" + UseCustomSpriteIntervalDefault = false + + SpriteInterval = "sprite_interval" + SpriteIntervalDefault = 30 + + MinimumSprites = "minimum_sprites" + MinimumSpritesDefault = 10 + + MaximumSprites = "maximum_sprites" + MaximumSpritesDefault = 500 + + SpriteScreenshotSize = "sprite_screenshot_width" + spriteScreenshotSizeDefault = 160 + PreviewPreset = "preview_preset" TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration" @@ -975,6 +990,50 @@ func (i *Config) GetParallelTasksWithAutoDetection() int { return parallelTasks } +// GetUseCustomSpriteInterval returns true if the sprite minimum, maximum, and interval settings +// should be used instead of the default +func (i *Config) GetUseCustomSpriteInterval() bool { + value := i.getBool(UseCustomSpriteInterval) + return value +} + +// GetSpriteInterval returns the time (in seconds) to be between each scrubber sprite +// A value of 0 indicates that the sprite interval should be automatically determined +// based on the minimum sprite setting. +func (i *Config) GetSpriteInterval() float64 { + value := i.getFloat64(SpriteInterval) + return value +} + +// GetMinimumSprites returns the minimum number of sprites that have to be generated +// A value of 0 will be overridden with the default of 10. +func (i *Config) GetMinimumSprites() int { + value := i.getInt(MinimumSprites) + if value <= 0 { + return MinimumSpritesDefault + } + return value +} + +// GetMaximumSprites returns the maximum number of sprites that can be generated +// A value of 0 indicates no maximum. +func (i *Config) GetMaximumSprites() int { + value := i.getInt(MaximumSprites) + return value +} + +// GetSpriteScreenshotSize returns the required size of the screenshots to be taken +// during sprite generation in pixels. This will be the width for landscape scenes +// and the height for portrait scenes, with the other dimension being scaled to maintain +// the aspect ratio. If the value is less than or equal to 0, the default will be used. +func (i *Config) GetSpriteScreenshotSize() int { + value := i.getInt(SpriteScreenshotSize) + if value <= 0 { + return spriteScreenshotSizeDefault + } + return value +} + func (i *Config) GetPreviewAudio() bool { return i.getBool(PreviewAudio) } @@ -1861,6 +1920,12 @@ func (i *Config) setDefaultValues() { i.setDefault(PreviewAudio, previewAudioDefault) i.setDefault(SoundOnPreview, false) + i.setDefault(UseCustomSpriteInterval, UseCustomSpriteIntervalDefault) + i.setDefault(SpriteInterval, SpriteIntervalDefault) + i.setDefault(MinimumSprites, MinimumSpritesDefault) + i.setDefault(MaximumSprites, MaximumSpritesDefault) + i.setDefault(SpriteScreenshotSize, spriteScreenshotSizeDefault) + i.setDefault(ThemeColor, DefaultThemeColor) i.setDefault(WriteImageThumbnails, writeImageThumbnailsDefault) diff --git a/internal/manager/generator_sprite.go b/internal/manager/generator_sprite.go index c28d28674..dc56fde88 100644 --- a/internal/manager/generator_sprite.go +++ b/internal/manager/generator_sprite.go @@ -21,8 +21,7 @@ type SpriteGenerator struct { VideoChecksum string ImageOutputPath string VTTOutputPath string - Rows int - Columns int + Config SpriteGeneratorConfig SlowSeek bool // use alternate seek function, very slow! Overwrite bool @@ -30,13 +29,81 @@ type SpriteGenerator struct { g *generate.Generator } -func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageOutputPath string, vttOutputPath string, rows int, cols int) (*SpriteGenerator, error) { +// SpriteGeneratorConfig holds configuration for the SpriteGenerator +type SpriteGeneratorConfig struct { + // MinimumSprites is the minimum number of sprites to generate, even if the video duration is short + // SpriteInterval will be adjusted accordingly to ensure at least this many sprites are generated. + // A value of 0 means no minimum, and the generator will use the provided SpriteInterval or + // calculate it based on the video duration and MaximumSprites + MinimumSprites int + + // MaximumSprites is the maximum number of sprites to generate, even if the video duration is long + // SpriteInterval will be adjusted accordingly to ensure no more than this many sprites are generated + // A value of 0 means no maximum, and the generator will use the provided SpriteInterval or + // calculate it based on the video duration and MinimumSprites + MaximumSprites int + + // SpriteInterval is the default interval in seconds between each sprite. + // If MinimumSprites or MaximumSprites are set, this value will be adjusted accordingly + // to ensure the desired number of sprites are generated + // A value of 0 means the generator will calculate the interval based on the video duration and + // the provided MinimumSprites and MaximumSprites + SpriteInterval float64 + + // SpriteSize is the size in pixels of the longest dimension of each sprite image. + // The other dimension will be automatically calculated to maintain the aspect ratio of the video + SpriteSize int +} + +const ( + // DefaultSpriteAmount is the default number of sprites to generate if no configuration is provided + // This corresponds to the legacy behavior of the generator, which generates 81 sprites at equal + // intervals across the video duration + DefaultSpriteAmount = 81 + + // DefaultSpriteSize is the default size in pixels of the longest dimension of each sprite image + // if no configuration is provided. This corresponds to the legacy behavior of the generator. + DefaultSpriteSize = 160 +) + +var DefaultSpriteGeneratorConfig = SpriteGeneratorConfig{ + MinimumSprites: DefaultSpriteAmount, + MaximumSprites: DefaultSpriteAmount, + SpriteInterval: 0, + SpriteSize: DefaultSpriteSize, +} + +// NewSpriteGenerator creates a new SpriteGenerator for the given video file and configuration +// It calculates the appropriate sprite interval and count based on the video duration and the provided configuration +func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageOutputPath string, vttOutputPath string, config SpriteGeneratorConfig) (*SpriteGenerator, error) { exists, err := fsutil.FileExists(videoFile.Path) if !exists { return nil, err } + + if videoFile.VideoStreamDuration <= 0 { + s := fmt.Sprintf("video %s: duration(%.3f)/frame count(%d) invalid, skipping sprite creation", videoFile.Path, videoFile.VideoStreamDuration, videoFile.FrameCount) + return nil, errors.New(s) + } + + config.SpriteInterval = calculateSpriteInterval(videoFile, config) + chunkCount := int(math.Ceil(videoFile.VideoStreamDuration / config.SpriteInterval)) + + // adjust the chunk count to the next highest perfect square, to ensure the sprite image + // is completely filled (no empty space in the grid) and the grid is as square as possible (minimizing the number of rows/columns) + gridSize := generate.GetSpriteGridSize(chunkCount) + newChunkCount := gridSize * gridSize + + if newChunkCount != chunkCount { + logger.Debugf("[generator] adjusting chunk count from %d to %d to fit a %dx%d grid", chunkCount, newChunkCount, gridSize, gridSize) + chunkCount = newChunkCount + } + + if config.SpriteSize <= 0 { + config.SpriteSize = DefaultSpriteSize + } + slowSeek := false - chunkCount := rows * cols // For files with small duration / low frame count try to seek using frame number intead of seconds if videoFile.VideoStreamDuration < 5 || (0 < videoFile.FrameCount && videoFile.FrameCount <= int64(chunkCount)) { // some files can have FrameCount == 0, only use SlowSeek if duration < 5 @@ -71,9 +138,8 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO VideoChecksum: videoChecksum, ImageOutputPath: imageOutputPath, VTTOutputPath: vttOutputPath, - Rows: rows, + Config: config, SlowSeek: slowSeek, - Columns: cols, g: &generate.Generator{ Encoder: instance.FFMpeg, FFMpegConfig: instance.Config, @@ -83,6 +149,40 @@ func NewSpriteGenerator(videoFile ffmpeg.VideoFile, videoChecksum string, imageO }, nil } +func calculateSpriteInterval(videoFile ffmpeg.VideoFile, config SpriteGeneratorConfig) float64 { + // If a custom sprite interval is provided, start with that + spriteInterval := config.SpriteInterval + + // If no custom interval is provided, calculate the interval based on the + // video duration and minimum sprite count + if spriteInterval <= 0 { + minSprites := config.MinimumSprites + if minSprites <= 0 { + panic("invalid configuration: MinimumSprites must be greater than 0 if SpriteInterval is not set") + } + + logger.Debugf("[generator] calculating sprite interval for video duration %.3fs with minimum sprites %d", videoFile.VideoStreamDuration, minSprites) + return videoFile.VideoStreamDuration / float64(minSprites) + } + + // Calculate the number of sprites that would be generated with the provided interval + spriteCount := int(math.Ceil(videoFile.VideoStreamDuration / spriteInterval)) + + // If the calculated sprite count is greater than the maximum, adjust the interval to meet the maximum + if config.MaximumSprites > 0 && spriteCount > int(config.MaximumSprites) { + spriteInterval = videoFile.VideoStreamDuration / float64(config.MaximumSprites) + logger.Debugf("[generator] provided sprite interval %.1fs results in %d sprites, which exceeds the maximum of %d, adjusting interval to %.1fs", config.SpriteInterval, spriteCount, config.MaximumSprites, spriteInterval) + } + + // If the calculated sprite count is less than the minimum, adjust the interval to meet the minimum + if config.MinimumSprites > 0 && spriteCount < int(config.MinimumSprites) { + spriteInterval = videoFile.VideoStreamDuration / float64(config.MinimumSprites) + logger.Debugf("[generator] provided sprite interval %.1fs results in %d sprites, which is less than the minimum of %d, adjusting interval to %.1fs", config.SpriteInterval, spriteCount, config.MinimumSprites, spriteInterval) + } + + return spriteInterval +} + func (g *SpriteGenerator) Generate() error { if err := g.generateSpriteImage(); err != nil { return err @@ -100,6 +200,8 @@ func (g *SpriteGenerator) generateSpriteImage() error { var images []image.Image + isPortrait := g.Info.VideoFile.Height > g.Info.VideoFile.Width + if !g.SlowSeek { logger.Infof("[generator] generating sprite image for %s", g.Info.VideoFile.Path) // generate `ChunkCount` thumbnails @@ -107,8 +209,7 @@ func (g *SpriteGenerator) generateSpriteImage() error { for i := 0; i < g.Info.ChunkCount; i++ { time := float64(i) * stepSize - - img, err := g.g.SpriteScreenshot(context.TODO(), g.Info.VideoFile.Path, time) + img, err := g.g.SpriteScreenshot(context.TODO(), g.Info.VideoFile.Path, time, g.Config.SpriteSize, isPortrait) if err != nil { return err } @@ -126,7 +227,7 @@ func (g *SpriteGenerator) generateSpriteImage() error { return errors.New("invalid frame number conversion") } - img, err := g.g.SpriteScreenshotSlow(context.TODO(), g.Info.VideoFile.Path, int(frame)) + img, err := g.g.SpriteScreenshotSlow(context.TODO(), g.Info.VideoFile.Path, int(frame), g.Config.SpriteSize) if err != nil { return err } @@ -158,7 +259,7 @@ func (g *SpriteGenerator) generateSpriteVTT() error { stepSize /= g.Info.FrameRate } - return g.g.SpriteVTT(context.TODO(), g.VTTOutputPath, g.ImageOutputPath, stepSize) + return g.g.SpriteVTT(context.TODO(), g.VTTOutputPath, g.ImageOutputPath, stepSize, g.Info.ChunkCount) } func (g *SpriteGenerator) imageExists() bool { diff --git a/internal/manager/task_generate_sprite.go b/internal/manager/task_generate_sprite.go index 0275830ab..c173147cd 100644 --- a/internal/manager/task_generate_sprite.go +++ b/internal/manager/task_generate_sprite.go @@ -34,7 +34,17 @@ func (t *GenerateSpriteTask) Start(ctx context.Context) { sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm) imagePath := instance.Paths.Scene.GetSpriteImageFilePath(sceneHash) vttPath := instance.Paths.Scene.GetSpriteVttFilePath(sceneHash) - generator, err := NewSpriteGenerator(*videoFile, sceneHash, imagePath, vttPath, 9, 9) + + cfg := DefaultSpriteGeneratorConfig + cfg.SpriteSize = instance.Config.GetSpriteScreenshotSize() + + if instance.Config.GetUseCustomSpriteInterval() { + cfg.MinimumSprites = instance.Config.GetMinimumSprites() + cfg.MaximumSprites = instance.Config.GetMaximumSprites() + cfg.SpriteInterval = instance.Config.GetSpriteInterval() + } + + generator, err := NewSpriteGenerator(*videoFile, sceneHash, imagePath, vttPath, cfg) if err != nil { logger.Errorf("error creating sprite generator: %s", err.Error()) diff --git a/pkg/ffmpeg/transcoder/screenshot.go b/pkg/ffmpeg/transcoder/screenshot.go index c3343d594..c65f23941 100644 --- a/pkg/ffmpeg/transcoder/screenshot.go +++ b/pkg/ffmpeg/transcoder/screenshot.go @@ -9,7 +9,11 @@ type ScreenshotOptions struct { // Quality is the quality scale. See https://ffmpeg.org/ffmpeg.html#Main-options Quality int + // Width is the width to scale the screenshot to. If 0, no scaling will be applied. Width int + // Height is the height to scale the screenshot to. If 0, no scaling will be applied. + // Not used if Width is set. + Height int // Verbosity is the logging verbosity. Defaults to LogLevelError if not set. Verbosity ffmpeg.LogLevel @@ -70,6 +74,9 @@ func ScreenshotTime(input string, t float64, options ScreenshotOptions) ffmpeg.A if options.Width > 0 { vf = vf.ScaleWidth(options.Width) args = args.VideoFilter(vf) + } else if options.Height > 0 { + vf = vf.ScaleHeight(options.Height) + args = args.VideoFilter(vf) } args = args.AppendArgs(options.OutputType) diff --git a/pkg/scene/generate/sprite.go b/pkg/scene/generate/sprite.go index c3b10f680..e0dea9659 100644 --- a/pkg/scene/generate/sprite.go +++ b/pkg/scene/generate/sprite.go @@ -18,22 +18,19 @@ import ( "github.com/stashapp/stash/pkg/utils" ) -const ( - spriteScreenshotWidth = 160 - - spriteRows = 9 - spriteCols = 9 - spriteChunks = spriteRows * spriteCols -) - -func (g Generator) SpriteScreenshot(ctx context.Context, input string, seconds float64) (image.Image, error) { +func (g Generator) SpriteScreenshot(ctx context.Context, input string, seconds float64, size int, isPortrait bool) (image.Image, error) { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() ssOptions := transcoder.ScreenshotOptions{ OutputPath: "-", OutputType: transcoder.ScreenshotOutputTypeBMP, - Width: spriteScreenshotWidth, + } + + if !isPortrait { + ssOptions.Width = size + } else { + ssOptions.Height = size } args := transcoder.ScreenshotTime(input, seconds, ssOptions) @@ -41,14 +38,14 @@ func (g Generator) SpriteScreenshot(ctx context.Context, input string, seconds f return g.generateImage(lockCtx, args) } -func (g Generator) SpriteScreenshotSlow(ctx context.Context, input string, frame int) (image.Image, error) { +func (g Generator) SpriteScreenshotSlow(ctx context.Context, input string, frame int, width int) (image.Image, error) { lockCtx := g.LockManager.ReadLock(ctx, input) defer lockCtx.Cancel() ssOptions := transcoder.ScreenshotOptions{ OutputPath: "-", OutputType: transcoder.ScreenshotOutputTypeBMP, - Width: spriteScreenshotWidth, + Width: width, } args := transcoder.ScreenshotFrame(input, frame, ssOptions) @@ -74,12 +71,13 @@ func (g Generator) CombineSpriteImages(images []image.Image) image.Image { // Combine all of the thumbnails into a sprite image width := images[0].Bounds().Size().X height := images[0].Bounds().Size().Y - canvasWidth := width * spriteCols - canvasHeight := height * spriteRows + gridSize := GetSpriteGridSize(len(images)) + canvasWidth := width * gridSize + canvasHeight := height * gridSize montage := imaging.New(canvasWidth, canvasHeight, color.NRGBA{}) for index := 0; index < len(images); index++ { - x := width * (index % spriteCols) - y := height * int(math.Floor(float64(index)/float64(spriteRows))) + x := width * (index % gridSize) + y := height * int(math.Floor(float64(index)/float64(gridSize))) img := images[index] montage = imaging.Paste(montage, img, image.Pt(x, y)) } @@ -87,14 +85,19 @@ func (g Generator) CombineSpriteImages(images []image.Image) image.Image { return montage } -func (g Generator) SpriteVTT(ctx context.Context, output string, spritePath string, stepSize float64) error { - lockCtx := g.LockManager.ReadLock(ctx, spritePath) - defer lockCtx.Cancel() - - return g.generateFile(lockCtx, g.ScenePaths, vttPattern, output, g.spriteVTT(spritePath, stepSize)) +// GetSpriteGridSize return the required size of a grid, where the number of images in width +// equals the number of images in height, to hold 'imageCount' images +func GetSpriteGridSize(imageCount int) int { + return int(math.Ceil(math.Sqrt(float64(imageCount)))) } -func (g Generator) spriteVTT(spritePath string, stepSize float64) generateFn { +func (g Generator) SpriteVTT(ctx context.Context, output string, spritePath string, stepSize float64, spriteChunks int) error { + lockCtx := g.LockManager.ReadLock(ctx, spritePath) + defer lockCtx.Cancel() + return g.generateFile(lockCtx, g.ScenePaths, vttPattern, output, g.spriteVTT(spritePath, stepSize, spriteChunks)) +} + +func (g Generator) spriteVTT(spritePath string, stepSize float64, spriteChunks int) generateFn { return func(lockCtx *fsutil.LockContext, tmpFn string) error { spriteImage, err := os.Open(spritePath) if err != nil { @@ -106,16 +109,17 @@ func (g Generator) spriteVTT(spritePath string, stepSize float64) generateFn { if err != nil { return err } - width := image.Width / spriteCols - height := image.Height / spriteRows + + gridSize := GetSpriteGridSize(spriteChunks) + width := image.Width / gridSize + height := image.Height / gridSize vttLines := []string{"WEBVTT", ""} for index := 0; index < spriteChunks; index++ { - x := width * (index % spriteCols) - y := height * int(math.Floor(float64(index)/float64(spriteRows))) + x := width * (index % gridSize) + y := height * int(math.Floor(float64(index)/float64(gridSize))) startTime := utils.GetVTTTime(float64(index) * stepSize) endTime := utils.GetVTTTime(float64(index+1) * stepSize) - vttLines = append(vttLines, startTime+" --> "+endTime) vttLines = append(vttLines, fmt.Sprintf("%s#xywh=%d,%d,%d,%d", spriteImageName, x, y, width, height)) vttLines = append(vttLines, "") diff --git a/ui/v2.5/graphql/data/config.graphql b/ui/v2.5/graphql/data/config.graphql index ca1f6a47c..ba8215fe3 100644 --- a/ui/v2.5/graphql/data/config.graphql +++ b/ui/v2.5/graphql/data/config.graphql @@ -39,6 +39,11 @@ fragment ConfigGeneralData on ConfigGeneralResult { logLevel logAccess logFileMaxSize + useCustomSpriteInterval + spriteInterval + minimumSprites + maximumSprites + spriteScreenshotSize createGalleriesFromFolders galleryCoverRegex videoExtensions diff --git a/ui/v2.5/src/components/Scenes/PreviewScrubber.tsx b/ui/v2.5/src/components/Scenes/PreviewScrubber.tsx index 143daca4f..8ecb6e557 100644 --- a/ui/v2.5/src/components/Scenes/PreviewScrubber.tsx +++ b/ui/v2.5/src/components/Scenes/PreviewScrubber.tsx @@ -67,6 +67,8 @@ export const PreviewScrubber: React.FC = ({ const clientRect = imageParent.getBoundingClientRect(); const scale = scaleToFit(sprite, clientRect); + const spriteSheet = new Image(); + spriteSheet.src = sprite.url; setStyle({ backgroundPosition: `${-sprite.x}px ${-sprite.y}px`, diff --git a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx index 34fb634b2..446ad09a1 100644 --- a/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsSystemPanel.tsx @@ -427,6 +427,44 @@ export const SettingsConfigurationPanel: React.FC = () => { /> + + saveGeneral({ spriteScreenshotSize: v })} + /> + saveGeneral({ useCustomSpriteInterval: v })} + /> + saveGeneral({ spriteInterval: v })} + /> + saveGeneral({ minimumSprites: v })} + /> + saveGeneral({ maximumSprites: v })} + /> + + **⚠️ Note:** If this is set too high it will decrease overall performance and causes failures (out of memory). +## Sprite generation + +### Sprite size + +Fixed size of a generated sprite, being the longest dimension in pixels. +Setting this to `0` will fallback to the default of `160`. +Althought it is possible to set this value to anything bigger than `0` it is recommended to set it to `160` at least. + +### Use custom sprite generation + +If this setting is disabled, the settings below will be ignored and the default sprite generation settings are used. + +### Sprite interval + +This represents the time in seconds between each sprite to be generated. This value will be adjusted if necessary to fit within the bounds of the `Minimum Sprites` and `Maximum Sprites` settings. + +Setting this to `0` means that the sprite interval will be calculated based on the value of the `Minimum Sprites` field. + +### Minimum sprites + +The minimal number of distinct sprites that will be generated for a scene. `Sprite interval` will be adjusted if necessary. +Setting this to `0` will fallback to the default of `10` + +### Maximum sprites + +The maximum number of distinct sprites that will be generated for a scene. `Sprite interval` will be adjusted if necessary. +Setting this to `0` indicates there is no maximum. + +> **⚠️ Note:** The number of generated sprites is adjusted upwards to the next perfect square to ensure the sprite image is completely filled (no empty space in the grid) and the grid is as square as possible (minimizing the number of rows/columns). This means that if you set a minimum of 10 sprites, 16 will actually be generated, and if you set a maximum of 15 sprites, 16 will actually be generated. + ## Hardware accelerated live transcoding Hardware accelerated live transcoding can be enabled by setting the `FFmpeg hardware encoding` setting. Stash outputs the supported hardware encoders to the log file on startup at the Info log level. If a given hardware encoder is not supported, it's error message is logged to the Debug log level for debugging purposes. diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 2aee4fe2b..4bfd4322d 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -440,7 +440,18 @@ "heading": "Scrapers path" }, "scraping": "Scraping", + "sprite_generation_head": "Sprite generation", + "sprite_interval_desc": "Time between each generated sprite in seconds.", + "sprite_interval_head": "Sprite interval", + "sprite_maximum_desc": "Maximum number of sprites to be generated for a scene. Set to 0 to disable the limit.", + "sprite_maximum_head": "Maximum sprites", + "sprite_minimum_desc": "Minimum number of sprites to be generated for a scene", + "sprite_minimum_head": "Minimum sprites", + "sprite_screenshot_size_desc": "Desired size of each sprite in pixels.", + "sprite_screenshot_size_head": "Sprite size", "sqlite_location": "File location for the SQLite database (requires restart). WARNING: storing the database on a different system to where the Stash server is run from (i.e. over the network) is unsupported!", + "use_custom_sprite_interval_head": "Use custom sprite interval", + "use_custom_sprite_interval_desc": "Enable the custom sprite interval according to the settings below.", "video_ext_desc": "Comma-delimited list of file extensions that will be identified as videos.", "video_ext_head": "Video extensions", "video_head": "Video"