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:
NodudeWasTaken 2023-03-10 01:25:55 +01:00 committed by GitHub
parent d4fb6b2acf
commit 0c1b02380e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 537 additions and 112 deletions

View File

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

View 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"]

View File

@ -19,6 +19,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
previewExcludeStart
previewExcludeEnd
previewPreset
transcodeHardwareAcceleration
maxTranscodeSize
maxStreamingTranscodeSize
writeImageThumbnails

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -11,6 +11,7 @@ func (c VideoCodec) Args() []string {
}
var (
// Software codec's
VideoCodecLibX264 VideoCodec = "libx264"
VideoCodecLibWebP VideoCodec = "libwebp"
VideoCodecBMP VideoCodec = "bmp"

View 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
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,7 +53,7 @@ type FFMpegConfig interface {
}
type Generator struct {
Encoder ffmpeg.FFMpeg
Encoder *ffmpeg.FFMpeg
FFMpegConfig FFMpegConfig
LockManager *fsutil.ReadLockManager
MarkerPaths MarkerPaths

View File

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

View File

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

View File

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

View File

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