mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add configurable transcode sizes (#178)
This commit is contained in:
@@ -2,6 +2,8 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
||||
stashes
|
||||
databasePath
|
||||
generatedPath
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
username
|
||||
password
|
||||
logFile
|
||||
|
||||
@@ -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"""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user