diff --git a/internal/config/config_ffmpeg.go b/internal/config/config_ffmpeg.go index 5af5c3bde..e5d9d9319 100644 --- a/internal/config/config_ffmpeg.go +++ b/internal/config/config_ffmpeg.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/photoprism/photoprism/internal/ffmpeg" + "github.com/photoprism/photoprism/internal/ffmpeg/encode" "github.com/photoprism/photoprism/internal/thumb" ) @@ -18,12 +19,12 @@ func (c *Config) FFmpegEnabled() bool { } // FFmpegEncoder returns the FFmpeg AVC encoder name. -func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder { - if c.options.FFmpegEncoder == "" || c.options.FFmpegEncoder == ffmpeg.SoftwareEncoder.String() { - return ffmpeg.SoftwareEncoder +func (c *Config) FFmpegEncoder() encode.Encoder { + if c.options.FFmpegEncoder == "" || c.options.FFmpegEncoder == encode.SoftwareAvc.String() { + return encode.SoftwareAvc } - return ffmpeg.FindEncoder(c.options.FFmpegEncoder) + return encode.FindEncoder(c.options.FFmpegEncoder) } // FFmpegSize returns the maximum ffmpeg video encoding size in pixels (720-7680). @@ -73,9 +74,9 @@ 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 encode.Encoder, bitrate string) (encode.Options, error) { // Transcode all other formats with FFmpeg. - opt := ffmpeg.Options{ + opt := encode.Options{ Bin: c.FFmpegBin(), Encoder: encoder, Size: c.FFmpegSize(), diff --git a/internal/config/config_ffmpeg_test.go b/internal/config/config_ffmpeg_test.go index 40d241723..46b32fbf0 100644 --- a/internal/config/config_ffmpeg_test.go +++ b/internal/config/config_ffmpeg_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/photoprism/photoprism/internal/ffmpeg" + "github.com/photoprism/photoprism/internal/ffmpeg/encode" "github.com/photoprism/photoprism/internal/thumb" "github.com/stretchr/testify/assert" @@ -11,15 +12,15 @@ import ( func TestConfig_FFmpegEncoder(t *testing.T) { c := NewConfig(CliTestContext()) - assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder()) + assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder()) c.options.FFmpegEncoder = "nvidia" - assert.Equal(t, ffmpeg.NvidiaEncoder, c.FFmpegEncoder()) + assert.Equal(t, encode.NvidiaAvc, c.FFmpegEncoder()) c.options.FFmpegEncoder = "intel" - assert.Equal(t, ffmpeg.IntelEncoder, c.FFmpegEncoder()) + assert.Equal(t, encode.IntelAvc, c.FFmpegEncoder()) c.options.FFmpegEncoder = "xxx" - assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder()) + assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder()) c.options.FFmpegEncoder = "" - assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder()) + assert.Equal(t, encode.SoftwareAvc, c.FFmpegEncoder()) } func TestConfig_FFmpegEnabled(t *testing.T) { @@ -104,10 +105,10 @@ func TestConfig_FFmpegMapAudio(t *testing.T) { func TestConfig_FFmpegOptions(t *testing.T) { c := NewConfig(CliTestContext()) bitrate := "25M" - opt, err := c.FFmpegOptions(ffmpeg.SoftwareEncoder, bitrate) + opt, err := c.FFmpegOptions(encode.SoftwareAvc, bitrate) assert.NoError(t, err) assert.Equal(t, c.FFmpegBin(), opt.Bin) - assert.Equal(t, ffmpeg.SoftwareEncoder, opt.Encoder) + assert.Equal(t, encode.SoftwareAvc, opt.Encoder) assert.Equal(t, bitrate, opt.Bitrate) assert.Equal(t, ffmpeg.MapVideoDefault, opt.MapVideo) assert.Equal(t, ffmpeg.MapAudioDefault, opt.MapAudio) diff --git a/internal/ffmpeg/apple/avc.go b/internal/ffmpeg/apple/avc.go new file mode 100644 index 000000000..1a2987b0d --- /dev/null +++ b/internal/ffmpeg/apple/avc.go @@ -0,0 +1,30 @@ +package apple + +import ( + "os/exec" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" +) + +// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd { + // ffmpeg -hide_banner -h encoder=h264_videotoolbox + return exec.Command( + opt.Bin, + "-y", + "-strict", "-2", + "-i", srcName, + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatYUV420P), + "-profile", "high", + "-level", "51", + "-r", "30", + "-b:v", opt.Bitrate, + "-f", "mp4", + "-movflags", "+faststart", + destName, + ) +} diff --git a/internal/ffmpeg/convert.go b/internal/ffmpeg/convert.go index 4030c8e07..fb0af0ff8 100644 --- a/internal/ffmpeg/convert.go +++ b/internal/ffmpeg/convert.go @@ -4,187 +4,72 @@ import ( "fmt" "os/exec" + "github.com/photoprism/photoprism/internal/ffmpeg/apple" + "github.com/photoprism/photoprism/internal/ffmpeg/encode" + "github.com/photoprism/photoprism/internal/ffmpeg/intel" + "github.com/photoprism/photoprism/internal/ffmpeg/nvidia" + "github.com/photoprism/photoprism/internal/ffmpeg/v4l" + "github.com/photoprism/photoprism/internal/ffmpeg/vaapi" "github.com/photoprism/photoprism/pkg/fs" ) -// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC. -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 == "" { - return nil, false, fmt.Errorf("empty output filename") +// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC. +func AvcConvertCmd(srcName, destName string, opt encode.Options) (cmd *exec.Cmd, useMutex bool, err error) { + if srcName == "" { + return nil, false, fmt.Errorf("empty source filename") + } else if destName == "" { + return nil, false, fmt.Errorf("empty destination filename") } // Don't transcode more than one video at the same time. useMutex = true - // Get configured ffmpeg command name. - ffmpeg := opt.Bin - - // Use default ffmpeg command name? - if ffmpeg == "" { - ffmpeg = DefaultBin + // Use default ffmpeg command name. + if opt.Bin == "" { + opt.Bin = DefaultBin } // Don't use hardware transcoding for animated images. - if fs.TypeAnimated[fs.FileType(fileName)] != "" { - result = exec.Command( - ffmpeg, + if fs.TypeAnimated[fs.FileType(srcName)] != "" { + cmd = exec.Command( + opt.Bin, "-y", "-strict", "-2", - "-i", fileName, - "-pix_fmt", FormatYUV420P.String(), - "-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2", + "-i", srcName, + "-pix_fmt", encode.FormatYUV420P.String(), + "-vf", "scale='trunc(iw/2)*2:trunc(ih/2)*2'", "-f", "mp4", "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, + destName, ) - return result, useMutex, nil + return cmd, useMutex, nil } // Display encoder info. - if opt.Encoder != SoftwareEncoder { + if opt.Encoder != encode.SoftwareAvc { log.Infof("convert: ffmpeg encoder %s selected", opt.Encoder.String()) } switch opt.Encoder { - case IntelEncoder: - // ffmpeg -hide_banner -h encoder=h264_qsv - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-hwaccel", "qsv", - "-hwaccel_output_format", "qsv", - "-qsv_device", "/dev/dri/renderD128", - "-i", fileName, - "-c:a", "aac", - "-vf", opt.VideoFilter(FormatQSV), - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-r", "30", - "-b:v", opt.Bitrate, - "-bitrate", opt.Bitrate, - "-f", "mp4", - "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, - ) + case encode.IntelAvc: + cmd = intel.AvcConvertCmd(srcName, destName, opt) - case AppleEncoder: - // ffmpeg -hide_banner -h encoder=h264_videotoolbox - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-i", fileName, - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-c:a", "aac", - "-vf", opt.VideoFilter(FormatYUV420P), - "-profile", "high", - "-level", "51", - "-r", "30", - "-b:v", opt.Bitrate, - "-f", "mp4", - "-movflags", "+faststart", - avcName, - ) + case encode.AppleAvc: + cmd = apple.AvcConvertCmd(srcName, destName, opt) - case VAAPIEncoder: - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-hwaccel", "vaapi", - "-i", fileName, - "-c:a", "aac", - "-vf", opt.VideoFilter(FormatNV12), - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-r", "30", - "-b:v", opt.Bitrate, - "-f", "mp4", - "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, - ) + case encode.VaapiAvc: + cmd = vaapi.AvcConvertCmd(srcName, destName, opt) - case NvidiaEncoder: - // ffmpeg -hide_banner -h encoder=h264_nvenc - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-hwaccel", "auto", - "-i", fileName, - "-pix_fmt", FormatYUV420P.String(), - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-c:a", "aac", - "-preset", "15", - "-pixel_format", "yuv420p", - "-gpu", "any", - "-vf", opt.VideoFilter(FormatYUV420P), - "-rc:v", "constqp", - "-cq", "0", - "-tune", "2", - "-r", "30", - "-b:v", opt.Bitrate, - "-profile:v", "1", - "-level:v", "auto", - "-coder:v", "1", - "-f", "mp4", - "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, - ) + case encode.NvidiaAvc: + cmd = nvidia.AvcConvertCmd(srcName, destName, opt) - case Video4LinuxEncoder: - // ffmpeg -hide_banner -h encoder=h264_v4l2m2m - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-i", fileName, - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-c:a", "aac", - "-vf", opt.VideoFilter(FormatYUV420P), - "-num_output_buffers", "72", - "-num_capture_buffers", "64", - "-max_muxing_queue_size", "1024", - "-crf", "23", - "-r", "30", - "-b:v", opt.Bitrate, - "-f", "mp4", - "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, - ) + case encode.V4LAvc: + cmd = v4l.AvcConvertCmd(srcName, destName, opt) default: - result = exec.Command( - ffmpeg, - "-y", - "-strict", "-2", - "-i", fileName, - "-c:v", opt.Encoder.String(), - "-map", opt.MapVideo, - "-map", opt.MapAudio, - "-c:a", "aac", - "-vf", opt.VideoFilter(FormatYUV420P), - "-max_muxing_queue_size", "1024", - "-crf", "23", - "-r", "30", - "-b:v", opt.Bitrate, - "-f", "mp4", - "-movflags", "+faststart", // puts headers at the beginning for faster streaming - avcName, - ) + cmd = encode.AvcConvertCmd(srcName, destName, opt) } - return result, useMutex, nil + return cmd, useMutex, nil } diff --git a/internal/ffmpeg/convert_test.go b/internal/ffmpeg/convert_test.go index e490d7de3..f8b9b0cc4 100644 --- a/internal/ffmpeg/convert_test.go +++ b/internal/ffmpeg/convert_test.go @@ -1,40 +1,62 @@ package ffmpeg import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + "strings" "testing" + "time" "github.com/stretchr/testify/assert" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" + "github.com/photoprism/photoprism/pkg/fs" ) -func TestAvcConvertCommand(t *testing.T) { - t.Run("empty filename", func(t *testing.T) { - opt := Options{ - Bin: "", - Encoder: "intel", - Size: 1500, - Bitrate: "50M", - MapVideo: MapVideoDefault, - MapAudio: MapAudioDefault, - } - _, _, err := AvcConvertCommand("", "", opt) +func TestAvcConvertCmd(t *testing.T) { + runCmd := func(t *testing.T, encoder encode.Encoder, srcName, destName string, cmd *exec.Cmd) { + var out bytes.Buffer + var stderr bytes.Buffer + cmd.Stdout = &out + cmd.Stderr = &stderr + cmd.Env = append(cmd.Env, []string{ + fmt.Sprintf("HOME=%s", fs.Abs("./testdata")), + }...) - assert.Equal(t, err.Error(), "empty input filename") - }) - t.Run("avc name empty", func(t *testing.T) { - opt := Options{ - Bin: "", - Encoder: "intel", - Size: 1500, - Bitrate: "50M", - MapVideo: MapVideoDefault, - MapAudio: MapAudioDefault, - } - _, _, err := AvcConvertCommand("VID123.mov", "", opt) + // Transcode source media file to AVC. + start := time.Now() + if err := cmd.Run(); err != nil { + if stderr.String() != "" { + err = errors.New(stderr.String()) + } - assert.Equal(t, err.Error(), "empty output filename") - }) - t.Run("animated file", func(t *testing.T) { - opt := Options{ + // Remove broken video file. + if !fs.FileExists(destName) { + // Do nothing. + } else if removeErr := os.Remove(destName); removeErr != nil { + t.Logf("%s: failed to remove %s after error (%s)", encoder, srcName, removeErr) + } + + // Log ffmpeg output for debugging. + if err.Error() != "" { + t.Error(err) + t.Fatalf("%s: failed to transcode %s [%s]", encoder, srcName, time.Since(start)) + } + } + + // Log filename and transcoding time. + t.Logf("%s: created %s [%s]", encoder, destName, time.Since(start)) + + if removeErr := os.Remove(destName); removeErr != nil { + t.Fatalf("%s: failed to remove %s after successful test (%s)", encoder, srcName, removeErr) + } + } + + t.Run("NoSource", func(t *testing.T) { + opt := encode.Options{ Bin: "", Encoder: "intel", Size: 1500, @@ -42,33 +64,101 @@ func TestAvcConvertCommand(t *testing.T) { MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.gif", "VID123.gif.avc", opt) + _, _, err := AvcConvertCmd("", "", opt) + + assert.Equal(t, "empty source filename", err.Error()) + }) + t.Run("NoDestination", func(t *testing.T) { + opt := encode.Options{ + Bin: "", + Encoder: "intel", + Size: 1500, + Bitrate: "50M", + MapVideo: MapVideoDefault, + MapAudio: MapAudioDefault, + } + _, _, err := AvcConvertCmd("VID123.mov", "", opt) + + assert.Equal(t, "empty destination filename", err.Error()) + }) + t.Run("Animation", func(t *testing.T) { + opt := encode.Options{ + Bin: "", + Encoder: "intel", + Size: 1500, + Bitrate: "50M", + MapVideo: MapVideoDefault, + MapAudio: MapAudioDefault, + } + r, _, err := AvcConvertCmd("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 -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("libx264", func(t *testing.T) { - opt := Options{ - Bin: "", - Encoder: "libx264", + t.Run("VP9", func(t *testing.T) { + encoder := encode.SoftwareAvc + + opt := encode.Options{ + Bin: "/usr/bin/ffmpeg", + Encoder: encoder, Size: 1500, Bitrate: "50M", MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) + + srcName := fs.Abs("./testdata/25fps.vp9") + destName := fs.Abs("./testdata/25fps.avc") + + cmd, _, err := AvcConvertCmd(srcName, destName, opt) if err != nil { t.Fatal(err) } - assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -i VID123.mov -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 50M -f mp4 -movflags +faststart VID123.mov.avc") + cmdStr := cmd.String() + 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 50M -f mp4 -movflags +faststart DEST", cmdStr) + + // Performs software transcoding. + runCmd(t, encoder, srcName, destName, cmd) }) - t.Run("h264_qsv", func(t *testing.T) { - opt := Options{ + t.Run("Vaapi", func(t *testing.T) { + encoder := encode.VaapiAvc + opt := encode.Options{ + Bin: "/usr/bin/ffmpeg", + Encoder: encoder, + Size: 1500, + Bitrate: "50M", + MapVideo: MapVideoDefault, + MapAudio: MapAudioDefault, + } + + srcName := fs.Abs("./testdata/25fps.vp9") + destName := fs.Abs("./testdata/25fps.vaapi.avc") + + cmd, _, err := AvcConvertCmd(srcName, destName, opt) + + if err != nil { + t.Fatal(err) + } + + cmdStr := cmd.String() + 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 50M -f mp4 -movflags +faststart DEST", cmdStr) + + // Requires Video4Linux transcoding device. + // runCmd(t, encoder, srcName, destName, cmd) + }) + t.Run("QSV", func(t *testing.T) { + opt := encode.Options{ Bin: "", Encoder: "h264_qsv", Size: 1500, @@ -76,7 +166,7 @@ func TestAvcConvertCommand(t *testing.T) { MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) + r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) @@ -84,8 +174,8 @@ func TestAvcConvertCommand(t *testing.T) { assert.Contains(t, r.String(), "/bin/ffmpeg -y -strict -2 -hwaccel qsv -hwaccel_output_format qsv -qsv_device /dev/dri/renderD128 -i VID123.mov -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 50M -bitrate 50M -f mp4 -movflags +faststart VID123.mov.avc") }) - t.Run("h264_videotoolbox", func(t *testing.T) { - opt := Options{ + t.Run("Apple", func(t *testing.T) { + opt := encode.Options{ Bin: "", Encoder: "h264_videotoolbox", Size: 1500, @@ -93,7 +183,7 @@ func TestAvcConvertCommand(t *testing.T) { MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) + r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) @@ -101,25 +191,8 @@ func TestAvcConvertCommand(t *testing.T) { 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 50M -f mp4 -movflags +faststart VID123.mov.avc") }) - t.Run("h264_vaapi", func(t *testing.T) { - opt := Options{ - Bin: "", - Encoder: "h264_vaapi", - Size: 1500, - Bitrate: "50M", - MapVideo: MapVideoDefault, - MapAudio: MapAudioDefault, - } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) - - if err != nil { - t.Fatal(err) - } - - assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -hwaccel vaapi -i VID123.mov -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 50M -f mp4 -movflags +faststart VID123.mov.avc") - }) - t.Run("h264_nvenc", func(t *testing.T) { - opt := Options{ + t.Run("Nvidia", func(t *testing.T) { + opt := encode.Options{ Bin: "", Encoder: "h264_nvenc", Size: 1500, @@ -127,7 +200,7 @@ func TestAvcConvertCommand(t *testing.T) { MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) + r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) @@ -135,8 +208,8 @@ func TestAvcConvertCommand(t *testing.T) { assert.Contains(t, r.String(), "bin/ffmpeg -y -strict -2 -hwaccel auto -i VID123.mov -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 50M -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags +faststart VID123.mov.avc") }) - t.Run("h264_v4l2m2m", func(t *testing.T) { - opt := Options{ + t.Run("Video4Linux", func(t *testing.T) { + opt := encode.Options{ Bin: "", Encoder: "h264_v4l2m2m", Size: 1500, @@ -144,7 +217,7 @@ func TestAvcConvertCommand(t *testing.T) { MapVideo: MapVideoDefault, MapAudio: MapAudioDefault, } - r, _, err := AvcConvertCommand("VID123.mov", "VID123.mov.avc", opt) + r, _, err := AvcConvertCmd("VID123.mov", "VID123.mov.avc", opt) if err != nil { t.Fatal(err) diff --git a/internal/ffmpeg/encode/avc.go b/internal/ffmpeg/encode/avc.go new file mode 100644 index 000000000..de71a2edd --- /dev/null +++ b/internal/ffmpeg/encode/avc.go @@ -0,0 +1,25 @@ +package encode + +import "os/exec" + +// AvcConvertCmd returns the command for software transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(srcName, destName string, opt Options) *exec.Cmd { + return exec.Command( + opt.Bin, + "-y", + "-strict", "-2", + "-i", srcName, + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-c:a", "aac", + "-vf", opt.VideoFilter(FormatYUV420P), + "-max_muxing_queue_size", "1024", + "-crf", "23", + "-r", "30", + "-b:v", opt.Bitrate, + "-f", "mp4", + "-movflags", "+faststart", // puts headers at the beginning for faster streaming + destName, + ) +} diff --git a/internal/ffmpeg/encode/encode.go b/internal/ffmpeg/encode/encode.go new file mode 100644 index 000000000..a41883189 --- /dev/null +++ b/internal/ffmpeg/encode/encode.go @@ -0,0 +1,31 @@ +/* +Package encode provides FFmpeg video encoder related types and functions. + +Copyright (c) 2018 - 2024 PhotoPrism UG. All rights reserved. + + This program is free software: you can redistribute it and/or modify + it under Version 3 of the GNU Affero General Public License (the "AGPL"): + + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + The AGPL is supplemented by our Trademark and Brand Guidelines, + which describe how our Brand Assets may be used: + + +Feel free to send an email to hello@photoprism.app if you have questions, +want to support our work, or just want to say hello. + +Additional information can be found in our Developer Guide: + +*/ +package encode + +import ( + "github.com/photoprism/photoprism/internal/event" +) + +var log = event.Log diff --git a/internal/ffmpeg/encode/encoder.go b/internal/ffmpeg/encode/encoder.go new file mode 100644 index 000000000..f1b621e5c --- /dev/null +++ b/internal/ffmpeg/encode/encoder.go @@ -0,0 +1,63 @@ +package encode + +import "github.com/photoprism/photoprism/pkg/clean" + +// Encoder represents a supported FFmpeg AVC encoder name. +type Encoder string + +// String returns the FFmpeg AVC encoder name as string. +func (name Encoder) String() string { + return string(name) +} + +// Supported FFmpeg AVC encoders. +const ( + SoftwareAvc Encoder = "libx264" // SoftwareAvc see https://trac.ffmpeg.org/wiki/HWAccelIntro. + IntelAvc Encoder = "h264_qsv" // IntelAvc is the Intel Quick Sync H.264 encoder. + AppleAvc Encoder = "h264_videotoolbox" // AppleAvc is the Apple Video Toolbox H.264 encoder. + VaapiAvc Encoder = "h264_vaapi" // VaapiAvc is the Video Acceleration API H.264 encoder. + NvidiaAvc Encoder = "h264_nvenc" // NvidiaAvc is the NVIDIA H.264 encoder. + V4LAvc Encoder = "h264_v4l2m2m" // V4LAvc is the Video4Linux H.264 encoder. +) + +// AvcEncoders is the list of supported H.264 encoders with aliases. +var AvcEncoders = map[string]Encoder{ + "": SoftwareAvc, + "default": SoftwareAvc, + "software": SoftwareAvc, + string(SoftwareAvc): SoftwareAvc, + "intel": IntelAvc, + "qsv": IntelAvc, + string(IntelAvc): IntelAvc, + "apple": AppleAvc, + "osx": AppleAvc, + "mac": AppleAvc, + "macos": AppleAvc, + "darwin": AppleAvc, + string(AppleAvc): AppleAvc, + "vaapi": VaapiAvc, + "libva": VaapiAvc, + string(VaapiAvc): VaapiAvc, + "nvidia": NvidiaAvc, + "nvenc": NvidiaAvc, + "cuda": NvidiaAvc, + string(NvidiaAvc): NvidiaAvc, + "v4l2": V4LAvc, + "v4l": V4LAvc, + "video4linux": V4LAvc, + "rp4": V4LAvc, + "raspberry": V4LAvc, + "raspberrypi": V4LAvc, + string(V4LAvc): V4LAvc, +} + +// FindEncoder finds an FFmpeg encoder by name. +func FindEncoder(s string) Encoder { + if encoder, ok := AvcEncoders[s]; ok { + return encoder + } else { + log.Warnf("ffmpeg: unsupported encoder %s", clean.Log(s)) + } + + return SoftwareAvc +} diff --git a/internal/ffmpeg/encoders_test.go b/internal/ffmpeg/encode/encoder_test.go similarity index 96% rename from internal/ffmpeg/encoders_test.go rename to internal/ffmpeg/encode/encoder_test.go index 1654b0c3e..7d635ebc1 100644 --- a/internal/ffmpeg/encoders_test.go +++ b/internal/ffmpeg/encode/encoder_test.go @@ -1,4 +1,4 @@ -package ffmpeg +package encode import ( "testing" diff --git a/internal/ffmpeg/config.go b/internal/ffmpeg/encode/options.go similarity index 96% rename from internal/ffmpeg/config.go rename to internal/ffmpeg/encode/options.go index 102c45d43..6c8cef9ef 100644 --- a/internal/ffmpeg/config.go +++ b/internal/ffmpeg/encode/options.go @@ -1,11 +1,11 @@ -package ffmpeg +package encode import "fmt" // Options represents transcoding options. type Options struct { Bin string - Encoder AvcEncoder + Encoder Encoder Size int Bitrate string MapVideo string diff --git a/internal/ffmpeg/config_test.go b/internal/ffmpeg/encode/options_test.go similarity index 97% rename from internal/ffmpeg/config_test.go rename to internal/ffmpeg/encode/options_test.go index 6cd9f2b69..07847ff91 100644 --- a/internal/ffmpeg/config_test.go +++ b/internal/ffmpeg/encode/options_test.go @@ -1,4 +1,4 @@ -package ffmpeg +package encode import ( "testing" diff --git a/internal/ffmpeg/format.go b/internal/ffmpeg/encode/pixel_format.go similarity index 95% rename from internal/ffmpeg/format.go rename to internal/ffmpeg/encode/pixel_format.go index 29fc22e81..c2fa3a208 100644 --- a/internal/ffmpeg/format.go +++ b/internal/ffmpeg/encode/pixel_format.go @@ -1,4 +1,4 @@ -package ffmpeg +package encode // PixelFormat represents a standard pixel format. type PixelFormat string diff --git a/internal/ffmpeg/encoders.go b/internal/ffmpeg/encoders.go deleted file mode 100644 index d41e69dc3..000000000 --- a/internal/ffmpeg/encoders.go +++ /dev/null @@ -1,63 +0,0 @@ -package ffmpeg - -import "github.com/photoprism/photoprism/pkg/clean" - -// AvcEncoder represents a supported FFmpeg AVC encoder name. -type AvcEncoder string - -// String returns the FFmpeg AVC encoder name as string. -func (name AvcEncoder) String() string { - return string(name) -} - -// Supported FFmpeg AVC encoders. -const ( - SoftwareEncoder AvcEncoder = "libx264" // SoftwareEncoder see https://trac.ffmpeg.org/wiki/HWAccelIntro. - IntelEncoder AvcEncoder = "h264_qsv" // IntelEncoder is the Intel Quick Sync H.264 encoder. - AppleEncoder AvcEncoder = "h264_videotoolbox" // AppleEncoder is the Apple Video Toolbox H.264 encoder. - VAAPIEncoder AvcEncoder = "h264_vaapi" // VAAPIEncoder is the Video Acceleration API H.264 encoder. - NvidiaEncoder AvcEncoder = "h264_nvenc" // NvidiaEncoder is the NVIDIA H.264 encoder. - Video4LinuxEncoder AvcEncoder = "h264_v4l2m2m" // Video4LinuxEncoder is the Video4Linux H.264 encoder. -) - -// AvcEncoders is the list of supported H.264 encoders with aliases. -var AvcEncoders = map[string]AvcEncoder{ - "": SoftwareEncoder, - "default": SoftwareEncoder, - "software": SoftwareEncoder, - string(SoftwareEncoder): SoftwareEncoder, - "intel": IntelEncoder, - "qsv": IntelEncoder, - string(IntelEncoder): IntelEncoder, - "apple": AppleEncoder, - "osx": AppleEncoder, - "mac": AppleEncoder, - "macos": AppleEncoder, - "darwin": AppleEncoder, - string(AppleEncoder): AppleEncoder, - "vaapi": VAAPIEncoder, - "libva": VAAPIEncoder, - string(VAAPIEncoder): VAAPIEncoder, - "nvidia": NvidiaEncoder, - "nvenc": NvidiaEncoder, - "cuda": NvidiaEncoder, - string(NvidiaEncoder): NvidiaEncoder, - "v4l2": Video4LinuxEncoder, - "v4l": Video4LinuxEncoder, - "video4linux": Video4LinuxEncoder, - "rp4": Video4LinuxEncoder, - "raspberry": Video4LinuxEncoder, - "raspberrypi": Video4LinuxEncoder, - string(Video4LinuxEncoder): Video4LinuxEncoder, -} - -// FindEncoder finds an FFmpeg encoder by name. -func FindEncoder(s string) AvcEncoder { - if encoder, ok := AvcEncoders[s]; ok { - return encoder - } else { - log.Warnf("ffmpeg: unsupported encoder %s", clean.Log(s)) - } - - return SoftwareEncoder -} diff --git a/internal/ffmpeg/intel/avc.go b/internal/ffmpeg/intel/avc.go new file mode 100644 index 000000000..25f76b446 --- /dev/null +++ b/internal/ffmpeg/intel/avc.go @@ -0,0 +1,32 @@ +package intel + +import ( + "os/exec" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" +) + +// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(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", + "-qsv_device", "/dev/dri/renderD128", + "-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.Bitrate, + "-bitrate", opt.Bitrate, + "-f", "mp4", + "-movflags", "+faststart", // puts headers at the beginning for faster streaming + destName, + ) +} diff --git a/internal/ffmpeg/nvidia/avc.go b/internal/ffmpeg/nvidia/avc.go new file mode 100644 index 000000000..c8d1e23dd --- /dev/null +++ b/internal/ffmpeg/nvidia/avc.go @@ -0,0 +1,39 @@ +package nvidia + +import ( + "os/exec" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" +) + +// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd { + // ffmpeg -hide_banner -h encoder=h264_nvenc + return exec.Command( + opt.Bin, + "-y", + "-strict", "-2", + "-hwaccel", "auto", + "-i", srcName, + "-pix_fmt", encode.FormatYUV420P.String(), + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-c:a", "aac", + "-preset", "15", + "-pixel_format", "yuv420p", + "-gpu", "any", + "-vf", opt.VideoFilter(encode.FormatYUV420P), + "-rc:v", "constqp", + "-cq", "0", + "-tune", "2", + "-r", "30", + "-b:v", opt.Bitrate, + "-profile:v", "1", + "-level:v", "auto", + "-coder:v", "1", + "-f", "mp4", + "-movflags", "+faststart", // puts headers at the beginning for faster streaming + destName, + ) +} diff --git a/internal/ffmpeg/testdata/25fps.vp9 b/internal/ffmpeg/testdata/25fps.vp9 new file mode 100644 index 000000000..385d1b4ab Binary files /dev/null and b/internal/ffmpeg/testdata/25fps.vp9 differ diff --git a/internal/ffmpeg/v4l/avc.go b/internal/ffmpeg/v4l/avc.go new file mode 100644 index 000000000..77d51b17a --- /dev/null +++ b/internal/ffmpeg/v4l/avc.go @@ -0,0 +1,32 @@ +package v4l + +import ( + "os/exec" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" +) + +// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(srcName, destName string, opt encode.Options) *exec.Cmd { + // ffmpeg -hide_banner -h encoder=h264_v4l2m2m + return exec.Command( + opt.Bin, + "-y", + "-strict", "-2", + "-i", srcName, + "-c:v", opt.Encoder.String(), + "-map", opt.MapVideo, + "-map", opt.MapAudio, + "-c:a", "aac", + "-vf", opt.VideoFilter(encode.FormatYUV420P), + "-num_output_buffers", "72", + "-num_capture_buffers", "64", + "-max_muxing_queue_size", "1024", + "-crf", "23", + "-r", "30", + "-b:v", opt.Bitrate, + "-f", "mp4", + "-movflags", "+faststart", // puts headers at the beginning for faster streaming + destName, + ) +} diff --git a/internal/ffmpeg/vaapi/avc.go b/internal/ffmpeg/vaapi/avc.go new file mode 100644 index 000000000..d193c8680 --- /dev/null +++ b/internal/ffmpeg/vaapi/avc.go @@ -0,0 +1,28 @@ +package vaapi + +import ( + "os/exec" + + "github.com/photoprism/photoprism/internal/ffmpeg/encode" +) + +// AvcConvertCmd returns the command for hardware-accelerated transcoding of video files to MPEG-4 AVC. +func AvcConvertCmd(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.Bitrate, + "-f", "mp4", + "-movflags", "+faststart", // puts headers at the beginning for faster streaming + destName, + ) +} diff --git a/internal/photoprism/convert_command.go b/internal/photoprism/convert_command.go index 00a318849..df07523e6 100644 --- a/internal/photoprism/convert_command.go +++ b/internal/photoprism/convert_command.go @@ -6,15 +6,15 @@ import ( "github.com/photoprism/photoprism/pkg/media" ) -// ConvertCommand represents a command to be executed for converting a MediaFile. +// ConvertCmd represents a command to be executed for converting a MediaFile. // including any options to be used for this. -type ConvertCommand struct { +type ConvertCmd struct { Cmd *exec.Cmd Orientation media.Orientation } // String returns the conversion command as string e.g. for logging. -func (c *ConvertCommand) String() string { +func (c *ConvertCmd) String() string { if c.Cmd == nil { return "" } @@ -23,33 +23,32 @@ func (c *ConvertCommand) String() string { } // WithOrientation sets the media Orientation after successful conversion. -func (c *ConvertCommand) WithOrientation(o media.Orientation) *ConvertCommand { +func (c *ConvertCmd) WithOrientation(o media.Orientation) *ConvertCmd { c.Orientation = media.ParseOrientation(o, c.Orientation) return c } // ResetOrientation resets the media Orientation after successful conversion. -func (c *ConvertCommand) ResetOrientation() *ConvertCommand { +func (c *ConvertCmd) ResetOrientation() *ConvertCmd { return c.WithOrientation(media.ResetOrientation) } -// NewConvertCommand returns a new file converter command with default options. -func NewConvertCommand(cmd *exec.Cmd) *ConvertCommand { +// NewConvertCmd returns a new file converter command with default options. +func NewConvertCmd(cmd *exec.Cmd) *ConvertCmd { if cmd == nil { return nil } - return &ConvertCommand{ + return &ConvertCmd{ Cmd: cmd, // File conversion command. Orientation: media.KeepOrientation, // Keep the orientation by default. } } -// ConvertCommands represents a list of possible ConvertCommand commands for converting a MediaFile, -// sorted by priority. -type ConvertCommands []*ConvertCommand +// ConvertCmds represents a list of possible ConvertCommand commands for converting a MediaFile, sorted by priority. +type ConvertCmds []*ConvertCmd -// NewConvertCommands returns a new, empty list of ConvertCommand commands. -func NewConvertCommands() ConvertCommands { - return make(ConvertCommands, 0, 8) +// NewConvertCmds returns a new, empty list of ConvertCommand commands. +func NewConvertCmds() ConvertCmds { + return make(ConvertCmds, 0, 8) } diff --git a/internal/photoprism/convert_command_test.go b/internal/photoprism/convert_command_test.go index 913a0a760..fa0437bd0 100644 --- a/internal/photoprism/convert_command_test.go +++ b/internal/photoprism/convert_command_test.go @@ -9,12 +9,12 @@ import ( "github.com/photoprism/photoprism/pkg/media" ) -func TestNewConvertCommand(t *testing.T) { +func TestNewConvertCmd(t *testing.T) { t.Run("Nil", func(t *testing.T) { - assert.Nil(t, NewConvertCommand(nil)) + assert.Nil(t, NewConvertCmd(nil)) }) t.Run("Default", func(t *testing.T) { - result := NewConvertCommand( + result := NewConvertCmd( exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"), ) assert.NotNil(t, result) @@ -23,7 +23,7 @@ func TestNewConvertCommand(t *testing.T) { assert.Equal(t, media.KeepOrientation, result.Orientation) }) t.Run("WithOrientation", func(t *testing.T) { - result := NewConvertCommand( + result := NewConvertCmd( exec.Command("/usr/bin/sips", "-Z", "123", "-s", "format", "jpeg", "--out", "file.jpeg", "file.heic"), ) result.WithOrientation(media.ResetOrientation) @@ -34,9 +34,9 @@ func TestNewConvertCommand(t *testing.T) { }) } -func TestNewConvertCommands(t *testing.T) { +func TestNewConvertCmds(t *testing.T) { t.Run("Success", func(t *testing.T) { - result := NewConvertCommands() + result := NewConvertCmds() assert.NotNil(t, result) }) } diff --git a/internal/photoprism/convert_image.go b/internal/photoprism/convert_image.go index 97bfc3584..e86c25735 100644 --- a/internal/photoprism/convert_image.go +++ b/internal/photoprism/convert_image.go @@ -108,16 +108,16 @@ func (w *Convert) ToImage(f *MediaFile, force bool) (result *MediaFile, err erro } // Run external commands for other formats. - var cmds ConvertCommands + var cmds ConvertCmds var useMutex bool var expectedMime string switch fs.LowerExt(imageName) { case fs.ExtPNG: - cmds, useMutex, err = w.PngConvertCommands(f, imageName) + cmds, useMutex, err = w.PngConvertCmds(f, imageName) expectedMime = fs.MimeTypePNG case fs.ExtJPEG: - cmds, useMutex, err = w.JpegConvertCommands(f, imageName, xmpName) + cmds, useMutex, err = w.JpegConvertCmds(f, imageName, xmpName) expectedMime = fs.MimeTypeJPEG default: return nil, fmt.Errorf("convert: unspported target format %s (%s)", fs.LowerExt(imageName), clean.Log(f.RootRelName())) diff --git a/internal/photoprism/convert_image_jpeg.go b/internal/photoprism/convert_image_jpeg.go index 271e71e79..728b11441 100644 --- a/internal/photoprism/convert_image_jpeg.go +++ b/internal/photoprism/convert_image_jpeg.go @@ -9,9 +9,9 @@ import ( "github.com/photoprism/photoprism/internal/ffmpeg" ) -// JpegConvertCommands returns the supported commands for converting a MediaFile to JPEG, sorted by priority. -func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName string) (result ConvertCommands, useMutex bool, err error) { - result = NewConvertCommands() +// JpegConvertCmds returns the supported commands for converting a MediaFile to JPEG, sorted by priority. +func (w *Convert) JpegConvertCmds(f *MediaFile, jpegName string, xmpName string) (result ConvertCmds, useMutex bool, err error) { + result = NewConvertCmds() if f == nil { return result, useMutex, fmt.Errorf("file is nil - you may have found a bug") @@ -23,7 +23,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str // Apple Scriptable image processing system: https://ss64.com/osx/sips.html if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "jpeg", "--out", jpegName, f.FileName())), ) } @@ -34,7 +34,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str // TODO: Adjust command flags for correct colors with HDR10-encoded HEVC videos, // see https://github.com/photoprism/photoprism/issues/4488 - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", timeOffset, "-i", f.FileName(), "-vframes", "1", // Unfortunately, this filter renders 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", @@ -44,7 +44,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str // Use heif-convert for HEIC/HEIF and AVIF image files. if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.HeifConvertBin(), "-q", w.conf.JpegQuality().String(), f.FileName(), jpegName)). WithOrientation(w.conf.HeifConvertOrientation()), ) @@ -82,7 +82,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str args = append(args, "--cachedir", dir) } - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.DarktableBin(), args...)), ) } @@ -93,7 +93,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str args := []string{"-o", jpegName, "-p", profile, "-s", "-d", jpegQuality, "-js3", "-b8", "-c", f.FileName()} - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.RawTherapeeBin(), args...)), ) } @@ -102,14 +102,14 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str // Extract preview image from DNG files. if f.IsDNG() && w.conf.ExifToolEnabled() { // Example: exiftool -b -PreviewImage -w IMG_4691.DNG.jpg IMG_4691.DNG - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.ExifToolBin(), "-q", "-q", "-b", "-PreviewImage", f.FileName())), ) } // Decode JPEG XL image if support is enabled. if f.IsJpegXL() && w.conf.JpegXLEnabled() { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), jpegName)), ) } @@ -120,7 +120,7 @@ func (w *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str quality := fmt.Sprintf("%d", w.conf.JpegQuality()) resize := fmt.Sprintf("%dx%d>", w.conf.JpegSize(), w.conf.JpegSize()) args := []string{f.FileName(), "-flatten", "-resize", resize, "-quality", quality, jpegName} - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.ImageMagickBin(), args...)), ) } diff --git a/internal/photoprism/convert_image_png.go b/internal/photoprism/convert_image_png.go index b1ceb197d..756e662d7 100644 --- a/internal/photoprism/convert_image_png.go +++ b/internal/photoprism/convert_image_png.go @@ -8,9 +8,9 @@ import ( "github.com/photoprism/photoprism/internal/ffmpeg" ) -// PngConvertCommands returns commands for converting a media file to PNG, if possible. -func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result ConvertCommands, useMutex bool, err error) { - result = NewConvertCommands() +// PngConvertCmds returns commands for converting a media file to PNG, if possible. +func (w *Convert) PngConvertCmds(f *MediaFile, pngName string) (result ConvertCmds, useMutex bool, err error) { + result = NewConvertCmds() if f == nil { return result, useMutex, fmt.Errorf("file is nil - you may have found a bug") @@ -22,7 +22,7 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve // Apple Scriptable image processing system: https://ss64.com/osx/sips.html if (f.IsRaw() || f.IsHEIF()) && w.conf.SipsEnabled() && w.sipsExclude.Allow(fileExt) { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.SipsBin(), "-Z", maxSize, "-s", "format", "png", "--out", pngName, f.FileName())), ) } @@ -30,14 +30,14 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve // Extract a video still image that can be used as preview. if f.IsAnimated() && !f.IsWebP() && w.conf.FFmpegEnabled() { // Use "ffmpeg" to extract a PNG still image from the video. - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.FFmpegBin(), "-y", "-strict", "-2", "-ss", ffmpeg.PreviewTimeOffset(f.Duration()), "-i", f.FileName(), "-vframes", "1", pngName)), ) } // Use heif-convert for HEIC/HEIF and AVIF image files. if (f.IsHEIC() || f.IsAVIF()) && w.conf.HeifConvertEnabled() { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.HeifConvertBin(), f.FileName(), pngName)). WithOrientation(w.conf.HeifConvertOrientation()), ) @@ -45,7 +45,7 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve // Decode JPEG XL image if support is enabled. if f.IsJpegXL() && w.conf.JpegXLEnabled() { - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.JpegXLDecoderBin(), f.FileName(), pngName)), ) } @@ -54,14 +54,14 @@ func (w *Convert) PngConvertCommands(f *MediaFile, pngName string) (result Conve // otherwise try to convert the media file with ImageMagick. if w.conf.RsvgConvertEnabled() && f.IsSVG() { args := []string{"-a", "-f", "png", "-o", pngName, f.FileName()} - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.RsvgConvertBin(), args...)), ) } else if w.conf.ImageMagickEnabled() && w.imageMagickExclude.Allow(fileExt) && (f.IsImage() && !f.IsJpegXL() && !f.IsRaw() && !f.IsHEIF() || f.IsVector() && w.conf.VectorEnabled()) { resize := fmt.Sprintf("%dx%d>", w.conf.PngSize(), w.conf.PngSize()) args := []string{f.FileName(), "-flatten", "-resize", resize, pngName} - result = append(result, NewConvertCommand( + result = append(result, NewConvertCmd( exec.Command(w.conf.ImageMagickBin(), args...)), ) } diff --git a/internal/photoprism/convert_image_test.go b/internal/photoprism/convert_image_test.go index fa8a5eb2b..0e9b1d9e6 100644 --- a/internal/photoprism/convert_image_test.go +++ b/internal/photoprism/convert_image_test.go @@ -192,7 +192,7 @@ func TestConvert_ToImage(t *testing.T) { }) } -func TestConvert_PngConvertCommands(t *testing.T) { +func TestConvert_PngConvertCmds(t *testing.T) { cnf := config.TestConfig() convert := NewConvert(cnf) @@ -208,7 +208,7 @@ func TestConvert_PngConvertCommands(t *testing.T) { t.Fatal(err) } - cmds, useMutex, err := convert.PngConvertCommands(mediaFile, pngFile) + cmds, useMutex, err := convert.PngConvertCmds(mediaFile, pngFile) if err != nil { t.Fatal(err) diff --git a/internal/photoprism/convert_video_avc.go b/internal/photoprism/convert_video_avc.go index 3d27d406c..845a44cb7 100644 --- a/internal/photoprism/convert_video_avc.go +++ b/internal/photoprism/convert_video_avc.go @@ -12,13 +12,13 @@ import ( "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/ffmpeg" - + "github.com/photoprism/photoprism/internal/ffmpeg/encode" "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" ) // ToAvc converts a single video file to MPEG-4 AVC. -func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force bool) (file *MediaFile, err error) { +func (w *Convert) ToAvc(f *MediaFile, encoder encode.Encoder, noMutex, force bool) (file *MediaFile, err error) { // Abort if the source media file is nil. if f == nil { return nil, fmt.Errorf("convert: file is nil - you may have found a bug") @@ -66,7 +66,7 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force avcName, _ = fs.FileName(f.FileName(), w.conf.SidecarPath(), w.conf.OriginalsPath(), fs.ExtAVC) } - cmd, useMutex, err := w.AvcConvertCommand(f, avcName, encoder) + cmd, useMutex, err := w.AvcConvertCmd(f, avcName, encoder) // Return if an error occurred. if err != nil { @@ -138,8 +138,8 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force } // Try again using software encoder. - if encoder != ffmpeg.SoftwareEncoder { - return w.ToAvc(f, ffmpeg.SoftwareEncoder, true, false) + if encoder != encode.SoftwareAvc { + return w.ToAvc(f, encode.SoftwareAvc, true, false) } else { return nil, err } @@ -152,8 +152,8 @@ func (w *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force return NewMediaFile(avcName) } -// AvcConvertCommand returns the command for converting video files to MPEG-4 AVC. -func (w *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg.AvcEncoder) (result *exec.Cmd, useMutex bool, err error) { +// AvcConvertCmd returns the command for converting video files to MPEG-4 AVC. +func (w *Convert) AvcConvertCmd(f *MediaFile, avcName string, encoder encode.Encoder) (result *exec.Cmd, useMutex bool, err error) { fileExt := f.Extension() fileName := f.FileName() @@ -170,11 +170,11 @@ func (w *Convert) AvcConvertCommand(f *MediaFile, avcName string, encoder ffmpeg } // Use FFmpeg to transcode all other media files to AVC. - var opt ffmpeg.Options + var opt encode.Options if opt, err = w.conf.FFmpegOptions(encoder, w.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) + return ffmpeg.AvcConvertCmd(fileName, avcName, opt) } } diff --git a/internal/photoprism/convert_video_avc_test.go b/internal/photoprism/convert_video_avc_test.go index 2e080a001..da1428164 100644 --- a/internal/photoprism/convert_video_avc_test.go +++ b/internal/photoprism/convert_video_avc_test.go @@ -8,7 +8,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/photoprism/photoprism/internal/config" - "github.com/photoprism/photoprism/internal/ffmpeg" + "github.com/photoprism/photoprism/internal/ffmpeg/encode" "github.com/photoprism/photoprism/pkg/fs" ) @@ -30,7 +30,7 @@ func TestConvert_ToAvc(t *testing.T) { t.Fatal(err) } - avcFile, err := convert.ToAvc(mf, ffmpeg.SoftwareEncoder, false, false) + avcFile, err := convert.ToAvc(mf, encode.SoftwareAvc, false, false) if err != nil { t.Fatal(err) @@ -137,7 +137,7 @@ func TestConvert_AvcBitrate(t *testing.T) { }) } -func TestConvert_AvcConvertCommand(t *testing.T) { +func TestConvert_AvcConvertCmd(t *testing.T) { conf := config.TestConfig() convert := NewConvert(conf) @@ -149,7 +149,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) { t.Fatal(err) } - r, _, err := convert.AvcConvertCommand(mf, "avc1", ffmpeg.SoftwareEncoder) + r, _, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc) if err != nil { t.Fatal(err) @@ -166,7 +166,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) { t.Fatal(err) } - r, useMutex, err := convert.AvcConvertCommand(mf, "avc1", ffmpeg.SoftwareEncoder) + r, useMutex, err := convert.AvcConvertCmd(mf, "avc1", encode.SoftwareAvc) assert.False(t, useMutex) assert.Error(t, err) @@ -181,7 +181,7 @@ func TestConvert_AvcConvertCommand(t *testing.T) { t.Fatal(err) } - r, useMutex, err := convert.AvcConvertCommand(mf, avcName, ffmpeg.SoftwareEncoder) + r, useMutex, err := convert.AvcConvertCmd(mf, avcName, encode.SoftwareAvc) if err != nil { t.Fatal(err)