Index: Improve detection of embedded videos #439 #1739 #2788

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2023-09-23 13:37:42 +02:00
parent 9e1d9702ae
commit 50abd9b632
4 changed files with 144 additions and 28 deletions

View file

@ -9,7 +9,7 @@ import (
)
func TestJSON_Motion(t *testing.T) {
t.Run("GooglePixel2_JPG", func(t *testing.T) {
t.Run("google_pixel2.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/google_pixel2.jpg.json", "")
if err != nil {
@ -40,7 +40,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "Pixel 2", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("GooglePixel4a_JPG", func(t *testing.T) {
t.Run("google_pixel4a.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/google_pixel4a.jpg.json", "")
if err != nil {
@ -71,7 +71,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "Pixel 4a", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("GooglePixel6_JPG", func(t *testing.T) {
t.Run("google_pixel6.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/google_pixel6.jpg.json", "")
if err != nil {
@ -102,7 +102,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "Pixel 6", data.CameraModel)
assert.Equal(t, "Pixel 6 back camera 6.81mm f/1.85", data.LensModel)
})
t.Run("GooglePixel7Pro_JPG", func(t *testing.T) {
t.Run("google_pixel7pro.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/google_pixel7pro.jpg.json", "")
if err != nil {
@ -133,7 +133,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "Pixel 7 Pro", data.CameraModel)
assert.Equal(t, "Pixel 7 Pro back camera 19.0mm f/3.5", data.LensModel)
})
t.Run("SamsungGalaxyS20_JPG", func(t *testing.T) {
t.Run("samsung_galaxys20.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxys20.jpg.json", "")
if err != nil {
@ -164,7 +164,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "SM-G780F", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("SamsungGalaxyS20_MP4", func(t *testing.T) {
t.Run("samsung_galaxys20.mp4.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxys20.mp4.json", "")
if err != nil {
@ -195,7 +195,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("SamsungGalaxyS20FE_HEIF", func(t *testing.T) {
t.Run("samsung_galaxys20fe.heif.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxys20fe.heif.json", "")
if err != nil {
@ -226,7 +226,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "SM-G781B", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("SamsungGalaxyS21Ultra_JPG", func(t *testing.T) {
t.Run("samsung_galaxys21ultra.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxys21ultra.jpg.json", "")
if err != nil {
@ -257,7 +257,7 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "SM-G998B", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("SamsungGalaxyS21Ultra_MP4", func(t *testing.T) {
t.Run("samsung_galaxys21ultra.mp4.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxys21ultra.mp4.json", "")
if err != nil {
@ -288,4 +288,33 @@ func TestJSON_Motion(t *testing.T) {
assert.Equal(t, "", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
t.Run("samsung_galaxya71.jpg.json", func(t *testing.T) {
data, err := JSON("testdata/motion/samsung_galaxya71.jpg.json", "")
if err != nil {
t.Fatal(err)
}
// t.Logf("DATA: %#v", data)
assert.Equal(t, "motion-photo.jpg", data.FileName)
assert.Equal(t, media.Live, data.MediaType)
assert.Equal(t, true, data.EmbeddedThumb)
assert.Equal(t, true, data.EmbeddedVideo)
assert.Equal(t, CodecJpeg, data.Codec)
assert.Equal(t, int64(0), data.Duration.Milliseconds())
assert.Equal(t, "0s", data.Duration.String())
assert.Equal(t, 308000000, data.TakenNs)
assert.Equal(t, "", data.TimeZone)
assert.Equal(t, 4624, data.Width)
assert.Equal(t, 3468, data.Height)
assert.Equal(t, 3468, data.ActualWidth())
assert.Equal(t, 4624, data.ActualHeight())
assert.Equal(t, 6, data.Orientation)
assert.Equal(t, float32(0), data.Lat)
assert.Equal(t, float32(0), data.Lng)
assert.Equal(t, "Samsung", data.CameraMake)
assert.Equal(t, "Galaxy A71", data.CameraModel)
assert.Equal(t, "", data.LensModel)
})
}

View file

@ -0,0 +1,84 @@
[{
"SourceFile": "motion-photo.jpg",
"ExifToolVersion": 12.40,
"FileName": "motion-photo.jpg",
"Directory": ".",
"FileSize": 6094850,
"FileModifyDate": "2023:09:23 12:48:37+02:00",
"FileAccessDate": "2023:09:23 12:49:31+02:00",
"FileInodeChangeDate": "2023:09:23 12:49:07+02:00",
"FilePermissions": 100664,
"FileType": "JPEG",
"FileTypeExtension": "JPG",
"MIMEType": "image/jpeg",
"ExifByteOrder": "II",
"Make": "Samsung",
"Model": "Galaxy A71",
"Orientation": 6,
"XResolution": 72,
"YResolution": 72,
"ResolutionUnit": 2,
"Software": "A715FXXU8DWB5",
"ModifyDate": "2023:04:24 13:03:58",
"YCbCrPositioning": 1,
"ExposureTime": 0.001328021248,
"FNumber": 1.8,
"ExposureProgram": 2,
"ISO": 32,
"ExifVersion": "0220",
"DateTimeOriginal": "2023:04:24 13:03:58",
"CreateDate": "2023:04:24 13:03:58",
"OffsetTime": "+02:00",
"OffsetTimeOriginal": "+02:00",
"ShutterSpeedValue": 0.999079909359437,
"ApertureValue": 1.79626474576787,
"BrightnessValue": 7.67,
"ExposureCompensation": 0,
"MaxApertureValue": 1.79626474576787,
"MeteringMode": 2,
"Flash": 0,
"FocalLength": 5.23,
"SubSecTime": 308,
"SubSecTimeOriginal": 308,
"SubSecTimeDigitized": 308,
"ColorSpace": 1,
"ExifImageWidth": 4624,
"ExifImageHeight": 3468,
"ExposureMode": 0,
"WhiteBalance": 0,
"DigitalZoomRatio": 1,
"FocalLengthIn35mmFormat": 24,
"SceneCaptureType": 0,
"ImageUniqueID": "A64QLMD00YM",
"Compression": 6,
"ThumbnailOffset": 998,
"ThumbnailLength": 59060,
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
"MotionPhoto": 1,
"MotionPhotoVersion": 1,
"MotionPhotoPresentationTimestampUs": 2998371,
"DirectoryItemMime": "image/jpeg",
"DirectoryItemSemantic": "Primary",
"DirectoryItemLength": 0,
"DirectoryItemPadding": 59,
"ImageWidth": 4624,
"ImageHeight": 3468,
"EncodingProcess": 0,
"BitsPerSample": 8,
"ColorComponents": 3,
"YCbCrSubSampling": "2 2",
"Aperture": 1.8,
"ImageSize": "4624 3468",
"Megapixels": 16.036032,
"ScaleFactor35efl": 4.58891013384321,
"ShutterSpeed": 0.001328021248,
"SubSecCreateDate": "2023:04:24 13:03:58.308",
"SubSecDateTimeOriginal": "2023:04:24 13:03:58.308+02:00",
"SubSecModifyDate": "2023:04:24 13:03:58.308+02:00",
"ThumbnailImage": "(Binary data 59060 bytes, use -b option to extract)",
"CircleOfConfusion": "0.00654758096204051",
"FOV": 73.7398575770812,
"FocalLength35efl": 24,
"HyperfocalDistance": 2.32086562100636,
"LightValue": 12.8963560579259
}]

View file

@ -359,6 +359,8 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
// Reset file perceptive diff and chroma percent.
file.FileDiff = -1
file.FileChroma = -1
file.FileVideo = m.IsVideo()
file.MediaType = m.Media().String()
// Handle file types.
switch {
@ -411,6 +413,16 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
file.SetDuration(info.Duration)
file.SetFPS(info.FPS)
file.SetFrames(info.Frames)
// Change file and photo type to "live" if the file has a video embedded.
file.FileVideo = true
file.MediaType = entity.MediaLive
if photo.TypeSrc == entity.SrcAuto {
photo.PhotoType = entity.MediaLive
}
} else if photo.TypeSrc == entity.SrcAuto && photo.PhotoType == entity.MediaLive {
// Image does not include a compatible video.
photo.PhotoType = entity.MediaImage
}
if file.OriginalName == "" && filepath.Base(file.FileName) != data.FileName {
@ -425,11 +437,6 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
file.InstanceID = data.InstanceID
}
// Change photo type to "live" if the file has a video embedded.
if photo.TypeSrc == entity.SrcAuto && data.MediaType == media.Live {
photo.PhotoType = entity.MediaLive
}
}
// Change the photo type to animated if it is an animated PNG.
@ -514,12 +521,20 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
file.SetDuration(data.Duration)
file.SetFPS(data.FPS)
file.SetFrames(data.Frames)
file.FileVideo = false
} else if info := m.VideoInfo(); info.Compatible {
file.SetDuration(info.Duration)
file.SetFPS(info.FPS)
file.SetFrames(info.Frames)
// Change file and photo type to "live" if the file has a video embedded.
file.FileVideo = true
file.MediaType = entity.MediaLive
if photo.TypeSrc == entity.SrcAuto {
photo.PhotoType = entity.MediaLive
}
} else if photo.TypeSrc == entity.SrcAuto && photo.PhotoType == entity.MediaLive {
// HEIC does not include a compatible video.
photo.PhotoType = entity.MediaImage
}
// Set photo resolution based on the largest media file.
@ -785,19 +800,11 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
// Update file properties.
file.FileSidecar = m.IsSidecar()
file.FileVideo = m.IsVideo() || m.MetaData().EmbeddedVideo
file.FileType = m.FileType().String()
file.FileMime = m.MimeType()
file.SetOrientation(m.Orientation(), entity.SrcMeta)
file.ModTime = modTime.UTC().Truncate(time.Second).Unix()
// Update file media type.
if mediaType := m.MetaData().MediaType; mediaType != media.Unknown {
file.MediaType = mediaType.String()
} else {
file.MediaType = m.Media().String()
}
// Detect ICC color profile for JPEGs if still unknown at this point.
if file.FileColorProfile == "" && fs.ImageJPEG.Equal(file.FileType) {
file.SetColorProfile(m.ColorProfile())

View file

@ -887,10 +887,6 @@ func (m *MediaFile) IsImageNative() bool {
// IsLive checks if the file is a live photo.
func (m *MediaFile) IsLive() bool {
if m.MetaData().MediaType == media.Live {
return true
}
if m.IsHEIC() {
return fs.VideoMOV.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
@ -899,7 +895,7 @@ func (m *MediaFile) IsLive() bool {
return fs.ImageHEIC.FindFirst(m.FileName(), []string{}, Config().OriginalsPath(), false) != ""
}
return false
return m.MetaData().MediaType == media.Live && m.VideoInfo().Compatible
}
// ExifSupported returns true if parsing exif metadata is supported for the media file type.