mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
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:
5
Makefile
5
Makefile
@@ -278,3 +278,8 @@ validate-backend: lint it
|
|||||||
.PHONY: docker-build
|
.PHONY: docker-build
|
||||||
docker-build: pre-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 .
|
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 .
|
||||||
|
|||||||
53
docker/build/x86_64/Dockerfile-CUDA
Normal file
53
docker/build/x86_64/Dockerfile-CUDA
Normal file
@@ -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
|
previewExcludeStart
|
||||||
previewExcludeEnd
|
previewExcludeEnd
|
||||||
previewPreset
|
previewPreset
|
||||||
|
transcodeHardwareAcceleration
|
||||||
maxTranscodeSize
|
maxTranscodeSize
|
||||||
maxStreamingTranscodeSize
|
maxStreamingTranscodeSize
|
||||||
writeImageThumbnails
|
writeImageThumbnails
|
||||||
|
|||||||
@@ -67,6 +67,8 @@ input ConfigGeneralInput {
|
|||||||
previewExcludeEnd: String
|
previewExcludeEnd: String
|
||||||
"""Preset when generating preview"""
|
"""Preset when generating preview"""
|
||||||
previewPreset: PreviewPreset
|
previewPreset: PreviewPreset
|
||||||
|
"""Transcode Hardware Acceleration"""
|
||||||
|
transcodeHardwareAcceleration: Boolean
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
maxTranscodeSize: StreamingResolutionEnum
|
maxTranscodeSize: StreamingResolutionEnum
|
||||||
"""Max streaming transcode size"""
|
"""Max streaming transcode size"""
|
||||||
@@ -170,6 +172,8 @@ type ConfigGeneralResult {
|
|||||||
previewExcludeEnd: String!
|
previewExcludeEnd: String!
|
||||||
"""Preset when generating preview"""
|
"""Preset when generating preview"""
|
||||||
previewPreset: PreviewPreset!
|
previewPreset: PreviewPreset!
|
||||||
|
"""Transcode Hardware Acceleration"""
|
||||||
|
transcodeHardwareAcceleration: Boolean!
|
||||||
"""Max generated transcode size"""
|
"""Max generated transcode size"""
|
||||||
maxTranscodeSize: StreamingResolutionEnum
|
maxTranscodeSize: StreamingResolutionEnum
|
||||||
"""Max streaming transcode size"""
|
"""Max streaming transcode size"""
|
||||||
|
|||||||
@@ -181,6 +181,9 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||||||
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
c.Set(config.PreviewPreset, input.PreviewPreset.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if input.TranscodeHardwareAcceleration != nil {
|
||||||
|
c.Set(config.TranscodeHardwareAcceleration, *input.TranscodeHardwareAcceleration)
|
||||||
|
}
|
||||||
if input.MaxTranscodeSize != nil {
|
if input.MaxTranscodeSize != nil {
|
||||||
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
c.Set(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,52 +83,53 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
|||||||
scraperCDPPath := config.GetScraperCDPPath()
|
scraperCDPPath := config.GetScraperCDPPath()
|
||||||
|
|
||||||
return &ConfigGeneralResult{
|
return &ConfigGeneralResult{
|
||||||
Stashes: config.GetStashPaths(),
|
Stashes: config.GetStashPaths(),
|
||||||
DatabasePath: config.GetDatabasePath(),
|
DatabasePath: config.GetDatabasePath(),
|
||||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||||
GeneratedPath: config.GetGeneratedPath(),
|
GeneratedPath: config.GetGeneratedPath(),
|
||||||
MetadataPath: config.GetMetadataPath(),
|
MetadataPath: config.GetMetadataPath(),
|
||||||
ConfigFilePath: config.GetConfigFile(),
|
ConfigFilePath: config.GetConfigFile(),
|
||||||
ScrapersPath: config.GetScrapersPath(),
|
ScrapersPath: config.GetScrapersPath(),
|
||||||
CachePath: config.GetCachePath(),
|
CachePath: config.GetCachePath(),
|
||||||
CalculateMd5: config.IsCalculateMD5(),
|
CalculateMd5: config.IsCalculateMD5(),
|
||||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||||
ParallelTasks: config.GetParallelTasks(),
|
ParallelTasks: config.GetParallelTasks(),
|
||||||
PreviewAudio: config.GetPreviewAudio(),
|
PreviewAudio: config.GetPreviewAudio(),
|
||||||
PreviewSegments: config.GetPreviewSegments(),
|
PreviewSegments: config.GetPreviewSegments(),
|
||||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||||
PreviewPreset: config.GetPreviewPreset(),
|
PreviewPreset: config.GetPreviewPreset(),
|
||||||
MaxTranscodeSize: &maxTranscodeSize,
|
TranscodeHardwareAcceleration: config.GetTranscodeHardwareAcceleration(),
|
||||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
MaxTranscodeSize: &maxTranscodeSize,
|
||||||
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||||
GalleryCoverRegex: config.GetGalleryCoverRegex(),
|
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
||||||
APIKey: config.GetAPIKey(),
|
GalleryCoverRegex: config.GetGalleryCoverRegex(),
|
||||||
Username: config.GetUsername(),
|
APIKey: config.GetAPIKey(),
|
||||||
Password: config.GetPasswordHash(),
|
Username: config.GetUsername(),
|
||||||
MaxSessionAge: config.GetMaxSessionAge(),
|
Password: config.GetPasswordHash(),
|
||||||
LogFile: &logFile,
|
MaxSessionAge: config.GetMaxSessionAge(),
|
||||||
LogOut: config.GetLogOut(),
|
LogFile: &logFile,
|
||||||
LogLevel: config.GetLogLevel(),
|
LogOut: config.GetLogOut(),
|
||||||
LogAccess: config.GetLogAccess(),
|
LogLevel: config.GetLogLevel(),
|
||||||
VideoExtensions: config.GetVideoExtensions(),
|
LogAccess: config.GetLogAccess(),
|
||||||
ImageExtensions: config.GetImageExtensions(),
|
VideoExtensions: config.GetVideoExtensions(),
|
||||||
GalleryExtensions: config.GetGalleryExtensions(),
|
ImageExtensions: config.GetImageExtensions(),
|
||||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
GalleryExtensions: config.GetGalleryExtensions(),
|
||||||
Excludes: config.GetExcludes(),
|
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||||
ImageExcludes: config.GetImageExcludes(),
|
Excludes: config.GetExcludes(),
|
||||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
ImageExcludes: config.GetImageExcludes(),
|
||||||
ScraperUserAgent: &scraperUserAgent,
|
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||||
ScraperCertCheck: config.GetScraperCertCheck(),
|
ScraperUserAgent: &scraperUserAgent,
|
||||||
ScraperCDPPath: &scraperCDPPath,
|
ScraperCertCheck: config.GetScraperCertCheck(),
|
||||||
StashBoxes: config.GetStashBoxes(),
|
ScraperCDPPath: &scraperCDPPath,
|
||||||
PythonPath: config.GetPythonPath(),
|
StashBoxes: config.GetStashBoxes(),
|
||||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
PythonPath: config.GetPythonPath(),
|
||||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||||
|
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,11 +69,12 @@ const (
|
|||||||
ParallelTasks = "parallel_tasks"
|
ParallelTasks = "parallel_tasks"
|
||||||
parallelTasksDefault = 1
|
parallelTasksDefault = 1
|
||||||
|
|
||||||
|
PreviewPreset = "preview_preset"
|
||||||
|
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
|
||||||
|
|
||||||
SequentialScanning = "sequential_scanning"
|
SequentialScanning = "sequential_scanning"
|
||||||
SequentialScanningDefault = false
|
SequentialScanningDefault = false
|
||||||
|
|
||||||
PreviewPreset = "preview_preset"
|
|
||||||
|
|
||||||
PreviewAudio = "preview_audio"
|
PreviewAudio = "preview_audio"
|
||||||
previewAudioDefault = true
|
previewAudioDefault = true
|
||||||
|
|
||||||
@@ -803,6 +804,10 @@ func (i *Instance) GetPreviewPreset() models.PreviewPreset {
|
|||||||
return models.PreviewPreset(ret)
|
return models.PreviewPreset(ret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (i *Instance) GetTranscodeHardwareAcceleration() bool {
|
||||||
|
return i.getBool(TranscodeHardwareAcceleration)
|
||||||
|
}
|
||||||
|
|
||||||
func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum {
|
func (i *Instance) GetMaxTranscodeSize() models.StreamingResolutionEnum {
|
||||||
ret := i.getString(MaxTranscodeSize)
|
ret := i.getString(MaxTranscodeSize)
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ type Manager struct {
|
|||||||
|
|
||||||
Paths *paths.Paths
|
Paths *paths.Paths
|
||||||
|
|
||||||
FFMPEG ffmpeg.FFMpeg
|
FFMPEG *ffmpeg.FFMpeg
|
||||||
FFProbe ffmpeg.FFProbe
|
FFProbe ffmpeg.FFProbe
|
||||||
StreamManager *ffmpeg.StreamManager
|
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.FFProbe = ffmpeg.FFProbe(ffprobePath)
|
||||||
|
|
||||||
|
instance.FFMPEG.InitHWSupport(ctx)
|
||||||
instance.RefreshStreamManager()
|
instance.RefreshStreamManager()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,7 +683,7 @@ func (s *Manager) Setup(ctx context.Context, input SetupInput) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Manager) validateFFMPEG() 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")
|
return errors.New("missing ffmpeg and/or ffprobe")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ func (c VideoCodec) Args() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
// Software codec's
|
||||||
VideoCodecLibX264 VideoCodec = "libx264"
|
VideoCodecLibX264 VideoCodec = "libx264"
|
||||||
VideoCodecLibWebP VideoCodec = "libwebp"
|
VideoCodecLibWebP VideoCodec = "libwebp"
|
||||||
VideoCodecBMP VideoCodec = "bmp"
|
VideoCodecBMP VideoCodec = "bmp"
|
||||||
|
|||||||
207
pkg/ffmpeg/codec_hardware.go
Normal file
207
pkg/ffmpeg/codec_hardware.go
Normal file
@@ -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.
|
// 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.
|
// Returns an exec.Cmd that can be used to run ffmpeg using args.
|
||||||
func (f *FFMpeg) Command(ctx context.Context, args []string) *exec.Cmd {
|
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
|
package ffmpeg
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
// VideoFilter represents video filter parameters to be passed to ffmpeg.
|
// VideoFilter represents video filter parameters to be passed to ffmpeg.
|
||||||
type VideoFilter string
|
type VideoFilter string
|
||||||
@@ -57,6 +59,35 @@ func (f VideoFilter) ScaleMax(inputWidth, inputHeight, maxSize int) VideoFilter
|
|||||||
return f.ScaleDimensions(maxSize, -2)
|
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.
|
// Fps returns a VideoFilter setting the frames per second.
|
||||||
func (f VideoFilter) Fps(fps int) VideoFilter {
|
func (f VideoFilter) Fps(fps int) VideoFilter {
|
||||||
return f.Append(fmt.Sprintf("fps=%v", fps))
|
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.
|
// 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.
|
// 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
|
var args Args
|
||||||
args = append(args, "-nostats")
|
args = append(args, "-nostats")
|
||||||
args = args.Input(v.Path).
|
args = args.Input(v.Path).
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
// Generate runs ffmpeg with the given args and waits for it to finish.
|
// 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
|
// Returns an error if the command fails. If the command fails, the return
|
||||||
// value will be of type *exec.ExitError.
|
// 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)
|
cmd := f.Command(ctx, args)
|
||||||
|
|
||||||
var stderr bytes.Buffer
|
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.
|
// 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 := f.Command(ctx, args)
|
||||||
cmd.Stdin = stdin
|
cmd.Stdin = stdin
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ const (
|
|||||||
|
|
||||||
type StreamManager struct {
|
type StreamManager struct {
|
||||||
cacheDir string
|
cacheDir string
|
||||||
encoder FFMpeg
|
encoder *FFMpeg
|
||||||
ffprobe FFProbe
|
ffprobe FFProbe
|
||||||
|
|
||||||
config StreamManagerConfig
|
config StreamManagerConfig
|
||||||
@@ -39,9 +39,10 @@ type StreamManagerConfig interface {
|
|||||||
GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum
|
GetMaxStreamingTranscodeSize() models.StreamingResolutionEnum
|
||||||
GetLiveTranscodeInputArgs() []string
|
GetLiveTranscodeInputArgs() []string
|
||||||
GetLiveTranscodeOutputArgs() []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 == "" {
|
if cacheDir == "" {
|
||||||
logger.Warn("cache directory is not set. Live HLS transcoding will be disabled")
|
logger.Warn("cache directory is not set. Live HLS transcoding will be disabled")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ type StreamType struct {
|
|||||||
Name string
|
Name string
|
||||||
SegmentType *SegmentType
|
SegmentType *SegmentType
|
||||||
ServeManifest func(sm *StreamManager, w http.ResponseWriter, r *http.Request, vf *file.VideoFile, resolution string)
|
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 (
|
var (
|
||||||
@@ -59,15 +59,11 @@ var (
|
|||||||
Name: "hls",
|
Name: "hls",
|
||||||
SegmentType: SegmentTypeTS,
|
SegmentType: SegmentTypeTS,
|
||||||
ServeManifest: serveHLSManifest,
|
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,
|
args = append(args,
|
||||||
"-c:v", "libx264",
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-preset", "veryfast",
|
|
||||||
"-crf", "25",
|
|
||||||
"-flags", "+cgop",
|
"-flags", "+cgop",
|
||||||
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
||||||
"-sc_threshold", "0",
|
|
||||||
)
|
)
|
||||||
args = args.VideoFilter(videoFilter)
|
args = args.VideoFilter(videoFilter)
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
@@ -97,10 +93,8 @@ var (
|
|||||||
Name: "hls-copy",
|
Name: "hls-copy",
|
||||||
SegmentType: SegmentTypeTS,
|
SegmentType: SegmentTypeTS,
|
||||||
ServeManifest: serveHLSManifest,
|
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 = append(args,
|
args = CodecInit(codec)
|
||||||
"-c:v", "copy",
|
|
||||||
)
|
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
args = append(args, "-an")
|
args = append(args, "-an")
|
||||||
} else {
|
} else {
|
||||||
@@ -128,23 +122,19 @@ var (
|
|||||||
Name: "dash-v",
|
Name: "dash-v",
|
||||||
SegmentType: SegmentTypeWEBMVideo,
|
SegmentType: SegmentTypeWEBMVideo,
|
||||||
ServeManifest: serveDASHManifest,
|
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)
|
// only generate the actual init segment (init_v.webm)
|
||||||
// when generating the first segment
|
// when generating the first segment
|
||||||
init := ".init"
|
init := ".init"
|
||||||
if segment == 0 {
|
if segment == 0 {
|
||||||
init = "init"
|
init = "init"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
args = CodecInit(codec)
|
||||||
args = append(args,
|
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),
|
"-force_key_frames", fmt.Sprintf("expr:gte(t,n_forced*%d)", segmentLength),
|
||||||
)
|
)
|
||||||
|
|
||||||
args = args.VideoFilter(videoFilter)
|
args = args.VideoFilter(videoFilter)
|
||||||
args = append(args,
|
args = append(args,
|
||||||
"-copyts",
|
"-copyts",
|
||||||
@@ -162,7 +152,7 @@ var (
|
|||||||
Name: "dash-a",
|
Name: "dash-a",
|
||||||
SegmentType: SegmentTypeWEBMAudio,
|
SegmentType: SegmentTypeWEBMAudio,
|
||||||
ServeManifest: serveDASHManifest,
|
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)
|
// only generate the actual init segment (init_a.webm)
|
||||||
// when generating the first segment
|
// when generating the first segment
|
||||||
init := ".init"
|
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 {
|
func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
||||||
extraInputArgs := sm.config.GetLiveTranscodeInputArgs()
|
extraInputArgs := sm.config.GetLiveTranscodeInputArgs()
|
||||||
extraOutputArgs := sm.config.GetLiveTranscodeOutputArgs()
|
extraOutputArgs := sm.config.GetLiveTranscodeOutputArgs()
|
||||||
@@ -317,6 +326,9 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
|||||||
args := Args{"-hide_banner"}
|
args := Args{"-hide_banner"}
|
||||||
args = args.LogLevel(LogLevelError)
|
args = args.LogLevel(LogLevelError)
|
||||||
|
|
||||||
|
codec := HLSGetCodec(sm, s.streamType.Name)
|
||||||
|
|
||||||
|
args = sm.encoder.hwDeviceInit(args, codec)
|
||||||
args = append(args, extraInputArgs...)
|
args = append(args, extraInputArgs...)
|
||||||
|
|
||||||
if segment > 0 {
|
if segment > 0 {
|
||||||
@@ -327,10 +339,9 @@ func (s *runningStream) makeStreamArgs(sm *StreamManager, segment int) Args {
|
|||||||
|
|
||||||
videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported
|
videoOnly := ProbeAudioCodec(s.vf.AudioCodec) == MissingUnsupported
|
||||||
|
|
||||||
var videoFilter VideoFilter
|
videoFilter := sm.encoder.hwMaxResFilter(codec, s.vf.Width, s.vf.Height, s.maxTranscodeSize)
|
||||||
videoFilter = videoFilter.ScaleMax(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...)
|
args = append(args, extraOutputArgs...)
|
||||||
|
|
||||||
|
|||||||
@@ -16,20 +16,78 @@ import (
|
|||||||
|
|
||||||
type StreamFormat struct {
|
type StreamFormat struct {
|
||||||
MimeType string
|
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 (
|
var (
|
||||||
StreamTypeMP4 = StreamFormat{
|
StreamTypeMP4 = StreamFormat{
|
||||||
MimeType: MimeMp4Video,
|
MimeType: MimeMp4Video,
|
||||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||||
args = args.VideoCodec(VideoCodecLibX264)
|
args = CodecInit(codec)
|
||||||
args = append(args,
|
args = append(args, "-movflags", "frag_keyframe+empty_moov")
|
||||||
"-movflags", "frag_keyframe+empty_moov",
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-preset", "veryfast",
|
|
||||||
"-crf", "25",
|
|
||||||
)
|
|
||||||
args = args.VideoFilter(videoFilter)
|
args = args.VideoFilter(videoFilter)
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
args = args.SkipAudio()
|
args = args.SkipAudio()
|
||||||
@@ -42,16 +100,8 @@ var (
|
|||||||
}
|
}
|
||||||
StreamTypeWEBM = StreamFormat{
|
StreamTypeWEBM = StreamFormat{
|
||||||
MimeType: MimeWebmVideo,
|
MimeType: MimeWebmVideo,
|
||||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||||
args = args.VideoCodec(VideoCodecVP9)
|
args = CodecInit(codec)
|
||||||
args = append(args,
|
|
||||||
"-pix_fmt", "yuv420p",
|
|
||||||
"-deadline", "realtime",
|
|
||||||
"-cpu-used", "5",
|
|
||||||
"-row-mt", "1",
|
|
||||||
"-crf", "30",
|
|
||||||
"-b:v", "0",
|
|
||||||
)
|
|
||||||
args = args.VideoFilter(videoFilter)
|
args = args.VideoFilter(videoFilter)
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
args = args.SkipAudio()
|
args = args.SkipAudio()
|
||||||
@@ -64,8 +114,8 @@ var (
|
|||||||
}
|
}
|
||||||
StreamTypeMKV = StreamFormat{
|
StreamTypeMKV = StreamFormat{
|
||||||
MimeType: MimeMkvVideo,
|
MimeType: MimeMkvVideo,
|
||||||
Args: func(videoFilter VideoFilter, videoOnly bool) (args Args) {
|
Args: func(codec VideoCodec, videoFilter VideoFilter, videoOnly bool) (args Args) {
|
||||||
args = args.VideoCodec(VideoCodecCopy)
|
args = CodecInit(codec)
|
||||||
if videoOnly {
|
if videoOnly {
|
||||||
args = args.SkipAudio()
|
args = args.SkipAudio()
|
||||||
} else {
|
} else {
|
||||||
@@ -89,6 +139,25 @@ type TranscodeOptions struct {
|
|||||||
StartTime float64
|
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 {
|
func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
||||||
maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution()
|
maxTranscodeSize := sm.config.GetMaxStreamingTranscodeSize().GetMaxResolution()
|
||||||
if o.Resolution != "" {
|
if o.Resolution != "" {
|
||||||
@@ -100,6 +169,9 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
|||||||
args := Args{"-hide_banner"}
|
args := Args{"-hide_banner"}
|
||||||
args = args.LogLevel(LogLevelError)
|
args = args.LogLevel(LogLevelError)
|
||||||
|
|
||||||
|
codec := FileGetCodec(sm, o.StreamType.MimeType)
|
||||||
|
|
||||||
|
args = sm.encoder.hwDeviceInit(args, codec)
|
||||||
args = append(args, extraInputArgs...)
|
args = append(args, extraInputArgs...)
|
||||||
|
|
||||||
if o.StartTime != 0 {
|
if o.StartTime != 0 {
|
||||||
@@ -110,10 +182,9 @@ func (o TranscodeOptions) makeStreamArgs(sm *StreamManager) Args {
|
|||||||
|
|
||||||
videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported
|
videoOnly := ProbeAudioCodec(o.VideoFile.AudioCodec) == MissingUnsupported
|
||||||
|
|
||||||
var videoFilter VideoFilter
|
videoFilter := sm.encoder.hwMaxResFilter(codec, o.VideoFile.Width, o.VideoFile.Height, maxTranscodeSize)
|
||||||
videoFilter = videoFilter.ScaleMax(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...)
|
args = append(args, extraOutputArgs...)
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ const (
|
|||||||
rows = 5
|
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)
|
sprite, err := generateSprite(encoder, videoFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -37,7 +37,7 @@ func Generate(encoder ffmpeg.FFMpeg, videoFile *file.VideoFile) (*uint64, error)
|
|||||||
return &hashValue, nil
|
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{
|
options := transcoder.ScreenshotOptions{
|
||||||
Width: screenshotSize,
|
Width: screenshotSize,
|
||||||
OutputPath: "-",
|
OutputPath: "-",
|
||||||
@@ -76,7 +76,7 @@ func combineImages(images []image.Image) image.Image {
|
|||||||
return montage
|
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)
|
logger.Infof("[generator] generating phash sprite for %s", videoFile.Path)
|
||||||
|
|
||||||
// Generate sprite image offset by 5% on each end to avoid intro/outros
|
// Generate sprite image offset by 5% on each end to avoid intro/outros
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ type ThumbnailGenerator interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ThumbnailEncoder struct {
|
type ThumbnailEncoder struct {
|
||||||
ffmpeg ffmpeg.FFMpeg
|
ffmpeg *ffmpeg.FFMpeg
|
||||||
vips *vipsEncoder
|
vips *vipsEncoder
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ func GetVipsPath() string {
|
|||||||
return vipsPath
|
return vipsPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewThumbnailEncoder(ffmpegEncoder ffmpeg.FFMpeg) ThumbnailEncoder {
|
func NewThumbnailEncoder(ffmpegEncoder *ffmpeg.FFMpeg) ThumbnailEncoder {
|
||||||
ret := ThumbnailEncoder{
|
ret := ThumbnailEncoder{
|
||||||
ffmpeg: ffmpegEncoder,
|
ffmpeg: ffmpegEncoder,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ type FFMpegConfig interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Generator struct {
|
type Generator struct {
|
||||||
Encoder ffmpeg.FFMpeg
|
Encoder *ffmpeg.FFMpeg
|
||||||
FFMpegConfig FFMpegConfig
|
FFMpegConfig FFMpegConfig
|
||||||
LockManager *fsutil.ReadLockManager
|
LockManager *fsutil.ReadLockManager
|
||||||
MarkerPaths MarkerPaths
|
MarkerPaths MarkerPaths
|
||||||
|
|||||||
@@ -228,6 +228,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||||||
))}
|
))}
|
||||||
</SelectSetting>
|
</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
|
<StringListSetting
|
||||||
id="transcode-input-args"
|
id="transcode-input-args"
|
||||||
headingID="config.general.ffmpeg.transcode.input_args.heading"
|
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.
|
##### 💥 Note: The cache directory is now required if using HLS streaming. Please set the cache directory in the System Settings page.
|
||||||
|
|
||||||
### ✨ New Features
|
### ✨ 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 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 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))
|
* 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).
|
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
|
## 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.
|
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",
|
"heading": "FFmpeg Live Transcode Output Args",
|
||||||
"desc": "Advanced: Additional arguments to pass to ffmpeg before the output field when live transcoding video."
|
"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",
|
"funscript_heatmap_draw_range": "Include range in generated heatmaps",
|
||||||
|
|||||||
Reference in New Issue
Block a user