diff --git a/docker-compose.yml b/docker-compose.yml index e376dd18f..896ddb00b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -109,6 +109,7 @@ services: # PHOTOPRISM_FFMPEG_ENCODER: "nvidia" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry", "vaapi") Intel: "intel" for Broadwell or later and "vaapi" for Haswell or earlier # PHOTOPRISM_FFMPEG_ENCODER: "intel" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry", "vaapi") Intel: "intel" for Broadwell or later and "vaapi" for Haswell or earlier` # PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50) + # PHOTOPRISM_FFMPEG_RESOLUTION: "1080" # FFmpeg encoding resolution limit in pixel height (default: 2160) # LIBVA_DRIVER_NAME: "i965" # For Intel architectures Haswell and older which do not support QSV yet but use VAAPI instead ## Share hardware devices with FFmpeg and TensorFlow (optional): # devices: diff --git a/internal/config/config_ffmpeg.go b/internal/config/config_ffmpeg.go index 713143f21..3b73428e5 100644 --- a/internal/config/config_ffmpeg.go +++ b/internal/config/config_ffmpeg.go @@ -37,6 +37,20 @@ func (c *Config) FFmpegBitrate() int { } } +// FFmpegResolution returns the ffmpeg resolution limit in pixel height. Goes from 144p to 8k. +func (c *Config) FFmpegResolution() int { + switch { + case c.options.FFmpegResolution <= 0: + return 4320 + case c.options.FFmpegResolution <= 144: + return 144 + case c.options.FFmpegBitrate >= 4320: + return 4320 + default: + return c.options.FFmpegBitrate + } +} + // FFmpegBitrateExceeded tests if the ffmpeg bitrate limit is exceeded. func (c *Config) FFmpegBitrateExceeded(mbit float64) bool { if mbit <= 0 { @@ -67,14 +81,15 @@ func (c *Config) FFmpegMapAudio() string { } // FFmpegOptions returns the FFmpeg transcoding options. -func (c *Config) FFmpegOptions(encoder ffmpeg.AvcEncoder, bitrate string) (ffmpeg.Options, error) { +func (c *Config) FFmpegOptions(encoder ffmpeg.AvcEncoder, bitrate string, resolution string) (ffmpeg.Options, error) { // Transcode all other formats with FFmpeg. opt := ffmpeg.Options{ - Bin: c.FFmpegBin(), - Encoder: encoder, - Bitrate: bitrate, - MapVideo: c.FFmpegMapVideo(), - MapAudio: c.FFmpegMapAudio(), + Bin: c.FFmpegBin(), + Encoder: encoder, + Bitrate: bitrate, + MapVideo: c.FFmpegMapVideo(), + MapAudio: c.FFmpegMapAudio(), + Resolution: resolution, } // Check diff --git a/internal/config/config_ffmpeg_test.go b/internal/config/config_ffmpeg_test.go index 7c05f3771..4fa2ecc72 100644 --- a/internal/config/config_ffmpeg_test.go +++ b/internal/config/config_ffmpeg_test.go @@ -43,6 +43,17 @@ func TestConfig_FFmpegBitrate(t *testing.T) { assert.Equal(t, 800, c.FFmpegBitrate()) } +func TestConfig_FFmpegResolution(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, 144, c.FFmpegResolution()) + + c.options.FFmpegResolution = 1920 + assert.Equal(t, 1920, c.FFmpegResolution()) + + c.options.FFmpegResolution = 8640 + assert.Equal(t, 4320, c.FFmpegResolution()) +} + func TestConfig_FFmpegBitrateExceeded(t *testing.T) { c := NewConfig(CliTestContext()) c.options.FFmpegBitrate = 0 @@ -77,7 +88,8 @@ func TestConfig_FFmpegMapAudio(t *testing.T) { func TestConfig_FFmpegOptions(t *testing.T) { c := NewConfig(CliTestContext()) bitrate := "25M" - opt, err := c.FFmpegOptions(ffmpeg.SoftwareEncoder, bitrate) + resolution := "1080" + opt, err := c.FFmpegOptions(ffmpeg.SoftwareEncoder, bitrate, resolution) assert.NoError(t, err) assert.Equal(t, c.FFmpegBin(), opt.Bin) assert.Equal(t, ffmpeg.SoftwareEncoder, opt.Encoder) @@ -86,4 +98,5 @@ func TestConfig_FFmpegOptions(t *testing.T) { assert.Equal(t, ffmpeg.MapAudioDefault, opt.MapAudio) assert.Equal(t, c.FFmpegMapVideo(), opt.MapVideo) assert.Equal(t, c.FFmpegMapAudio(), opt.MapAudio) + assert.Equal(t, resolution, opt.Resolution) } diff --git a/internal/config/flags.go b/internal/config/flags.go index 9af5ff159..a3fc45557 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -580,6 +580,12 @@ var Flags = CliFlags{ Value: 50, EnvVar: EnvVar("FFMPEG_BITRATE"), }}, { + Flag: cli.IntFlag{ + Name: "ffmpeg-resolution", + Usage: "maximum FFmpeg encoding `RESOLUTION` (height)", + Value: 2160, + EnvVar: EnvVar("FFMPEG_RESOLUTION"), + }}, { Flag: cli.StringFlag{ Name: "ffmpeg-map-video", Usage: "video `STREAMS` that should be transcoded", diff --git a/internal/config/options.go b/internal/config/options.go index 7634c90bf..b9f402a6e 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -129,6 +129,7 @@ type Options struct { FFmpegBin string `yaml:"FFmpegBin" json:"-" flag:"ffmpeg-bin"` FFmpegEncoder string `yaml:"FFmpegEncoder" json:"FFmpegEncoder" flag:"ffmpeg-encoder"` FFmpegBitrate int `yaml:"FFmpegBitrate" json:"FFmpegBitrate" flag:"ffmpeg-bitrate"` + FFmpegResolution int `yaml:"FFmpegResolution" json:"FFmpegResolution" flag:"ffmpeg-resolution"` 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 0203b8769..b9c047ab2 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -184,6 +184,7 @@ func (c *Config) Report() (rows [][]string, cols []string) { {"ffmpeg-bin", c.FFmpegBin()}, {"ffmpeg-encoder", c.FFmpegEncoder().String()}, {"ffmpeg-bitrate", fmt.Sprintf("%d", c.FFmpegBitrate())}, + {"ffmpeg-resolution", fmt.Sprintf("%d", c.FFmpegResolution())}, {"ffmpeg-map-video", c.FFmpegMapVideo()}, {"ffmpeg-map-audio", c.FFmpegMapAudio()}, {"exiftool-bin", c.ExifToolBin()}, diff --git a/internal/ffmpeg/config.go b/internal/ffmpeg/config.go index 0c85b5524..a7c64dafc 100644 --- a/internal/ffmpeg/config.go +++ b/internal/ffmpeg/config.go @@ -2,9 +2,10 @@ package ffmpeg // Options represents transcoding options. type Options struct { - Bin string - Encoder AvcEncoder - Bitrate string - MapVideo string - MapAudio string + Bin string + Encoder AvcEncoder + Bitrate string + MapVideo string + MapAudio string + Resolution string } diff --git a/internal/ffmpeg/convert.go b/internal/ffmpeg/convert.go index a22136099..30e789bc4 100644 --- a/internal/ffmpeg/convert.go +++ b/internal/ffmpeg/convert.go @@ -25,6 +25,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-i", fileName, "-movflags", "faststart", "-pix_fmt", "yuv420p", + // "-vf", "scale='-2:trunc("+opt.Resolution+"/2)*2'", "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", "-f", "mp4", "-y", @@ -48,7 +49,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-qsv_device", "/dev/dri/renderD128", "-i", fileName, "-c:a", "aac", - "-vf", format, + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-c:v", opt.Encoder.String(), "-map", opt.MapVideo, "-map", opt.MapAudio, @@ -71,7 +72,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-map", opt.MapVideo, "-map", opt.MapAudio, "-c:a", "aac", - "-vf", format, + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-profile", "high", "-level", "51", "-vsync", "vfr", @@ -89,7 +90,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-hwaccel", "vaapi", "-i", fileName, "-c:a", "aac", - "-vf", format, + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-c:v", opt.Encoder.String(), "-map", opt.MapVideo, "-map", opt.MapAudio, @@ -103,6 +104,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, case NvidiaEncoder: // ffmpeg -hide_banner -h encoder=h264_nvenc + format := "format=yuv420p" result = exec.Command( opt.Bin, "-hwaccel", "auto", @@ -115,7 +117,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-preset", "15", "-pixel_format", "yuv420p", "-gpu", "any", - "-vf", "format=yuv420p", + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-rc:v", "constqp", "-cq", "0", "-tune", "2", @@ -139,7 +141,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-map", opt.MapVideo, "-map", opt.MapAudio, "-c:a", "aac", - "-vf", format, + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-num_output_buffers", "72", "-num_capture_buffers", "64", "-max_muxing_queue_size", "1024", @@ -161,7 +163,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, "-map", opt.MapVideo, "-map", opt.MapAudio, "-c:a", "aac", - "-vf", format, + "-vf", "\"scale='-2:"+opt.Resolution+"',"+format+"\"", "-max_muxing_queue_size", "1024", "-crf", "23", "-vsync", "vfr", diff --git a/internal/photoprism/convert_video_avc.go b/internal/photoprism/convert_video_avc.go index ae53f4c30..c4a712e24 100644 --- a/internal/photoprism/convert_video_avc.go +++ b/internal/photoprism/convert_video_avc.go @@ -150,7 +150,7 @@ func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg // Transcode all other formats with FFmpeg. var opt ffmpeg.Options - if opt, err = c.conf.FFmpegOptions(encoder, c.AvcBitrate(f)); err != nil { + if opt, err = c.conf.FFmpegOptions(encoder, c.AvcBitrate(f), c.AvcResolution(f)); err != nil { return nil, false, fmt.Errorf("convert: failed to transcode %s (%s)", clean.Log(f.BaseName()), err) } else { return ffmpeg.AvcConvertCommand(fileName, avcName, opt) @@ -178,3 +178,24 @@ func (c *Convert) AvcBitrate(f *MediaFile) string { return fmt.Sprintf("%dM", bitrate) } + +// AvcResolution returns the resolution to use for transcoding. +func (c *Convert) AvcResolution(f *MediaFile) string { + const defaultResolution = "2160" + + if f == nil { + return defaultResolution + } + + limit := c.conf.FFmpegResolution() + + resolution := f.height + + if resolution <= 144 { + return defaultResolution + } else if resolution > limit { + resolution = limit + } + + return fmt.Sprintf("%d", resolution) +} diff --git a/setup/docker/arm64/docker-compose.yml b/setup/docker/arm64/docker-compose.yml index c37b451f3..085985a6a 100644 --- a/setup/docker/arm64/docker-compose.yml +++ b/setup/docker/arm64/docker-compose.yml @@ -87,6 +87,7 @@ services: ## Hardware Video Transcoding: # PHOTOPRISM_FFMPEG_ENCODER: "raspberry" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry") # PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50) + # PHOTOPRISM_FFMPEG_RESOLUTION: "1080" # FFmpeg encoding resolution limit in pixel height (default: 2160) ## Run as a non-root user after initialization (supported: 0, 33, 50-99, 500-600, and 900-1200): # PHOTOPRISM_UID: 1000 # PHOTOPRISM_GID: 1000 diff --git a/setup/docker/docker-compose.yml b/setup/docker/docker-compose.yml index d11d3251c..3e30387de 100644 --- a/setup/docker/docker-compose.yml +++ b/setup/docker/docker-compose.yml @@ -78,6 +78,7 @@ services: ## Hardware Video Transcoding: # PHOTOPRISM_FFMPEG_ENCODER: "software" # FFmpeg encoder ("software", "intel", "nvidia", "apple", "raspberry") # PHOTOPRISM_FFMPEG_BITRATE: "32" # FFmpeg encoding bitrate limit in Mbit/s (default: 50) + # PHOTOPRISM_FFMPEG_RESOLUTION: "1080" # FFmpeg encoding resolution limit in pixel height (default: 2160) ## Run as a non-root user after initialization (supported: 0, 33, 50-99, 500-600, and 900-1200): # PHOTOPRISM_UID: 1000 # PHOTOPRISM_GID: 1000 diff --git a/setup/docker/nvidia/docker-compose.yml b/setup/docker/nvidia/docker-compose.yml index fd578c2da..0eadc0d6c 100644 --- a/setup/docker/nvidia/docker-compose.yml +++ b/setup/docker/nvidia/docker-compose.yml @@ -83,6 +83,7 @@ services: ## see https://docs.photoprism.app/getting-started/advanced/transcoding/#nvidia-container-toolkit PHOTOPRISM_FFMPEG_ENCODER: "nvidia" PHOTOPRISM_FFMPEG_BITRATE: "50" + PHOTOPRISM_FFMPEG_RESOLUTION: "2160" NVIDIA_VISIBLE_DEVICES: "all" NVIDIA_DRIVER_CAPABILITIES: "compute,video,utility" ## Run as a non-root user after initialization (supported: 0, 33, 50-99, 500-600, and 900-1200):