From 709683ef59ba4d57c4353571324a80ed8fb8f5af Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Fri, 23 Dec 2022 01:33:35 +0100 Subject: [PATCH] Videos: Extract still image after 3 seconds if duration > 3100ms #1241 This way, still images of live photos remain unchanged, while other videos might get better preview images, especially if the first few frames are only black or white. Signed-off-by: Michael Mayer --- internal/photoprism/convert_jpeg.go | 6 +++++- internal/photoprism/index_mediafile.go | 2 +- internal/photoprism/livephoto.go | 6 ++++++ internal/photoprism/mediafile.go | 9 +++++++++ internal/photoprism/mediafile_test.go | 27 +++++++++++++++++--------- 5 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 internal/photoprism/livephoto.go diff --git a/internal/photoprism/convert_jpeg.go b/internal/photoprism/convert_jpeg.go index 679ff92ae..4fd54d86b 100644 --- a/internal/photoprism/convert_jpeg.go +++ b/internal/photoprism/convert_jpeg.go @@ -173,7 +173,11 @@ func (c *Convert) JpegConvertCommands(f *MediaFile, jpegName string, xmpName str // Video thumbnails can be created with FFmpeg. if f.IsVideo() && c.conf.FFmpegEnabled() { - result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)) + if f.Duration() > LivePhotoDurationLimit { + result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:03.000", "-vframes", "1", jpegName)) + } else { + result = append(result, exec.Command(c.conf.FFmpegBin(), "-y", "-i", f.FileName(), "-ss", "00:00:00.001", "-vframes", "1", jpegName)) + } } // RAW files may be concerted with Darktable and Rawtherapee. diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 70ff38523..acc767e7c 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -566,7 +566,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot if photo.TypeSrc == entity.SrcAuto { // Update photo type only if not manually modified. - if file.FileDuration == 0 || file.FileDuration > time.Millisecond*3100 { + if file.FileDuration == 0 || file.FileDuration > LivePhotoDurationLimit { photo.PhotoType = entity.MediaVideo } else { photo.PhotoType = entity.MediaLive diff --git a/internal/photoprism/livephoto.go b/internal/photoprism/livephoto.go new file mode 100644 index 000000000..32e0e45f2 --- /dev/null +++ b/internal/photoprism/livephoto.go @@ -0,0 +1,6 @@ +package photoprism + +import "time" + +// LivePhotoDurationLimit is the maximum duration of a live photo. +var LivePhotoDurationLimit = time.Millisecond * 3100 diff --git a/internal/photoprism/mediafile.go b/internal/photoprism/mediafile.go index a3d22a6fb..77a03beeb 100644 --- a/internal/photoprism/mediafile.go +++ b/internal/photoprism/mediafile.go @@ -763,6 +763,15 @@ func (m *MediaFile) IsVideo() bool { return strings.HasPrefix(m.MimeType(), "video/") || m.Media() == media.Video } +// Duration returns the duration if the file is a video. +func (m *MediaFile) Duration() time.Duration { + if !m.IsVideo() { + return 0 + } + + return m.MetaData().Duration +} + // IsAnimatedGif returns true if it is an animated GIF. func (m *MediaFile) IsAnimatedGif() bool { return m.IsGif() && m.MetaData().Frames > 1 diff --git a/internal/photoprism/mediafile_test.go b/internal/photoprism/mediafile_test.go index 5e4fd1594..5f15b56fb 100644 --- a/internal/photoprism/mediafile_test.go +++ b/internal/photoprism/mediafile_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/assert" @@ -2428,55 +2429,63 @@ func TestMediaFile_RemoveSidecarFiles(t *testing.T) { } func TestMediaFile_ColorProfile(t *testing.T) { - c := config.TestConfig() - t.Run("iphone_7.json", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.json") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.json") if err != nil { t.Fatal(err) } assert.Equal(t, "", mediaFile.ColorProfile()) }) t.Run("iphone_7.xmp", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.xmp") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.xmp") if err != nil { t.Fatal(err) } assert.Equal(t, "", mediaFile.ColorProfile()) }) t.Run("iphone_7.heic", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/iphone_7.heic") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/iphone_7.heic") if err != nil { t.Fatal(err) } assert.Equal(t, "", mediaFile.ColorProfile()) }) t.Run("canon_eos_6d.dng", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/canon_eos_6d.dng") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/canon_eos_6d.dng") if err != nil { t.Fatal(err) } assert.Equal(t, "", mediaFile.ColorProfile()) }) t.Run("elephants.jpg", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/elephants.jpg") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/elephants.jpg") if err != nil { t.Fatal(err) } assert.Equal(t, "Adobe RGB (1998)", mediaFile.ColorProfile()) }) t.Run("/beach_wood.jpg", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/beach_wood.jpg") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/beach_wood.jpg") if err != nil { t.Fatal(err) } assert.Equal(t, "", mediaFile.ColorProfile()) }) t.Run("/peacock_blue.jpg", func(t *testing.T) { - mediaFile, err := NewMediaFile(c.ExamplesPath() + "/peacock_blue.jpg") + mediaFile, err := NewMediaFile(conf.ExamplesPath() + "/peacock_blue.jpg") if err != nil { t.Fatal(err) } assert.Equal(t, "sRGB IEC61966-2.1", mediaFile.ColorProfile()) }) } + +func TestMediaFile_Duration(t *testing.T) { + t.Run("earth.mov", func(t *testing.T) { + if f, err := NewMediaFile(filepath.Join(conf.ExamplesPath(), "blue-go-video.mp4")); err != nil { + t.Fatal(err) + } else { + assert.Equal(t, time.Duration(2000000000), f.Duration()) + } + }) +}