diff --git a/internal/config/config_ffmpeg.go b/internal/config/config_ffmpeg.go index 1c198142f..2a591d1b9 100644 --- a/internal/config/config_ffmpeg.go +++ b/internal/config/config_ffmpeg.go @@ -5,6 +5,8 @@ import ( "github.com/photoprism/photoprism/internal/ffmpeg/encode" "github.com/photoprism/photoprism/internal/thumb" + "github.com/photoprism/photoprism/pkg/fs" + "github.com/photoprism/photoprism/pkg/txt" ) // FFmpegBin returns the ffmpeg executable file name. @@ -33,13 +35,31 @@ func (c *Config) FFmpegSize() int { return thumb.VideoSize(c.options.FFmpegSize).Width } -// FFmpegBitrate returns the ffmpeg bitrate limit in Mbps. +// FFmpegQuality returns the ffmpeg encoding quality from 1 to 100, +// with a default of 50 and where 100 is almost lossless. +func (c *Config) FFmpegQuality() int { + switch { + case c.options.FFmpegQuality <= 0: + return encode.DefaultQuality + case c.options.FFmpegQuality < encode.WorstQuality: + return encode.WorstQuality + case c.options.FFmpegQuality > 100: + return encode.BestQuality + default: + return c.options.FFmpegQuality + } +} + +// FFmpegBitrate returns the ffmpeg bitrate limit in Mbps for non-AVC videos to be transcoded +// even if they could be played natively (optional). func (c *Config) FFmpegBitrate() int { switch { case c.options.FFmpegBitrate <= 0: - return 60 - case c.options.FFmpegBitrate >= 960: - return 960 + return encode.DefaultBitrateLimit + case c.options.FFmpegBitrate < encode.MinBitrateLimit: + return encode.MinBitrateLimit + case c.options.FFmpegBitrate >= encode.MaxBitrateLimit: + return encode.MaxBitrateLimit default: return c.options.FFmpegBitrate } @@ -56,10 +76,33 @@ func (c *Config) FFmpegBitrateExceeded(bitrate float64) bool { } } +// FFmpegPreset returns the ffmpeg encoding preset from "ultrafast" to "veryslow", +// see https://trac.ffmpeg.org/wiki/Encode/H.264#Preset. +func (c *Config) FFmpegPreset() string { + if c.options.FFmpegPreset == "" { + return encode.PresetFast + } + + return c.options.FFmpegPreset +} + +// FFmpegDevice returns the ffmpeg device path for supported hardware encoders (optional). +func (c *Config) FFmpegDevice() string { + if c.options.FFmpegDevice == "" { + return "" + } else if txt.IsUInt(c.options.FFmpegDevice) { + return c.options.FFmpegDevice + } else if fs.DeviceExists(c.options.FFmpegDevice) { + return c.options.FFmpegDevice + } + + return "" +} + // FFmpegMapVideo returns the video streams to be transcoded as string. func (c *Config) FFmpegMapVideo() string { if c.options.FFmpegMapVideo == "" { - return encode.MapVideo + return encode.DefaultMapVideo } return c.options.FFmpegMapVideo @@ -68,7 +111,7 @@ func (c *Config) FFmpegMapVideo() string { // FFmpegMapAudio returns the audio streams to be transcoded as string. func (c *Config) FFmpegMapAudio() string { if c.options.FFmpegMapAudio == "" { - return encode.MapAudio + return encode.DefaultMapAudio } return c.options.FFmpegMapAudio @@ -77,7 +120,7 @@ func (c *Config) FFmpegMapAudio() string { // FFmpegOptions returns the FFmpeg options to use for video transcoding. func (c *Config) FFmpegOptions(encoder encode.Encoder, bitrate string) (encode.Options, error) { // Get options to transcode other formats with FFmpeg. - opt := encode.NewVideoOptions(c.FFmpegBin(), encoder, c.FFmpegSize(), bitrate, c.FFmpegMapVideo(), c.FFmpegMapAudio()) + opt := encode.NewVideoOptions(c.FFmpegBin(), encoder, c.FFmpegSize(), c.FFmpegQuality(), c.FFmpegPreset(), c.FFmpegDevice(), c.FFmpegMapVideo(), c.FFmpegMapAudio()) // Check options and return error if invalid. if opt.Bin == "" { diff --git a/internal/config/config_ffmpeg_test.go b/internal/config/config_ffmpeg_test.go index 28b50137b..89c4e82d7 100644 --- a/internal/config/config_ffmpeg_test.go +++ b/internal/config/config_ffmpeg_test.go @@ -32,13 +32,16 @@ func TestConfig_FFmpegEnabled(t *testing.T) { func TestConfig_FFmpegBitrate(t *testing.T) { c := NewConfig(CliTestContext()) - assert.Equal(t, 60, c.FFmpegBitrate()) + assert.Equal(t, encode.DefaultBitrateLimit, c.FFmpegBitrate()) c.options.FFmpegBitrate = 1000 - assert.Equal(t, 960, c.FFmpegBitrate()) + assert.Equal(t, encode.MaxBitrateLimit, c.FFmpegBitrate()) c.options.FFmpegBitrate = -5 - assert.Equal(t, 60, c.FFmpegBitrate()) + assert.Equal(t, encode.DefaultBitrateLimit, c.FFmpegBitrate()) + + c.options.FFmpegBitrate = 1 + assert.Equal(t, encode.MinBitrateLimit, c.FFmpegBitrate()) c.options.FFmpegBitrate = 800 assert.Equal(t, 800, c.FFmpegBitrate()) @@ -70,6 +73,11 @@ func TestConfig_FFmpegSize(t *testing.T) { assert.Equal(t, thumb.Sizes[thumb.Fit7680].Width, c.FFmpegSize()) } +func TestConfig_FFmpegQuality(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, encode.DefaultQuality, c.FFmpegQuality()) +} + func TestConfig_FFmpegBitrateExceeded(t *testing.T) { c := NewConfig(CliTestContext()) c.options.FFmpegBitrate = 0 @@ -80,7 +88,7 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) { assert.False(t, c.FFmpegBitrateExceeded(0.95)) assert.False(t, c.FFmpegBitrateExceeded(1.0)) assert.True(t, c.FFmpegBitrateExceeded(1.05)) - assert.True(t, c.FFmpegBitrateExceeded(2.05)) + assert.True(t, c.FFmpegBitrateExceeded(6.05)) c.options.FFmpegBitrate = 50 assert.False(t, c.FFmpegBitrateExceeded(0.95)) assert.False(t, c.FFmpegBitrateExceeded(1.05)) @@ -91,14 +99,28 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) { assert.False(t, c.FFmpegBitrateExceeded(2.05)) } +func TestConfig_FFmpegPreset(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, encode.PresetFast, c.FFmpegPreset()) +} + +func TestConfig_FFmpegDevice(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, "", c.FFmpegDevice()) + c.options.FFmpegDevice = "0" + assert.Equal(t, "0", c.FFmpegDevice()) + c.options.FFmpegDevice = "" + assert.Equal(t, "", c.FFmpegDevice()) +} + func TestConfig_FFmpegMapVideo(t *testing.T) { c := NewConfig(CliTestContext()) - assert.Equal(t, encode.MapVideo, c.FFmpegMapVideo()) + assert.Equal(t, encode.DefaultMapVideo, c.FFmpegMapVideo()) } func TestConfig_FFmpegMapAudio(t *testing.T) { c := NewConfig(CliTestContext()) - assert.Equal(t, encode.MapAudio, c.FFmpegMapAudio()) + assert.Equal(t, encode.DefaultMapAudio, c.FFmpegMapAudio()) } func TestConfig_FFmpegOptions(t *testing.T) { @@ -108,9 +130,8 @@ func TestConfig_FFmpegOptions(t *testing.T) { assert.NoError(t, err) assert.Equal(t, c.FFmpegBin(), opt.Bin) assert.Equal(t, encode.SoftwareAvc, opt.Encoder) - assert.Equal(t, bitrate, opt.BitrateLimit) - assert.Equal(t, encode.MapVideo, opt.MapVideo) - assert.Equal(t, encode.MapAudio, opt.MapAudio) + assert.Equal(t, encode.DefaultMapVideo, opt.MapVideo) + assert.Equal(t, encode.DefaultMapAudio, opt.MapAudio) assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo) assert.Equal(t, c.FFmpegMapAudio(), opt.MapAudio) } diff --git a/internal/config/flags.go b/internal/config/flags.go index a60ae04fe..8eeabd975 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -794,36 +794,51 @@ var Flags = CliFlags{ Flag: &cli.StringFlag{ Name: "ffmpeg-encoder", Aliases: []string{"vc"}, - Usage: "FFmpeg AVC encoder `NAME`", + Usage: "FFmpeg AVC video encoder `NAME`", Value: "libx264", EnvVars: EnvVars("FFMPEG_ENCODER"), }}, { Flag: &cli.IntFlag{ Name: "ffmpeg-size", - Aliases: []string{"vs"}, - Usage: "maximum video size in `PIXELS` (720-7680)", + Usage: "encoding resolution limit in `PIXELS` (720-7680)", Value: thumb.Sizes[thumb.Fit4096].Width, EnvVars: EnvVars("FFMPEG_SIZE"), }}, { + Flag: &cli.IntFlag{ + Name: "ffmpeg-quality", + Usage: fmt.Sprintf("encoding `QUALITY` (%d-%d, where %d is almost lossless)", encode.WorstQuality, encode.BestQuality, encode.BestQuality), + Value: encode.DefaultQuality, + EnvVars: EnvVars("FFMPEG_QUALITY"), + }}, { Flag: &cli.IntFlag{ Name: "ffmpeg-bitrate", - Aliases: []string{"vb"}, - Usage: "maximum video `BITRATE` in Mbps", - Value: 60, + Usage: fmt.Sprintf("bitrate `LIMIT` in Mbps for forced transcoding of non-AVC videos (%d-%d; %d to disable)", encode.MinBitrateLimit, encode.MaxBitrateLimit, encode.NoBitrateLimit), + Value: encode.DefaultBitrateLimit, EnvVars: EnvVars("FFMPEG_BITRATE"), }}, { + Flag: &cli.StringFlag{ + Name: "ffmpeg-preset", + Usage: "FFmpeg compression `PRESET` when using an encoder that supports it, e.g. fast, medium, or slow", + Value: encode.PresetFast, + EnvVars: EnvVars("FFMPEG_PRESET"), + }}, { + Flag: &cli.StringFlag{ + Name: "ffmpeg-device", + Usage: "FFmpeg device `PATH` when using a hardware encoder that supports it as parameter", + EnvVars: EnvVars("FFMPEG_DEVICE"), + }}, { Flag: &cli.StringFlag{ Name: "ffmpeg-map-video", - Usage: "video `STREAMS` that should be transcoded", - Value: encode.MapVideo, + Usage: "transcoding video stream `MAP`", + Value: encode.DefaultMapVideo, EnvVars: EnvVars("FFMPEG_MAP_VIDEO"), - }, DocDefault: fmt.Sprintf("`%s`", encode.MapVideo)}, { + }, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapVideo)}, { Flag: &cli.StringFlag{ Name: "ffmpeg-map-audio", - Usage: "audio `STREAMS` that should be transcoded", - Value: encode.MapAudio, + Usage: "transcoding audio stream `MAP`", + Value: encode.DefaultMapAudio, EnvVars: EnvVars("FFMPEG_MAP_AUDIO"), - }, DocDefault: fmt.Sprintf("`%s`", encode.MapAudio)}, { + }, DocDefault: fmt.Sprintf("`%s`", encode.DefaultMapAudio)}, { Flag: &cli.StringFlag{ Name: "exiftool-bin", Usage: "ExifTool `COMMAND` for extracting metadata", diff --git a/internal/config/options.go b/internal/config/options.go index 449175d15..855bc23df 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -167,7 +167,10 @@ type Options struct { FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"` FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"` FFmpegSize int `yaml:"FFmpegSize" json:"FFmpegSize" flag:"ffmpeg-size"` + FFmpegQuality int `yaml:"FFmpegQuality" json:"FFmpegQuality" flag:"ffmpeg-quality"` FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"` + FFmpegPreset string `yaml:"FFmpegPreset" json:"FFmpegPreset" flag:"ffmpeg-preset"` + FFmpegDevice string `yaml:"FFmpegDevice" json:"-" flag:"ffmpeg-device"` FFmpegMapVideo string `yaml:"FFmpegMapVideo" json:"FFmpegMapVideo" flag:"ffmpeg-map-video"` FFmpegMapAudio string `yaml:"FFmpegMapAudio" json:"FFmpegMapAudio" flag:"ffmpeg-map-audio"` ExifToolBin string `yaml:"ExifToolBin" json:"-" flag:"exiftool-bin"` diff --git a/internal/config/report.go b/internal/config/report.go index ea8f16e67..e87ef11ba 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -211,7 +211,10 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"ffmpeg-bin", c.FFmpegBin()}, {"ffmpeg-encoder", c.FFmpegEncoder().String()}, {"ffmpeg-size", fmt.Sprintf("%d", c.FFmpegSize())}, + {"ffmpeg-quality", fmt.Sprintf("%d", c.FFmpegQuality())}, {"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())}, + {"ffmpeg-preset", c.FFmpegPreset()}, + {"ffmpeg-device", c.FFmpegDevice()}, {"ffmpeg-map-video", c.FFmpegMapVideo()}, {"ffmpeg-map-audio", c.FFmpegMapAudio()}, {"exiftool-bin", c.ExifToolBin()}, diff --git a/internal/ffmpeg/apple/avc.go b/internal/ffmpeg/apple/avc.go index 700c71117..05f2d7305 100644 --- a/internal/ffmpeg/apple/avc.go +++ b/internal/ffmpeg/apple/avc.go @@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { // ffmpeg -hide_banner -h encoder=h264_videotoolbox return exec.Command( opt.Bin, - "-y", + "-hide_banner", "-y", "-strict", "-2", "-i", srcName, "-c:v", opt.Encoder.String(), @@ -22,7 +22,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { "-profile", "high", "-level", "51", "-r", "30", - "-b:v", opt.BitrateLimit, + "-q:v", opt.QvQuality(), "-f", "mp4", "-movflags", opt.MovFlags, destName, diff --git a/internal/ffmpeg/encode/avc.go b/internal/ffmpeg/encode/avc.go index 02b2bf67d..e239fd638 100644 --- a/internal/ffmpeg/encode/avc.go +++ b/internal/ffmpeg/encode/avc.go @@ -6,18 +6,18 @@ import "os/exec" func TranscodeToAvcCmd(srcName, destName string, opt Options) *exec.Cmd { return exec.Command( opt.Bin, - "-y", + "-hide_banner", "-y", "-strict", "-2", "-i", srcName, "-c:v", opt.Encoder.String(), "-map", opt.MapVideo, "-map", opt.MapAudio, "-c:a", "aac", + "-preset", opt.Preset, "-vf", opt.VideoFilter(FormatYUV420P), "-max_muxing_queue_size", "1024", - "-crf", "23", "-r", "30", - "-b:v", opt.BitrateLimit, + "-crf", opt.CrfQuality(), "-f", "mp4", "-movflags", opt.MovFlags, destName, diff --git a/internal/ffmpeg/encode/const.go b/internal/ffmpeg/encode/const.go new file mode 100644 index 000000000..a4388b804 --- /dev/null +++ b/internal/ffmpeg/encode/const.go @@ -0,0 +1,18 @@ +package encode + +// FFmpegBin defines the default ffmpeg binary name. +const FFmpegBin = "ffmpeg" + +// Bitrate limit min, max, and default settings in MBps. +const ( + NoBitrateLimit = -1 + MinBitrateLimit = 1 + DefaultBitrateLimit = 60 + MaxBitrateLimit = 960 +) + +// Default video and audio track mapping. +const ( + DefaultMapVideo = "0:v:0" + DefaultMapAudio = "0:a:0?" +) diff --git a/internal/ffmpeg/encode/flags.go b/internal/ffmpeg/encode/flags.go index 73a73ee37..6254aa938 100644 --- a/internal/ffmpeg/encode/flags.go +++ b/internal/ffmpeg/encode/flags.go @@ -7,9 +7,3 @@ package encode // - https://medium.com/@vlad.pbr/in-browser-live-video-using-fragmented-mp4-3aedb600a07e // - https://github.com/video-dev/hls.js?tab=readme-ov-file#features var MovFlags = "frag_keyframe+empty_moov+default_base_moof+faststart" - -const ( - FFmpegBin = "ffmpeg" - MapVideo = "0:v:0" - MapAudio = "0:a:0?" -) diff --git a/internal/ffmpeg/encode/options.go b/internal/ffmpeg/encode/options.go index dcb1d7879..be1c27d6a 100644 --- a/internal/ffmpeg/encode/options.go +++ b/internal/ffmpeg/encode/options.go @@ -7,19 +7,21 @@ import ( // Options represents FFmpeg encoding options. type Options struct { - Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg. - Encoder Encoder // Supported FFmpeg output Encoder. - SizeLimit int // Maximum width and height of the output video file in pixels. - BitrateLimit string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate. - MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly. - MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly. - TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax. - Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options. - MovFlags string + Bin string // FFmpeg binary filename, e.g. /usr/bin/ffmpeg + Encoder Encoder // Supported FFmpeg output Encoder + SizeLimit int // Maximum width and height of the output video file in pixels. + Quality int // See https://ffmpeg.org/ffmpeg-codecs.html + Preset string // See https://trac.ffmpeg.org/wiki/Encode/H.264#Preset + Device string // See https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate + MapVideo string // See https://trac.ffmpeg.org/wiki/Map#Videostreamsonly + MapAudio string // See https://trac.ffmpeg.org/wiki/Map#Audiostreamsonly + TimeOffset string // See https://trac.ffmpeg.org/wiki/Seeking and https://ffmpeg.org/ffmpeg-utils.html#time-duration-syntax + Duration time.Duration // See https://ffmpeg.org/ffmpeg.html#Main-options + MovFlags string } // NewVideoOptions creates and returns new FFmpeg video transcoding options. -func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit int, bitrateLimit, mapVideo, mapAudio string) Options { +func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit, quality int, preset, device, mapVideo, mapAudio string) Options { if ffmpegBin == "" { ffmpegBin = FFmpegBin } @@ -34,26 +36,36 @@ func NewVideoOptions(ffmpegBin string, encoder Encoder, sizeLimit int, bitrateLi sizeLimit = 15360 } - if bitrateLimit == "" { - bitrateLimit = "60M" + if quality <= 0 { + quality = DefaultQuality + } else if quality < WorstQuality { + quality = WorstQuality + } else if quality >= BestQuality { + quality = BestQuality + } + + if preset == "" { + preset = PresetFast } if mapVideo == "" { - mapVideo = MapVideo + mapVideo = DefaultMapVideo } if mapAudio == "" { - mapAudio = MapAudio + mapAudio = DefaultMapAudio } return Options{ - Bin: ffmpegBin, - Encoder: encoder, - SizeLimit: sizeLimit, - BitrateLimit: bitrateLimit, - MapVideo: mapVideo, - MapAudio: mapAudio, - MovFlags: MovFlags, + Bin: ffmpegBin, + Encoder: encoder, + SizeLimit: sizeLimit, + Quality: quality, + Preset: preset, + Device: device, + MapVideo: mapVideo, + MapAudio: mapAudio, + MovFlags: MovFlags, } } @@ -76,3 +88,28 @@ func (o *Options) VideoFilter(format PixelFormat) string { return fmt.Sprintf("scale='if(gte(iw,ih), min(%d, iw), -2):if(gte(iw,ih), -2, min(%d, ih))',format=%s", o.SizeLimit, o.SizeLimit, format) } } + +// QvQuality returns the video encoding quality as "-q:v" parameter string. +func (o *Options) QvQuality() string { + return QvQuality(o.Quality) +} + +// GlobalQuality returns the video encoding quality as "-global_quality" parameter string. +func (o *Options) GlobalQuality() string { + return GlobalQuality(o.Quality) +} + +// CrfQuality returns the video encoding quality as "-crf" parameter string. +func (o *Options) CrfQuality() string { + return CrfQuality(o.Quality) +} + +// QpQuality returns the video encoding quality as "-qp" parameter string. +func (o *Options) QpQuality() string { + return QpQuality(o.Quality) +} + +// CqQuality returns the video encoding quality as "-cq" parameter string. +func (o *Options) CqQuality() string { + return CqQuality(o.Quality) +} diff --git a/internal/ffmpeg/encode/options_test.go b/internal/ffmpeg/encode/options_test.go index c0b01d89b..753ff17e1 100644 --- a/internal/ffmpeg/encode/options_test.go +++ b/internal/ffmpeg/encode/options_test.go @@ -8,29 +8,34 @@ import ( func TestNewOptions(t *testing.T) { t.Run("Defaults", func(t *testing.T) { - opt := NewVideoOptions("", "", 0, "", "", "") + opt := NewVideoOptions("", "", 0, 50, "", "", "", "") assert.Equal(t, "ffmpeg", opt.Bin) assert.Equal(t, FFmpegBin, opt.Bin) assert.Equal(t, DefaultAvcEncoder(), opt.Encoder) assert.Equal(t, 1920, opt.SizeLimit) - assert.Equal(t, "60M", opt.BitrateLimit) + assert.Equal(t, DefaultQuality, opt.Quality) + assert.Equal(t, "50", opt.QvQuality()) + assert.Equal(t, "25", opt.GlobalQuality()) + assert.Equal(t, "25", opt.CrfQuality()) + assert.Equal(t, "25", opt.QpQuality()) + assert.Equal(t, "25", opt.CqQuality()) + assert.Equal(t, PresetFast, opt.Preset) + assert.Equal(t, "", opt.Device) assert.Equal(t, "0:v:0", opt.MapVideo) assert.Equal(t, "0:a:0?", opt.MapAudio) - assert.Equal(t, MapVideo, opt.MapVideo) - assert.Equal(t, MapAudio, opt.MapAudio) + assert.Equal(t, DefaultMapVideo, opt.MapVideo) + assert.Equal(t, DefaultMapAudio, opt.MapAudio) }) - } func TestOptions_VideoFilter(t *testing.T) { opt := &Options{ - Bin: "", - Encoder: "intel", - SizeLimit: 1500, - BitrateLimit: "60M", - MapVideo: "", - MapAudio: "", - MovFlags: "", + Bin: "", + Encoder: "intel", + SizeLimit: 1500, + MapVideo: "", + MapAudio: "", + MovFlags: "", } t.Run("Empty", func(t *testing.T) { diff --git a/internal/ffmpeg/encode/preset.go b/internal/ffmpeg/encode/preset.go new file mode 100644 index 000000000..3582c8dea --- /dev/null +++ b/internal/ffmpeg/encode/preset.go @@ -0,0 +1,15 @@ +package encode + +// FFmpeg encoding preset names from fastest to slowest, +// see https://trac.ffmpeg.org/wiki/Encode/H.264#Preset. +const ( + PresetUltraFast = "ultrafast" + PresetSuperFast = "superfast" + PresetVeryFast = "veryfast" + PresetFaster = "faster" + PresetFast = "fast" + PresetMedium = "medium" + PresetSlow = "slow" + PresetSlower = "slower" + PresetVerySlow = "veryslow" +) diff --git a/internal/ffmpeg/encode/quality.go b/internal/ffmpeg/encode/quality.go new file mode 100644 index 000000000..d92f571e7 --- /dev/null +++ b/internal/ffmpeg/encode/quality.go @@ -0,0 +1,107 @@ +package encode + +import ( + "fmt" +) + +// Encoding quality min, max, and default settings, +// where 100 is almost lossless. +const ( + BestQuality = 100 + DefaultQuality = 50 + WorstQuality = 1 +) + +// QvQuality returns the video encoding quality as "-q:v" parameter string. +func QvQuality(q int) string { + switch { + case q < 0: + return "50" + case q < 1: + return "1" + case q > 100: + return "100" + default: + return fmt.Sprintf("%d", q) + } +} + +// GlobalQuality returns the video encoding quality as "-global_quality" parameter string. +func GlobalQuality(q int) string { + if q <= 0 { + q = DefaultQuality + } else if q > BestQuality { + q = BestQuality + } + + result := (100 - q) / 2 + + switch { + case result < 1: + return "1" + case result > 50: + return "50" + default: + return fmt.Sprintf("%d", result) + } +} + +// CrfQuality returns the video encoding quality as "-crf" parameter string. +func CrfQuality(q int) string { + if q <= 0 { + q = DefaultQuality + } else if q > BestQuality { + q = BestQuality + } + + result := (100 - q) / 2 + + switch { + case result < 1: + return "0" + case result > 50: + return "51" + default: + return fmt.Sprintf("%d", result) + } +} + +// QpQuality returns the video encoding quality as "-qp" parameter string. +func QpQuality(q int) string { + if q <= 0 { + q = DefaultQuality + } else if q > BestQuality { + q = BestQuality + } + + result := (100 - q) / 2 + + switch { + case result < 1: + return "0" + case result > 50: + return "51" + default: + return fmt.Sprintf("%d", result) + } +} + +// CqQuality returns the video encoding quality as "-cq" parameter string. +func CqQuality(q int) string { + if q <= 0 { + q = DefaultQuality + } else if q > BestQuality { + q = BestQuality + } + + result := (100 - q) / 2 + + switch { + case result < 1: + return "1" + case result > 50: + return "50" + default: + return fmt.Sprintf("%d", result) + } +} diff --git a/internal/ffmpeg/encode/quality_test.go b/internal/ffmpeg/encode/quality_test.go new file mode 100644 index 000000000..fb6106916 --- /dev/null +++ b/internal/ffmpeg/encode/quality_test.go @@ -0,0 +1,47 @@ +package encode + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConstantQuality(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + assert.Equal(t, "100", QvQuality(BestQuality)) + assert.Equal(t, "50", QvQuality(DefaultQuality)) + assert.Equal(t, "1", QvQuality(WorstQuality)) + }) +} + +func TestGlobalQuality(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + assert.Equal(t, "1", GlobalQuality(BestQuality)) + assert.Equal(t, "25", GlobalQuality(DefaultQuality)) + assert.Equal(t, "49", GlobalQuality(WorstQuality)) + }) +} + +func TestCrfQuality(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + assert.Equal(t, "0", CrfQuality(BestQuality)) + assert.Equal(t, "25", CrfQuality(DefaultQuality)) + assert.Equal(t, "49", CrfQuality(WorstQuality)) + }) +} + +func TestQpQuality(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + assert.Equal(t, "0", QpQuality(BestQuality)) + assert.Equal(t, "25", QpQuality(DefaultQuality)) + assert.Equal(t, "49", QpQuality(WorstQuality)) + }) +} + +func TestCqQuality(t *testing.T) { + t.Run("Defaults", func(t *testing.T) { + assert.Equal(t, "1", CqQuality(BestQuality)) + assert.Equal(t, "25", CqQuality(DefaultQuality)) + assert.Equal(t, "49", CqQuality(WorstQuality)) + }) +} diff --git a/internal/ffmpeg/extract_image_cmd.go b/internal/ffmpeg/extract_image_cmd.go index 5a69e320a..233ffa8bf 100644 --- a/internal/ffmpeg/extract_image_cmd.go +++ b/internal/ffmpeg/extract_image_cmd.go @@ -26,10 +26,10 @@ func ExtractJpegImageCmd(videoName, imageName string, opt *encode.Options) *exec // see https://github.com/photoprism/photoprism/issues/4488. // Unfortunately, this filter would render thumbnails of non-HDR videos too dark: // "-vf", "zscale=t=linear:npl=100,format=gbrpf32le,zscale=p=bt709,tonemap=tonemap=gamma:desat=0,zscale=t=bt709:m=bt709:r=tv,format=yuv420p", - return exec.Command(opt.Bin, "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName) + return exec.Command(opt.Bin, "-hide_banner", "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName) } // ExtractPngImageCmd extracts a PNG still image from the specified source video file. func ExtractPngImageCmd(videoName, imageName string, opt *encode.Options) *exec.Cmd { - return exec.Command(opt.Bin, "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName) + return exec.Command(opt.Bin, "-hide_banner", "-y", "-strict", "-2", "-ss", opt.TimeOffset, "-i", videoName, "-vframes", "1", imageName) } diff --git a/internal/ffmpeg/extract_image_cmd_test.go b/internal/ffmpeg/extract_image_cmd_test.go index aecf5dbe8..ba583cada 100644 --- a/internal/ffmpeg/extract_image_cmd_test.go +++ b/internal/ffmpeg/extract_image_cmd_test.go @@ -23,7 +23,7 @@ func TestExtractImageCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) RunCommandTest(t, "jpg", srcName, destName, cmd, true) } @@ -40,7 +40,7 @@ func TestExtractJpegImageCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) RunCommandTest(t, "jpeg", srcName, destName, cmd, true) } @@ -57,7 +57,7 @@ func TestExtractPngImageCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -ss 00:00:03.000 -i SRC -vframes 1 DEST", cmdStr) RunCommandTest(t, "png", srcName, destName, cmd, true) } diff --git a/internal/ffmpeg/intel/avc.go b/internal/ffmpeg/intel/avc.go index dd8230062..beae1a044 100644 --- a/internal/ffmpeg/intel/avc.go +++ b/internal/ffmpeg/intel/avc.go @@ -9,23 +9,46 @@ import ( // TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC. func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { // ffmpeg -hide_banner -h encoder=h264_qsv - return exec.Command( - opt.Bin, - "-y", - "-strict", "-2", - "-hwaccel", "qsv", - "-hwaccel_output_format", "qsv", - "-i", srcName, - "-c:a", "aac", - "-vf", opt.VideoFilter(encode.FormatQSV), - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-r", "30", - "-b:v", opt.BitrateLimit, - "-bitrate", opt.BitrateLimit, - "-f", "mp4", - "-movflags", opt.MovFlags, - destName, - ) + if opt.Device != "" { + return exec.Command( + opt.Bin, + "-hide_banner", "-y", + "-strict", "-2", + "-hwaccel", "qsv", + "-hwaccel_device", opt.Device, + "-hwaccel_output_format", "qsv", + "-i", srcName, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatQSV), + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-preset", opt.Preset, + "-r", "30", + "-global_quality", opt.GlobalQuality(), + "-f", "mp4", + "-movflags", opt.MovFlags, + destName, + ) + } else { + return exec.Command( + opt.Bin, + "-hide_banner", "-y", + "-strict", "-2", + "-hwaccel", "qsv", + "-hwaccel_output_format", "qsv", + "-i", srcName, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatQSV), + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-preset", opt.Preset, + "-r", "30", + "-global_quality", opt.GlobalQuality(), + "-f", "mp4", + "-movflags", opt.MovFlags, + destName, + ) + } } diff --git a/internal/ffmpeg/nvidia/avc.go b/internal/ffmpeg/nvidia/avc.go index a72739e69..27c9b94b2 100644 --- a/internal/ffmpeg/nvidia/avc.go +++ b/internal/ffmpeg/nvidia/avc.go @@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { // ffmpeg -hide_banner -h encoder=h264_nvenc return exec.Command( opt.Bin, - "-y", + "-hide_banner", "-y", "-strict", "-2", "-hwaccel", "auto", "-i", srcName, @@ -20,15 +20,14 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { "-map", opt.MapVideo, "-map", opt.MapAudio, "-c:a", "aac", - "-preset", "15", + "-preset", opt.Preset, "-pixel_format", "yuv420p", "-gpu", "any", "-vf", opt.VideoFilter(encode.FormatYUV420P), "-rc:v", "constqp", - "-cq", "0", + "-cq", opt.CqQuality(), "-tune", "2", "-r", "30", - "-b:v", opt.BitrateLimit, "-profile:v", "1", "-level:v", "auto", "-coder:v", "1", diff --git a/internal/ffmpeg/transcode_cmd.go b/internal/ffmpeg/transcode_cmd.go index b763fdba5..891c95b7f 100644 --- a/internal/ffmpeg/transcode_cmd.go +++ b/internal/ffmpeg/transcode_cmd.go @@ -33,7 +33,7 @@ func TranscodeCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, if fs.TypeAnimated[fs.FileType(srcName)] != "" { cmd = exec.Command( opt.Bin, - "-y", + "-hide_banner", "-y", "-strict", "-2", "-i", srcName, "-pix_fmt", encode.FormatYUV420P.String(), diff --git a/internal/ffmpeg/transcode_cmd_test.go b/internal/ffmpeg/transcode_cmd_test.go index 8c4904561..7ed7b7768 100644 --- a/internal/ffmpeg/transcode_cmd_test.go +++ b/internal/ffmpeg/transcode_cmd_test.go @@ -15,29 +15,29 @@ func TestTranscodeCmd(t *testing.T) { ffmpegBin := "/usr/bin/ffmpeg" t.Run("NoSource", func(t *testing.T) { - opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") _, _, err := TranscodeCmd("", "", opt) assert.Equal(t, "empty source filename", err.Error()) }) t.Run("NoDestination", func(t *testing.T) { - opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") _, _, err := TranscodeCmd("VID123.mov", "", opt) assert.Equal(t, "empty destination filename", err.Error()) }) t.Run("Animation", func(t *testing.T) { - opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions("", encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") r, _, err := TranscodeCmd("VID123.gif", "VID123.gif.avc", opt) if err != nil { t.Fatal(err) } - assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.gif -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc") + assert.Contains(t, r.String(), "bin/ffmpeg -hide_banner -y -strict -2 -i VID123.gif -pix_fmt yuv420p -vf scale='trunc(iw/2)*2:trunc(ih/2)*2' -f mp4 -movflags +faststart VID123.gif.avc") }) t.Run("VP9toAVC", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.SoftwareAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.SoftwareAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") srcName := fs.Abs("./testdata/25fps.vp9") destName := fs.Abs("./testdata/25fps.avc") @@ -52,13 +52,13 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -i SRC -c:v libx264 -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -max_muxing_queue_size 1024 -r 30 -crf 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // Run generated command to test software transcoding. RunCommandTest(t, opt.Encoder, srcName, destName, cmd, true) }) t.Run("Vaapi", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.VaapiAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.VaapiAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") srcName := fs.Abs("./testdata/25fps.vp9") destName := fs.Abs("./testdata/25fps.vaapi.avc") @@ -73,7 +73,7 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel vaapi -i SRC -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=nv12,hwupload -c:v h264_vaapi -map 0:v:0 -map 0:a:0? -r 30 -qp 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // This transcoding test requires a supported hardware device that is properly configured: if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "vaapi" { @@ -81,7 +81,7 @@ func TestTranscodeCmd(t *testing.T) { } }) t.Run("IntelHvc", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "/dev/dri/renderD128", "", "") // QuickTime MOV container with HVC1 (HEVC) codec. srcName := fs.Abs("./testdata/30fps.mov") @@ -97,7 +97,7 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -bitrate 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // This transcoding test requires a supported hardware device that is properly configured: if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" { @@ -105,7 +105,7 @@ func TestTranscodeCmd(t *testing.T) { } }) t.Run("IntelVp9", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.IntelAvc, 1500, encode.DefaultQuality, encode.PresetFast, "/dev/dri/renderD128", "", "") srcName := fs.Abs("./testdata/25fps.vp9") destName := fs.Abs("./testdata/25fps.intel.avc") @@ -120,7 +120,7 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 60M -bitrate 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel qsv -hwaccel_device /dev/dri/renderD128 -hwaccel_output_format qsv -i SRC -c:a aac -vf scale_qsv=w='if(gte(iw,ih), min(1500, iw), -1)':h='if(gte(iw,ih), -1, min(1500, ih))':format=nv12 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -preset fast -r 30 -global_quality 25 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // This transcoding test requires a supported hardware device that is properly configured: if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "intel" { @@ -128,7 +128,7 @@ func TestTranscodeCmd(t *testing.T) { } }) t.Run("NvidiaHvc", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") // QuickTime MOV container with HVC1 (HEVC) codec. srcName := fs.Abs("./testdata/30fps.mov") @@ -144,7 +144,7 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset 15 -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 0 -tune 2 -r 30 -b:v 60M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // This transcoding test requires a supported hardware device that is properly configured: if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" { @@ -152,7 +152,7 @@ func TestTranscodeCmd(t *testing.T) { } }) t.Run("NvidiaVp9", func(t *testing.T) { - opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions(ffmpegBin, encode.NvidiaAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") srcName := fs.Abs("./testdata/25fps.vp9") destName := fs.Abs("./testdata/25fps.nvidia.avc") @@ -167,7 +167,7 @@ func TestTranscodeCmd(t *testing.T) { cmdStr = strings.Replace(cmdStr, srcName, "SRC", 1) cmdStr = strings.Replace(cmdStr, destName, "DEST", 1) - assert.Equal(t, "/usr/bin/ffmpeg -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset 15 -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 0 -tune 2 -r 30 -b:v 60M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) + assert.Equal(t, "/usr/bin/ffmpeg -hide_banner -y -strict -2 -hwaccel auto -i SRC -pix_fmt yuv420p -c:v h264_nvenc -map 0:v:0 -map 0:a:0? -c:a aac -preset fast -pixel_format yuv420p -gpu any -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -rc:v constqp -cq 25 -tune 2 -r 30 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart DEST", cmdStr) // This transcoding test requires a supported hardware device that is properly configured: if os.Getenv("PHOTOPRISM_FFMPEG_ENCODER") == "nvidia" { @@ -175,23 +175,23 @@ func TestTranscodeCmd(t *testing.T) { } }) t.Run("Apple", func(t *testing.T) { - opt := encode.NewVideoOptions("", encode.AppleAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions("", encode.AppleAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) } - assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc") + assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_videotoolbox -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -profile high -level 51 -r 30 -q:v 50 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc") }) t.Run("Video4Linux", func(t *testing.T) { - opt := encode.NewVideoOptions("", encode.V4LAvc, 1500, "60M", "", "") + opt := encode.NewVideoOptions("", encode.V4LAvc, 1500, encode.DefaultQuality, encode.PresetFast, "", "", "") r, _, err := TranscodeCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) } - assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 60M -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc") + assert.Contains(t, r.String(), "ffmpeg -hide_banner -y -strict -2 -i VID123.mov -c:v h264_v4l2m2m -map 0:v:0 -map 0:a:0? -c:a aac -vf scale='if(gte(iw,ih), min(1500, iw), -2):if(gte(iw,ih), -2, min(1500, ih))',format=yuv420p -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -f mp4 -movflags frag_keyframe+empty_moov+default_base_moof+faststart VID123.mov.avc") }) } diff --git a/internal/ffmpeg/v4l/avc.go b/internal/ffmpeg/v4l/avc.go index 2868c42ac..9f2ab8b20 100644 --- a/internal/ffmpeg/v4l/avc.go +++ b/internal/ffmpeg/v4l/avc.go @@ -11,7 +11,7 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { // ffmpeg -hide_banner -h encoder=h264_v4l2m2m return exec.Command( opt.Bin, - "-y", + "-hide_banner", "-y", "-strict", "-2", "-i", srcName, "-c:v", opt.Encoder.String(), @@ -22,9 +22,6 @@ func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { "-num_output_buffers", "72", "-num_capture_buffers", "64", "-max_muxing_queue_size", "1024", - "-crf", "23", - "-r", "30", - "-b:v", opt.BitrateLimit, "-f", "mp4", "-movflags", opt.MovFlags, destName, diff --git a/internal/ffmpeg/vaapi/avc.go b/internal/ffmpeg/vaapi/avc.go index f8191abf3..a43f19d0b 100644 --- a/internal/ffmpeg/vaapi/avc.go +++ b/internal/ffmpeg/vaapi/avc.go @@ -8,21 +8,42 @@ import ( // TranscodeToAvcCmd returns the FFmpeg command for hardware-accelerated transcoding to MPEG-4 AVC. func TranscodeToAvcCmd(srcName, destName string, opt encode.Options) *exec.Cmd { - return exec.Command( - opt.Bin, - "-y", - "-strict", "-2", - "-hwaccel", "vaapi", - "-i", srcName, - "-c:a", "aac", - "-vf", opt.VideoFilter(encode.FormatNV12), - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-r", "30", - "-b:v", opt.BitrateLimit, - "-f", "mp4", - "-movflags", opt.MovFlags, - destName, - ) + if opt.Device != "" { + return exec.Command( + opt.Bin, + "-hide_banner", "-y", + "-strict", "-2", + "-hwaccel", "vaapi", + "-hwaccel_device", opt.Device, + "-i", srcName, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatNV12), + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-r", "30", + "-qp", opt.QpQuality(), + "-f", "mp4", + "-movflags", opt.MovFlags, + destName, + ) + } else { + return exec.Command( + opt.Bin, + "-hide_banner", "-y", + "-strict", "-2", + "-hwaccel", "vaapi", + "-i", srcName, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatNV12), + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-r", "30", + "-qp", opt.QpQuality(), + "-f", "mp4", + "-movflags", opt.MovFlags, + destName, + ) + } } diff --git a/scripts/dist/install-gpu.sh b/scripts/dist/install-gpu.sh index bf84b1954..441986444 100755 --- a/scripts/dist/install-gpu.sh +++ b/scripts/dist/install-gpu.sh @@ -62,7 +62,6 @@ for t in ${GPU_DETECTED[@]}; do echo "Installing AMD VA-API GPU Drivers..." apt-get -qq install mesa-va-drivers vainfo libva2 ;; - "null") # ignore