mirror of https://github.com/stashapp/stash.git
Simple hardware encoding (#3419)
* HW Accel * CUDA Docker build and adjust the NVENC encoder * Removed NVENC preset Using legacy presets is removed in SDK 12 and deprecated since SDK 10. This commit removed the preset to allow ffmpeg to select the default one. --------- Co-authored-by: Nodude <> Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
parent
d4fb6b2acf
commit
0c1b02380e
5
Makefile
5
Makefile
|
@ -278,3 +278,8 @@ validate-backend: lint it
|
|||
.PHONY: docker-build
|
||||
docker-build: pre-build
|
||||
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/build -f docker/build/x86_64/Dockerfile .
|
||||
|
||||
# locally builds and tags a 'stash/cuda-build' docker image
|
||||
.PHONY: docker-cuda-build
|
||||
docker-cuda-build: pre-build
|
||||
docker build --build-arg GITHASH=$(GITHASH) --build-arg STASH_VERSION=$(STASH_VERSION) -t stash/cuda-build -f docker/build/x86_64/Dockerfile-CUDA .
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
# This dockerfile should be built with `make docker-cuda-build` from the stash root.
|
||||
|
||||
# Build Frontend
|
||||
FROM node:alpine as frontend
|
||||
RUN apk add --no-cache make
|
||||
## cache node_modules separately
|
||||
COPY ./ui/v2.5/package.json ./ui/v2.5/yarn.lock /stash/ui/v2.5/
|
||||
WORKDIR /stash
|
||||
RUN yarn --cwd ui/v2.5 install --frozen-lockfile.
|
||||
COPY Makefile /stash/
|
||||
COPY ./graphql /stash/graphql/
|
||||
COPY ./ui /stash/ui/
|
||||
RUN make generate-frontend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN BUILD_DATE=$(date +"%Y-%m-%d %H:%M:%S") make ui
|
||||
|
||||
# Build Backend
|
||||
FROM golang:1.19-bullseye as backend
|
||||
RUN apt update && apt install -y build-essential golang
|
||||
WORKDIR /stash
|
||||
COPY ./go* ./*.go Makefile gqlgen.yml .gqlgenc.yml /stash/
|
||||
COPY ./scripts /stash/scripts/
|
||||
COPY ./vendor /stash/vendor/
|
||||
COPY ./pkg /stash/pkg/
|
||||
COPY ./cmd /stash/cmd
|
||||
COPY ./internal /stash/internal
|
||||
COPY --from=frontend /stash /stash/
|
||||
RUN make generate-backend
|
||||
ARG GITHASH
|
||||
ARG STASH_VERSION
|
||||
RUN make build
|
||||
|
||||
# Final Runnable Image
|
||||
FROM nvidia/cuda:12.0.1-runtime-ubuntu22.04
|
||||
RUN apt update
|
||||
RUN apt upgrade -y
|
||||
RUN apt install -y ca-certificates libvips-tools ffmpeg wget
|
||||
RUN rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=backend /stash/stash /usr/bin/
|
||||
|
||||
# NVENC Patch
|
||||
RUN mkdir -p /usr/local/bin /patched-lib
|
||||
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/patch.sh -O /usr/local/bin/patch.sh
|
||||
RUN wget https://raw.githubusercontent.com/keylase/nvidia-patch/master/docker-entrypoint.sh -O /usr/local/bin/docker-entrypoint.sh
|
||||
RUN chmod +x /usr/local/bin/patch.sh /usr/local/bin/docker-entrypoint.sh /usr/bin/stash
|
||||
|
||||
ENV LANG C.UTF-8
|
||||
ENV NVIDIA_VISIBLE_DEVICES all
|
||||
ENV NVIDIA_DRIVER_CAPABILITIES=video,utility
|
||||
ENV STASH_CONFIG_FILE=/root/.stash/config.yml
|
||||
EXPOSE 9999
|
||||
ENTRYPOINT ["docker-entrypoint.sh", "stash"]
|
|
@ -19,6 +19,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
previewExcludeStart
|
||||
previewExcludeEnd
|
||||
previewPreset
|
||||
transcodeHardwareAcceleration
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
writeImageThumbnails
|
||||
|
|
|
@ -67,6 +67,8 @@ input ConfigGeneralInput {
|
|||
previewExcludeEnd: String
|
||||
"""Preset when generating preview"""
|
||||
previewPreset: PreviewPreset
|
||||
"""Transcode Hardware Acceleration"""
|
||||
transcodeHardwareAcceleration: Boolean
|
||||
"""Max generated transcode size"""
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
|
@ -170,6 +172,8 @@ type ConfigGeneralResult {
|
|||
previewExcludeEnd: String!
|
||||
"""Preset when generating preview"""
|
||||
previewPreset: PreviewPreset!
|
||||
"""Transcode Hardware Acceleration"""
|
||||
transcodeHardwareAcceleration: Boolean!
|
||||
"""Max generated transcode size"""
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"""Max streaming transcode size"""
|
||||
|
|
|
@ -181,6 +181,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||
}
|
||||
|
||||
if input.TranscodeHardwareAcceleration != nil {
|
||||
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
|
||||
}
|
||||
if input.MaxTranscodeSize != nil {
|
||||
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
|
|
@ -83,52 +83,53 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
|||
scraperCDPPath := config.GetScraperCDPPath()
|
||||
|
||||
return &ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
PreviewAudio: config.GetPreviewAudio(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
||||
GalleryCoverRegex: config.GetGalleryCoverRegex(),
|
||||
APIKey: config.GetAPIKey(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
LogAccess: config.GetLogAccess(),
|
||||
VideoExtensions: config.GetVideoExtensions(),
|
||||
ImageExtensions: config.GetImageExtensions(),
|
||||
GalleryExtensions: config.GetGalleryExtensions(),
|
||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
PreviewAudio: config.GetPreviewAudio(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
TranscodeHardwareAcceleration: config.GetTranscodeHardwareAcceleration(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
||||
GalleryCoverRegex: config.GetGalleryCoverRegex(),
|
||||
APIKey: config.GetAPIKey(),
|
||||
Username: config.GetUsername(),
|
||||
Password: config.GetPasswordHash(),
|
||||
MaxSessionAge: config.GetMaxSessionAge(),
|
||||
LogFile: &logFile,
|
||||
LogOut: config.GetLogOut(),
|
||||
LogLevel: config.GetLogLevel(),
|
||||
LogAccess: config.GetLogAccess(),
|
||||
VideoExtensions: config.GetVideoExtensions(),
|
||||
ImageExtensions: config.GetImageExtensions(),
|
||||
GalleryExtensions: config.GetGalleryExtensions(),
|
||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||
ScraperUserAgent: &scraperUserAgent,
|
||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||
ScraperCDPPath: &scraperCDPPath,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -69,11 +69,12 @@ const (
|
|||
ParallelTasks = "parallel_tasks"
|
||||
parallelTasksDefault = 1
|
||||
|
||||
PreviewPreset = "preview_preset"
|
||||
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
|
||||
|
||||
SequentialScanning = "sequential_scanning"
|
||||
SequentialScanningDefault = false
|
||||
|
||||
PreviewPreset = "preview_preset"
|
||||
|
||||
PreviewAudio = "preview_audio"
|
||||
previewAudioDefault = true
|
||||
|
||||
|
@ -803,6 +804,10 @@ func (i *Instance) GetPreviewPreset() models.PreviewPreset {
|
|||
return models.PreviewPreset(ret)
|
||||
}
|
||||
|
||||
func (i *Instance) GetTranscodeHardwareAcceleration() bool {
|
||||
return i.getBool(TranscodeHardwareAcceleration)
|
||||
}
|
||||
|
||||
func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum {
|
||||
ret := i.getString(MaxTranscodeSize)
|
||||
|
||||
|
|
|
@ -110,7 +110,7 @@ type Manager struct {
|
|||
|
||||
Paths *paths.Paths
|
||||
|
||||
FFMPEG ffmpeg.FFMpeg
|
||||
FFMPEG *ffmpeg.FFMpeg
|
||||
FFProbe ffmpeg.FFProbe
|
||||
StreamManager *ffmpeg.StreamManager
|
||||
|
||||
|
@ -431,8 +431,10 @@ func initFFMPEG(ctx context.Context) error {
|
|||
}
|
||||
}
|
||||
|
||||
instance.FFMPEG = ffmpeg.FFMpeg(ffmpegPath)
|
||||
instance.FFMPEG = ffmpeg.NewEncoder(ffmpegPath)
|
||||
instance.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||
|
||||
instance.FFMPEG.InitHWSupport(ctx)
|
||||
instance.RefreshStreamManager()
|
||||
}
|
||||
|
||||
|
@ -681,7 +683,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
|||
}
|
||||
|
||||
func (s *Manager) validateFFMPEG() error {
|
||||
if s.FFMPEG == "" || s.FFProbe == "" {
|
||||
if s.FFMPEG == nil || s.FFProbe == "" {
|
||||
return errors.New("missing ffmpeg and/or ffprobe")
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ func (c VideoCodec) Args() []string {
|
|||
}
|
||||
|
||||
var (
|
||||
// Software codec's
|
||||
VideoCodecLibX264 VideoCodec = "libx264"
|
||||
VideoCodecLibWebP VideoCodec = "libwebp"
|
||||
VideoCodecBMP VideoCodec = "bmp"
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
package ffmpeg
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
// Hardware codec's
|
||||
VideoCodecN264 VideoCodec = "h264_nvenc"
|
||||
VideoCodecI264 VideoCodec = "h264_qsv"
|
||||
VideoCodecA264 VideoCodec = "h264_amf"
|
||||
VideoCodecM264 VideoCodec = "h264_videotoolbox"
|
||||
VideoCodecV264 VideoCodec = "h264_vaapi"
|
||||
VideoCodecR264 VideoCodec = "h264_v4l2m2m"
|
||||
VideoCodecO264 VideoCodec = "h264_omx"
|
||||
VideoCodecIVP9 VideoCodec = "vp9_qsv"
|
||||
VideoCodecVVP9 VideoCodec = "vp9_vaapi"
|
||||
VideoCodecVVPX VideoCodec = "vp8_vaapi"
|
||||
)
|
||||
|
||||
// Tests all (given) hardware codec's
|
||||
func (f *FFMpeg) InitHWSupport(ctx context.Context) {
|
||||
var hwCodecSupport []VideoCodec
|
||||
|
||||
for _, codec := range []VideoCodec{
|
||||
VideoCodecN264,
|
||||
VideoCodecI264,
|
||||
VideoCodecV264,
|
||||
VideoCodecR264,
|
||||
VideoCodecIVP9,
|
||||
VideoCodecVVP9,
|
||||
} {
|
||||
var args Args
|
||||
args = append(args, "-hide_banner")
|
||||
args = args.LogLevel(LogLevelWarning)
|
||||
args = f.hwDeviceInit(args, codec)
|
||||
args = args.Format("lavfi")
|
||||
args = args.Input("color=c=red")
|
||||
args = args.Duration(0.1)
|
||||
|
||||
videoFilter := f.hwFilterInit(codec)
|
||||
// Test scaling
|
||||
videoFilter = videoFilter.ScaleDimensions(-2, 160)
|
||||
videoFilter = f.hwCodecFilter(videoFilter, codec)
|
||||
args = append(args, CodecInit(codec)...)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
|
||||
args = args.Format("null")
|
||||
args = args.Output("-")
|
||||
|
||||
cmd := f.Command(ctx, args)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logger.Debugf("[InitHWSupport] error starting command: %w", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
errOutput := stderr.String()
|
||||
|
||||
if len(errOutput) == 0 {
|
||||
errOutput = err.Error()
|
||||
}
|
||||
|
||||
logger.Debugf("[InitHWSupport] Codec %s not supported. Error output:\n%s", codec, errOutput)
|
||||
} else {
|
||||
hwCodecSupport = append(hwCodecSupport, codec)
|
||||
}
|
||||
}
|
||||
|
||||
outstr := "[InitHWSupport] Supported HW codecs:\n"
|
||||
for _, codec := range hwCodecSupport {
|
||||
outstr += fmt.Sprintf("\t%s\n", codec)
|
||||
}
|
||||
logger.Info(outstr)
|
||||
|
||||
f.hwCodecSupport = hwCodecSupport
|
||||
}
|
||||
|
||||
// Prepend input for hardware encoding only
|
||||
func (f *FFMpeg) hwDeviceInit(args Args, codec VideoCodec) Args {
|
||||
switch codec {
|
||||
case VideoCodecN264:
|
||||
args = append(args, "-hwaccel_device")
|
||||
args = append(args, "0")
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
args = append(args, "-vaapi_device")
|
||||
args = append(args, "/dev/dri/renderD128")
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
args = append(args, "-init_hw_device")
|
||||
args = append(args, "qsv=hw")
|
||||
args = append(args, "-filter_hw_device")
|
||||
args = append(args, "hw")
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Initialise a video filter for HW encoding
|
||||
func (f *FFMpeg) hwFilterInit(codec VideoCodec) VideoFilter {
|
||||
var videoFilter VideoFilter
|
||||
switch codec {
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload")
|
||||
case VideoCodecN264:
|
||||
videoFilter = videoFilter.Append("format=nv12")
|
||||
videoFilter = videoFilter.Append("hwupload_cuda")
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
videoFilter = videoFilter.Append("hwupload=extra_hw_frames=64")
|
||||
videoFilter = videoFilter.Append("format=qsv")
|
||||
}
|
||||
|
||||
return videoFilter
|
||||
}
|
||||
|
||||
// Replace video filter scaling with hardware scaling for full hardware transcoding
|
||||
func (f *FFMpeg) hwCodecFilter(args VideoFilter, codec VideoCodec) VideoFilter {
|
||||
sargs := string(args)
|
||||
|
||||
if strings.Contains(sargs, "scale=") {
|
||||
switch codec {
|
||||
case VideoCodecN264:
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_cuda=", 1))
|
||||
case VideoCodecV264,
|
||||
VideoCodecVVP9:
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_vaapi=", 1))
|
||||
case VideoCodecI264,
|
||||
VideoCodecIVP9:
|
||||
// BUG: [scale_qsv]: Size values less than -1 are not acceptable.
|
||||
// Fix: Replace all instances of -2 with -1 in a scale operation
|
||||
re := regexp.MustCompile(`(scale=)([\d:]*)(-2)(.*)`)
|
||||
sargs = re.ReplaceAllString(sargs, "scale=$2-1$4")
|
||||
args = VideoFilter(strings.Replace(sargs, "scale=", "scale_qsv=", 1))
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
// Returns the max resolution for a given codec, or a default
|
||||
func (f *FFMpeg) hwCodecMaxRes(codec VideoCodec, dW int, dH int) (int, int) {
|
||||
if codec == VideoCodecN264 {
|
||||
return 4096, 4096
|
||||
}
|
||||
|
||||
return dW, dH
|
||||
}
|
||||
|
||||
// Return a maxres filter
|
||||
func (f *FFMpeg) hwMaxResFilter(codec VideoCodec, width int, height int, max int) VideoFilter {
|
||||
videoFilter := f.hwFilterInit(codec)
|
||||
maxWidth, maxHeight := f.hwCodecMaxRes(codec, width, height)
|
||||
videoFilter = videoFilter.ScaleMaxLM(width, height, max, maxWidth, maxHeight)
|
||||
return f.hwCodecFilter(videoFilter, codec)
|
||||
}
|
||||
|
||||
// Return if a hardware accelerated for HLS is available
|
||||
func (f *FFMpeg) hwCodecHLSCompatible() *VideoCodec {
|
||||
for _, element := range f.hwCodecSupport {
|
||||
switch element {
|
||||
case VideoCodecN264,
|
||||
VideoCodecI264,
|
||||
VideoCodecV264,
|
||||
VideoCodecR264:
|
||||
return &element
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return if a hardware accelerated codec for MP4 is available
|
||||
func (f *FFMpeg) hwCodecMP4Compatible() *VideoCodec {
|
||||
for _, element := range f.hwCodecSupport {
|
||||
switch element {
|
||||
case VideoCodecN264,
|
||||
VideoCodecI264:
|
||||
return &element
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return if a hardware accelerated codec for WebM is available
|
||||
func (f *FFMpeg) hwCodecWEBMCompatible() *VideoCodec {
|
||||
for _, element := range f.hwCodecSupport {
|
||||
switch element {
|
||||
case VideoCodecIVP9,
|
||||
VideoCodecVVP9:
|
||||
return &element
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
|
@ -9,9 +9,21 @@ import (
|
|||
)
|
||||
|
||||
// FFMpeg provides an interface to ffmpeg.
|
||||
type FFMpeg string
|
||||
type FFMpeg struct {
|
||||
ffmpeg string
|
||||
hwCodecSupport []VideoCodec
|
||||
}
|
||||
|
||||
// Creates a new FFMpeg encoder
|
||||
func NewEncoder(ffmpegPath string) *FFMpeg {
|
||||
ret := &FFMpeg{
|
||||
ffmpeg: ffmpegPath,
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// Returns an exec.Cmd that can be used to run ffmpeg using args.
|
||||
func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd {
|
||||
return stashExec.CommandContext(ctx, string(*f), args...)
|
||||
return stashExec.CommandContext(ctx, string(f.ffmpeg), args...)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package ffmpeg
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// VideoFilter represents video filter parameters to be passed to ffmpeg.
|
||||
type VideoFilter string
|
||||
|
@ -57,6 +59,35 @@ func (f VideoFilter) ScaleMax(inputWidth, inputHeight, maxSize int) VideoFilter
|
|||
return f.ScaleDimensions(maxSize, -2)
|
||||
}
|
||||
|
||||
// ScaleMaxLM returns a VideoFilter scaling to maxSize with respect to a max size.
|
||||
func (f VideoFilter) ScaleMaxLM(width int, height int, reqHeight int, maxWidth int, maxHeight int) VideoFilter {
|
||||
// calculate the aspect ratio of the current resolution
|
||||
aspectRatio := width / height
|
||||
|
||||
// find the max height
|
||||
desiredHeight := reqHeight
|
||||
if desiredHeight == 0 {
|
||||
desiredHeight = height
|
||||
}
|
||||
|
||||
// calculate the desired width based on the desired height and the aspect ratio
|
||||
desiredWidth := int(desiredHeight * aspectRatio)
|
||||
|
||||
// check which dimension to scale based on the maximum resolution
|
||||
if desiredHeight > maxHeight || desiredWidth > maxWidth {
|
||||
if desiredHeight-maxHeight > desiredWidth-maxWidth {
|
||||
// scale the height down to the maximum height
|
||||
return f.ScaleDimensions(-2, maxHeight)
|
||||
} else {
|
||||
// scale the width down to the maximum width
|
||||
return f.ScaleDimensions(maxWidth, -2)
|
||||
}
|
||||
}
|
||||
|
||||
// the current resolution can be scaled to the desired height without exceeding the maximum resolution
|
||||
return f.ScaleMax(width, height, reqHeight)
|
||||
}
|
||||
|
||||
// Fps returns a VideoFilter setting the frames per second.
|
||||
func (f VideoFilter) Fps(fps int) VideoFilter {
|
||||
return f.Append(fmt.Sprintf("fps=%v", fps))
|
||||
|
|
|
@ -16,7 +16,7 @@ type FrameInfo struct {
|
|||
|
||||
// CalculateFrameRate calculates the frame rate and number of frames of the video file.
|
||||
// Used where the frame rate or NbFrames is missing or invalid in the ffprobe output.
|
||||
func (f FFMpeg) CalculateFrameRate(ctx context.Context, v *VideoFile) (*FrameInfo, error) {
|
||||
func (f *FFMpeg) CalculateFrameRate(ctx context.Context, v *VideoFile) (*FrameInfo, error) {
|
||||
var args Args
|
||||
args = append(args, "-nostats")
|
||||
args = args.Input(v.Path).
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
// Generate runs ffmpeg with the given args and waits for it to finish.
|
||||
// Returns an error if the command fails. If the command fails, the return
|
||||
// value will be of type *exec.ExitError.
|
||||
func (f FFMpeg) Generate(ctx context.Context, args Args) error {
|
||||
func (f *FFMpeg) Generate(ctx context.Context, args Args) error {
|
||||
cmd := f.Command(ctx, args)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
|
@ -36,7 +36,7 @@ func (f FFMpeg) Generate(ctx context.Context, args Args) error {
|
|||
}
|
||||
|
||||
// GenerateOutput runs ffmpeg with the given args and returns it standard output.
|
||||
func (f FFMpeg) GenerateOutput(ctx context.Context, args []string, stdin io.Reader) ([]byte, error) {
|
||||
func (f *FFMpeg) GenerateOutput(ctx context.Context, args []string, stdin io.Reader) ([]byte, error) {
|
||||
cmd := f.Command(ctx, args)
|
||||
cmd.Stdin = stdin
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ const (
|
|||
|
||||
type StreamManager struct {
|
||||
cacheDir string
|
||||
encoder FFMpeg
|
||||
encoder *FFMpeg
|
||||
ffprobe FFProbe
|
||||
|
||||
config StreamManagerConfig
|
||||
|
@ -39,9 +39,10 @@ type StreamManagerConfig interface {
|
|||
GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum
|
||||
GetLiveTranscodeInputArgs() []string
|
||||
GetLiveTranscodeOutputArgs() []string
|
||||
GetTranscodeHardwareAcceleration() bool
|
||||
}
|
||||
|
||||
func NewStreamManager(cacheDir string, encoder FFMpeg, ffprobe FFProbe, config StreamManagerConfig, lockManager *fsutil.ReadLockManager) *StreamManager {
|
||||
func NewStreamManager(cacheDir string, encoder *FFMpeg, ffprobe FFProbe, config StreamManagerConfig, lockManager *fsutil.ReadLockManager) *StreamManager {
|
||||
if cacheDir == "" {
|
||||
logger.Warn("cache directory is not set. Live HLS transcoding will be disabled")
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ type StreamType struct {
|
|||
Name string
|
||||
SegmentType *SegmentType
|
||||
ServeManifest func(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string)
|
||||
Args func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) Args
|
||||
Args func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) Args
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -59,15 +59,11 @@ var (
|
|||
Name: "hls",
|
||||
SegmentType: SegmentTypeTS,
|
||||
ServeManifest: serveHLSManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
args = CodecInit(codec)
|
||||
args = append(args,
|
||||
"-c:v", "libx264",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", "veryfast",
|
||||
"-crf", "25",
|
||||
"-flags", "+cgop",
|
||||
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
||||
"-sc_threshold", "0",
|
||||
)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
if videoOnly {
|
||||
|
@ -97,10 +93,8 @@ var (
|
|||
Name: "hls-copy",
|
||||
SegmentType: SegmentTypeTS,
|
||||
ServeManifest: serveHLSManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
args = append(args,
|
||||
"-c:v", "copy",
|
||||
)
|
||||
Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
args = CodecInit(codec)
|
||||
if videoOnly {
|
||||
args = append(args, "-an")
|
||||
} else {
|
||||
|
@ -128,23 +122,19 @@ var (
|
|||
Name: "dash-v",
|
||||
SegmentType: SegmentTypeWEBMVideo,
|
||||
ServeManifest: serveDASHManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
// only generate the actual init segment (init_v.webm)
|
||||
// when generating the first segment
|
||||
init := ".init"
|
||||
if segment == 0 {
|
||||
init = "init"
|
||||
}
|
||||
|
||||
args = CodecInit(codec)
|
||||
args = append(args,
|
||||
"-c:v", "libvpx-vp9",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
||||
)
|
||||
|
||||
args = args.VideoFilter(videoFilter)
|
||||
args = append(args,
|
||||
"-copyts",
|
||||
|
@ -162,7 +152,7 @@ var (
|
|||
Name: "dash-a",
|
||||
SegmentType: SegmentTypeWEBMAudio,
|
||||
ServeManifest: serveDASHManifest,
|
||||
Args: func(segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
Args: func(codec VideoCodec, segment int, videoFilter VideoFilter, videoOnly bool, outputDir string) (args Args) {
|
||||
// only generate the actual init segment (init_a.webm)
|
||||
// when generating the first segment
|
||||
init := ".init"
|
||||
|
@ -310,6 +300,25 @@ func (t StreamType) FileDir(hash string, maxTranscodeSize int) string {
|
|||
}
|
||||
}
|
||||
|
||||
func HLSGetCodec(sm *StreamManager, name string) (codec VideoCodec) {
|
||||
switch name {
|
||||
case "hls":
|
||||
codec = VideoCodecLibX264
|
||||
if hwcodec := sm.encoder.hwCodecHLSCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
}
|
||||
case "dash-v":
|
||||
codec = VideoCodecVP9
|
||||
if hwcodec := sm.encoder.hwCodecWEBMCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
}
|
||||
case "hls-copy":
|
||||
codec = VideoCodecCopy
|
||||
}
|
||||
|
||||
return codec
|
||||
}
|
||||
|
||||
func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
||||
extraInputArgs := sm.config.GetLiveTranscodeInputArgs()
|
||||
extraOutputArgs := sm.config.GetLiveTranscodeOutputArgs()
|
||||
|
@ -317,6 +326,9 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
|||
args := Args{"-hide_banner"}
|
||||
args = args.LogLevel(LogLevelError)
|
||||
|
||||
codec := HLSGetCodec(sm, s.streamType.Name)
|
||||
|
||||
args = sm.encoder.hwDeviceInit(args, codec)
|
||||
args = append(args, extraInputArgs...)
|
||||
|
||||
if segment > 0 {
|
||||
|
@ -327,10 +339,9 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
|||
|
||||
videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported
|
||||
|
||||
var videoFilter VideoFilter
|
||||
videoFilter = videoFilter.ScaleMax(s.vf.Width, s.vf.Height, s.maxTranscodeSize)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize)
|
||||
|
||||
args = append(args, s.streamType.Args(segment, videoFilter, videoOnly, s.outputDir)...)
|
||||
args = append(args, s.streamType.Args(codec, segment, videoFilter, videoOnly, s.outputDir)...)
|
||||
|
||||
args = append(args, extraOutputArgs...)
|
||||
|
||||
|
|
|
@ -16,20 +16,78 @@ import (
|
|||
|
||||
type StreamFormat struct {
|
||||
MimeType string
|
||||
Args func(videoFilter VideoFilter, videoOnly bool) Args
|
||||
Args func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) Args
|
||||
}
|
||||
|
||||
func CodecInit(codec VideoCodec) (args Args) {
|
||||
args = args.VideoCodec(codec)
|
||||
|
||||
switch codec {
|
||||
// CPU Codecs
|
||||
case VideoCodecLibX264:
|
||||
args = append(args,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", "veryfast",
|
||||
"-crf", "25",
|
||||
"-sc_threshold", "0",
|
||||
)
|
||||
case VideoCodecVP9:
|
||||
args = append(args,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
)
|
||||
// HW Codecs
|
||||
case VideoCodecN264:
|
||||
args = append(args,
|
||||
"-rc", "vbr",
|
||||
"-cq", "15",
|
||||
)
|
||||
case VideoCodecI264:
|
||||
args = append(args,
|
||||
"-global_quality", "20",
|
||||
"-preset", "faster",
|
||||
)
|
||||
case VideoCodecV264:
|
||||
args = append(args,
|
||||
"-qp", "20",
|
||||
)
|
||||
case VideoCodecA264:
|
||||
args = append(args,
|
||||
"-quality", "speed",
|
||||
)
|
||||
case VideoCodecM264:
|
||||
args = append(args,
|
||||
"-prio_speed", "1",
|
||||
)
|
||||
case VideoCodecO264:
|
||||
args = append(args,
|
||||
"-preset", "superfast",
|
||||
"-crf", "25",
|
||||
)
|
||||
case VideoCodecIVP9:
|
||||
args = append(args,
|
||||
"-global_quality", "20",
|
||||
"-preset", "faster",
|
||||
)
|
||||
case VideoCodecVVP9:
|
||||
args = append(args,
|
||||
"-qp", "20",
|
||||
)
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
var (
|
||||
StreamTypeMP4 = StreamFormat{
|
||||
MimeType: MimeMp4Video,
|
||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = args.VideoCodec(VideoCodecLibX264)
|
||||
args = append(args,
|
||||
"-movflags", "frag_keyframe+empty_moov",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", "veryfast",
|
||||
"-crf", "25",
|
||||
)
|
||||
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = CodecInit(codec)
|
||||
args = append(args, "-movflags", "frag_keyframe+empty_moov")
|
||||
args = args.VideoFilter(videoFilter)
|
||||
if videoOnly {
|
||||
args = args.SkipAudio()
|
||||
|
@ -42,16 +100,8 @@ var (
|
|||
}
|
||||
StreamTypeWEBM = StreamFormat{
|
||||
MimeType: MimeWebmVideo,
|
||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = args.VideoCodec(VideoCodecVP9)
|
||||
args = append(args,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-deadline", "realtime",
|
||||
"-cpu-used", "5",
|
||||
"-row-mt", "1",
|
||||
"-crf", "30",
|
||||
"-b:v", "0",
|
||||
)
|
||||
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = CodecInit(codec)
|
||||
args = args.VideoFilter(videoFilter)
|
||||
if videoOnly {
|
||||
args = args.SkipAudio()
|
||||
|
@ -64,8 +114,8 @@ var (
|
|||
}
|
||||
StreamTypeMKV = StreamFormat{
|
||||
MimeType: MimeMkvVideo,
|
||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = args.VideoCodec(VideoCodecCopy)
|
||||
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||
args = CodecInit(codec)
|
||||
if videoOnly {
|
||||
args = args.SkipAudio()
|
||||
} else {
|
||||
|
@ -89,6 +139,25 @@ type TranscodeOptions struct {
|
|||
StartTime float64
|
||||
}
|
||||
|
||||
func FileGetCodec(sm *StreamManager, mimetype string) (codec VideoCodec) {
|
||||
switch mimetype {
|
||||
case MimeMp4Video:
|
||||
codec = VideoCodecLibX264
|
||||
if hwcodec := sm.encoder.hwCodecMP4Compatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
}
|
||||
case MimeWebmVideo:
|
||||
codec = VideoCodecVP9
|
||||
if hwcodec := sm.encoder.hwCodecWEBMCompatible(); hwcodec != nil && sm.config.GetTranscodeHardwareAcceleration() {
|
||||
codec = *hwcodec
|
||||
}
|
||||
case MimeMkvVideo:
|
||||
codec = VideoCodecCopy
|
||||
}
|
||||
|
||||
return codec
|
||||
}
|
||||
|
||||
func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
||||
maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution()
|
||||
if o.Resolution != "" {
|
||||
|
@ -100,6 +169,9 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
|||
args := Args{"-hide_banner"}
|
||||
args = args.LogLevel(LogLevelError)
|
||||
|
||||
codec := FileGetCodec(sm, o.StreamType.MimeType)
|
||||
|
||||
args = sm.encoder.hwDeviceInit(args, codec)
|
||||
args = append(args, extraInputArgs...)
|
||||
|
||||
if o.StartTime != 0 {
|
||||
|
@ -110,10 +182,9 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
|||
|
||||
videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported
|
||||
|
||||
var videoFilter VideoFilter
|
||||
videoFilter = videoFilter.ScaleMax(o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize)
|
||||
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize)
|
||||
|
||||
args = append(args, o.StreamType.Args(videoFilter, videoOnly)...)
|
||||
args = append(args, o.StreamType.Args(codec, videoFilter, videoOnly)...)
|
||||
|
||||
args = append(args, extraOutputArgs...)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ const (
|
|||
rows = 5
|
||||
)
|
||||
|
||||
func Generate(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) {
|
||||
func Generate(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error) {
|
||||
sprite, err := generateSprite(encoder, videoFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -37,7 +37,7 @@ func Generate(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error)
|
|||
return &hashValue, nil
|
||||
}
|
||||
|
||||
func generateSpriteScreenshot(encoder ffmpeg.FFMpeg, input string, t float64) (image.Image, error) {
|
||||
func generateSpriteScreenshot(encoder *ffmpeg.FFMpeg, input string, t float64) (image.Image, error) {
|
||||
options := transcoder.ScreenshotOptions{
|
||||
Width: screenshotSize,
|
||||
OutputPath: "-",
|
||||
|
@ -76,7 +76,7 @@ func combineImages(images []image.Image) image.Image {
|
|||
return montage
|
||||
}
|
||||
|
||||
func generateSprite(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (image.Image, error) {
|
||||
func generateSprite(encoder *ffmpeg.FFMpeg, videoFile *file.VideoFile) (image.Image, error) {
|
||||
logger.Infof("[generator] generating phash sprite for %s", videoFile.Path)
|
||||
|
||||
// Generate sprite image offset by 5% on each end to avoid intro/outros
|
||||
|
|
|
@ -32,7 +32,7 @@ type ThumbnailGenerator interface {
|
|||
}
|
||||
|
||||
type ThumbnailEncoder struct {
|
||||
ffmpeg ffmpeg.FFMpeg
|
||||
ffmpeg *ffmpeg.FFMpeg
|
||||
vips *vipsEncoder
|
||||
}
|
||||
|
||||
|
@ -43,7 +43,7 @@ func GetVipsPath() string {
|
|||
return vipsPath
|
||||
}
|
||||
|
||||
func NewThumbnailEncoder(ffmpegEncoder ffmpeg.FFMpeg) ThumbnailEncoder {
|
||||
func NewThumbnailEncoder(ffmpegEncoder *ffmpeg.FFMpeg) ThumbnailEncoder {
|
||||
ret := ThumbnailEncoder{
|
||||
ffmpeg: ffmpegEncoder,
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ type FFMpegConfig interface {
|
|||
}
|
||||
|
||||
type Generator struct {
|
||||
Encoder ffmpeg.FFMpeg
|
||||
Encoder *ffmpeg.FFMpeg
|
||||
FFMpegConfig FFMpegConfig
|
||||
LockManager *fsutil.ReadLockManager
|
||||
MarkerPaths MarkerPaths
|
||||
|
|
|
@ -228,6 +228,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
))}
|
||||
</SelectSetting>
|
||||
|
||||
<BooleanSetting
|
||||
id="hardware-encoding"
|
||||
headingID="config.general.ffmpeg.hardware_acceleration.heading"
|
||||
subHeadingID="config.general.ffmpeg.hardware_acceleration.desc"
|
||||
checked={general.transcodeHardwareAcceleration ?? false}
|
||||
onChange={(v) => saveGeneral({ transcodeHardwareAcceleration: v })}
|
||||
/>
|
||||
|
||||
<StringListSetting
|
||||
id="transcode-input-args"
|
||||
headingID="config.general.ffmpeg.transcode.input_args.heading"
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
##### 💥 Note: The cache directory is now required if using HLS streaming. Please set the cache directory in the System Settings page.
|
||||
|
||||
### ✨ New Features
|
||||
* Added hardware acceleration support (for a limited number of encoders) for transcoding. ([#3419](https://github.com/stashapp/stash/pull/3419))
|
||||
* Added support for DASH streaming. ([#3275](https://github.com/stashapp/stash/pull/3275))
|
||||
* Added configuration option for the maximum number of items in selector drop-downs. ([#3277](https://github.com/stashapp/stash/pull/3277))
|
||||
* Added configuration option to perform generation operations sequentially after scanning a new video file. ([#3378](https://github.com/stashapp/stash/pull/3378))
|
||||
|
|
|
@ -77,6 +77,10 @@ This setting can be used to increase/decrease overall CPU utilisation in two sce
|
|||
|
||||
Note: If this is set too high it will decrease overall performance and causes failures (out of memory).
|
||||
|
||||
## Hardware Accelerated Live Transcoding
|
||||
|
||||
Hardware accelerated live transcoding can be enabled by setting the `FFmpeg hardware encoding` setting. Stash outputs the supported hardware encoders to the log file on startup at the Info log level. If a given hardware encoder is not supported, it's error message is logged to the Debug log level for debugging purposes.
|
||||
|
||||
## HLS Streaming
|
||||
|
||||
If using HLS streaming (such as on Apple devices), the Cache path must be set. This directory is used to store temporary files during the live-transcoding process. The Cache path can be set in the System settings page.
|
||||
|
|
|
@ -294,6 +294,10 @@
|
|||
"heading": "FFmpeg Live Transcode Output Args",
|
||||
"desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when live transcoding video."
|
||||
}
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"heading": "FFmpeg hardware encoding",
|
||||
"desc": "Uses available hardware to encode video for live transcoding."
|
||||
}
|
||||
},
|
||||
"funscript_heatmap_draw_range": "Include range in generated heatmaps",
|
||||
|
|
Loading…
Reference in New Issue