Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow customisation of preview generation #673

Merged
merged 9 commits into from
Jul 23, 2020
4 changes: 4 additions & 0 deletions graphql/documents/data/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ fragment ConfigGeneralData on ConfigGeneralResult {
databasePath
generatedPath
cachePath
previewSegments
previewSegmentDuration
previewExcludeStart
previewExcludeEnd
previewPreset
maxTranscodeSize
maxStreamingTranscodeSize
Expand Down
16 changes: 16 additions & 0 deletions graphql/schema/types/config.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ input ConfigGeneralInput {
generatedPath: String
"""Path to cache"""
cachePath: String
"""Number of segments in a preview file"""
previewSegments: Int
"""Preview segment duration, in seconds"""
previewSegmentDuration: Float
"""Duration of start of video to exclude when generating previews"""
previewExcludeStart: String
"""Duration of end of video to exclude when generating previews"""
previewExcludeEnd: String
"""Preset when generating preview"""
previewPreset: PreviewPreset
"""Max generated transcode size"""
Expand Down Expand Up @@ -61,6 +69,14 @@ type ConfigGeneralResult {
generatedPath: String!
"""Path to cache"""
cachePath: String!
"""Number of segments in a preview file"""
previewSegments: Int!
"""Preview segment duration, in seconds"""
previewSegmentDuration: Float!
"""Duration of start of video to exclude when generating previews"""
previewExcludeStart: String!
"""Duration of end of video to exclude when generating previews"""
previewExcludeEnd: String!
"""Preset when generating preview"""
previewPreset: PreviewPreset!
"""Max generated transcode size"""
Expand Down
14 changes: 14 additions & 0 deletions graphql/schema/types/metadata.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ input GenerateMetadataInput {
sprites: Boolean!
previews: Boolean!
imagePreviews: Boolean!
previewOptions: GeneratePreviewOptionsInput
markers: Boolean!
transcodes: Boolean!
"""gallery thumbnails for cache usage"""
Expand All @@ -18,6 +19,19 @@ input GenerateMetadataInput {
overwrite: Boolean
}

input GeneratePreviewOptionsInput {
"""Number of segments in a preview file"""
previewSegments: Int
"""Preview segment duration, in seconds"""
previewSegmentDuration: Float
"""Duration of start of video to exclude when generating previews"""
previewExcludeStart: String
"""Duration of end of video to exclude when generating previews"""
previewExcludeEnd: String
"""Preset when generating preview"""
previewPreset: PreviewPreset
}

input ScanMetadataInput {
useFileMetadata: Boolean!
}
Expand Down
12 changes: 12 additions & 0 deletions pkg/api/resolver_mutation_configure.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,18 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co
config.Set(config.Cache, input.CachePath)
}

if input.PreviewSegments != nil {
config.Set(config.PreviewSegments, *input.PreviewSegments)
}
if input.PreviewSegmentDuration != nil {
config.Set(config.PreviewSegmentDuration, *input.PreviewSegmentDuration)
}
if input.PreviewExcludeStart != nil {
config.Set(config.PreviewExcludeStart, *input.PreviewExcludeStart)
}
if input.PreviewExcludeEnd != nil {
config.Set(config.PreviewExcludeEnd, *input.PreviewExcludeEnd)
}
if input.PreviewPreset != nil {
config.Set(config.PreviewPreset, input.PreviewPreset.String())
}
Expand Down
4 changes: 4 additions & 0 deletions pkg/api/resolver_query_configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ func makeConfigGeneralResult() *models.ConfigGeneralResult {
DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(),
CachePath: config.GetCachePath(),
PreviewSegments: config.GetPreviewSegments(),
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
PreviewExcludeStart: config.GetPreviewExcludeStart(),
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
PreviewPreset: config.GetPreviewPreset(),
MaxTranscodeSize: &maxTranscodeSize,
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/routes_scene.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func (rs sceneRoutes) Screenshot(w http.ResponseWriter, r *http.Request) {
func (rs sceneRoutes) Preview(w http.ResponseWriter, r *http.Request) {
scene := r.Context().Value(sceneKey).(*models.Scene)
filepath := manager.GetInstance().Paths.Scene.GetStreamPreviewPath(scene.Checksum)
http.ServeFile(w, r, filepath)
utils.ServeFileNoCache(w, r, filepath)
}

func (rs sceneRoutes) Webp(w http.ResponseWriter, r *http.Request) {
Expand Down
7 changes: 4 additions & 3 deletions pkg/ffmpeg/encoder_scene_preview_chunk.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import (
)

type ScenePreviewChunkOptions struct {
Time int
StartTime float64
Duration float64
Width int
OutputPath string
}
Expand All @@ -17,9 +18,9 @@ func (e *Encoder) ScenePreviewVideoChunk(probeResult VideoFile, options ScenePre
args := []string{
"-v", "error",
"-xerror",
"-ss", strconv.Itoa(options.Time),
"-ss", strconv.FormatFloat(options.StartTime, 'f', 2, 64),
"-i", probeResult.Path,
"-t", "0.75",
"-t", strconv.FormatFloat(options.Duration, 'f', 2, 64),
"-max_muxing_queue_size", "1024", // https://trac.ffmpeg.org/ticket/6375
"-y",
"-c:v", "libx264",
Expand Down
51 changes: 51 additions & 0 deletions pkg/manager/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,18 @@ const PreviewPreset = "preview_preset"
const MaxTranscodeSize = "max_transcode_size"
const MaxStreamingTranscodeSize = "max_streaming_transcode_size"

const PreviewSegmentDuration = "preview_segment_duration"
const previewSegmentDurationDefault = 0.75

const PreviewSegments = "preview_segments"
const previewSegmentsDefault = 12

const PreviewExcludeStart = "preview_exclude_start"
const previewExcludeStartDefault = "0"

const PreviewExcludeEnd = "preview_exclude_end"
const previewExcludeEndDefault = "0"

const Host = "host"
const Port = "port"
const ExternalHost = "external_host"
Expand Down Expand Up @@ -158,6 +170,36 @@ func GetExternalHost() string {
return viper.GetString(ExternalHost)
}

// GetPreviewSegmentDuration returns the duration of a single segment in a
// scene preview file, in seconds.
func GetPreviewSegmentDuration() float64 {
return viper.GetFloat64(PreviewSegmentDuration)
}

// GetPreviewSegments returns the amount of segments in a scene preview file.
func GetPreviewSegments() int {
return viper.GetInt(PreviewSegments)
}

// GetPreviewExcludeStart returns the configuration setting string for
// excluding the start of scene videos for preview generation. This can
// be in two possible formats. A float value is interpreted as the amount
// of seconds to exclude from the start of the video before it is included
// in the preview. If the value is suffixed with a '%' character (for example
// '2%'), then it is interpreted as a proportion of the total video duration.
func GetPreviewExcludeStart() string {
return viper.GetString(PreviewExcludeStart)
}

// GetPreviewExcludeEnd returns the configuration setting string for
// excluding the end of scene videos for preview generation. A float value
// is interpreted as the amount of seconds to exclude from the end of the video
// when generating previews. If the value is suffixed with a '%' character,
// then it is interpreted as a proportion of the total video duration.
func GetPreviewExcludeEnd() string {
return viper.GetString(PreviewExcludeEnd)
}

// GetPreviewPreset returns the preset when generating previews. Defaults to
// Slow.
func GetPreviewPreset() models.PreviewPreset {
Expand Down Expand Up @@ -371,6 +413,13 @@ func IsValid() bool {
return setPaths
}

func setDefaultValues() {
viper.SetDefault(PreviewSegmentDuration, previewSegmentDurationDefault)
viper.SetDefault(PreviewSegments, previewSegmentsDefault)
viper.SetDefault(PreviewExcludeStart, previewExcludeStartDefault)
viper.SetDefault(PreviewExcludeEnd, previewExcludeEndDefault)
}

// SetInitialConfig fills in missing required config fields
func SetInitialConfig() error {
// generate some api keys
Expand All @@ -386,5 +435,7 @@ func SetInitialConfig() error {
Set(SessionStoreKey, sessionStoreKey)
}

setDefaultValues()

return Write()
}
63 changes: 56 additions & 7 deletions pkg/manager/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os/exec"
"runtime"
"strconv"
"strings"

"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
Expand All @@ -17,7 +18,13 @@ type GeneratorInfo struct {
ChunkCount int
FrameRate float64
NumberOfFrames int
NthFrame int

// NthFrame used for sprite generation
NthFrame int

ChunkDuration float64
ExcludeStart string
ExcludeEnd string

VideoFile ffmpeg.VideoFile
}
Expand All @@ -33,12 +40,7 @@ func newGeneratorInfo(videoFile ffmpeg.VideoFile) (*GeneratorInfo, error) {
return generator, nil
}

func (g *GeneratorInfo) configure() error {
videoStream := g.VideoFile.VideoStream
if videoStream == nil {
return fmt.Errorf("missing video stream")
}

func (g *GeneratorInfo) calculateFrameRate(videoStream *ffmpeg.FFProbeStream) error {
var framerate float64
if g.VideoFile.FrameRate == 0 {
framerate, _ = strconv.ParseFloat(videoStream.RFrameRate, 64)
Expand Down Expand Up @@ -94,7 +96,54 @@ func (g *GeneratorInfo) configure() error {

g.FrameRate = framerate
g.NumberOfFrames = numberOfFrames

return nil
}

func (g *GeneratorInfo) configure() error {
videoStream := g.VideoFile.VideoStream
if videoStream == nil {
return fmt.Errorf("missing video stream")
}

if err := g.calculateFrameRate(videoStream); err != nil {
return err
}

g.NthFrame = g.NumberOfFrames / g.ChunkCount

return nil
}

func (g GeneratorInfo) getExcludeValue(v string) float64 {
if strings.HasSuffix(v, "%") && len(v) > 1 {
// proportion of video duration
v = v[0 : len(v)-1]
prop, _ := strconv.ParseFloat(v, 64)
return prop / 100.0 * g.VideoFile.Duration
}

prop, _ := strconv.ParseFloat(v, 64)
return prop
}

// getStepSizeAndOffset calculates the step size for preview generation and
// the starting offset.
//
// Step size is calculated based on the duration of the video file, minus the
// excluded duration. The offset is based on the ExcludeStart. If the total
// excluded duration exceeds the duration of the video, then offset is 0, and
// the video duration is used to calculate the step size.
func (g GeneratorInfo) getStepSizeAndOffset() (stepSize float64, offset float64) {
duration := g.VideoFile.Duration
excludeStart := g.getExcludeValue(g.ExcludeStart)
excludeEnd := g.getExcludeValue(g.ExcludeEnd)

if duration > excludeStart+excludeEnd {
duration = duration - excludeStart - excludeEnd
offset = excludeStart
}

stepSize = duration / float64(g.ChunkCount)
return
}
16 changes: 10 additions & 6 deletions pkg/manager/generator_preview.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,6 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image
return nil, err
}
generator.ChunkCount = 12 // 12 segments to the preview
if err := generator.configure(); err != nil {
return nil, err
}

return &PreviewGenerator{
Info: generator,
Expand All @@ -53,6 +50,11 @@ func NewPreviewGenerator(videoFile ffmpeg.VideoFile, videoFilename string, image

func (g *PreviewGenerator) Generate() error {
logger.Infof("[generator] generating scene preview for %s", g.Info.VideoFile.Path)

if err := g.Info.configure(); err != nil {
return err
}

encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)

if err := g.generateConcatFile(); err != nil {
Expand Down Expand Up @@ -95,15 +97,17 @@ func (g *PreviewGenerator) generateVideo(encoder *ffmpeg.Encoder) error {
return nil
}

stepSize := int(g.Info.VideoFile.Duration / float64(g.Info.ChunkCount))
stepSize, offset := g.Info.getStepSizeAndOffset()

for i := 0; i < g.Info.ChunkCount; i++ {
time := i * stepSize
time := offset + (float64(i) * stepSize)
num := fmt.Sprintf("%.3d", i)
filename := "preview" + num + ".mp4"
chunkOutputPath := instance.Paths.Generated.GetTmpPath(filename)

options := ffmpeg.ScenePreviewChunkOptions{
Time: time,
StartTime: time,
Duration: g.Info.ChunkDuration,
Width: 640,
OutputPath: chunkOutputPath,
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ func initConfig() {
}
logger.Infof("using config file: %s", viper.ConfigFileUsed())

config.SetInitialConfig()

viper.SetDefault(config.Database, paths.GetDefaultDatabaseFilePath())

// Set generated to the metadata path for backwards compat
Expand Down
Loading