From d1ea2fffa5708a2629c6168ae3a98720e3894dbe Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 5 Nov 2019 08:38:33 +1100 Subject: [PATCH] Add configurable transcode sizes (#178) --- graphql/documents/data/config.graphql | 2 + graphql/schema/types/config.graphql | 17 +++ pkg/api/resolver_mutation_configure.go | 10 +- pkg/api/resolver_query_configuration.go | 24 +-- pkg/api/routes_scene.go | 3 +- pkg/ffmpeg/encoder_transcode.go | 52 ++++++- pkg/manager/config/config.go | 26 ++++ pkg/manager/task_transcode.go | 12 +- .../Settings/SettingsConfigurationPanel.tsx | 140 +++++++++++++----- .../Shared/FolderSelect/FolderSelect.tsx | 10 +- 10 files changed, 241 insertions(+), 55 deletions(-) diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 49cbd7bbc..91bf0495b 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -2,6 +2,8 @@ fragment ConfigGeneralData on ConfigGeneralResult { stashes databasePath generatedPath + maxTranscodeSize + maxStreamingTranscodeSize username password logFile diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index d126354bf..557281a98 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -1,3 +1,12 @@ +enum StreamingResolutionEnum { + "240p", LOW + "480p", STANDARD + "720p", STANDARD_HD + "1080p", FULL_HD + "4k", FOUR_K + "Original", ORIGINAL +} + input ConfigGeneralInput { """Array of file paths to content""" stashes: [String!] @@ -5,6 +14,10 @@ input ConfigGeneralInput { databasePath: String """Path to generated files""" generatedPath: String + """Max generated transcode size""" + maxTranscodeSize: StreamingResolutionEnum + """Max streaming transcode size""" + maxStreamingTranscodeSize: StreamingResolutionEnum """Username""" username: String """Password""" @@ -26,6 +39,10 @@ type ConfigGeneralResult { databasePath: String! """Path to generated files""" generatedPath: String! + """Max generated transcode size""" + maxTranscodeSize: StreamingResolutionEnum + """Max streaming transcode size""" + maxStreamingTranscodeSize: StreamingResolutionEnum """Username""" username: String! """Password""" diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 4ca980ca6..4b81d2bfb 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -5,8 +5,8 @@ import ( "fmt" "path/filepath" - "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) @@ -37,6 +37,14 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input models.Co config.Set(config.Generated, input.GeneratedPath) } + if input.MaxTranscodeSize != nil { + config.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String()) + } + + if input.MaxStreamingTranscodeSize != nil { + config.Set(config.MaxStreamingTranscodeSize, input.MaxStreamingTranscodeSize.String()) + } + if input.Username != nil { config.Set(config.Username, input.Username) } diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index ae8e07d4c..d9da7f6b9 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -29,16 +29,22 @@ func makeConfigResult() *models.ConfigResult { func makeConfigGeneralResult() *models.ConfigGeneralResult { logFile := config.GetLogFile() + + maxTranscodeSize := config.GetMaxTranscodeSize() + maxStreamingTranscodeSize := config.GetMaxStreamingTranscodeSize() + return &models.ConfigGeneralResult{ - Stashes: config.GetStashPaths(), - DatabasePath: config.GetDatabasePath(), - GeneratedPath: config.GetGeneratedPath(), - Username: config.GetUsername(), - Password: config.GetPasswordHash(), - LogFile: &logFile, - LogOut: config.GetLogOut(), - LogLevel: config.GetLogLevel(), - LogAccess: config.GetLogAccess(), + Stashes: config.GetStashPaths(), + DatabasePath: config.GetDatabasePath(), + GeneratedPath: config.GetGeneratedPath(), + MaxTranscodeSize: &maxTranscodeSize, + MaxStreamingTranscodeSize: &maxStreamingTranscodeSize, + Username: config.GetUsername(), + Password: config.GetPasswordHash(), + LogFile: &logFile, + LogOut: config.GetLogOut(), + LogLevel: config.GetLogLevel(), + LogAccess: config.GetLogAccess(), } } diff --git a/pkg/api/routes_scene.go b/pkg/api/routes_scene.go index 6e3d02e6f..df7be0092 100644 --- a/pkg/api/routes_scene.go +++ b/pkg/api/routes_scene.go @@ -11,6 +11,7 @@ import ( "github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/manager" + "github.com/stashapp/stash/pkg/manager/config" "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) @@ -68,7 +69,7 @@ func (rs sceneRoutes) Stream(w http.ResponseWriter, r *http.Request) { encoder := ffmpeg.NewEncoder(manager.GetInstance().FFMPEGPath) - stream, process, err := encoder.StreamTranscode(*videoFile, startTime) + stream, process, err := encoder.StreamTranscode(*videoFile, startTime, config.GetMaxStreamingTranscodeSize()) if err != nil { logger.Errorf("[stream] error transcoding video file: %s", err.Error()) return diff --git a/pkg/ffmpeg/encoder_transcode.go b/pkg/ffmpeg/encoder_transcode.go index 5f336d439..a908f00ac 100644 --- a/pkg/ffmpeg/encoder_transcode.go +++ b/pkg/ffmpeg/encoder_transcode.go @@ -3,13 +3,56 @@ package ffmpeg import ( "io" "os" + "strconv" + + "github.com/stashapp/stash/pkg/models" ) type TranscodeOptions struct { - OutputPath string + OutputPath string + MaxTranscodeSize models.StreamingResolutionEnum +} + +func calculateTranscodeScale(probeResult VideoFile, maxTranscodeSize models.StreamingResolutionEnum) string { + maxSize := 0 + switch maxTranscodeSize { + case models.StreamingResolutionEnumLow: + maxSize = 240 + case models.StreamingResolutionEnumStandard: + maxSize = 480 + case models.StreamingResolutionEnumStandardHd: + maxSize = 720 + case models.StreamingResolutionEnumFullHd: + maxSize = 1080 + case models.StreamingResolutionEnumFourK: + maxSize = 2160 + } + + // get the smaller dimension of the video file + videoSize := probeResult.Height + if probeResult.Width < videoSize { + videoSize = probeResult.Width + } + + // if our streaming resolution is larger than the video dimension + // or we are streaming the original resolution, then just set the + // input width + if maxSize >= videoSize || maxSize == 0 { + return "iw:-2" + } + + // we're setting either the width or height + // we'll set the smaller dimesion + if probeResult.Width > probeResult.Height { + // set the height + return "-2:" + strconv.Itoa(maxSize) + } + + return strconv.Itoa(maxSize) + ":-2" } func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) { + scale := calculateTranscodeScale(probeResult, options.MaxTranscodeSize) args := []string{ "-i", probeResult.Path, "-c:v", "libx264", @@ -18,7 +61,7 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) { "-level", "4.2", "-preset", "superfast", "-crf", "23", - "-vf", "scale=iw:-2", + "-vf", "scale=" + scale, "-c:a", "aac", "-strict", "-2", options.OutputPath, @@ -26,7 +69,8 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) { _, _ = e.run(probeResult, args) } -func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.ReadCloser, *os.Process, error) { +func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string, maxTranscodeSize models.StreamingResolutionEnum) (io.ReadCloser, *os.Process, error) { + scale := calculateTranscodeScale(probeResult, maxTranscodeSize) args := []string{} if startTime != "" { @@ -36,7 +80,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.R args = append(args, "-i", probeResult.Path, "-c:v", "libvpx-vp9", - "-vf", "scale=iw:-2", + "-vf", "scale="+scale, "-deadline", "realtime", "-cpu-used", "5", "-row-mt", "1", diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index fee8e3397..2fb67dd9b 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -8,6 +8,7 @@ import ( "github.com/spf13/viper" + "github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/utils" ) @@ -21,6 +22,9 @@ const Password = "password" const Database = "database" +const MaxTranscodeSize = "max_transcode_size" +const MaxStreamingTranscodeSize = "max_streaming_transcode_size" + const Host = "host" const Port = "port" @@ -77,6 +81,28 @@ func GetPort() int { return viper.GetInt(Port) } +func GetMaxTranscodeSize() models.StreamingResolutionEnum { + ret := viper.GetString(MaxTranscodeSize) + + // default to original + if ret == "" { + return models.StreamingResolutionEnumOriginal + } + + return models.StreamingResolutionEnum(ret) +} + +func GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum { + ret := viper.GetString(MaxStreamingTranscodeSize) + + // default to original + if ret == "" { + return models.StreamingResolutionEnumOriginal + } + + return models.StreamingResolutionEnum(ret) +} + func GetUsername() string { return viper.GetString(Username) } diff --git a/pkg/manager/task_transcode.go b/pkg/manager/task_transcode.go index d0d606bef..49404c98f 100644 --- a/pkg/manager/task_transcode.go +++ b/pkg/manager/task_transcode.go @@ -1,11 +1,13 @@ package manager import ( - "github.com/stashapp/stash/pkg/ffmpeg" - "github.com/stashapp/stash/pkg/logger" - "github.com/stashapp/stash/pkg/models" "os" "sync" + + "github.com/stashapp/stash/pkg/ffmpeg" + "github.com/stashapp/stash/pkg/logger" + "github.com/stashapp/stash/pkg/manager/config" + "github.com/stashapp/stash/pkg/models" ) type GenerateTranscodeTask struct { @@ -33,8 +35,10 @@ func (t *GenerateTranscodeTask) Start(wg *sync.WaitGroup) { } outputPath := instance.Paths.Generated.GetTmpPath(t.Scene.Checksum + ".mp4") + transcodeSize := config.GetMaxTranscodeSize() options := ffmpeg.TranscodeOptions{ - OutputPath: outputPath, + OutputPath: outputPath, + MaxTranscodeSize: transcodeSize, } encoder := ffmpeg.NewEncoder(instance.FFMPEGPath) encoder.Transcode(*videoFile, options) diff --git a/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx index 06c7e86be..268830054 100644 --- a/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx +++ b/ui/v2/src/components/Settings/SettingsConfigurationPanel.tsx @@ -25,6 +25,8 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr const [stashes, setStashes] = useState([]); const [databasePath, setDatabasePath] = useState(undefined); const [generatedPath, setGeneratedPath] = useState(undefined); + const [maxTranscodeSize, setMaxTranscodeSize] = useState(undefined); + const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState(undefined); const [username, setUsername] = useState(undefined); const [password, setPassword] = useState(undefined); const [logFile, setLogFile] = useState(); @@ -38,6 +40,8 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr stashes, databasePath, generatedPath, + maxTranscodeSize, + maxStreamingTranscodeSize, username, password, logFile, @@ -53,6 +57,8 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr setStashes(conf.general.stashes || []); setDatabasePath(conf.general.databasePath); setGeneratedPath(conf.general.generatedPath); + setMaxTranscodeSize(conf.general.maxTranscodeSize); + setMaxStreamingTranscodeSize(conf.general.maxStreamingTranscodeSize); setUsername(conf.general.username); setPassword(conf.general.password); setLogFile(conf.general.logFile); @@ -76,46 +82,114 @@ export const SettingsConfigurationPanel: FunctionComponent = (props: IPr } } + const transcodeQualities = [ + GQL.StreamingResolutionEnum.Low, + GQL.StreamingResolutionEnum.Standard, + GQL.StreamingResolutionEnum.StandardHd, + GQL.StreamingResolutionEnum.FullHd, + GQL.StreamingResolutionEnum.FourK, + GQL.StreamingResolutionEnum.Original + ].map(resolutionToString); + + function resolutionToString(r : GQL.StreamingResolutionEnum | undefined) { + switch (r) { + case GQL.StreamingResolutionEnum.Low: return "240p"; + case GQL.StreamingResolutionEnum.Standard: return "480p"; + case GQL.StreamingResolutionEnum.StandardHd: return "720p"; + case GQL.StreamingResolutionEnum.FullHd: return "1080p"; + case GQL.StreamingResolutionEnum.FourK: return "4k"; + case GQL.StreamingResolutionEnum.Original: return "Original"; + } + + return "Original"; + } + + function translateQuality(quality : string) { + switch (quality) { + case "240p": return GQL.StreamingResolutionEnum.Low; + case "480p": return GQL.StreamingResolutionEnum.Standard; + case "720p": return GQL.StreamingResolutionEnum.StandardHd; + case "1080p": return GQL.StreamingResolutionEnum.FullHd; + case "4k": return GQL.StreamingResolutionEnum.FourK; + case "Original": return GQL.StreamingResolutionEnum.Original; + } + + return GQL.StreamingResolutionEnum.Original; + } + return ( <> {!!error ?

{error.message}

: undefined} {(!data || !data.configuration || loading) ? : undefined}

Library

- - - - - setDatabasePath(e.target.value)} /> - - - setGeneratedPath(e.target.value)} /> - + + + + + + + + + setDatabasePath(e.target.value)} /> + - -

Authentication

- - setUsername(e.target.value)} /> + + setGeneratedPath(e.target.value)} /> + - - setPassword(e.target.value)} /> + + + +

Video

+ + setMaxTranscodeSize(translateQuality(event.target.value))} + value={resolutionToString(maxTranscodeSize)} + /> + + + setMaxStreamingTranscodeSize(translateQuality(event.target.value))} + value={resolutionToString(maxStreamingTranscodeSize)} + /> + +
+ + + +

Authentication

+ + setUsername(e.target.value)} /> + + + setPassword(e.target.value)} /> +
diff --git a/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx b/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx index 31f599b21..67b8bd6b7 100644 --- a/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx +++ b/ui/v2/src/components/Shared/FolderSelect/FolderSelect.tsx @@ -4,6 +4,7 @@ import { Dialog, InputGroup, Spinner, + FormGroup, } from "@blueprintjs/core"; import _ from "lodash"; import React, { FunctionComponent, useEffect, useState } from "react"; @@ -76,9 +77,12 @@ export const FolderSelect: FunctionComponent = (props: IProps) => { <> {!!error ?

{error.message}

: undefined} {renderDialog()} - {selectedDirectories.map((path) => { - return ; - })} + + {selectedDirectories.map((path) => { + return ; + })} + + );