Add configurable transcode sizes (#178)

This commit is contained in:
WithoutPants
2019-11-05 08:38:33 +11:00
committed by Leopere
parent be12a9f5a1
commit d1ea2fffa5
10 changed files with 241 additions and 55 deletions

View File

@@ -2,6 +2,8 @@ fragment ConfigGeneralData on ConfigGeneralResult {
stashes stashes
databasePath databasePath
generatedPath generatedPath
maxTranscodeSize
maxStreamingTranscodeSize
username username
password password
logFile logFile

View File

@@ -1,3 +1,12 @@
enum StreamingResolutionEnum {
"240p", LOW
"480p", STANDARD
"720p", STANDARD_HD
"1080p", FULL_HD
"4k", FOUR_K
"Original", ORIGINAL
}
input ConfigGeneralInput { input ConfigGeneralInput {
"""Array of file paths to content""" """Array of file paths to content"""
stashes: [String!] stashes: [String!]
@@ -5,6 +14,10 @@ input ConfigGeneralInput {
databasePath: String databasePath: String
"""Path to generated files""" """Path to generated files"""
generatedPath: String generatedPath: String
"""Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Username""" """Username"""
username: String username: String
"""Password""" """Password"""
@@ -26,6 +39,10 @@ type ConfigGeneralResult {
databasePath: String! databasePath: String!
"""Path to generated files""" """Path to generated files"""
generatedPath: String! generatedPath: String!
"""Max generated transcode size"""
maxTranscodeSize: StreamingResolutionEnum
"""Max streaming transcode size"""
maxStreamingTranscodeSize: StreamingResolutionEnum
"""Username""" """Username"""
username: String! username: String!
"""Password""" """Password"""

View File

@@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "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) 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 { if input.Username != nil {
config.Set(config.Username, input.Username) config.Set(config.Username, input.Username)
} }

View File

@@ -29,16 +29,22 @@ func makeConfigResult() *models.ConfigResult {
func makeConfigGeneralResult() *models.ConfigGeneralResult { func makeConfigGeneralResult() *models.ConfigGeneralResult {
logFile := config.GetLogFile() logFile := config.GetLogFile()
maxTranscodeSize := config.GetMaxTranscodeSize()
maxStreamingTranscodeSize := config.GetMaxStreamingTranscodeSize()
return &models.ConfigGeneralResult{ return &models.ConfigGeneralResult{
Stashes: config.GetStashPaths(), Stashes: config.GetStashPaths(),
DatabasePath: config.GetDatabasePath(), DatabasePath: config.GetDatabasePath(),
GeneratedPath: config.GetGeneratedPath(), GeneratedPath: config.GetGeneratedPath(),
Username: config.GetUsername(), MaxTranscodeSize: &maxTranscodeSize,
Password: config.GetPasswordHash(), MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
LogFile: &logFile, Username: config.GetUsername(),
LogOut: config.GetLogOut(), Password: config.GetPasswordHash(),
LogLevel: config.GetLogLevel(), LogFile: &logFile,
LogAccess: config.GetLogAccess(), LogOut: config.GetLogOut(),
LogLevel: config.GetLogLevel(),
LogAccess: config.GetLogAccess(),
} }
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/stashapp/stash/pkg/ffmpeg" "github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger" "github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/manager" "github.com/stashapp/stash/pkg/manager"
"github.com/stashapp/stash/pkg/manager/config"
"github.com/stashapp/stash/pkg/models" "github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "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) 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 { if err != nil {
logger.Errorf("[stream] error transcoding video file: %s", err.Error()) logger.Errorf("[stream] error transcoding video file: %s", err.Error())
return return

View File

@@ -3,13 +3,56 @@ package ffmpeg
import ( import (
"io" "io"
"os" "os"
"strconv"
"github.com/stashapp/stash/pkg/models"
) )
type TranscodeOptions struct { 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) { func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
scale := calculateTranscodeScale(probeResult, options.MaxTranscodeSize)
args := []string{ args := []string{
"-i", probeResult.Path, "-i", probeResult.Path,
"-c:v", "libx264", "-c:v", "libx264",
@@ -18,7 +61,7 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
"-level", "4.2", "-level", "4.2",
"-preset", "superfast", "-preset", "superfast",
"-crf", "23", "-crf", "23",
"-vf", "scale=iw:-2", "-vf", "scale=" + scale,
"-c:a", "aac", "-c:a", "aac",
"-strict", "-2", "-strict", "-2",
options.OutputPath, options.OutputPath,
@@ -26,7 +69,8 @@ func (e *Encoder) Transcode(probeResult VideoFile, options TranscodeOptions) {
_, _ = e.run(probeResult, args) _, _ = 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{} args := []string{}
if startTime != "" { if startTime != "" {
@@ -36,7 +80,7 @@ func (e *Encoder) StreamTranscode(probeResult VideoFile, startTime string) (io.R
args = append(args, args = append(args,
"-i", probeResult.Path, "-i", probeResult.Path,
"-c:v", "libvpx-vp9", "-c:v", "libvpx-vp9",
"-vf", "scale=iw:-2", "-vf", "scale="+scale,
"-deadline", "realtime", "-deadline", "realtime",
"-cpu-used", "5", "-cpu-used", "5",
"-row-mt", "1", "-row-mt", "1",

View File

@@ -8,6 +8,7 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stashapp/stash/pkg/models"
"github.com/stashapp/stash/pkg/utils" "github.com/stashapp/stash/pkg/utils"
) )
@@ -21,6 +22,9 @@ const Password = "password"
const Database = "database" const Database = "database"
const MaxTranscodeSize = "max_transcode_size"
const MaxStreamingTranscodeSize = "max_streaming_transcode_size"
const Host = "host" const Host = "host"
const Port = "port" const Port = "port"
@@ -77,6 +81,28 @@ func GetPort() int {
return viper.GetInt(Port) 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 { func GetUsername() string {
return viper.GetString(Username) return viper.GetString(Username)
} }

View File

@@ -1,11 +1,13 @@
package manager package manager
import ( import (
"github.com/stashapp/stash/pkg/ffmpeg"
"github.com/stashapp/stash/pkg/logger"
"github.com/stashapp/stash/pkg/models"
"os" "os"
"sync" "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 { type GenerateTranscodeTask struct {
@@ -33,8 +35,10 @@ func (t *GenerateTranscodeTask) Start(wg *sync.WaitGroup) {
} }
outputPath := instance.Paths.Generated.GetTmpPath(t.Scene.Checksum + ".mp4") outputPath := instance.Paths.Generated.GetTmpPath(t.Scene.Checksum + ".mp4")
transcodeSize := config.GetMaxTranscodeSize()
options := ffmpeg.TranscodeOptions{ options := ffmpeg.TranscodeOptions{
OutputPath: outputPath, OutputPath: outputPath,
MaxTranscodeSize: transcodeSize,
} }
encoder := ffmpeg.NewEncoder(instance.FFMPEGPath) encoder := ffmpeg.NewEncoder(instance.FFMPEGPath)
encoder.Transcode(*videoFile, options) encoder.Transcode(*videoFile, options)

View File

@@ -25,6 +25,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IPr
const [stashes, setStashes] = useState<string[]>([]); const [stashes, setStashes] = useState<string[]>([]);
const [databasePath, setDatabasePath] = useState<string | undefined>(undefined); const [databasePath, setDatabasePath] = useState<string | undefined>(undefined);
const [generatedPath, setGeneratedPath] = useState<string | undefined>(undefined); const [generatedPath, setGeneratedPath] = useState<string | undefined>(undefined);
const [maxTranscodeSize, setMaxTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);
const [maxStreamingTranscodeSize, setMaxStreamingTranscodeSize] = useState<GQL.StreamingResolutionEnum | undefined>(undefined);
const [username, setUsername] = useState<string | undefined>(undefined); const [username, setUsername] = useState<string | undefined>(undefined);
const [password, setPassword] = useState<string | undefined>(undefined); const [password, setPassword] = useState<string | undefined>(undefined);
const [logFile, setLogFile] = useState<string | undefined>(); const [logFile, setLogFile] = useState<string | undefined>();
@@ -38,6 +40,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IPr
stashes, stashes,
databasePath, databasePath,
generatedPath, generatedPath,
maxTranscodeSize,
maxStreamingTranscodeSize,
username, username,
password, password,
logFile, logFile,
@@ -53,6 +57,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IPr
setStashes(conf.general.stashes || []); setStashes(conf.general.stashes || []);
setDatabasePath(conf.general.databasePath); setDatabasePath(conf.general.databasePath);
setGeneratedPath(conf.general.generatedPath); setGeneratedPath(conf.general.generatedPath);
setMaxTranscodeSize(conf.general.maxTranscodeSize);
setMaxStreamingTranscodeSize(conf.general.maxStreamingTranscodeSize);
setUsername(conf.general.username); setUsername(conf.general.username);
setPassword(conf.general.password); setPassword(conf.general.password);
setLogFile(conf.general.logFile); setLogFile(conf.general.logFile);
@@ -76,46 +82,114 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (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 ( return (
<> <>
{!!error ? <h1>{error.message}</h1> : undefined} {!!error ? <h1>{error.message}</h1> : undefined}
{(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined} {(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
<H4>Library</H4> <H4>Library</H4>
<FormGroup <FormGroup>
label="Stashes" <FormGroup>
helperText="Directory locations to your content" <FormGroup
> label="Stashes"
<FolderSelect helperText="Directory locations to your content"
directories={stashes} >
onDirectoriesChanged={onStashesChanged} <FolderSelect
/> directories={stashes}
</FormGroup> onDirectoriesChanged={onStashesChanged}
<FormGroup />
label="Database Path" </FormGroup>
helperText="File location for the SQLite database (requires restart)" </FormGroup>
>
<InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} /> <FormGroup
</FormGroup> label="Database Path"
<FormGroup helperText="File location for the SQLite database (requires restart)"
label="Generated Path" >
helperText="Directory location for the generated files (scene markers, scene previews, sprites, etc)" <InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
> </FormGroup>
<InputGroup value={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
<FormGroup
label="Generated Path"
helperText="Directory location for the generated files (scene markers, scene previews, sprites, etc)"
>
<InputGroup value={generatedPath} onChange={(e: any) => setGeneratedPath(e.target.value)} />
</FormGroup>
</FormGroup> </FormGroup>
<Divider /> <Divider />
<H4>Authentication</H4> <FormGroup>
<FormGroup <H4>Video</H4>
label="Username" <FormGroup
helperText="Username to access Stash. Leave blank to disable user authentication" label="Maximum transcode size"
> helperText="Maximum size for generated transcodes"
<InputGroup value={username} onChange={(e: any) => setUsername(e.target.value)} /> >
</FormGroup> <HTMLSelect
<FormGroup options={transcodeQualities}
label="Password" onChange={(event) => setMaxTranscodeSize(translateQuality(event.target.value))}
helperText="Password to access Stash. Leave blank to disable user authentication" value={resolutionToString(maxTranscodeSize)}
> />
<InputGroup type="password" value={password} onChange={(e: any) => setPassword(e.target.value)} /> </FormGroup>
<FormGroup
label="Maximum streaming transcode size"
helperText="Maximum size for transcoded streams"
>
<HTMLSelect
options={transcodeQualities}
onChange={(event) => setMaxStreamingTranscodeSize(translateQuality(event.target.value))}
value={resolutionToString(maxStreamingTranscodeSize)}
/>
</FormGroup>
</FormGroup>
<Divider />
<FormGroup>
<H4>Authentication</H4>
<FormGroup
label="Username"
helperText="Username to access Stash. Leave blank to disable user authentication"
>
<InputGroup value={username} onChange={(e: any) => setUsername(e.target.value)} />
</FormGroup>
<FormGroup
label="Password"
helperText="Password to access Stash. Leave blank to disable user authentication"
>
<InputGroup type="password" value={password} onChange={(e: any) => setPassword(e.target.value)} />
</FormGroup>
</FormGroup> </FormGroup>
<Divider /> <Divider />

View File

@@ -4,6 +4,7 @@ import {
Dialog, Dialog,
InputGroup, InputGroup,
Spinner, Spinner,
FormGroup,
} from "@blueprintjs/core"; } from "@blueprintjs/core";
import _ from "lodash"; import _ from "lodash";
import React, { FunctionComponent, useEffect, useState } from "react"; import React, { FunctionComponent, useEffect, useState } from "react";
@@ -76,9 +77,12 @@ export const FolderSelect: FunctionComponent<IProps> = (props: IProps) => {
<> <>
{!!error ? <h1>{error.message}</h1> : undefined} {!!error ? <h1>{error.message}</h1> : undefined}
{renderDialog()} {renderDialog()}
{selectedDirectories.map((path) => { <FormGroup>
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>; {selectedDirectories.map((path) => {
})} return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
})}
</FormGroup>
<Button small={true} onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button> <Button small={true} onClick={() => setIsDisplayingDialog(true)}>Add Directory</Button>
</> </>
); );