diff --git a/internal/config/config_ffmpeg.go b/internal/config/config_ffmpeg.go index fde61662c..7c748a5b3 100644 --- a/internal/config/config_ffmpeg.go +++ b/internal/config/config_ffmpeg.go @@ -1,6 +1,10 @@ package config -import "github.com/photoprism/photoprism/internal/ffmpeg" +import ( + "fmt" + + "github.com/photoprism/photoprism/internal/ffmpeg" +) // FFmpegBin returns the ffmpeg executable file name. func (c *Config) FFmpegBin() string { @@ -46,3 +50,46 @@ func (c *Config) FFmpegBitrateExceeded(mbit float64) bool { return mbit > float64(max) } } + +// FFmpegMapVideo returns the video streams to be transcoded as string. +func (c *Config) FFmpegMapVideo() string { + if c.options.FFmpegMapVideo == "" { + return ffmpeg.MapVideoDefault + } + + return c.options.FFmpegMapVideo +} + +// FFmpegMapAudio returns the audio streams to be transcoded as string. +func (c *Config) FFmpegMapAudio() string { + if c.options.FFmpegMapAudio == "" { + return ffmpeg.MapAudioDefault + } + + return c.options.FFmpegMapAudio +} + +// FFmpegOptions returns the FFmpeg transcoding options. +func (c *Config) FFmpegOptions(encoder ffmpeg.AvcEncoder, bitrate 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(), + } + + // Check + if opt.Bin == "" { + return opt, fmt.Errorf("ffmpeg is not installed") + } else if c.DisableFFmpeg() { + return opt, fmt.Errorf("ffmpeg is disabled") + } else if bitrate == "" { + return opt, fmt.Errorf("bitrate must not be empty") + } else if encoder.String() == "" { + return opt, fmt.Errorf("encoder must not be empty") + } + + return opt, nil +} diff --git a/internal/config/config_ffmpeg_test.go b/internal/config/config_ffmpeg_test.go index 66b5c3329..7c05f3771 100644 --- a/internal/config/config_ffmpeg_test.go +++ b/internal/config/config_ffmpeg_test.go @@ -63,3 +63,27 @@ func TestConfig_FFmpegBitrateExceeded(t *testing.T) { assert.False(t, c.FFmpegBitrateExceeded(1.05)) assert.False(t, c.FFmpegBitrateExceeded(2.05)) } + +func TestConfig_FFmpegMapVideo(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, ffmpeg.MapVideoDefault, c.FFmpegMapVideo()) +} + +func TestConfig_FFmpegMapAudio(t *testing.T) { + c := NewConfig(CliTestContext()) + assert.Equal(t, ffmpeg.MapAudioDefault, c.FFmpegMapAudio()) +} + +func TestConfig_FFmpegOptions(t *testing.T) { + c := NewConfig(CliTestContext()) + bitrate := "25M" + opt, err := c.FFmpegOptions(ffmpeg.SoftwareEncoder, bitrate) + assert.NoError(t, err) + assert.Equal(t, c.FFmpegBin(), opt.Bin) + assert.Equal(t, ffmpeg.SoftwareEncoder, opt.Encoder) + assert.Equal(t, bitrate, opt.Bitrate) + assert.Equal(t, ffmpeg.MapVideoDefault, opt.MapVideo) + assert.Equal(t, ffmpeg.MapAudioDefault, 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 5675bf020..21222a144 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -3,6 +3,8 @@ package config import ( "fmt" + "github.com/photoprism/photoprism/internal/ffmpeg" + "github.com/klauspost/cpuid/v2" "github.com/urfave/cli" @@ -566,6 +568,18 @@ var Flags = CliFlags{ Value: 50, EnvVar: "PHOTOPRISM_FFMPEG_BITRATE", }}, { + Flag: cli.StringFlag{ + Name: "ffmpeg-map-video", + Usage: "video `STREAMS` that should be transcoded", + Value: ffmpeg.MapVideoDefault, + EnvVar: "PHOTOPRISM_FFMPEG_MAP_VIDEO", + }}, { + Flag: cli.StringFlag{ + Name: "ffmpeg-map-audio", + Usage: "audio `STREAMS` that should be transcoded", + Value: ffmpeg.MapAudioDefault, + EnvVar: "PHOTOPRISM_FFMPEG_MAP_AUDIO", + }}, { 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 1a64d5e32..331bb9510 100644 --- a/internal/config/options.go +++ b/internal/config/options.go @@ -125,6 +125,8 @@ 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"` + 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"` DarktableBin string `yaml:"DarktableBin" json:"-" flag:"darktable-bin"` DarktableCachePath string `yaml:"DarktableCachePath" json:"-" flag:"darktable-cache-path"` diff --git a/internal/config/report.go b/internal/config/report.go index 208e04032..4d5661397 100644 --- a/internal/config/report.go +++ b/internal/config/report.go @@ -176,6 +176,8 @@ 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-map-video", c.FFmpegMapVideo()}, + {"ffmpeg-map-audio", c.FFmpegMapAudio()}, {"exiftool-bin", c.ExifToolBin()}, {"darktable-bin", c.DarktableBin()}, {"darktable-cache-path", c.DarktableCachePath()}, diff --git a/internal/ffmpeg/config.go b/internal/ffmpeg/config.go new file mode 100644 index 000000000..0c85b5524 --- /dev/null +++ b/internal/ffmpeg/config.go @@ -0,0 +1,10 @@ +package ffmpeg + +// Options represents transcoding options. +type Options struct { + Bin string + Encoder AvcEncoder + Bitrate string + MapVideo string + MapAudio string +} diff --git a/internal/ffmpeg/convert.go b/internal/ffmpeg/convert.go index acb85ad3d..a22136099 100644 --- a/internal/ffmpeg/convert.go +++ b/internal/ffmpeg/convert.go @@ -8,7 +8,7 @@ import ( ) // AvcConvertCommand returns the command for converting video files to MPEG-4 AVC. -func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder AvcEncoder) (result *exec.Cmd, useMutex bool, err error) { +func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd, useMutex bool, err error) { if fileName == "" { return nil, false, fmt.Errorf("empty input filename") } else if avcName == "" { @@ -21,7 +21,7 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc // Don't use hardware transcoding for animated images. if fs.TypeAnimated[fs.FileType(fileName)] != "" { result = exec.Command( - ffmpegBin, + opt.Bin, "-i", fileName, "-movflags", "faststart", "-pix_fmt", "yuv420p", @@ -35,27 +35,27 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc } // Display encoder info. - if encoder != SoftwareEncoder { - log.Infof("convert: ffmpeg encoder %s selected", string(encoder)) + if opt.Encoder != SoftwareEncoder { + log.Infof("convert: ffmpeg encoder %s selected", opt.Encoder.String()) } - switch encoder { + switch opt.Encoder { case IntelEncoder: // ffmpeg -hide_banner -h encoder=h264_qsv format := "format=rgb32" result = exec.Command( - ffmpegBin, + opt.Bin, "-qsv_device", "/dev/dri/renderD128", "-i", fileName, "-c:a", "aac", "-vf", format, - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-vsync", "vfr", "-r", "30", - "-b:v", bitrate, - "-bitrate", bitrate, + "-b:v", opt.Bitrate, + "-bitrate", opt.Bitrate, "-f", "mp4", "-y", avcName, @@ -65,18 +65,18 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc // ffmpeg -hide_banner -h encoder=h264_videotoolbox format := "format=yuv420p" result = exec.Command( - ffmpegBin, + opt.Bin, "-i", fileName, - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-c:a", "aac", "-vf", format, "-profile", "high", "-level", "51", "-vsync", "vfr", "-r", "30", - "-b:v", bitrate, + "-b:v", opt.Bitrate, "-f", "mp4", "-y", avcName, @@ -85,17 +85,17 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc case VAAPIEncoder: format := "format=nv12,hwupload" result = exec.Command( - ffmpegBin, + opt.Bin, "-hwaccel", "vaapi", "-i", fileName, "-c:a", "aac", "-vf", format, - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-vsync", "vfr", "-r", "30", - "-b:v", bitrate, + "-b:v", opt.Bitrate, "-f", "mp4", "-y", avcName, @@ -104,13 +104,13 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc case NvidiaEncoder: // ffmpeg -hide_banner -h encoder=h264_nvenc result = exec.Command( - ffmpegBin, + opt.Bin, "-hwaccel", "auto", "-i", fileName, "-pix_fmt", "yuv420p", - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-c:a", "aac", "-preset", "15", "-pixel_format", "yuv420p", @@ -120,7 +120,7 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc "-cq", "0", "-tune", "2", "-r", "30", - "-b:v", bitrate, + "-b:v", opt.Bitrate, "-profile:v", "1", "-level:v", "auto", "-coder:v", "1", @@ -133,11 +133,11 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc // ffmpeg -hide_banner -h encoder=h264_v4l2m2m format := "format=yuv420p" result = exec.Command( - ffmpegBin, + opt.Bin, "-i", fileName, - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-c:a", "aac", "-vf", format, "-num_output_buffers", "72", @@ -146,7 +146,7 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc "-crf", "23", "-vsync", "vfr", "-r", "30", - "-b:v", bitrate, + "-b:v", opt.Bitrate, "-f", "mp4", "-y", avcName, @@ -155,18 +155,18 @@ func AvcConvertCommand(fileName, avcName, ffmpegBin, bitrate string, encoder Avc default: format := "format=yuv420p" result = exec.Command( - ffmpegBin, + opt.Bin, "-i", fileName, - "-c:v", string(encoder), - "-map", "0:v:0", - "-map", "0:a:0?", + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, "-c:a", "aac", "-vf", format, "-max_muxing_queue_size", "1024", "-crf", "23", "-vsync", "vfr", "-r", "30", - "-b:v", bitrate, + "-b:v", opt.Bitrate, "-f", "mp4", "-y", avcName, diff --git a/internal/ffmpeg/defaults.go b/internal/ffmpeg/defaults.go new file mode 100644 index 000000000..e8f056307 --- /dev/null +++ b/internal/ffmpeg/defaults.go @@ -0,0 +1,6 @@ +package ffmpeg + +const ( + MapVideoDefault = "0:v:0" + MapAudioDefault = "0:a:0?" +) diff --git a/internal/photoprism/convert_video_avc.go b/internal/photoprism/convert_video_avc.go index 409b33a25..ae53f4c30 100644 --- a/internal/photoprism/convert_video_avc.go +++ b/internal/photoprism/convert_video_avc.go @@ -134,14 +134,10 @@ func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg.AvcEncoder) (result *exec.Cmd, useMutex bool, err error) { fileExt := f.Extension() fileName := f.FileName() - bitrate := c.AvcBitrate(f) - ffmpegBin := c.conf.FFmpegBin() switch { case fileName == "": return nil, false, fmt.Errorf("convert: %s video filename is empty - possible bug", f.FileType()) - case bitrate == "": - return nil, false, fmt.Errorf("convert: transcoding bitrate is empty - possible bug") case !f.IsAnimated(): return nil, false, fmt.Errorf("convert: file type %s of %s cannot be transcoded", f.FileType(), clean.Log(f.BaseName())) } @@ -152,13 +148,13 @@ func (c *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg } // Transcode all other formats with FFmpeg. - if ffmpegBin == "" { - return nil, false, fmt.Errorf("convert: ffmpeg must be installed to transcode %s", clean.Log(f.BaseName())) - } else if c.conf.DisableFFmpeg() { - return nil, false, fmt.Errorf("convert: ffmpeg must be enabled to transcode %s", clean.Log(f.BaseName())) - } + var opt ffmpeg.Options - return ffmpeg.AvcConvertCommand(fileName, avcName, ffmpegBin, c.AvcBitrate(f), encoder) + if opt, err = c.conf.FFmpegOptions(encoder, c.AvcBitrate(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) + } } // AvcBitrate returns the ideal AVC encoding bitrate in megabits per second.