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
databasePath
generatedPath
maxTranscodeSize
maxStreamingTranscodeSize
username
password
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 {
"""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"""

View File

@@ -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)
}

View File

@@ -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(),
}
}

View File

@@ -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

View File

@@ -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",

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -25,6 +25,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IPr
const [stashes, setStashes] = useState<string[]>([]);
const [databasePath, setDatabasePath] = 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 [password, setPassword] = useState<string | undefined>(undefined);
const [logFile, setLogFile] = useState<string | undefined>();
@@ -38,6 +40,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (props: IPr
stashes,
databasePath,
generatedPath,
maxTranscodeSize,
maxStreamingTranscodeSize,
username,
password,
logFile,
@@ -53,6 +57,8 @@ export const SettingsConfigurationPanel: FunctionComponent<IProps> = (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<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 (
<>
{!!error ? <h1>{error.message}</h1> : undefined}
{(!data || !data.configuration || loading) ? <Spinner size={Spinner.SIZE_LARGE} /> : undefined}
<H4>Library</H4>
<FormGroup
label="Stashes"
helperText="Directory locations to your content"
>
<FolderSelect
directories={stashes}
onDirectoriesChanged={onStashesChanged}
/>
</FormGroup>
<FormGroup
label="Database Path"
helperText="File location for the SQLite database (requires restart)"
>
<InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
</FormGroup>
<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>
<FormGroup
label="Stashes"
helperText="Directory locations to your content"
>
<FolderSelect
directories={stashes}
onDirectoriesChanged={onStashesChanged}
/>
</FormGroup>
</FormGroup>
<FormGroup
label="Database Path"
helperText="File location for the SQLite database (requires restart)"
>
<InputGroup value={databasePath} onChange={(e: any) => setDatabasePath(e.target.value)} />
</FormGroup>
<Divider />
<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
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
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)} />
<Divider />
<FormGroup>
<H4>Video</H4>
<FormGroup
label="Maximum transcode size"
helperText="Maximum size for generated transcodes"
>
<HTMLSelect
options={transcodeQualities}
onChange={(event) => setMaxTranscodeSize(translateQuality(event.target.value))}
value={resolutionToString(maxTranscodeSize)}
/>
</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>
<Divider />

View File

@@ -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<IProps> = (props: IProps) => {
<>
{!!error ? <h1>{error.message}</h1> : undefined}
{renderDialog()}
{selectedDirectories.map((path) => {
return <div key={path}>{path} <a onClick={() => onRemoveDirectory(path)}>Remove</a></div>;
})}
<FormGroup>
{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>
</>
);