mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Allow hardware acceleration for generation tasks
Adds a separate generationHardwareAcceleration config toggle that opts the preview, marker, transcode, and clip-thumbnail tasks into the existing HW pipeline. Generator methods take path/width/height primitives; the marker task only probes for HW when a video preview is actually requested. On NVENC: 8K/41min/15-marker scene goes from 8m to 5m; 1080p/9min with no markers 15s to 14s. Short 720p clips are slower. Reduce concurrent tasks if VRAM is limited.
This commit is contained in:
parent
900305685c
commit
4ca6fcb5c0
19 changed files with 273 additions and 190 deletions
|
|
@ -109,6 +109,8 @@ input ConfigGeneralInput {
|
|||
previewPreset: PreviewPreset
|
||||
"Transcode Hardware Acceleration"
|
||||
transcodeHardwareAcceleration: Boolean
|
||||
"Generation Hardware Acceleration"
|
||||
generationHardwareAcceleration: Boolean
|
||||
"Max generated transcode size"
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"Max streaming transcode size"
|
||||
|
|
@ -247,6 +249,8 @@ type ConfigGeneralResult {
|
|||
previewPreset: PreviewPreset!
|
||||
"Transcode Hardware Acceleration"
|
||||
transcodeHardwareAcceleration: Boolean!
|
||||
"Generation Hardware Acceleration"
|
||||
generationHardwareAcceleration: Boolean!
|
||||
"Max generated transcode size"
|
||||
maxTranscodeSize: StreamingResolutionEnum
|
||||
"Max streaming transcode size"
|
||||
|
|
|
|||
|
|
@ -301,6 +301,7 @@ func (r *mutationResolver) ConfigureGeneral(ctx context.Context, input ConfigGen
|
|||
r.setConfigInt(config.SpriteScreenshotSize, input.SpriteScreenshotSize)
|
||||
|
||||
r.setConfigBool(config.TranscodeHardwareAcceleration, input.TranscodeHardwareAcceleration)
|
||||
r.setConfigBool(config.GenerationHardwareAcceleration, input.GenerationHardwareAcceleration)
|
||||
if input.MaxTranscodeSize != nil {
|
||||
c.SetString(config.MaxTranscodeSize, input.MaxTranscodeSize.String())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,65 +79,66 @@ func makeConfigGeneralResult() *ConfigGeneralResult {
|
|||
customPerformerImageLocation := config.GetCustomPerformerImageLocation()
|
||||
|
||||
return &ConfigGeneralResult{
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||
DeleteTrashPath: config.GetDeleteTrashPath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
PluginsPath: config.GetPluginsPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
BlobsPath: config.GetBlobsPath(),
|
||||
BlobsStorage: config.GetBlobsStorage(),
|
||||
FfmpegPath: config.GetFFMpegPath(),
|
||||
FfprobePath: config.GetFFProbePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
UseCustomSpriteInterval: config.GetUseCustomSpriteInterval(),
|
||||
SpriteInterval: config.GetSpriteInterval(),
|
||||
SpriteScreenshotSize: config.GetSpriteScreenshotSize(),
|
||||
MinimumSprites: config.GetMinimumSprites(),
|
||||
MaximumSprites: config.GetMaximumSprites(),
|
||||
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(),
|
||||
CreateImageClipsFromVideos: config.IsCreateImageClipsFromVideos(),
|
||||
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(),
|
||||
LogFileMaxSize: config.GetLogFileMaxSize(),
|
||||
VideoExtensions: config.GetVideoExtensions(),
|
||||
ImageExtensions: config.GetImageExtensions(),
|
||||
GalleryExtensions: config.GetGalleryExtensions(),
|
||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||
ScraperPackageSources: config.GetScraperPackageSources(),
|
||||
PluginPackageSources: config.GetPluginPackageSources(),
|
||||
Stashes: config.GetStashPaths(),
|
||||
DatabasePath: config.GetDatabasePath(),
|
||||
BackupDirectoryPath: config.GetBackupDirectoryPath(),
|
||||
DeleteTrashPath: config.GetDeleteTrashPath(),
|
||||
GeneratedPath: config.GetGeneratedPath(),
|
||||
MetadataPath: config.GetMetadataPath(),
|
||||
ConfigFilePath: config.GetConfigFile(),
|
||||
ScrapersPath: config.GetScrapersPath(),
|
||||
PluginsPath: config.GetPluginsPath(),
|
||||
CachePath: config.GetCachePath(),
|
||||
BlobsPath: config.GetBlobsPath(),
|
||||
BlobsStorage: config.GetBlobsStorage(),
|
||||
FfmpegPath: config.GetFFMpegPath(),
|
||||
FfprobePath: config.GetFFProbePath(),
|
||||
CalculateMd5: config.IsCalculateMD5(),
|
||||
VideoFileNamingAlgorithm: config.GetVideoFileNamingAlgorithm(),
|
||||
ParallelTasks: config.GetParallelTasks(),
|
||||
UseCustomSpriteInterval: config.GetUseCustomSpriteInterval(),
|
||||
SpriteInterval: config.GetSpriteInterval(),
|
||||
SpriteScreenshotSize: config.GetSpriteScreenshotSize(),
|
||||
MinimumSprites: config.GetMinimumSprites(),
|
||||
MaximumSprites: config.GetMaximumSprites(),
|
||||
PreviewAudio: config.GetPreviewAudio(),
|
||||
PreviewSegments: config.GetPreviewSegments(),
|
||||
PreviewSegmentDuration: config.GetPreviewSegmentDuration(),
|
||||
PreviewExcludeStart: config.GetPreviewExcludeStart(),
|
||||
PreviewExcludeEnd: config.GetPreviewExcludeEnd(),
|
||||
PreviewPreset: config.GetPreviewPreset(),
|
||||
TranscodeHardwareAcceleration: config.GetTranscodeHardwareAcceleration(),
|
||||
GenerationHardwareAcceleration: config.GetGenerationHardwareAcceleration(),
|
||||
MaxTranscodeSize: &maxTranscodeSize,
|
||||
MaxStreamingTranscodeSize: &maxStreamingTranscodeSize,
|
||||
WriteImageThumbnails: config.IsWriteImageThumbnails(),
|
||||
CreateImageClipsFromVideos: config.IsCreateImageClipsFromVideos(),
|
||||
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(),
|
||||
LogFileMaxSize: config.GetLogFileMaxSize(),
|
||||
VideoExtensions: config.GetVideoExtensions(),
|
||||
ImageExtensions: config.GetImageExtensions(),
|
||||
GalleryExtensions: config.GetGalleryExtensions(),
|
||||
CreateGalleriesFromFolders: config.GetCreateGalleriesFromFolders(),
|
||||
Excludes: config.GetExcludes(),
|
||||
ImageExcludes: config.GetImageExcludes(),
|
||||
CustomPerformerImageLocation: &customPerformerImageLocation,
|
||||
StashBoxes: config.GetStashBoxes(),
|
||||
PythonPath: config.GetPythonPath(),
|
||||
TranscodeInputArgs: config.GetTranscodeInputArgs(),
|
||||
TranscodeOutputArgs: config.GetTranscodeOutputArgs(),
|
||||
LiveTranscodeInputArgs: config.GetLiveTranscodeInputArgs(),
|
||||
LiveTranscodeOutputArgs: config.GetLiveTranscodeOutputArgs(),
|
||||
DrawFunscriptHeatmapRange: config.GetDrawFunscriptHeatmapRange(),
|
||||
ScraperPackageSources: config.GetScraperPackageSources(),
|
||||
PluginPackageSources: config.GetPluginPackageSources(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ func (rs imageRoutes) serveThumbnail(w http.ResponseWriter, r *http.Request, img
|
|||
InputArgs: manager.GetInstance().Config.GetTranscodeInputArgs(),
|
||||
OutputArgs: manager.GetInstance().Config.GetTranscodeOutputArgs(),
|
||||
Preset: manager.GetInstance().Config.GetPreviewPreset().String(),
|
||||
HWAccel: manager.GetInstance().Config.GetGenerationHardwareAcceleration(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(manager.GetInstance().FFMpeg, manager.GetInstance().FFProbe, clipPreviewOptions)
|
||||
|
|
|
|||
|
|
@ -98,8 +98,9 @@ const (
|
|||
SpriteScreenshotSize = "sprite_screenshot_width"
|
||||
spriteScreenshotSizeDefault = 160
|
||||
|
||||
PreviewPreset = "preview_preset"
|
||||
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
|
||||
PreviewPreset = "preview_preset"
|
||||
TranscodeHardwareAcceleration = "ffmpeg.hardware_acceleration"
|
||||
GenerationHardwareAcceleration = "ffmpeg.hardware_acceleration_generate"
|
||||
|
||||
SequentialScanning = "sequential_scanning"
|
||||
SequentialScanningDefault = false
|
||||
|
|
@ -1079,6 +1080,10 @@ func (i *Config) GetTranscodeHardwareAcceleration() bool {
|
|||
return i.getBool(TranscodeHardwareAcceleration)
|
||||
}
|
||||
|
||||
func (i *Config) GetGenerationHardwareAcceleration() bool {
|
||||
return i.getBool(GenerationHardwareAcceleration)
|
||||
}
|
||||
|
||||
func (i *Config) GetMaxTranscodeSize() models.StreamingResolutionEnum {
|
||||
ret := i.getString(MaxTranscodeSize)
|
||||
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ func (t *GenerateClipPreviewTask) Start(ctx context.Context) {
|
|||
InputArgs: GetInstance().Config.GetTranscodeInputArgs(),
|
||||
OutputArgs: GetInstance().Config.GetTranscodeOutputArgs(),
|
||||
Preset: GetInstance().Config.GetPreviewPreset().String(),
|
||||
HWAccel: GetInstance().Config.GetGenerationHardwareAcceleration(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(GetInstance().FFMpeg, GetInstance().FFProbe, clipPreviewOptions)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ func (t *GenerateImageThumbnailTask) Start(ctx context.Context) {
|
|||
InputArgs: c.GetTranscodeInputArgs(),
|
||||
OutputArgs: c.GetTranscodeOutputArgs(),
|
||||
Preset: c.GetPreviewPreset().String(),
|
||||
HWAccel: c.GetGenerationHardwareAcceleration(),
|
||||
}
|
||||
|
||||
encoder := image.NewThumbnailEncoder(mgr.FFMpeg, mgr.FFProbe, clipPreviewOptions)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,15 @@ import (
|
|||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/stashapp/stash/pkg/ffmpeg"
|
||||
"github.com/stashapp/stash/pkg/fsutil"
|
||||
"github.com/stashapp/stash/pkg/logger"
|
||||
"github.com/stashapp/stash/pkg/models"
|
||||
"github.com/stashapp/stash/pkg/scene/generate"
|
||||
)
|
||||
|
||||
const markerPreviewTargetWidth = 640
|
||||
|
||||
type GenerateMarkersTask struct {
|
||||
repository models.Repository
|
||||
Scene *models.Scene
|
||||
|
|
@ -66,10 +69,20 @@ func (t *GenerateMarkersTask) Start(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
t.generateMarker(videoFile, scene, t.Marker)
|
||||
codec, fullhw := t.determineMarkerCodec(ctx, videoFile)
|
||||
|
||||
t.generateMarker(videoFile, scene, t.Marker, codec, fullhw)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GenerateMarkersTask) determineMarkerCodec(ctx context.Context, videoFile *models.VideoFile) (ffmpeg.VideoCodec, bool) {
|
||||
if !t.VideoPreview {
|
||||
return ffmpeg.VideoCodecLibX264, false
|
||||
}
|
||||
targetHeight := ffmpeg.ScaledHeight(videoFile.Width, videoFile.Height, markerPreviewTargetWidth)
|
||||
return t.generator.DetermineCodecAndHW(ctx, videoFile.Path, videoFile.Width, videoFile.Height, targetHeight)
|
||||
}
|
||||
|
||||
func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) {
|
||||
var sceneMarkers []*models.SceneMarker
|
||||
r := t.repository
|
||||
|
|
@ -88,6 +101,7 @@ func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
codec, fullhw := t.determineMarkerCodec(ctx, videoFile)
|
||||
sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm)
|
||||
|
||||
// Make the folder for the scenes markers
|
||||
|
|
@ -100,11 +114,11 @@ func (t *GenerateMarkersTask) generateSceneMarkers(ctx context.Context) {
|
|||
index := i + 1
|
||||
logger.Progressf("[generator] <%s> scene marker %d of %d", sceneHash, index, len(sceneMarkers))
|
||||
|
||||
t.generateMarker(videoFile, t.Scene, sceneMarker)
|
||||
t.generateMarker(videoFile, t.Scene, sceneMarker, codec, fullhw)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker) {
|
||||
func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene *models.Scene, sceneMarker *models.SceneMarker, codec ffmpeg.VideoCodec, fullhw bool) {
|
||||
sceneHash := scene.GetHash(t.fileNamingAlgorithm)
|
||||
seconds := float64(sceneMarker.Seconds)
|
||||
|
||||
|
|
@ -117,7 +131,7 @@ func (t *GenerateMarkersTask) generateMarker(videoFile *models.VideoFile, scene
|
|||
g := t.generator
|
||||
|
||||
if t.VideoPreview {
|
||||
if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio()); err != nil {
|
||||
if err := g.MarkerPreviewVideo(context.TODO(), videoFile.Path, videoFile.Width, videoFile.Height, sceneHash, seconds, sceneMarker.EndSeconds, instance.Config.GetPreviewAudio(), codec, fullhw); err != nil {
|
||||
logger.Errorf("[generator] failed to generate marker video: %v", err)
|
||||
logErrorOutput(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
if err := t.generateVideo(videoChecksum, videoFile.VideoStreamDuration, videoFile.FrameRate); err != nil {
|
||||
if err := t.generateVideo(videoChecksum, videoFile.Path, videoFile.Width, videoFile.Height, videoFile.VideoStreamDuration, videoFile.FrameRate); err != nil {
|
||||
logger.Errorf("error generating preview: %v", err)
|
||||
logErrorOutput(err)
|
||||
return
|
||||
|
|
@ -55,18 +55,17 @@ func (t *GeneratePreviewTask) Start(ctx context.Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func (t *GeneratePreviewTask) generateVideo(videoChecksum string, videoDuration float64, videoFrameRate float64) error {
|
||||
videoFilename := t.Scene.Path
|
||||
func (t *GeneratePreviewTask) generateVideo(videoChecksum string, path string, width, height int, duration float64, frameRate float64) error {
|
||||
useVsync2 := false
|
||||
|
||||
if videoFrameRate <= 0.01 {
|
||||
logger.Errorf("[generator] Video framerate very low/high (%f) most likely vfr so using -vsync 2", videoFrameRate)
|
||||
if frameRate <= 0.01 {
|
||||
logger.Errorf("[generator] Video framerate very low/high (%f) most likely vfr so using -vsync 2", frameRate)
|
||||
useVsync2 = true
|
||||
}
|
||||
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, false, useVsync2); err != nil {
|
||||
if err := t.generator.PreviewVideo(context.TODO(), path, width, height, duration, videoChecksum, t.Options, false, useVsync2); err != nil {
|
||||
logger.Warnf("[generator] failed generating scene preview, trying fallback")
|
||||
if err := t.generator.PreviewVideo(context.TODO(), videoFilename, videoDuration, videoChecksum, t.Options, true, useVsync2); err != nil {
|
||||
if err := t.generator.PreviewVideo(context.TODO(), path, width, height, duration, videoChecksum, t.Options, true, useVsync2); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,9 @@ func (t *GenerateTranscodeTask) Start(ctx context.Context) {
|
|||
sceneHash := t.Scene.GetHash(t.fileNamingAlgorithm)
|
||||
transcodeSize := config.GetInstance().GetMaxTranscodeSize()
|
||||
|
||||
w, h := videoFile.TranscodeScale(transcodeSize.GetMaxResolution())
|
||||
maxResolution := transcodeSize.GetMaxResolution()
|
||||
|
||||
w, h := videoFile.TranscodeScale(maxResolution)
|
||||
|
||||
// if scale is being set, then we can't use stream copy
|
||||
scaleSet := w == 0 && h == 0
|
||||
|
|
@ -83,15 +85,14 @@ func (t *GenerateTranscodeTask) Start(ctx context.Context) {
|
|||
}
|
||||
} else {
|
||||
options := generate.TranscodeOptions{
|
||||
Width: w,
|
||||
Height: h,
|
||||
MaxSize: maxResolution,
|
||||
}
|
||||
|
||||
if audioCodec == ffmpeg.MissingUnsupported {
|
||||
// ffmpeg fails if it tries to transcode an unsupported audio codec
|
||||
err = t.g.TranscodeVideo(ctx, videoFile.Path, sceneHash, options)
|
||||
err = t.g.TranscodeVideo(ctx, videoFile.Path, videoFile.Width, videoFile.Height, sceneHash, options)
|
||||
} else {
|
||||
err = t.g.Transcode(ctx, videoFile.Path, sceneHash, options)
|
||||
err = t.g.Transcode(ctx, videoFile.Path, videoFile.Width, videoFile.Height, sceneHash, options)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ func (f VideoFilter) ScaleMaxSize(maxDimensions int) VideoFilter {
|
|||
return f.Append(fmt.Sprintf("scale=%v:%v:force_original_aspect_ratio=decrease", maxDimensions, maxDimensions))
|
||||
}
|
||||
|
||||
// ScaledHeight returns the height a source would have if scaled to targetWidth
|
||||
// with preserved aspect ratio, rounded down to an even value.
|
||||
func ScaledHeight(width, height, targetWidth int) int {
|
||||
return (targetWidth * height / width) &^ 1
|
||||
}
|
||||
|
||||
// ScaleMax scales to reqHeight (0 = source height), optionally clamped to a
|
||||
// maxWidth x maxHeight rect. Aspect ratio is preserved.
|
||||
func (f VideoFilter) ScaleMax(width, height, reqHeight, maxWidth, maxHeight int) VideoFilter {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ import (
|
|||
"github.com/stashapp/stash/pkg/models"
|
||||
)
|
||||
|
||||
const ffmpegImageQuality = 5
|
||||
const (
|
||||
ffmpegImageQuality = 5
|
||||
clipPreviewCRF = 25
|
||||
)
|
||||
|
||||
var vipsPath string
|
||||
var once sync.Once
|
||||
|
|
@ -36,6 +39,7 @@ type ClipPreviewOptions struct {
|
|||
InputArgs []string
|
||||
OutputArgs []string
|
||||
Preset string
|
||||
HWAccel bool
|
||||
}
|
||||
|
||||
func GetVipsPath() string {
|
||||
|
|
@ -140,7 +144,7 @@ func (e *ThumbnailEncoder) GetPreview(inPath string, outPath string, maxSize int
|
|||
if clipDuration > 30.0 {
|
||||
clipDuration = 30.0
|
||||
}
|
||||
return e.getClipPreview(inPath, outPath, maxSize, clipDuration, fileData.FrameRate)
|
||||
return e.getClipPreview(inPath, outPath, maxSize, clipDuration, fileData.FrameRate, fileData)
|
||||
}
|
||||
|
||||
func (e *ThumbnailEncoder) ffmpegImageThumbnail(image *bytes.Buffer, maxSize int) ([]byte, error) {
|
||||
|
|
@ -170,21 +174,24 @@ func (e *ThumbnailEncoder) ffmpegImageThumbnailPath(inputPath string, maxSize in
|
|||
return e.FFMpeg.GenerateOutput(context.TODO(), args, nil)
|
||||
}
|
||||
|
||||
func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize int, clipDuration float64, frameRate float64) error {
|
||||
var thumbFilter ffmpeg.VideoFilter
|
||||
thumbFilter = thumbFilter.ScaleMaxSize(maxSize)
|
||||
func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize int, clipDuration float64, frameRate float64, fileData *ffmpeg.VideoFile) error {
|
||||
codec := ffmpeg.VideoCodecVP9
|
||||
if e.ClipPreviewOptions.HWAccel {
|
||||
codec = e.FFMpeg.HWCodecWEBMCompatible(codec)
|
||||
}
|
||||
|
||||
var thumbArgs ffmpeg.Args
|
||||
thumbArgs = thumbArgs.VideoFilter(thumbFilter)
|
||||
var thumbFilter ffmpeg.VideoFilter
|
||||
// TODO: switching this to a HW-compatible scale filter would let us enable
|
||||
// full hardware encoding here. ScaleMaxSize uses force_original_aspect_ratio
|
||||
// which the HW scaler templates don't handle, so we fall back to non-fullhw.
|
||||
thumbFilter = thumbFilter.ScaleMaxSize(maxSize)
|
||||
|
||||
o := e.ClipPreviewOptions
|
||||
|
||||
thumbArgs := codec.ExtraArgsHQ(o.Preset, clipPreviewCRF)
|
||||
thumbArgs = thumbArgs.VideoFilter(thumbFilter)
|
||||
|
||||
thumbArgs = append(thumbArgs,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-preset", o.Preset,
|
||||
"-crf", "25",
|
||||
"-threads", "4",
|
||||
"-strict", "-2",
|
||||
"-f", "webm",
|
||||
)
|
||||
|
||||
|
|
@ -192,6 +199,8 @@ func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize
|
|||
thumbArgs = append(thumbArgs, "-vsync", "2")
|
||||
}
|
||||
|
||||
extraInputArgs := e.FFMpeg.HWDeviceInit(o.InputArgs, codec, false)
|
||||
|
||||
thumbOptions := transcoder.TranscodeOptions{
|
||||
OutputPath: outPath,
|
||||
StartTime: 0,
|
||||
|
|
@ -200,10 +209,10 @@ func (e *ThumbnailEncoder) getClipPreview(inPath string, outPath string, maxSize
|
|||
XError: true,
|
||||
SlowSeek: false,
|
||||
|
||||
VideoCodec: ffmpeg.VideoCodecVP9,
|
||||
VideoCodec: codec,
|
||||
VideoArgs: thumbArgs,
|
||||
|
||||
ExtraInputArgs: o.InputArgs,
|
||||
ExtraInputArgs: extraInputArgs,
|
||||
ExtraOutputArgs: o.OutputArgs,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package generate
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
|
@ -48,6 +49,7 @@ type ScenePaths interface {
|
|||
type FFMpegConfig interface {
|
||||
GetTranscodeInputArgs() []string
|
||||
GetTranscodeOutputArgs() []string
|
||||
GetGenerationHardwareAcceleration() bool
|
||||
}
|
||||
|
||||
type Generator struct {
|
||||
|
|
@ -70,6 +72,17 @@ func (g Generator) tempFile(p Paths, pattern string) (*os.File, error) {
|
|||
return tmpFile, err
|
||||
}
|
||||
|
||||
func (g *Generator) DetermineCodecAndHW(ctx context.Context, path string, width, height, targetHeight int) (ffmpeg.VideoCodec, bool) {
|
||||
codec := ffmpeg.VideoCodecLibX264
|
||||
fullhw := false
|
||||
if g.FFMpegConfig.GetGenerationHardwareAcceleration() {
|
||||
codec = g.Encoder.HWCodecMP4Compatible(codec)
|
||||
fullhw = codec != ffmpeg.VideoCodecLibX264 &&
|
||||
g.Encoder.HWCanFullHWTranscode(ctx, codec, path, width, height, targetHeight)
|
||||
}
|
||||
return codec, fullhw
|
||||
}
|
||||
|
||||
// generateFile performs a generate operation by generating a temporary file using p and pattern, then
|
||||
// moving it to output on success.
|
||||
func (g Generator) generateFile(lockCtx *fsutil.LockContext, p Paths, pattern string, output string, generateFn generateFn) error {
|
||||
|
|
|
|||
|
|
@ -17,11 +17,12 @@ const (
|
|||
markerImageDuration = 5
|
||||
markerWebpFPS = 12
|
||||
|
||||
markerPreviewCRF = 24
|
||||
markerScreenshotQuality = 2
|
||||
)
|
||||
|
||||
func (g Generator) MarkerPreviewVideo(ctx context.Context, input string, hash string, seconds float64, endSeconds *float64, includeAudio bool) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) MarkerPreviewVideo(ctx context.Context, path string, width, height int, hash string, seconds float64, endSeconds *float64, includeAudio bool, codec ffmpeg.VideoCodec, fullhw bool) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
output := g.MarkerPaths.GetVideoPreviewPath(hash, int(seconds))
|
||||
|
|
@ -38,7 +39,7 @@ func (g Generator) MarkerPreviewVideo(ctx context.Context, input string, hash st
|
|||
duration = float64(*endSeconds) - seconds
|
||||
}
|
||||
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, mp4Pattern, output, g.markerPreviewVideo(input, sceneMarkerOptions{
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, mp4Pattern, output, g.markerPreviewVideo(path, width, height, codec, fullhw, sceneMarkerOptions{
|
||||
Seconds: seconds,
|
||||
Duration: duration,
|
||||
Audio: includeAudio,
|
||||
|
|
@ -57,32 +58,28 @@ type sceneMarkerOptions struct {
|
|||
Audio bool
|
||||
}
|
||||
|
||||
func (g Generator) markerPreviewVideo(input string, options sceneMarkerOptions) generateFn {
|
||||
func (g Generator) markerPreviewVideo(path string, width, height int, codec ffmpeg.VideoCodec, fullhw bool, options sceneMarkerOptions) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleWidth(markerPreviewWidth)
|
||||
targetHeight := ffmpeg.ScaledHeight(width, height, markerPreviewWidth)
|
||||
|
||||
var videoArgs ffmpeg.Args
|
||||
videoFilter := g.Encoder.HWMaxResFilter(codec, width, height, targetHeight, fullhw)
|
||||
|
||||
videoArgs := codec.ExtraArgsHQ("veryslow", markerPreviewCRF)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
|
||||
videoArgs = append(videoArgs,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "veryslow",
|
||||
"-crf", "24",
|
||||
"-movflags", "+faststart",
|
||||
"-threads", "4",
|
||||
"-sws_flags", "lanczos",
|
||||
"-strict", "-2",
|
||||
)
|
||||
|
||||
extraInputArgs := g.Encoder.HWDeviceInit(ffmpeg.Args{}, codec, fullhw)
|
||||
|
||||
trimOptions := transcoder.TranscodeOptions{
|
||||
Duration: options.Duration,
|
||||
StartTime: options.Seconds,
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: ffmpeg.VideoCodecLibX264,
|
||||
VideoArgs: videoArgs,
|
||||
Duration: options.Duration,
|
||||
StartTime: options.Seconds,
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: codec,
|
||||
VideoArgs: videoArgs,
|
||||
ExtraInputArgs: extraInputArgs,
|
||||
}
|
||||
|
||||
if options.Audio {
|
||||
|
|
@ -93,14 +90,14 @@ func (g Generator) markerPreviewVideo(input string, options sceneMarkerOptions)
|
|||
trimOptions.AudioArgs = audioArgs
|
||||
}
|
||||
|
||||
args := transcoder.Transcode(input, trimOptions)
|
||||
args := transcoder.Transcode(path, trimOptions)
|
||||
|
||||
return g.generate(lockCtx, args)
|
||||
}
|
||||
}
|
||||
|
||||
func (g Generator) SceneMarkerWebp(ctx context.Context, input string, hash string, seconds float64) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) SceneMarkerWebp(ctx context.Context, path string, hash string, seconds float64) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
output := g.MarkerPaths.GetWebpPreviewPath(hash, int(seconds))
|
||||
|
|
@ -110,7 +107,7 @@ func (g Generator) SceneMarkerWebp(ctx context.Context, input string, hash strin
|
|||
}
|
||||
}
|
||||
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, webpPattern, output, g.sceneMarkerWebp(input, sceneMarkerOptions{
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, webpPattern, output, g.sceneMarkerWebp(path, sceneMarkerOptions{
|
||||
Seconds: seconds,
|
||||
})); err != nil {
|
||||
return err
|
||||
|
|
@ -121,7 +118,7 @@ func (g Generator) SceneMarkerWebp(ctx context.Context, input string, hash strin
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g Generator) sceneMarkerWebp(input string, options sceneMarkerOptions) generateFn {
|
||||
func (g Generator) sceneMarkerWebp(path string, options sceneMarkerOptions) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleWidth(markerPreviewWidth)
|
||||
|
|
@ -146,14 +143,14 @@ func (g Generator) sceneMarkerWebp(input string, options sceneMarkerOptions) gen
|
|||
VideoArgs: videoArgs,
|
||||
}
|
||||
|
||||
args := transcoder.Transcode(input, trimOptions)
|
||||
args := transcoder.Transcode(path, trimOptions)
|
||||
|
||||
return g.generate(lockCtx, args)
|
||||
}
|
||||
}
|
||||
|
||||
func (g Generator) SceneMarkerScreenshot(ctx context.Context, input string, hash string, seconds float64, width int) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) SceneMarkerScreenshot(ctx context.Context, path string, hash string, seconds float64, width int) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
output := g.MarkerPaths.GetScreenshotPath(hash, int(seconds))
|
||||
|
|
@ -163,7 +160,7 @@ func (g Generator) SceneMarkerScreenshot(ctx context.Context, input string, hash
|
|||
}
|
||||
}
|
||||
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, jpgPattern, output, g.sceneMarkerScreenshot(input, SceneMarkerScreenshotOptions{
|
||||
if err := g.generateFile(lockCtx, g.MarkerPaths, jpgPattern, output, g.sceneMarkerScreenshot(path, SceneMarkerScreenshotOptions{
|
||||
Seconds: seconds,
|
||||
Width: width,
|
||||
})); err != nil {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ const (
|
|||
|
||||
scenePreviewImageFPS = 12
|
||||
|
||||
scenePreviewCRF = 21
|
||||
|
||||
minSegmentDuration = 0.75
|
||||
)
|
||||
|
||||
|
|
@ -68,8 +70,8 @@ func (g PreviewOptions) getStepSizeAndOffset(videoDuration float64) (stepSize fl
|
|||
return
|
||||
}
|
||||
|
||||
func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration float64, hash string, options PreviewOptions, fallback bool, useVsync2 bool) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) PreviewVideo(ctx context.Context, path string, width, height int, videoStreamDuration float64, hash string, options PreviewOptions, fallback bool, useVsync2 bool) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
output := g.ScenePaths.GetVideoPreviewPath(hash)
|
||||
|
|
@ -79,9 +81,9 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration
|
|||
}
|
||||
}
|
||||
|
||||
logger.Infof("[generator] generating video preview for %s", input)
|
||||
logger.Infof("[generator] generating video preview for %s", path)
|
||||
|
||||
if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(input, videoDuration, options, fallback, useVsync2)); err != nil {
|
||||
if err := g.generateFile(lockCtx, g.ScenePaths, mp4Pattern, output, g.previewVideo(path, width, height, videoStreamDuration, options, fallback, useVsync2)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
@ -90,10 +92,10 @@ func (g Generator) PreviewVideo(ctx context.Context, input string, videoDuration
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g *Generator) previewVideo(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
func (g *Generator) previewVideo(path string, width, height int, videoStreamDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
// #2496 - generate a single preview video for videos shorter than segments * segment duration
|
||||
if videoDuration < options.SegmentDuration*float64(options.Segments) {
|
||||
return g.previewVideoSingle(input, videoDuration, options, fallback, useVsync2)
|
||||
if videoStreamDuration < options.SegmentDuration*float64(options.Segments) {
|
||||
return g.previewVideoSingle(path, width, height, videoStreamDuration, options, fallback, useVsync2)
|
||||
}
|
||||
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
|
|
@ -103,7 +105,10 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr
|
|||
// remove tmpFiles when done
|
||||
defer func() { removeFiles(tmpFiles) }()
|
||||
|
||||
stepSize, offset := options.getStepSizeAndOffset(videoDuration)
|
||||
stepSize, offset := options.getStepSizeAndOffset(videoStreamDuration)
|
||||
|
||||
targetHeight := ffmpeg.ScaledHeight(width, height, scenePreviewWidth)
|
||||
codec, fullhw := g.DetermineCodecAndHW(lockCtx, path, width, height, targetHeight)
|
||||
|
||||
segmentDuration := options.SegmentDuration
|
||||
// TODO - move this out into calling function
|
||||
|
|
@ -131,7 +136,7 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr
|
|||
Preset: options.Preset,
|
||||
}
|
||||
|
||||
if err := g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2); err != nil {
|
||||
if err := g.previewVideoChunk(lockCtx, path, width, height, chunkOptions, fallback, useVsync2, codec, fullhw); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
@ -150,17 +155,20 @@ func (g *Generator) previewVideo(input string, videoDuration float64, options Pr
|
|||
}
|
||||
}
|
||||
|
||||
func (g *Generator) previewVideoSingle(input string, videoDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
func (g *Generator) previewVideoSingle(path string, width, height int, videoStreamDuration float64, options PreviewOptions, fallback bool, useVsync2 bool) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
targetHeight := ffmpeg.ScaledHeight(width, height, scenePreviewWidth)
|
||||
codec, fullhw := g.DetermineCodecAndHW(lockCtx, path, width, height, targetHeight)
|
||||
|
||||
chunkOptions := previewChunkOptions{
|
||||
StartTime: 0,
|
||||
Duration: videoDuration,
|
||||
Duration: videoStreamDuration,
|
||||
OutputPath: tmpFn,
|
||||
Audio: options.Audio,
|
||||
Preset: options.Preset,
|
||||
}
|
||||
|
||||
return g.previewVideoChunk(lockCtx, input, chunkOptions, fallback, useVsync2)
|
||||
return g.previewVideoChunk(lockCtx, path, width, height, chunkOptions, fallback, useVsync2, codec, fullhw)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -172,22 +180,16 @@ type previewChunkOptions struct {
|
|||
Preset string
|
||||
}
|
||||
|
||||
func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, options previewChunkOptions, fallback bool, useVsync2 bool) error {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleWidth(scenePreviewWidth)
|
||||
func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, path string, width, height int, options previewChunkOptions, fallback bool, useVsync2 bool, codec ffmpeg.VideoCodec, fullhw bool) error {
|
||||
targetHeight := ffmpeg.ScaledHeight(width, height, scenePreviewWidth)
|
||||
|
||||
var videoArgs ffmpeg.Args
|
||||
videoFilter := g.Encoder.HWMaxResFilter(codec, width, height, targetHeight, fullhw)
|
||||
|
||||
videoArgs := codec.ExtraArgsHQ(options.Preset, scenePreviewCRF)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
videoArgs = append(videoArgs, "-strict", "-2")
|
||||
|
||||
videoArgs = append(videoArgs,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", options.Preset,
|
||||
"-crf", "21",
|
||||
"-threads", "4",
|
||||
"-strict", "-2",
|
||||
)
|
||||
extraInputArgs := g.Encoder.HWDeviceInit(g.FFMpegConfig.GetTranscodeInputArgs(), codec, fullhw)
|
||||
|
||||
if useVsync2 {
|
||||
videoArgs = append(videoArgs, "-vsync", "2")
|
||||
|
|
@ -201,10 +203,10 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt
|
|||
XError: !fallback,
|
||||
SlowSeek: fallback,
|
||||
|
||||
VideoCodec: ffmpeg.VideoCodecLibX264,
|
||||
VideoCodec: codec,
|
||||
VideoArgs: videoArgs,
|
||||
|
||||
ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(),
|
||||
ExtraInputArgs: extraInputArgs,
|
||||
ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(),
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +218,7 @@ func (g Generator) previewVideoChunk(lockCtx *fsutil.LockContext, fn string, opt
|
|||
trimOptions.AudioArgs = audioArgs
|
||||
}
|
||||
|
||||
args := transcoder.Transcode(fn, trimOptions)
|
||||
args := transcoder.Transcode(path, trimOptions)
|
||||
|
||||
return g.generate(lockCtx, args)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,26 +9,29 @@ import (
|
|||
"github.com/stashapp/stash/pkg/logger"
|
||||
)
|
||||
|
||||
const transcodeCRF = 23
|
||||
|
||||
type TranscodeOptions struct {
|
||||
Width int
|
||||
Height int
|
||||
// MaxSize is the maximum resolution of the smaller dimension.
|
||||
// 0 means no scaling.
|
||||
MaxSize int
|
||||
}
|
||||
|
||||
func (g Generator) Transcode(ctx context.Context, input string, hash string, options TranscodeOptions) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) Transcode(ctx context.Context, path string, width, height int, hash string, options TranscodeOptions) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
return g.makeTranscode(lockCtx, hash, g.transcode(input, options))
|
||||
return g.makeTranscode(lockCtx, hash, g.transcode(path, width, height, options))
|
||||
}
|
||||
|
||||
// TranscodeVideo transcodes the video, and removes the audio.
|
||||
// In some videos where the audio codec is not supported by ffmpeg,
|
||||
// ffmpeg fails if you try to transcode the audio
|
||||
func (g Generator) TranscodeVideo(ctx context.Context, input string, hash string, options TranscodeOptions) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, input)
|
||||
func (g Generator) TranscodeVideo(ctx context.Context, path string, width, height int, hash string, options TranscodeOptions) error {
|
||||
lockCtx := g.LockManager.ReadLock(ctx, path)
|
||||
defer lockCtx.Cancel()
|
||||
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeVideo(input, options))
|
||||
return g.makeTranscode(lockCtx, hash, g.transcodeVideo(path, width, height, options))
|
||||
}
|
||||
|
||||
// TranscodeAudio will copy the video stream as is, and transcode audio.
|
||||
|
|
@ -64,30 +67,36 @@ func (g Generator) makeTranscode(lockCtx *fsutil.LockContext, hash string, gener
|
|||
return nil
|
||||
}
|
||||
|
||||
func (g Generator) transcode(input string, options TranscodeOptions) generateFn {
|
||||
func (g Generator) transcode(path string, width, height int, options TranscodeOptions) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoArgs ffmpeg.Args
|
||||
if options.Width != 0 && options.Height != 0 {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleDimensions(options.Width, options.Height)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
targetHeight := height
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
if options.MaxSize != 0 && options.MaxSize < min(width, height) {
|
||||
if width >= height {
|
||||
targetHeight = options.MaxSize
|
||||
} else {
|
||||
targetHeight = ffmpeg.ScaledHeight(width, height, options.MaxSize)
|
||||
}
|
||||
}
|
||||
|
||||
videoArgs = append(videoArgs,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "superfast",
|
||||
"-crf", "23",
|
||||
)
|
||||
codec, fullhw := g.DetermineCodecAndHW(lockCtx, path, width, height, targetHeight)
|
||||
|
||||
args := transcoder.Transcode(input, transcoder.TranscodeOptions{
|
||||
if options.MaxSize != 0 {
|
||||
videoFilter = g.Encoder.HWMaxResFilter(codec, width, height, targetHeight, fullhw)
|
||||
}
|
||||
|
||||
videoArgs := codec.ExtraArgsHQ("superfast", transcodeCRF)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
|
||||
extraInputArgs := g.Encoder.HWDeviceInit(g.FFMpegConfig.GetTranscodeInputArgs(), codec, fullhw)
|
||||
|
||||
args := transcoder.Transcode(path, transcoder.TranscodeOptions{
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: ffmpeg.VideoCodecLibX264,
|
||||
VideoCodec: codec,
|
||||
VideoArgs: videoArgs,
|
||||
AudioCodec: ffmpeg.AudioCodecAAC,
|
||||
|
||||
ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(),
|
||||
ExtraInputArgs: extraInputArgs,
|
||||
ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(),
|
||||
})
|
||||
|
||||
|
|
@ -95,33 +104,39 @@ func (g Generator) transcode(input string, options TranscodeOptions) generateFn
|
|||
}
|
||||
}
|
||||
|
||||
func (g Generator) transcodeVideo(input string, options TranscodeOptions) generateFn {
|
||||
func (g Generator) transcodeVideo(path string, width, height int, options TranscodeOptions) generateFn {
|
||||
return func(lockCtx *fsutil.LockContext, tmpFn string) error {
|
||||
var videoArgs ffmpeg.Args
|
||||
if options.Width != 0 && options.Height != 0 {
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
videoFilter = videoFilter.ScaleDimensions(options.Width, options.Height)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
targetHeight := height
|
||||
var videoFilter ffmpeg.VideoFilter
|
||||
if options.MaxSize != 0 && options.MaxSize < min(width, height) {
|
||||
if width >= height {
|
||||
targetHeight = options.MaxSize
|
||||
} else {
|
||||
targetHeight = ffmpeg.ScaledHeight(width, height, options.MaxSize)
|
||||
}
|
||||
}
|
||||
|
||||
videoArgs = append(videoArgs,
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-profile:v", "high",
|
||||
"-level", "4.2",
|
||||
"-preset", "superfast",
|
||||
"-crf", "23",
|
||||
)
|
||||
codec, fullhw := g.DetermineCodecAndHW(lockCtx, path, width, height, targetHeight)
|
||||
|
||||
if options.MaxSize != 0 {
|
||||
videoFilter = g.Encoder.HWMaxResFilter(codec, width, height, targetHeight, fullhw)
|
||||
}
|
||||
|
||||
videoArgs := codec.ExtraArgsHQ("superfast", transcodeCRF)
|
||||
videoArgs = videoArgs.VideoFilter(videoFilter)
|
||||
|
||||
var audioArgs ffmpeg.Args
|
||||
audioArgs = audioArgs.SkipAudio()
|
||||
|
||||
args := transcoder.Transcode(input, transcoder.TranscodeOptions{
|
||||
extraInputArgs := g.Encoder.HWDeviceInit(g.FFMpegConfig.GetTranscodeInputArgs(), codec, fullhw)
|
||||
|
||||
args := transcoder.Transcode(path, transcoder.TranscodeOptions{
|
||||
OutputPath: tmpFn,
|
||||
VideoCodec: ffmpeg.VideoCodecLibX264,
|
||||
VideoCodec: codec,
|
||||
VideoArgs: videoArgs,
|
||||
AudioArgs: audioArgs,
|
||||
|
||||
ExtraInputArgs: g.FFMpegConfig.GetTranscodeInputArgs(),
|
||||
ExtraInputArgs: extraInputArgs,
|
||||
ExtraOutputArgs: g.FFMpegConfig.GetTranscodeOutputArgs(),
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ fragment ConfigGeneralData on ConfigGeneralResult {
|
|||
previewExcludeEnd
|
||||
previewPreset
|
||||
transcodeHardwareAcceleration
|
||||
generationHardwareAcceleration
|
||||
maxTranscodeSize
|
||||
maxStreamingTranscodeSize
|
||||
writeImageThumbnails
|
||||
|
|
|
|||
|
|
@ -336,6 +336,14 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
|||
onChange={(v) => saveGeneral({ transcodeHardwareAcceleration: v })}
|
||||
/>
|
||||
|
||||
<BooleanSetting
|
||||
id="hardware-encoding-generation"
|
||||
headingID="config.general.ffmpeg.hardware_acceleration_generate.heading"
|
||||
subHeadingID="config.general.ffmpeg.hardware_acceleration_generate.desc"
|
||||
checked={general.generationHardwareAcceleration ?? false}
|
||||
onChange={(v) => saveGeneral({ generationHardwareAcceleration: v })}
|
||||
/>
|
||||
|
||||
<StringListSetting
|
||||
advanced
|
||||
id="transcode-input-args"
|
||||
|
|
|
|||
|
|
@ -377,8 +377,12 @@
|
|||
"heading": "FFprobe executable path"
|
||||
},
|
||||
"hardware_acceleration": {
|
||||
"desc": "Uses available hardware to encode video for live transcoding.",
|
||||
"heading": "FFmpeg hardware encoding"
|
||||
"desc": "Uses available hardware for live transcoding.",
|
||||
"heading": "FFmpeg transcoding hardware acceleration"
|
||||
},
|
||||
"hardware_acceleration_generate": {
|
||||
"desc": "Uses available hardware for video encoding tasks.",
|
||||
"heading": "FFmpeg generation hardware acceleration"
|
||||
},
|
||||
"live_transcode": {
|
||||
"input_args": {
|
||||
|
|
|
|||
Loading…
Reference in a new issue