Index: Add native support for MP4 and Samsung/Google Motion Photos #439
Related Issues: - Samsung: Initial support for Motion Photos (#439) - Google: Initial support for Motion Photos (#1739) - Metadata: Flag Samsung/Google Motion Photos as Live Photos (#2788) Related Pull Requests: - Live Photos: Add Support for Samsung Motion Photos (#3588) - Samsung: Improved support for Motion Photos (#3660) - Google: Initial support for Motion Photos (#3709) - Google: Add support for Motion Photos (#3722) Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
31a2cff5b6
commit
529103462c
18
go.mod
18
go.mod
|
@ -75,7 +75,6 @@ require github.com/go-ldap/ldap/v3 v3.4.6
|
|||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/bytedance/sonic v1.10.0 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.0 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||
|
@ -84,10 +83,8 @@ require (
|
|||
github.com/dsoprea/go-utility/v2 v2.0.0-20221003172846-a3e1774ef349 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
|
||||
github.com/go-errors/errors v1.5.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.3 // indirect
|
||||
github.com/go-sql-driver/mysql v1.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
|
@ -100,23 +97,32 @@ require (
|
|||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
golang.org/x/oauth2 v0.6.0 // indirect
|
||||
golang.org/x/sys v0.12.0 // indirect
|
||||
golang.org/x/text v0.13.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/abema/go-mp4 v0.13.0
|
||||
github.com/bytedance/sonic v1.10.0 // indirect
|
||||
github.com/go-errors/errors v1.5.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.15.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0
|
||||
golang.org/x/arch v0.5.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/emersion/go-webdav v0.4.0
|
||||
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||
|
|
12
go.sum
12
go.sum
|
@ -24,6 +24,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
|
|||
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
|
||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||
github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc=
|
||||
github.com/abema/go-mp4 v0.13.0 h1:gjEZLt7g0ePpYA5sUDrI2r8X+WuI8o+USkgG5wMgmkI=
|
||||
github.com/abema/go-mp4 v0.13.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
|
||||
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
|
||||
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
|
||||
github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw=
|
||||
|
@ -59,6 +61,7 @@ github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38
|
|||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
|
@ -218,6 +221,7 @@ github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OI
|
|||
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
|
||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
|
@ -270,6 +274,7 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn
|
|||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
|
@ -313,6 +318,8 @@ github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt
|
|||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
|
||||
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=
|
||||
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/paulmach/go.geojson v1.5.0 h1:7mhpMK89SQdHFcEGomT7/LuJhwhEgfmpWYVlVmLEdQw=
|
||||
|
@ -370,6 +377,9 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
|||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0 h1:zu38kFbv0KuuiwZQeuYeS02U9AM14j0pVA9xkHOCJ2A=
|
||||
github.com/sunfish-shogi/bufseekio v0.1.0/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=
|
||||
github.com/teambition/rrule-go v1.7.2/go.mod h1:mBJ1Ht5uboJ6jexKdNUJg2NcwP8uUMNvStWXlJD3MvU=
|
||||
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
|
||||
github.com/tensorflow/tensorflow v1.15.2/go.mod h1:itOSERT4trABok4UOoG+X4BoKds9F3rIsySdn+Lvu90=
|
||||
|
@ -694,6 +704,8 @@ gopkg.in/photoprism/go-tz.v2 v2.1.1 h1:XdNAQRneJmJdXDFovXJbf5eewp3zsir+jJ1Bxdmbn
|
|||
gopkg.in/photoprism/go-tz.v2 v2.1.1/go.mod h1:E1aQvLJs3YA4wbrPMOdX4YEx1TgRO2PLSxnO+J1Kqiw=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI=
|
||||
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=
|
||||
gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -11,10 +11,6 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/session"
|
||||
)
|
||||
|
||||
const (
|
||||
ContentTypeAvc = `video/mp4; codecs="avc1"`
|
||||
)
|
||||
|
||||
// AddCountHeader adds the actual result count to the response.
|
||||
func AddCountHeader(c *gin.Context, count int) {
|
||||
c.Header("X-Count", strconv.Itoa(count))
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/entity"
|
||||
"github.com/photoprism/photoprism/internal/get"
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
|
@ -64,14 +65,35 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
return
|
||||
}
|
||||
|
||||
conf := get.Config()
|
||||
fileName := photoprism.FileName(f.FileRoot, f.FileName)
|
||||
|
||||
// If file is not a video, try to find and stream embedded video data.
|
||||
if f.MediaType != entity.MediaVideo {
|
||||
if info, videoErr := video.ProbeFile(fileName); info.VideoOffset < 0 || !info.Compatible || videoErr != nil {
|
||||
logError("video", videoErr)
|
||||
log.Warnf("video: no data found in %s", clean.Log(f.FileName))
|
||||
AddContentTypeHeader(c, video.ContentTypeAVC)
|
||||
c.File(get.Config().StaticFile("video/404.mp4"))
|
||||
} else if reader, readErr := video.NewReader(fileName, info.VideoOffset); readErr != nil {
|
||||
log.Errorf("video: failed to read data from %s (%s)", clean.Log(f.FileName), readErr)
|
||||
AddContentTypeHeader(c, video.ContentTypeAVC)
|
||||
c.File(get.Config().StaticFile("video/404.mp4"))
|
||||
} else {
|
||||
defer reader.Close()
|
||||
AddVideoCacheHeader(c, conf.CdnVideo())
|
||||
c.DataFromReader(http.StatusOK, info.VideoSize(), info.VideoContentType(), reader, nil)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fileBitrate := f.Bitrate()
|
||||
|
||||
// File format supported by the client/browser?
|
||||
supported := f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.UnknownCodec && f.FileType == string(format.File)
|
||||
supported := f.FileCodec != "" && f.FileCodec == string(format.Codec) || format.Codec == video.CodecUnknown && f.FileType == string(format.FileType)
|
||||
|
||||
// File bitrate too high (for streaming)?
|
||||
conf := get.Config()
|
||||
transcode := !supported || conf.FFmpegEnabled() && conf.FFmpegBitrateExceeded(fileBitrate)
|
||||
|
||||
if mf, err := photoprism.NewMediaFile(fileName); err != nil {
|
||||
|
@ -81,7 +103,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
// Log error and default to 404.mp4
|
||||
log.Errorf("video: file %s is missing", clean.Log(f.FileName))
|
||||
fileName = get.Config().StaticFile("video/404.mp4")
|
||||
AddContentTypeHeader(c, ContentTypeAvc)
|
||||
AddContentTypeHeader(c, video.ContentTypeAVC)
|
||||
} else if transcode {
|
||||
if f.FileCodec != "" {
|
||||
log.Debugf("video: %s is %s compressed and cannot be streamed directly, average bitrate %.1f MBit/s", clean.Log(f.FileName), clean.Log(strings.ToUpper(f.FileCodec)), fileBitrate)
|
||||
|
@ -99,7 +121,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
fileName = get.Config().StaticFile("video/404.mp4")
|
||||
}
|
||||
|
||||
AddContentTypeHeader(c, ContentTypeAvc)
|
||||
AddContentTypeHeader(c, video.ContentTypeAVC)
|
||||
} else {
|
||||
if f.FileCodec != "" && f.FileCodec != f.FileType {
|
||||
log.Debugf("video: %s is %s compressed and requires no transcoding, average bitrate %.1f MBit/s", clean.Log(f.FileName), clean.Log(strings.ToUpper(f.FileCodec)), fileBitrate)
|
||||
|
|
|
@ -9,11 +9,12 @@ import (
|
|||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
func TestGetVideo(t *testing.T) {
|
||||
t.Run("ContentTypeAvc", func(t *testing.T) {
|
||||
assert.Equal(t, ContentTypeAvc, fmt.Sprintf("%s; codecs=\"%s\"", "video/mp4", clean.Codec("avc1")))
|
||||
t.Run("ContentTypeAVC", func(t *testing.T) {
|
||||
assert.Equal(t, video.ContentTypeAVC, fmt.Sprintf("%s; codecs=\"%s\"", "video/mp4", clean.Codec("avc1")))
|
||||
})
|
||||
|
||||
t.Run("InvalidHash", func(t *testing.T) {
|
||||
|
|
|
@ -1,18 +1,36 @@
|
|||
package entity
|
||||
|
||||
var CameraMakes = map[string]string{
|
||||
"apple": "Apple",
|
||||
"google": "Google",
|
||||
"samsung": "Samsung",
|
||||
"OLYMPUS": "Olympus",
|
||||
"OLYMPUS CORPORATION": "Olympus",
|
||||
"OLYMPUS DIGITAL CAMERA": "Olympus",
|
||||
"OLYMPUS IMAGING CORP.": "Olympus",
|
||||
"OLYMPUS OPTICAL CO.,LTD": "Olympus",
|
||||
"samsung": "Samsung",
|
||||
}
|
||||
|
||||
var CameraModels = map[string]string{
|
||||
"iPhone SE (1st generation)": "iPhone SE",
|
||||
"iPhone SE (2nd generation)": "iPhone SE",
|
||||
"iPhone SE (3rd generation)": "iPhone SE",
|
||||
"SM-G780F": "Galaxy S20",
|
||||
"SM-G781B": "Galaxy S20 FE",
|
||||
"SM-G991A": "Galaxy S21",
|
||||
"SM-G991B": "Galaxy S21",
|
||||
"SM-G990A": "Galaxy S21 FE",
|
||||
"SM-G990B": "Galaxy S21 FE",
|
||||
"SM-G996A": "Galaxy S21+",
|
||||
"SM-G996B": "Galaxy S21+",
|
||||
"SM-G998A": "Galaxy S21 Ultra",
|
||||
"SM-G998B": "Galaxy S21 Ultra",
|
||||
"SM-S911A": "Galaxy S23",
|
||||
"SM-S911B": "Galaxy S23",
|
||||
"SM-S916A": "Galaxy S23+",
|
||||
"SM-S916B": "Galaxy S23+",
|
||||
"SM-S918A": "Galaxy S23 Ultra",
|
||||
"SM-S918B": "Galaxy S23 Ultra",
|
||||
"WAS-LX1": "P10 lite",
|
||||
"WAS-LX2": "P10 lite",
|
||||
"WAS-LX3": "P10 lite",
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
// Default values.
|
||||
// Defaults.
|
||||
const (
|
||||
Unknown = ""
|
||||
UnknownYear = -1
|
||||
|
@ -15,7 +15,7 @@ const (
|
|||
UnknownID = "zz"
|
||||
)
|
||||
|
||||
// Media content types.
|
||||
// Media types.
|
||||
const (
|
||||
MediaUnknown = ""
|
||||
MediaImage = string(media.Image)
|
||||
|
@ -27,7 +27,7 @@ const (
|
|||
MediaText = string(media.Text)
|
||||
)
|
||||
|
||||
// Storage root folders.
|
||||
// Base folders.
|
||||
const (
|
||||
RootUnknown = ""
|
||||
RootOriginals = "/"
|
||||
|
@ -37,14 +37,14 @@ const (
|
|||
RootPath = "/"
|
||||
)
|
||||
|
||||
// Event type.
|
||||
// Event types.
|
||||
const (
|
||||
Created = "created"
|
||||
Updated = "updated"
|
||||
Deleted = "deleted"
|
||||
)
|
||||
|
||||
// Photo stack states.
|
||||
// Stacking states.
|
||||
const (
|
||||
IsStacked int8 = 1
|
||||
IsStackable int8 = 0
|
||||
|
|
|
@ -31,10 +31,10 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
result = exec.Command(
|
||||
ffmpeg,
|
||||
"-i", fileName,
|
||||
"-movflags", "faststart",
|
||||
"-pix_fmt", FormatYUV420P.String(),
|
||||
"-vf", "scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -63,6 +63,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-b:v", opt.Bitrate,
|
||||
"-bitrate", opt.Bitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -82,6 +83,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart",
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -99,6 +101,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -127,6 +130,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-level:v", "auto",
|
||||
"-coder:v", "1",
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -148,6 +152,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
@ -166,6 +171,7 @@ func AvcConvertCommand(fileName, avcName string, opt Options) (result *exec.Cmd,
|
|||
"-r", "30",
|
||||
"-b:v", opt.Bitrate,
|
||||
"-f", "mp4",
|
||||
"-movflags", "+faststart", // puts headers at the beginning for faster streaming
|
||||
"-y",
|
||||
avcName,
|
||||
)
|
||||
|
|
|
@ -47,7 +47,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -i VID123.gif -movflags faststart -pix_fmt yuv420p -vf scale=trunc(iw/2)*2:trunc(ih/2)*2 -f mp4 -y VID123.gif.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -i VID123.gif -pix_fmt yuv420p -vf scale=trunc(iw/2)*2:trunc(ih/2)*2 -f mp4 -movflags +faststart -y VID123.gif.avc", r.String())
|
||||
})
|
||||
t.Run("libx264", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -63,7 +63,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
t.Run("h264_qsv", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -79,7 +79,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -qsv_device /dev/dri/renderD128 -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=rgb32 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 50 -bitrate 50 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -qsv_device /dev/dri/renderD128 -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=rgb32 -c:v h264_qsv -map 0:v:0 -map 0:a:0? -r 30 -b:v 50 -bitrate 50 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
t.Run("h264_videotoolbox", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -95,7 +95,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
t.Run("h264_vaapi", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -111,7 +111,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
t.Run("h264_nvenc", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -127,7 +127,7 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -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 50 -profile:v 1 -level:v auto -coder:v 1 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
t.Run("h264_v4l2m2m", func(t *testing.T) {
|
||||
Options := Options{
|
||||
|
@ -143,6 +143,6 @@ func TestAvcConvertCommand(t *testing.T) {
|
|||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -i VID123.mov -c:v h264_v4l2m2m -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 -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 50 -f mp4 -y VID123.mov.avc", r.String())
|
||||
assert.Equal(t, "/usr/bin/ffmpeg -i VID123.mov -c:v h264_v4l2m2m -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 -num_output_buffers 72 -num_capture_buffers 64 -max_muxing_queue_size 1024 -crf 23 -r 30 -b:v 50 -f mp4 -movflags +faststart -y VID123.mov.avc", r.String())
|
||||
})
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ func TestImage(t *testing.T) {
|
|||
_ = os.Remove(saveName)
|
||||
})
|
||||
|
||||
t.Run("UnknownType", func(t *testing.T) {
|
||||
t.Run("TypeUnknown", func(t *testing.T) {
|
||||
img, err := imaging.Open("testdata/500x500.jpg")
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ import (
|
|||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/s2"
|
||||
)
|
||||
|
@ -15,6 +16,7 @@ const (
|
|||
// Data represents image metadata.
|
||||
type Data struct {
|
||||
FileName string `meta:"FileName"`
|
||||
MimeType string `meta:"MIMEType"`
|
||||
DocumentID string `meta:"BurstUUID,MediaGroupUUID,ImageUniqueID,OriginalDocumentID,DocumentID,DigitalImageGUID"`
|
||||
InstanceID string `meta:"InstanceID,DocumentID"`
|
||||
CreatedAt time.Time `meta:"SubSecCreateDate,CreationDate,CreateDate,MediaCreateDate,ContentCreateDate,TrackCreateDate"`
|
||||
|
@ -23,6 +25,9 @@ type Data struct {
|
|||
TakenGps time.Time `meta:"GPSDateTime,GPSDateStamp"`
|
||||
TakenNs int `meta:"-"`
|
||||
TimeZone string `meta:"-"`
|
||||
MediaType media.Type `meta:"-"`
|
||||
EmbeddedThumb bool `meta:"ThumbnailImage,PhotoshopThumbnail"`
|
||||
EmbeddedVideo bool `meta:"EmbeddedVideoFile,MotionPhoto,MotionPhotoVideo,MicroVideo"`
|
||||
Duration time.Duration `meta:"Duration,MediaDuration,TrackDuration,PreviewDuration"`
|
||||
FPS float64 `meta:"VideoFrameRate,VideoAvgFrameRate"`
|
||||
Frames int `meta:"FrameCount,AnimationFrames"`
|
||||
|
@ -65,14 +70,14 @@ type Data struct {
|
|||
Rotation int `meta:"Rotation"`
|
||||
Views int `meta:"-"`
|
||||
Albums []string `meta:"-"`
|
||||
EmbeddedVideo string `meta:"-"`
|
||||
Warning string `meta:"Warning"`
|
||||
Error error `meta:"-"`
|
||||
json map[string]string
|
||||
exif map[string]string
|
||||
}
|
||||
|
||||
// New returns a new metadata struct.
|
||||
func New() Data {
|
||||
// NewData returns a new Data struct with default values.
|
||||
func NewData() Data {
|
||||
return Data{}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,8 @@ import (
|
|||
"gopkg.in/photoprism/go-tz.v2/tz"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/projection"
|
||||
"github.com/photoprism/photoprism/pkg/rnd"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
|
@ -154,7 +156,20 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
continue
|
||||
}
|
||||
|
||||
fieldValue.SetBool(jsonValue.Bool())
|
||||
boolVal := false
|
||||
strVal := jsonValue.String()
|
||||
|
||||
// Cast string to bool.
|
||||
switch strVal {
|
||||
case "1", "true":
|
||||
boolVal = true
|
||||
case "", "0", "false":
|
||||
boolVal = false
|
||||
default:
|
||||
boolVal = txt.NotEmpty(strVal)
|
||||
}
|
||||
|
||||
fieldValue.SetBool(boolVal)
|
||||
default:
|
||||
log.Warnf("metadata: cannot assign value of type %s to %s (exiftool)", t, tagValue)
|
||||
}
|
||||
|
@ -365,11 +380,9 @@ func (data *Data) Exiftool(jsonData []byte, originalName string) (err error) {
|
|||
data.Subject = SanitizeMeta(data.Subject)
|
||||
data.Artist = SanitizeMeta(data.Artist)
|
||||
|
||||
// Set the name of the embedded video data field, if any.
|
||||
if embeddedVideo, ok := data.json["EmbeddedVideoFile"]; ok && embeddedVideo != "" {
|
||||
data.EmbeddedVideo = "EmbeddedVideoFile"
|
||||
} else if embeddedVideo, ok = data.json["MotionPhotoVideo"]; ok && embeddedVideo != "" {
|
||||
data.EmbeddedVideo = "MotionPhotoVideo"
|
||||
// Flag Samsung/Google Motion Photos as live media.
|
||||
if data.EmbeddedVideo && (data.MimeType == fs.MimeTypeJPEG || data.MimeType == fs.MimeTypeHEIC) {
|
||||
data.MediaType = media.Live
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
291
internal/meta/json_motion_test.go
Normal file
291
internal/meta/json_motion_test.go
Normal file
|
@ -0,0 +1,291 @@
|
|||
package meta
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
func TestJSON_Motion(t *testing.T) {
|
||||
t.Run("GooglePixel2_JPG", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/google_pixel2.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "pixel2.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, "2018-03-18 19:21:15 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2018-03-18 23:21:15 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 796940000, data.TakenNs)
|
||||
assert.Equal(t, "America/New_York", data.TimeZone)
|
||||
assert.Equal(t, 3024, data.Width)
|
||||
assert.Equal(t, 4032, data.Height)
|
||||
assert.Equal(t, 3024, data.ActualWidth())
|
||||
assert.Equal(t, 4032, data.ActualHeight())
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, float32(35.42307), data.Lat)
|
||||
assert.Equal(t, float32(-78.65212), data.Lng)
|
||||
assert.Equal(t, "Google", data.CameraMake)
|
||||
assert.Equal(t, "Pixel 2", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("GooglePixel4a_JPG", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/google_pixel4a.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "pixel4a.jpg", data.FileName)
|
||||
assert.Equal(t, media.Live, data.MediaType)
|
||||
assert.Equal(t, false, 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, "2021-09-17 19:31:36 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2021-09-17 23:31:36 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 844000000, data.TakenNs)
|
||||
assert.Equal(t, "America/New_York", data.TimeZone)
|
||||
assert.Equal(t, 4032, data.Width)
|
||||
assert.Equal(t, 3024, data.Height)
|
||||
assert.Equal(t, 3024, data.ActualWidth())
|
||||
assert.Equal(t, 4032, data.ActualHeight())
|
||||
assert.Equal(t, 6, data.Orientation)
|
||||
assert.Equal(t, float32(35.778152), data.Lat)
|
||||
assert.Equal(t, float32(-78.63687), data.Lng)
|
||||
assert.Equal(t, "Google", data.CameraMake)
|
||||
assert.Equal(t, "Pixel 4a", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("GooglePixel6_JPG", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/google_pixel6.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "PXL_20211227_151322429.MP.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, "2021-12-27 16:13:22 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2021-12-27 15:13:22 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 429000000, data.TakenNs)
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
assert.Equal(t, 4032, data.Width)
|
||||
assert.Equal(t, 2268, data.Height)
|
||||
assert.Equal(t, 4032, data.ActualWidth())
|
||||
assert.Equal(t, 2268, data.ActualHeight())
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, float32(48.610027), data.Lat)
|
||||
assert.Equal(t, float32(8.861558), data.Lng)
|
||||
assert.Equal(t, "Google", data.CameraMake)
|
||||
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) {
|
||||
data, err := JSON("testdata/motion/google_pixel7pro.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "PXL_20230329_144843201.MP.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, "2023-03-29 15:48:43 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2023-03-29 14:48:43 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 201000000, data.TakenNs)
|
||||
assert.Equal(t, "Europe/London", data.TimeZone)
|
||||
assert.Equal(t, 4080, data.Width)
|
||||
assert.Equal(t, 3072, data.Height)
|
||||
assert.Equal(t, 4080, data.ActualWidth())
|
||||
assert.Equal(t, 3072, data.ActualHeight())
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, float32(52.70636), data.Lat)
|
||||
assert.Equal(t, float32(-2.7605944), data.Lng)
|
||||
assert.Equal(t, "Google", data.CameraMake)
|
||||
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) {
|
||||
data, err := JSON("testdata/motion/samsung_galaxys20.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "20230822_143803.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, "2023-08-22 14:38:03 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2023-08-22 14:38:03 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 583000000, data.TakenNs)
|
||||
assert.Equal(t, "", data.TimeZone)
|
||||
assert.Equal(t, 4032, data.Width)
|
||||
assert.Equal(t, 3024, data.Height)
|
||||
assert.Equal(t, 3024, data.ActualWidth())
|
||||
assert.Equal(t, 4032, 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, "SM-G780F", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("SamsungGalaxyS20_MP4", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/samsung_galaxys20.mp4.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "20230822_143803.mp4", data.FileName)
|
||||
assert.Equal(t, media.Unknown, data.MediaType)
|
||||
assert.Equal(t, false, data.EmbeddedThumb)
|
||||
assert.Equal(t, false, data.EmbeddedVideo)
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, int64(2990), data.Duration.Milliseconds())
|
||||
assert.Equal(t, "2.99s", data.Duration.String())
|
||||
assert.Equal(t, "2023-08-22 14:38:06 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2023-08-22 11:38:06 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
assert.Equal(t, "Europe/Kiev", data.TimeZone)
|
||||
assert.Equal(t, 1440, data.Width)
|
||||
assert.Equal(t, 1080, data.Height)
|
||||
assert.Equal(t, 1080, data.ActualWidth())
|
||||
assert.Equal(t, 1440, data.ActualHeight())
|
||||
assert.Equal(t, 6, data.Orientation)
|
||||
assert.Equal(t, float32(48.4565), data.Lat)
|
||||
assert.Equal(t, float32(35.072), data.Lng)
|
||||
assert.Equal(t, "", data.CameraMake)
|
||||
assert.Equal(t, "", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("SamsungGalaxyS20FE_HEIF", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/samsung_galaxys20fe.heif.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "20220423_085935.heif", data.FileName)
|
||||
assert.Equal(t, media.Live, data.MediaType)
|
||||
assert.Equal(t, false, data.EmbeddedThumb)
|
||||
assert.Equal(t, true, data.EmbeddedVideo)
|
||||
assert.Equal(t, CodecHeic, data.Codec)
|
||||
assert.Equal(t, int64(0), data.Duration.Milliseconds())
|
||||
assert.Equal(t, "0s", data.Duration.String())
|
||||
assert.Equal(t, "2022-04-23 08:59:35 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2022-04-23 06:59:35 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
assert.Equal(t, "Europe/Berlin", data.TimeZone)
|
||||
assert.Equal(t, 4032, data.Width)
|
||||
assert.Equal(t, 3024, data.Height)
|
||||
assert.Equal(t, 3024, data.ActualWidth())
|
||||
assert.Equal(t, 4032, data.ActualHeight())
|
||||
assert.Equal(t, 6, data.Orientation)
|
||||
assert.Equal(t, float32(51.433468), data.Lat)
|
||||
assert.Equal(t, float32(12.110732), data.Lng)
|
||||
assert.Equal(t, "samsung", data.CameraMake)
|
||||
assert.Equal(t, "SM-G781B", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("SamsungGalaxyS21Ultra_JPG", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/samsung_galaxys21ultra.jpg.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "20211011_113427.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, "2021-10-11 11:34:27 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2021-10-11 11:34:27 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
assert.Equal(t, "", data.TimeZone)
|
||||
assert.Equal(t, 4000, data.Width)
|
||||
assert.Equal(t, 2252, data.Height)
|
||||
assert.Equal(t, 4000, data.ActualWidth())
|
||||
assert.Equal(t, 2252, data.ActualHeight())
|
||||
assert.Equal(t, 1, 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, "SM-G998B", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
t.Run("SamsungGalaxyS21Ultra_MP4", func(t *testing.T) {
|
||||
data, err := JSON("testdata/motion/samsung_galaxys21ultra.mp4.json", "")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// t.Logf("DATA: %#v", data)
|
||||
|
||||
assert.Equal(t, "20211011_113427.mp4", data.FileName)
|
||||
assert.Equal(t, media.Unknown, data.MediaType)
|
||||
assert.Equal(t, false, data.EmbeddedThumb)
|
||||
assert.Equal(t, false, data.EmbeddedVideo)
|
||||
assert.Equal(t, CodecAvc1, data.Codec)
|
||||
assert.Equal(t, int64(2670), data.Duration.Milliseconds())
|
||||
assert.Equal(t, "2.67s", data.Duration.String())
|
||||
assert.Equal(t, "2021-10-11 09:34:29 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2021-10-11 09:34:29 +0000 UTC", data.TakenAt.String())
|
||||
assert.Equal(t, 0, data.TakenNs)
|
||||
assert.Equal(t, "UTC", data.TimeZone)
|
||||
assert.Equal(t, 1920, data.Width)
|
||||
assert.Equal(t, 1080, data.Height)
|
||||
assert.Equal(t, 1920, data.ActualWidth())
|
||||
assert.Equal(t, 1080, data.ActualHeight())
|
||||
assert.Equal(t, 1, data.Orientation)
|
||||
assert.Equal(t, float32(0), data.Lat)
|
||||
assert.Equal(t, float32(0), data.Lng)
|
||||
assert.Equal(t, "", data.CameraMake)
|
||||
assert.Equal(t, "", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
})
|
||||
}
|
|
@ -726,7 +726,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHEVC), data.Codec)
|
||||
assert.Equal(t, string(video.CodecHVC), data.Codec)
|
||||
assert.Equal(t, "6.83s", data.Duration.String())
|
||||
assert.Equal(t, "2020-12-22 02:45:43 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2020-12-22 01:45:43 +0000 UTC", data.TakenAt.String())
|
||||
|
@ -750,7 +750,7 @@ func TestJSON(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
assert.Equal(t, string(video.CodecHEVC), data.Codec)
|
||||
assert.Equal(t, string(video.CodecHVC), data.Codec)
|
||||
assert.Equal(t, "2.15s", data.Duration.String())
|
||||
assert.Equal(t, "2019-12-12 20:47:21 +0000 UTC", data.TakenAtLocal.String())
|
||||
assert.Equal(t, "2019-12-13 01:47:21 +0000 UTC", data.TakenAt.String())
|
||||
|
@ -823,6 +823,7 @@ func TestJSON(t *testing.T) {
|
|||
assert.Equal(t, "OLYMPUS IMAGING CORP.", data.CameraMake)
|
||||
assert.Equal(t, "TG-830", data.CameraModel)
|
||||
assert.Equal(t, "", data.LensModel)
|
||||
assert.Equal(t, "Bad PrintIM data", data.Warning)
|
||||
})
|
||||
|
||||
t.Run("datetime-zero.json", func(t *testing.T) {
|
||||
|
|
|
@ -8,7 +8,7 @@ import (
|
|||
|
||||
func TestData_AddKeywords(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
data := New()
|
||||
data := NewData()
|
||||
|
||||
assert.Equal(t, "", data.Keywords.String())
|
||||
|
||||
|
@ -22,7 +22,7 @@ func TestData_AddKeywords(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("ignore", func(t *testing.T) {
|
||||
data := New()
|
||||
data := NewData()
|
||||
|
||||
assert.Equal(t, "", data.Keywords.String())
|
||||
|
||||
|
@ -34,7 +34,7 @@ func TestData_AddKeywords(t *testing.T) {
|
|||
|
||||
func TestData_AutoAddKeywords(t *testing.T) {
|
||||
t.Run("success", func(t *testing.T) {
|
||||
data := New()
|
||||
data := NewData()
|
||||
|
||||
assert.Equal(t, "", data.Keywords.String())
|
||||
|
||||
|
@ -44,7 +44,7 @@ func TestData_AutoAddKeywords(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("ignore", func(t *testing.T) {
|
||||
data := New()
|
||||
data := NewData()
|
||||
|
||||
assert.Equal(t, "", data.Keywords.String())
|
||||
|
||||
|
@ -54,7 +54,7 @@ func TestData_AutoAddKeywords(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("ignore because too short", func(t *testing.T) {
|
||||
data := New()
|
||||
data := NewData()
|
||||
|
||||
assert.Equal(t, "", data.Keywords.String())
|
||||
|
||||
|
|
133
internal/meta/testdata/motion/google_pixel2.jpg.json
vendored
Normal file
133
internal/meta/testdata/motion/google_pixel2.jpg.json
vendored
Normal file
|
@ -0,0 +1,133 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Google/pixel2.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "pixel2.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Google",
|
||||
"FileSize": 6240813,
|
||||
"FileModifyDate": "2023:09:21 02:46:36+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:46:41+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:46:41+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"ResolutionUnit": 2,
|
||||
"ImageDescription": "Maker:S,Date:2017-9-20,Ver:6,Lens:Kan03,Act:Lar02,E-Y",
|
||||
"Make": "Google",
|
||||
"Model": "Pixel 2",
|
||||
"Software": "HDR+ 1.0.177471000z",
|
||||
"Orientation": 1,
|
||||
"ModifyDate": "2018:03:18 19:21:15",
|
||||
"YCbCrPositioning": 1,
|
||||
"ISO": 225,
|
||||
"ExposureProgram": 2,
|
||||
"FNumber": 1.8,
|
||||
"ExposureTime": 0.016671,
|
||||
"SensingMethod": 2,
|
||||
"SubSecTimeDigitized": 796940,
|
||||
"SubSecTimeOriginal": 796940,
|
||||
"SubSecTime": 796940,
|
||||
"SubjectDistanceRange": 1,
|
||||
"Sharpness": 0,
|
||||
"FocalLength": 4.442,
|
||||
"Saturation": 0,
|
||||
"Flash": 24,
|
||||
"Contrast": 0,
|
||||
"MeteringMode": 2,
|
||||
"SceneCaptureType": 0,
|
||||
"SubjectDistance": 0.265,
|
||||
"FocalLengthIn35mmFormat": 27,
|
||||
"InteropIndex": "R98",
|
||||
"InteropVersion": "0100",
|
||||
"MaxApertureValue": 1.80250092522166,
|
||||
"CreateDate": "2018:03:18 19:21:15",
|
||||
"ExposureCompensation": 0,
|
||||
"ExifImageHeight": 4032,
|
||||
"WhiteBalance": 0,
|
||||
"DateTimeOriginal": "2018:03:18 19:21:15",
|
||||
"BrightnessValue": 1.43,
|
||||
"ExposureMode": 0,
|
||||
"ExifImageWidth": 3024,
|
||||
"ApertureValue": 1.80250092522166,
|
||||
"ComponentsConfiguration": "1 2 3 0",
|
||||
"SceneType": 1,
|
||||
"CustomRendered": 1,
|
||||
"ColorSpace": 1,
|
||||
"ShutterSpeedValue": 0.0166307841008337,
|
||||
"ExifVersion": "0220",
|
||||
"FlashpixVersion": "0100",
|
||||
"GPSVersionID": "2 2 0 0",
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitudeRef": "W",
|
||||
"GPSAltitudeRef": 1,
|
||||
"GPSTimeStamp": "23:21:13",
|
||||
"GPSDOP": 149.6,
|
||||
"GPSProcessingMethod": "fused",
|
||||
"GPSDateStamp": "2018:03:18",
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ThumbnailOffset": 22208,
|
||||
"ThumbnailLength": 10486,
|
||||
"Compression": 6,
|
||||
"XMPToolkit": "Image::ExifTool 11.11",
|
||||
"MicroVideo": 1,
|
||||
"MicroVideoOffset": 1848116,
|
||||
"MicroVideoPresentationTimestampUs": 266571,
|
||||
"MicroVideoVersion": 1,
|
||||
"OriginalFileName": "MVIMG_20180318_192115.jpg",
|
||||
"JFIFVersion": "1 2",
|
||||
"ProfileCMMType": "",
|
||||
"ProfileVersion": 1024,
|
||||
"ProfileClass": "mntr",
|
||||
"ColorSpaceData": "RGB ",
|
||||
"ProfileConnectionSpace": "XYZ ",
|
||||
"ProfileDateTime": "2016:12:08 09:38:28",
|
||||
"ProfileFileSignature": "acsp",
|
||||
"PrimaryPlatform": "",
|
||||
"CMMFlags": 0,
|
||||
"DeviceManufacturer": "GOOG",
|
||||
"DeviceModel": "",
|
||||
"DeviceAttributes": "0 0",
|
||||
"RenderingIntent": 0,
|
||||
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||
"ProfileCreator": "GOOG",
|
||||
"ProfileID": "117 225 166 177 60 52 55 99 16 200 171 102 6 50 162 138",
|
||||
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||
"ProfileCopyright": "Copyright (c) 2016 Google Inc.",
|
||||
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||
"MediaBlackPoint": "0 0 0",
|
||||
"RedMatrixColumn": "0.43604 0.22249 0.01392",
|
||||
"GreenMatrixColumn": "0.38512 0.7169 0.09706",
|
||||
"BlueMatrixColumn": "0.14305 0.06061 0.71391",
|
||||
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ChromaticAdaptation": "1.04788 0.02292 -0.05019 0.02959 0.99048 -0.01704 -0.00922 0.01508 0.75168",
|
||||
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ImageWidth": 3024,
|
||||
"ImageHeight": 4032,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"Aperture": 1.8,
|
||||
"ImageSize": "3024 4032",
|
||||
"Megapixels": 12.192768,
|
||||
"ScaleFactor35efl": 6.07834308869878,
|
||||
"ShutterSpeed": 0.016671,
|
||||
"SubSecCreateDate": "2018:03:18 19:21:15.796940",
|
||||
"SubSecDateTimeOriginal": "2018:03:18 19:21:15.796940",
|
||||
"SubSecModifyDate": "2018:03:18 19:21:15.796940",
|
||||
"ThumbnailImage": "(Binary data 10486 bytes, use -b option to extract)",
|
||||
"GPSAltitude": 0,
|
||||
"GPSDateTime": "2018:03:18 23:21:13Z",
|
||||
"GPSLatitude": 35.4230694444444,
|
||||
"GPSLongitude": -78.6521222222222,
|
||||
"CircleOfConfusion": "0.00494316628568242",
|
||||
"DOF": "0.237137202360868 0.300282104298363",
|
||||
"FOV": 67.380191965573,
|
||||
"FocalLength35efl": 27,
|
||||
"GPSPosition": "35.4230694444444 -78.6521222222222",
|
||||
"HyperfocalDistance": 2.21758044446922,
|
||||
"LightValue": 6.43258435532012
|
||||
}]
|
135
internal/meta/testdata/motion/google_pixel4a.jpg.json
vendored
Normal file
135
internal/meta/testdata/motion/google_pixel4a.jpg.json
vendored
Normal file
|
@ -0,0 +1,135 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Google/pixel4a.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "pixel4a.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Google",
|
||||
"FileSize": 6512170,
|
||||
"FileModifyDate": "2023:09:21 02:46:37+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:46:41+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:46:41+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "Google",
|
||||
"Model": "Pixel 4a",
|
||||
"Orientation": 6,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": 2,
|
||||
"Software": "HDR+ 1.0.377695977zd",
|
||||
"ModifyDate": "2021:09:17 19:31:36",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExposureTime": 0.066683,
|
||||
"FNumber": 1.73,
|
||||
"ExposureProgram": 2,
|
||||
"ISO": 1734,
|
||||
"ExifVersion": "0231",
|
||||
"DateTimeOriginal": "2021:09:17 19:31:36",
|
||||
"CreateDate": "2021:09:17 19:31:36",
|
||||
"OffsetTime": "-04:00",
|
||||
"OffsetTimeOriginal": "-04:00",
|
||||
"OffsetTimeDigitized": "-04:00",
|
||||
"ComponentsConfiguration": "1 2 3 0",
|
||||
"ShutterSpeedValue": 0.066523136403335,
|
||||
"ApertureValue": 1.72907446261573,
|
||||
"BrightnessValue": -3.63,
|
||||
"ExposureCompensation": 0,
|
||||
"MaxApertureValue": 1.72907446261573,
|
||||
"SubjectDistance": 0.267,
|
||||
"MeteringMode": 2,
|
||||
"Flash": 16,
|
||||
"FocalLength": 4.38,
|
||||
"SubSecTime": 844,
|
||||
"SubSecTimeOriginal": 844,
|
||||
"SubSecTimeDigitized": 844,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": 1,
|
||||
"ExifImageWidth": 4032,
|
||||
"ExifImageHeight": 3024,
|
||||
"InteropIndex": "R98",
|
||||
"InteropVersion": "0100",
|
||||
"SensingMethod": 2,
|
||||
"SceneType": 1,
|
||||
"CustomRendered": 1,
|
||||
"ExposureMode": 0,
|
||||
"WhiteBalance": 0,
|
||||
"DigitalZoomRatio": 0,
|
||||
"FocalLengthIn35mmFormat": 27,
|
||||
"SceneCaptureType": 0,
|
||||
"Contrast": 0,
|
||||
"Saturation": 0,
|
||||
"Sharpness": 0,
|
||||
"SubjectDistanceRange": 1,
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitudeRef": "W",
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSTimeStamp": "23:30:37",
|
||||
"GPSImgDirectionRef": "M",
|
||||
"GPSImgDirection": 22,
|
||||
"GPSDateStamp": "2021:09:17",
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 869089,
|
||||
"HasExtendedXMP": "9631085130373832F38C39094AB24AA7",
|
||||
"DirectoryItemMime": "image/jpeg",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 0,
|
||||
"JFIFVersion": "1 2",
|
||||
"ProfileCMMType": "",
|
||||
"ProfileVersion": 1024,
|
||||
"ProfileClass": "mntr",
|
||||
"ColorSpaceData": "RGB ",
|
||||
"ProfileConnectionSpace": "XYZ ",
|
||||
"ProfileDateTime": "2016:12:08 09:38:28",
|
||||
"ProfileFileSignature": "acsp",
|
||||
"PrimaryPlatform": "",
|
||||
"CMMFlags": 0,
|
||||
"DeviceManufacturer": "GOOG",
|
||||
"DeviceModel": "",
|
||||
"DeviceAttributes": "0 0",
|
||||
"RenderingIntent": 0,
|
||||
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||
"ProfileCreator": "GOOG",
|
||||
"ProfileID": "117 225 166 177 60 52 55 99 16 200 171 102 6 50 162 138",
|
||||
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||
"ProfileCopyright": "Copyright (c) 2016 Google Inc.",
|
||||
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||
"MediaBlackPoint": "0 0 0",
|
||||
"RedMatrixColumn": "0.43604 0.22249 0.01392",
|
||||
"GreenMatrixColumn": "0.38512 0.7169 0.09706",
|
||||
"BlueMatrixColumn": "0.14305 0.06061 0.71391",
|
||||
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ChromaticAdaptation": "1.04788 0.02292 -0.05019 0.02959 0.99048 -0.01704 -0.00922 0.01508 0.75168",
|
||||
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ImageWidth": 4032,
|
||||
"ImageHeight": 3024,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"HDRPMakerNote": "(Binary data 23899 bytes, use -b option to extract)",
|
||||
"Aperture": 1.73,
|
||||
"ImageSize": "4032 3024",
|
||||
"Megapixels": 12.192768,
|
||||
"ScaleFactor35efl": 6.16438356164384,
|
||||
"ShutterSpeed": 0.066683,
|
||||
"SubSecCreateDate": "2021:09:17 19:31:36.844-04:00",
|
||||
"SubSecDateTimeOriginal": "2021:09:17 19:31:36.844-04:00",
|
||||
"SubSecModifyDate": "2021:09:17 19:31:36.844-04:00",
|
||||
"GPSAltitude": 68.3,
|
||||
"GPSDateTime": "2021:09:17 23:30:37Z",
|
||||
"GPSLatitude": 35.7781527777778,
|
||||
"GPSLongitude": -78.636875,
|
||||
"CircleOfConfusion": 0.0048741711686828,
|
||||
"DOF": "0.239369127870041 0.301842276531055",
|
||||
"FOV": 67.3801919655729,
|
||||
"FocalLength35efl": 27,
|
||||
"GPSPosition": "35.7781527777778 -78.636875",
|
||||
"HyperfocalDistance": 2.27510445799754,
|
||||
"LightValue": 1.3720492608922
|
||||
}]
|
143
internal/meta/testdata/motion/google_pixel6.jpg.json
vendored
Normal file
143
internal/meta/testdata/motion/google_pixel6.jpg.json
vendored
Normal file
|
@ -0,0 +1,143 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Google/PXL_20211227_151322429.MP.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "PXL_20211227_151322429.MP.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Google",
|
||||
"FileSize": 5883888,
|
||||
"FileModifyDate": "2021:12:27 15:13:24+00:00",
|
||||
"FileAccessDate": "2023:09:21 10:04:59+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 10:04:58+00:00",
|
||||
"FilePermissions": 100775,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "Google",
|
||||
"Model": "Pixel 6",
|
||||
"Orientation": 1,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": 2,
|
||||
"Software": "HDR+ 1.0.414775603zd",
|
||||
"ModifyDate": "2021:12:27 16:13:22",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExposureTime": 0.00424,
|
||||
"FNumber": 1.85,
|
||||
"ExposureProgram": 2,
|
||||
"ISO": 654,
|
||||
"ExifVersion": "0231",
|
||||
"DateTimeOriginal": "2021:12:27 16:13:22",
|
||||
"CreateDate": "2021:12:27 16:13:22",
|
||||
"OffsetTime": "+01:00",
|
||||
"OffsetTimeOriginal": "+01:00",
|
||||
"OffsetTimeDigitized": "+01:00",
|
||||
"ComponentsConfiguration": "1 2 3 0",
|
||||
"ShutterSpeedValue": "0.00424505805674241",
|
||||
"ApertureValue": 1.85317612378074,
|
||||
"BrightnessValue": 1.95,
|
||||
"ExposureCompensation": 0,
|
||||
"MaxApertureValue": 1.85317612378074,
|
||||
"SubjectDistance": 3.245,
|
||||
"MeteringMode": 2,
|
||||
"Flash": 16,
|
||||
"FocalLength": 6.81,
|
||||
"SubSecTime": 429,
|
||||
"SubSecTimeOriginal": 429,
|
||||
"SubSecTimeDigitized": 429,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": 1,
|
||||
"ExifImageWidth": 4032,
|
||||
"ExifImageHeight": 2268,
|
||||
"InteropIndex": "R98",
|
||||
"InteropVersion": "0100",
|
||||
"SensingMethod": 2,
|
||||
"SceneType": 1,
|
||||
"CustomRendered": 1,
|
||||
"ExposureMode": 0,
|
||||
"WhiteBalance": 0,
|
||||
"DigitalZoomRatio": 0,
|
||||
"FocalLengthIn35mmFormat": 24,
|
||||
"SceneCaptureType": 0,
|
||||
"Contrast": 0,
|
||||
"Saturation": 0,
|
||||
"Sharpness": 0,
|
||||
"SubjectDistanceRange": 3,
|
||||
"LensMake": "Google",
|
||||
"LensModel": "Pixel 6 back camera 6.81mm f/1.85",
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitudeRef": "E",
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSTimeStamp": "15:12:56",
|
||||
"GPSImgDirectionRef": "M",
|
||||
"GPSImgDirection": 159,
|
||||
"GPSDateStamp": "2021:12:27",
|
||||
"Compression": 6,
|
||||
"ThumbnailOffset": 1331,
|
||||
"ThumbnailLength": 42677,
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 0,
|
||||
"HasExtendedXMP": "0A74827E42D8679520C0D4D10F40BF92",
|
||||
"DirectoryItemMime": "image/jpeg",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 0,
|
||||
"JFIFVersion": "1 2",
|
||||
"ProfileCMMType": "",
|
||||
"ProfileVersion": 1024,
|
||||
"ProfileClass": "mntr",
|
||||
"ColorSpaceData": "RGB ",
|
||||
"ProfileConnectionSpace": "XYZ ",
|
||||
"ProfileDateTime": "2016:12:08 09:38:28",
|
||||
"ProfileFileSignature": "acsp",
|
||||
"PrimaryPlatform": "",
|
||||
"CMMFlags": 0,
|
||||
"DeviceManufacturer": "GOOG",
|
||||
"DeviceModel": "",
|
||||
"DeviceAttributes": "0 0",
|
||||
"RenderingIntent": 0,
|
||||
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||
"ProfileCreator": "GOOG",
|
||||
"ProfileID": "117 225 166 177 60 52 55 99 16 200 171 102 6 50 162 138",
|
||||
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||
"ProfileCopyright": "Copyright (c) 2016 Google Inc.",
|
||||
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||
"MediaBlackPoint": "0 0 0",
|
||||
"RedMatrixColumn": "0.43604 0.22249 0.01392",
|
||||
"GreenMatrixColumn": "0.38512 0.7169 0.09706",
|
||||
"BlueMatrixColumn": "0.14305 0.06061 0.71391",
|
||||
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ChromaticAdaptation": "1.04788 0.02292 -0.05019 0.02959 0.99048 -0.01704 -0.00922 0.01508 0.75168",
|
||||
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ImageWidth": 4032,
|
||||
"ImageHeight": 2268,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"HDRPMakerNote": "(Binary data 54662 bytes, use -b option to extract)",
|
||||
"ShotLogData": "(Binary data 373 bytes, use -b option to extract)",
|
||||
"Aperture": 1.85,
|
||||
"ImageSize": "4032 2268",
|
||||
"Megapixels": 9.144576,
|
||||
"ScaleFactor35efl": 3.52422907488987,
|
||||
"ShutterSpeed": 0.00424,
|
||||
"SubSecCreateDate": "2021:12:27 16:13:22.429+01:00",
|
||||
"SubSecDateTimeOriginal": "2021:12:27 16:13:22.429+01:00",
|
||||
"SubSecModifyDate": "2021:12:27 16:13:22.429+01:00",
|
||||
"ThumbnailImage": "(Binary data 42677 bytes, use -b option to extract)",
|
||||
"GPSAltitude": 520.6,
|
||||
"GPSDateTime": "2021:12:27 15:12:56Z",
|
||||
"GPSLatitude": 48.6100277777778,
|
||||
"GPSLongitude": 8.86155833333333,
|
||||
"CircleOfConfusion": "0.00852562645344089",
|
||||
"DOF": "1.54428119240211 0",
|
||||
"FOV": 73.7398575770811,
|
||||
"FocalLength35efl": 24,
|
||||
"GPSPosition": "48.6100277777778 8.86155833333333",
|
||||
"HyperfocalDistance": 2.94033081311518,
|
||||
"LightValue": 6.94747992563343,
|
||||
"LensID": "Pixel 6 back camera 6.81mm f/1.85"
|
||||
}]
|
144
internal/meta/testdata/motion/google_pixel7pro.jpg.json
vendored
Normal file
144
internal/meta/testdata/motion/google_pixel7pro.jpg.json
vendored
Normal file
|
@ -0,0 +1,144 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Google/PXL_20230329_144843201.MP.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "PXL_20230329_144843201.MP.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Google",
|
||||
"FileSize": 7843873,
|
||||
"FileModifyDate": "2023:09:21 02:46:05+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:49:10+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:46:12+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "Google",
|
||||
"Model": "Pixel 7 Pro",
|
||||
"Orientation": 1,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": 2,
|
||||
"Software": "HDR+ 1.0.514217843zd",
|
||||
"ModifyDate": "2023:03:29 15:48:43",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExposureTime": 0.003638,
|
||||
"FNumber": 3.5,
|
||||
"ExposureProgram": 2,
|
||||
"ISO": 59,
|
||||
"ExifVersion": "0232",
|
||||
"DateTimeOriginal": "2023:03:29 15:48:43",
|
||||
"CreateDate": "2023:03:29 15:48:43",
|
||||
"OffsetTime": "+01:00",
|
||||
"OffsetTimeOriginal": "+01:00",
|
||||
"OffsetTimeDigitized": "+01:00",
|
||||
"ComponentsConfiguration": "1 2 3 0",
|
||||
"ShutterSpeedValue": "0.00364466012319065",
|
||||
"ApertureValue": 3.49429158366678,
|
||||
"BrightnessValue": 7.48,
|
||||
"ExposureCompensation": 0,
|
||||
"MaxApertureValue": 3.49429158366678,
|
||||
"SubjectDistance": 6.401,
|
||||
"MeteringMode": 2,
|
||||
"Flash": 16,
|
||||
"FocalLength": 19,
|
||||
"SubSecTime": 201,
|
||||
"SubSecTimeOriginal": 201,
|
||||
"SubSecTimeDigitized": 201,
|
||||
"FlashpixVersion": "0100",
|
||||
"ColorSpace": 1,
|
||||
"ExifImageWidth": 4080,
|
||||
"ExifImageHeight": 3072,
|
||||
"InteropIndex": "R98",
|
||||
"InteropVersion": "0100",
|
||||
"SensingMethod": 2,
|
||||
"SceneType": 1,
|
||||
"CustomRendered": 1,
|
||||
"ExposureMode": 0,
|
||||
"WhiteBalance": 0,
|
||||
"DigitalZoomRatio": 1.03,
|
||||
"FocalLengthIn35mmFormat": 117,
|
||||
"SceneCaptureType": 0,
|
||||
"Contrast": 0,
|
||||
"Saturation": 0,
|
||||
"Sharpness": 0,
|
||||
"SubjectDistanceRange": 3,
|
||||
"LensMake": "Google",
|
||||
"LensModel": "Pixel 7 Pro back camera 19.0mm f/3.5",
|
||||
"CompositeImage": 3,
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitudeRef": "W",
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSTimeStamp": "14:48:14",
|
||||
"GPSImgDirectionRef": "M",
|
||||
"GPSImgDirection": 210,
|
||||
"GPSDateStamp": "2023:03:29",
|
||||
"Compression": 6,
|
||||
"ThumbnailOffset": 1350,
|
||||
"ThumbnailLength": 40140,
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 1019083,
|
||||
"HasExtendedXMP": "BC95033CA299157D4A00E4464FEFE66F",
|
||||
"DirectoryItemMime": "image/jpeg",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 0,
|
||||
"JFIFVersion": "1 2",
|
||||
"ProfileCMMType": "",
|
||||
"ProfileVersion": 1024,
|
||||
"ProfileClass": "mntr",
|
||||
"ColorSpaceData": "RGB ",
|
||||
"ProfileConnectionSpace": "XYZ ",
|
||||
"ProfileDateTime": "2016:12:08 09:38:28",
|
||||
"ProfileFileSignature": "acsp",
|
||||
"PrimaryPlatform": "",
|
||||
"CMMFlags": 0,
|
||||
"DeviceManufacturer": "GOOG",
|
||||
"DeviceModel": "",
|
||||
"DeviceAttributes": "0 0",
|
||||
"RenderingIntent": 0,
|
||||
"ConnectionSpaceIlluminant": "0.9642 1 0.82491",
|
||||
"ProfileCreator": "GOOG",
|
||||
"ProfileID": "117 225 166 177 60 52 55 99 16 200 171 102 6 50 162 138",
|
||||
"ProfileDescription": "sRGB IEC61966-2.1",
|
||||
"ProfileCopyright": "Copyright (c) 2016 Google Inc.",
|
||||
"MediaWhitePoint": "0.95045 1 1.08905",
|
||||
"MediaBlackPoint": "0 0 0",
|
||||
"RedMatrixColumn": "0.43604 0.22249 0.01392",
|
||||
"GreenMatrixColumn": "0.38512 0.7169 0.09706",
|
||||
"BlueMatrixColumn": "0.14305 0.06061 0.71391",
|
||||
"RedTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ChromaticAdaptation": "1.04788 0.02292 -0.05019 0.02959 0.99048 -0.01704 -0.00922 0.01508 0.75168",
|
||||
"BlueTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"GreenTRC": "(Binary data 32 bytes, use -b option to extract)",
|
||||
"ImageWidth": 4080,
|
||||
"ImageHeight": 3072,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"HDRPMakerNote": "(Binary data 48819 bytes, use -b option to extract)",
|
||||
"ShotLogData": "(Binary data 547 bytes, use -b option to extract)",
|
||||
"Aperture": 3.5,
|
||||
"ImageSize": "4080 3072",
|
||||
"Megapixels": 12.53376,
|
||||
"ScaleFactor35efl": 6.15789473684211,
|
||||
"ShutterSpeed": 0.003638,
|
||||
"SubSecCreateDate": "2023:03:29 15:48:43.201+01:00",
|
||||
"SubSecDateTimeOriginal": "2023:03:29 15:48:43.201+01:00",
|
||||
"SubSecModifyDate": "2023:03:29 15:48:43.201+01:00",
|
||||
"ThumbnailImage": "(Binary data 40140 bytes, use -b option to extract)",
|
||||
"GPSAltitude": 107.19,
|
||||
"GPSDateTime": "2023:03:29 14:48:14Z",
|
||||
"GPSLatitude": 52.7063611111111,
|
||||
"GPSLongitude": -2.76059444444444,
|
||||
"CircleOfConfusion": "0.00487930728161081",
|
||||
"DOF": "4.91662692847249 9.16928941764372",
|
||||
"FOV": 17.4923393002573,
|
||||
"FocalLength35efl": 117,
|
||||
"GPSPosition": "52.7063611111111 -2.76059444444444",
|
||||
"HyperfocalDistance": 21.1388320492917,
|
||||
"LightValue": 12.4785617262008,
|
||||
"LensID": "Pixel 7 Pro back camera 19.0mm f/3.5"
|
||||
}]
|
94
internal/meta/testdata/motion/samsung_galaxys20.jpg.json
vendored
Normal file
94
internal/meta/testdata/motion/samsung_galaxys20.jpg.json
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Samsung/20230822_143803.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "20230822_143803.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Samsung",
|
||||
"FileSize": 7557289,
|
||||
"FileModifyDate": "2023:09:21 02:45:09+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:45:11+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:45:11+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "samsung",
|
||||
"Orientation": 6,
|
||||
"ModifyDate": "2023:08:22 14:38:03",
|
||||
"GPSAltitude": "undef",
|
||||
"GPSLatitudeRef": "",
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSLongitudeRef": "",
|
||||
"YResolution": 72,
|
||||
"XResolution": 72,
|
||||
"Model": "SM-G780F",
|
||||
"Software": "G780FXXSDFWGA",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExifVersion": "0220",
|
||||
"ApertureValue": 1.79626474576787,
|
||||
"ExposureCompensation": 0,
|
||||
"ExposureProgram": 2,
|
||||
"ColorSpace": 1,
|
||||
"ImageUniqueID": "X12LLND01AM",
|
||||
"MaxApertureValue": 1.79626474576787,
|
||||
"ExifImageHeight": 3024,
|
||||
"DateTimeOriginal": "2023:08:22 14:38:03",
|
||||
"SubSecTimeOriginal": 583,
|
||||
"WhiteBalance": 0,
|
||||
"ExposureMode": 0,
|
||||
"ExposureTime": 0.04,
|
||||
"OffsetTime": "+03:00",
|
||||
"Flash": 0,
|
||||
"SubSecTime": 583,
|
||||
"FNumber": 1.8,
|
||||
"ISO": 640,
|
||||
"ExifImageWidth": 4032,
|
||||
"FocalLengthIn35mmFormat": 26,
|
||||
"SubSecTimeDigitized": 583,
|
||||
"DigitalZoomRatio": 1,
|
||||
"CreateDate": "2023:08:22 14:38:03",
|
||||
"ShutterSpeedValue": 0.972654947412286,
|
||||
"MeteringMode": 2,
|
||||
"FocalLength": 5.4,
|
||||
"OffsetTimeOriginal": "+03:00",
|
||||
"SceneCaptureType": 0,
|
||||
"LightSource": 0,
|
||||
"ResolutionUnit": 2,
|
||||
"Compression": 6,
|
||||
"ThumbnailOffset": 1003,
|
||||
"ThumbnailLength": 63433,
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 2967325,
|
||||
"DirectoryItemMime": "image/jpeg",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 59,
|
||||
"ImageWidth": 4032,
|
||||
"ImageHeight": 3024,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"TimeStamp": "2023:08:22 11:38:03.703+00:00",
|
||||
"MCCData": 255,
|
||||
"EmbeddedVideoType": "MotionPhoto_Data",
|
||||
"EmbeddedVideoFile": "(Binary data 4708861 bytes, use -b option to extract)",
|
||||
"Aperture": 1.8,
|
||||
"ImageSize": "4032 3024",
|
||||
"Megapixels": 12.192768,
|
||||
"ScaleFactor35efl": 4.81481481481481,
|
||||
"ShutterSpeed": 0.04,
|
||||
"SubSecCreateDate": "2023:08:22 14:38:03.583",
|
||||
"SubSecDateTimeOriginal": "2023:08:22 14:38:03.583+03:00",
|
||||
"SubSecModifyDate": "2023:08:22 14:38:03.583+03:00",
|
||||
"ThumbnailImage": "(Binary data 63433 bytes, use -b option to extract)",
|
||||
"GPSLatitude": "",
|
||||
"GPSLongitude": "",
|
||||
"CircleOfConfusion": "0.00624037720753383",
|
||||
"FOV": 69.3903656740025,
|
||||
"FocalLength35efl": 26,
|
||||
"HyperfocalDistance": 2.59599691833407,
|
||||
"LightValue": 3.66177809777199
|
||||
}]
|
78
internal/meta/testdata/motion/samsung_galaxys20.mp4.json
vendored
Normal file
78
internal/meta/testdata/motion/samsung_galaxys20.mp4.json
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/sidecar/Samsung/20230822_143803.mp4",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "20230822_143803.mp4",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/sidecar/Samsung",
|
||||
"FileSize": 4708861,
|
||||
"FileModifyDate": "2023:09:21 02:52:16+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:52:16+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:52:16+00:00",
|
||||
"FilePermissions": 100644,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "mp42",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["isom","mp42"],
|
||||
"MediaDataSize": 4706235,
|
||||
"MediaDataOffset": 32,
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2023:08:22 11:38:06",
|
||||
"ModifyDate": "2023:08:22 11:38:06",
|
||||
"TimeScale": 10000,
|
||||
"Duration": 2.9864,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 3,
|
||||
"PlayMode": "SEQ_PLAY",
|
||||
"GPSCoordinates": "48.4565 35.072",
|
||||
"AndroidVersion": 13,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2023:08:22 11:38:06",
|
||||
"TrackModifyDate": "2023:08:22 11:38:06",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 2.9864,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"ImageWidth": 1440,
|
||||
"ImageHeight": 1080,
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 1440,
|
||||
"SourceImageHeight": 1080,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"BitDepth": 24,
|
||||
"ColorProfiles": "nclx",
|
||||
"ColorPrimaries": 5,
|
||||
"TransferCharacteristics": 1,
|
||||
"MatrixCoefficients": 6,
|
||||
"VideoFrameRate": 26.1185977810354,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2023:08:22 11:38:06",
|
||||
"MediaModifyDate": "2023:08:22 11:38:06",
|
||||
"MediaTimeScale": 48000,
|
||||
"MediaDuration": 2.79466666666667,
|
||||
"HandlerType": "soun",
|
||||
"HandlerDescription": "SoundHandle",
|
||||
"Balance": 0,
|
||||
"AudioFormat": "mp4a",
|
||||
"AudioChannels": 2,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 48000,
|
||||
"ImageSize": "1440 1080",
|
||||
"Megapixels": 1.5552,
|
||||
"AvgBitrate": 12607112,
|
||||
"GPSLatitude": 48.4565,
|
||||
"GPSLongitude": 35.072,
|
||||
"Rotation": 90,
|
||||
"GPSPosition": "48.4565 35.072"
|
||||
}]
|
104
internal/meta/testdata/motion/samsung_galaxys20fe.heif.json
vendored
Normal file
104
internal/meta/testdata/motion/samsung_galaxys20fe.heif.json
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
[{
|
||||
"SourceFile": "20220423_085935.heif",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "20220423_085935.heif",
|
||||
"Directory": ".",
|
||||
"FileSize": 3366300,
|
||||
"FileModifyDate": "2022:08:16 10:26:55+00:00",
|
||||
"FileAccessDate": "2023:09:22 11:50:16+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 10:00:45+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "HEIC",
|
||||
"FileTypeExtension": "HEIC",
|
||||
"MIMEType": "image/heic",
|
||||
"MajorBrand": "heic",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["mif1","heic"],
|
||||
"MediaDataSize": 975908,
|
||||
"MediaDataOffset": 32,
|
||||
"HandlerType": "pict",
|
||||
"PrimaryItemReference": 49,
|
||||
"MetaImageSize": "0 1287 4032 3024",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "samsung",
|
||||
"Model": "SM-G781B",
|
||||
"Orientation": 6,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": 2,
|
||||
"Software": "G781BXXU4FVC4",
|
||||
"ModifyDate": "2022:04:23 08:59:35",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExposureTime": 0.004347826087,
|
||||
"FNumber": 1.8,
|
||||
"ExposureProgram": 2,
|
||||
"ISO": 40,
|
||||
"ExifVersion": "0220",
|
||||
"DateTimeOriginal": "2022:04:23 08:59:35",
|
||||
"CreateDate": "2022:04:23 08:59:35",
|
||||
"OffsetTime": "+02:00",
|
||||
"OffsetTimeOriginal": "+02:00",
|
||||
"ShutterSpeedValue": 0.996990853191608,
|
||||
"ApertureValue": 1.79626474576787,
|
||||
"BrightnessValue": 5.86,
|
||||
"ExposureCompensation": 0,
|
||||
"MaxApertureValue": 1.79626474576787,
|
||||
"MeteringMode": 2,
|
||||
"Flash": 0,
|
||||
"FocalLength": 5.4,
|
||||
"ColorSpace": 1,
|
||||
"ExifImageWidth": 4032,
|
||||
"ExifImageHeight": 3024,
|
||||
"ExposureMode": 0,
|
||||
"WhiteBalance": 0,
|
||||
"DigitalZoomRatio": 1,
|
||||
"FocalLengthIn35mmFormat": 26,
|
||||
"SceneCaptureType": 0,
|
||||
"ImageUniqueID": "X12QSND02PM",
|
||||
"GPSLatitudeRef": "N",
|
||||
"GPSLongitudeRef": "E",
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 2968555,
|
||||
"DirectoryItemMime": "image/heic",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 67,
|
||||
"HEVCConfigurationVersion": 1,
|
||||
"GeneralProfileSpace": 0,
|
||||
"GeneralTierFlag": 0,
|
||||
"GeneralProfileIDC": 1,
|
||||
"GenProfileCompatibilityFlags": 1610612736,
|
||||
"ConstraintIndicatorFlags": "176 0 0 0 0 0",
|
||||
"GeneralLevelIDC": 90,
|
||||
"MinSpatialSegmentationIDC": 0,
|
||||
"ParallelismType": 0,
|
||||
"ChromaFormat": 1,
|
||||
"BitDepthLuma": 8,
|
||||
"BitDepthChroma": 8,
|
||||
"AverageFrameRate": 0,
|
||||
"ConstantFrameRate": 0,
|
||||
"NumTemporalLayers": 0,
|
||||
"TemporalIDNested": 0,
|
||||
"ImageWidth": 4032,
|
||||
"ImageHeight": 3024,
|
||||
"ImageSpatialExtent": "4032 3024",
|
||||
"Rotation": 270,
|
||||
"MotionPhotoVideo": "(Binary data 2387559 bytes, use -b option to extract)",
|
||||
"Aperture": 1.8,
|
||||
"ImageSize": "4032 3024",
|
||||
"Megapixels": 12.192768,
|
||||
"ScaleFactor35efl": 4.81481481481481,
|
||||
"ShutterSpeed": 0.004347826087,
|
||||
"SubSecDateTimeOriginal": "2022:04:23 08:59:35+02:00",
|
||||
"SubSecModifyDate": "2022:04:23 08:59:35+02:00",
|
||||
"GPSLatitude": 51.4334693997222,
|
||||
"GPSLongitude": 12.1107323997222,
|
||||
"CircleOfConfusion": "0.00624037720753383",
|
||||
"FOV": 69.3903656740025,
|
||||
"FocalLength35efl": 26,
|
||||
"GPSPosition": "51.4334693997222 12.1107323997222",
|
||||
"HyperfocalDistance": 2.59599691833407,
|
||||
"LightValue": 10.8634119589272
|
||||
}]
|
84
internal/meta/testdata/motion/samsung_galaxys21ultra.jpg.json
vendored
Normal file
84
internal/meta/testdata/motion/samsung_galaxys21ultra.jpg.json
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/originals/Samsung/20211011_113427.jpg",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "20211011_113427.jpg",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/originals/Samsung",
|
||||
"FileSize": 6938624,
|
||||
"FileModifyDate": "2023:09:21 02:42:02+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:46:54+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:44:39+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "JPEG",
|
||||
"FileTypeExtension": "JPG",
|
||||
"MIMEType": "image/jpeg",
|
||||
"ExifByteOrder": "II",
|
||||
"Make": "samsung",
|
||||
"Model": "SM-G998B",
|
||||
"Orientation": 1,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"ResolutionUnit": 2,
|
||||
"Software": "G998BXXU3AUIE",
|
||||
"ModifyDate": "2021:10:11 11:34:27",
|
||||
"YCbCrPositioning": 1,
|
||||
"ExposureTime": 0.0002821670429,
|
||||
"FNumber": 1.8,
|
||||
"ExposureProgram": 2,
|
||||
"ISO": 50,
|
||||
"ExifVersion": "0220",
|
||||
"DateTimeOriginal": "2021:10:11 11:34:27",
|
||||
"CreateDate": "2021:10:11 11:34:27",
|
||||
"OffsetTime": "+02:00",
|
||||
"OffsetTimeOriginal": "+02:00",
|
||||
"ShutterSpeedValue": 0.999804435834932,
|
||||
"ApertureValue": 1.79626474576787,
|
||||
"BrightnessValue": 28.15,
|
||||
"ExposureCompensation": 0,
|
||||
"MaxApertureValue": 1.79626474576787,
|
||||
"MeteringMode": 2,
|
||||
"Flash": 0,
|
||||
"FocalLength": 6.7,
|
||||
"ColorSpace": 1,
|
||||
"ExifImageWidth": 4000,
|
||||
"ExifImageHeight": 2252,
|
||||
"ExposureMode": 0,
|
||||
"WhiteBalance": 0,
|
||||
"DigitalZoomRatio": 1,
|
||||
"FocalLengthIn35mmFormat": 24,
|
||||
"SceneCaptureType": 0,
|
||||
"ImageUniqueID": "XA8XLNF00SM",
|
||||
"Compression": 6,
|
||||
"ThumbnailOffset": 814,
|
||||
"ThumbnailLength": 42252,
|
||||
"XMPToolkit": "Adobe XMP Core 5.1.0-jc003",
|
||||
"MotionPhoto": 1,
|
||||
"MotionPhotoVersion": 1,
|
||||
"MotionPhotoPresentationTimestampUs": 2656273,
|
||||
"DirectoryItemMime": "image/jpeg",
|
||||
"DirectoryItemSemantic": "Primary",
|
||||
"DirectoryItemLength": 0,
|
||||
"DirectoryItemPadding": 59,
|
||||
"ImageWidth": 4000,
|
||||
"ImageHeight": 2252,
|
||||
"EncodingProcess": 0,
|
||||
"BitsPerSample": 8,
|
||||
"ColorComponents": 3,
|
||||
"YCbCrSubSampling": "2 2",
|
||||
"TimeStamp": "2021:10:11 09:34:27.911+00:00",
|
||||
"MCCData": 238,
|
||||
"EmbeddedVideoType": "MotionPhoto_Data",
|
||||
"EmbeddedVideoFile": "(Binary data 4397713 bytes, use -b option to extract)",
|
||||
"Aperture": 1.8,
|
||||
"ImageSize": "4000 2252",
|
||||
"Megapixels": 9.008,
|
||||
"ScaleFactor35efl": 3.58208955223881,
|
||||
"ShutterSpeed": 0.0002821670429,
|
||||
"SubSecDateTimeOriginal": "2021:10:11 11:34:27+02:00",
|
||||
"SubSecModifyDate": "2021:10:11 11:34:27+02:00",
|
||||
"ThumbnailImage": "(Binary data 42252 bytes, use -b option to extract)",
|
||||
"CircleOfConfusion": "0.00838791442555859",
|
||||
"FOV": 73.7398575770811,
|
||||
"FocalLength35efl": 24,
|
||||
"HyperfocalDistance": 2.97319305176723,
|
||||
"LightValue": 14.4871567016107
|
||||
}]
|
75
internal/meta/testdata/motion/samsung_galaxys21ultra.mp4.json
vendored
Normal file
75
internal/meta/testdata/motion/samsung_galaxys21ultra.mp4.json
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
[{
|
||||
"SourceFile": "/go/src/github.com/photoprism/photoprism/storage/sidecar/Samsung/20211011_113427.mp4",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "20211011_113427.mp4",
|
||||
"Directory": "/go/src/github.com/photoprism/photoprism/storage/sidecar/Samsung",
|
||||
"FileSize": 4397713,
|
||||
"FileModifyDate": "2023:09:21 02:52:13+00:00",
|
||||
"FileAccessDate": "2023:09:21 02:52:13+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 02:52:13+00:00",
|
||||
"FilePermissions": 100644,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "mp42",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["isom","mp42"],
|
||||
"MediaDataSize": 4395073,
|
||||
"MediaDataOffset": 32,
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2021:10:11 09:34:29",
|
||||
"ModifyDate": "2021:10:11 09:34:29",
|
||||
"TimeScale": 10000,
|
||||
"Duration": 2.667,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 3,
|
||||
"PlayMode": "SEQ_PLAY",
|
||||
"AndroidVersion": 11,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2021:10:11 09:34:29",
|
||||
"TrackModifyDate": "2021:10:11 09:34:29",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 2.667,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"ImageWidth": 1920,
|
||||
"ImageHeight": 1080,
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 1920,
|
||||
"SourceImageHeight": 1080,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"BitDepth": 24,
|
||||
"PixelAspectRatio": "65536:65536",
|
||||
"ColorProfiles": "nclx",
|
||||
"ColorPrimaries": 5,
|
||||
"TransferCharacteristics": 1,
|
||||
"MatrixCoefficients": 6,
|
||||
"VideoFrameRate": 29.9958755671095,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2021:10:11 09:34:29",
|
||||
"MediaModifyDate": "2021:10:11 09:34:29",
|
||||
"MediaTimeScale": 48000,
|
||||
"MediaDuration": 2.65745833333333,
|
||||
"HandlerType": "soun",
|
||||
"HandlerDescription": "SoundHandle",
|
||||
"Balance": 0,
|
||||
"AudioFormat": "mp4a",
|
||||
"AudioChannels": 2,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 48000,
|
||||
"ImageSize": "1920 1080",
|
||||
"Megapixels": 2.0736,
|
||||
"AvgBitrate": 13183571,
|
||||
"Rotation": 0
|
||||
}]
|
|
@ -7,7 +7,7 @@ import (
|
|||
const CodecUnknown = ""
|
||||
const CodecAv1 = string(video.CodecAV1)
|
||||
const CodecAvc1 = string(video.CodecAVC)
|
||||
const CodecHvc1 = string(video.CodecHEVC)
|
||||
const CodecHvc1 = string(video.CodecHVC)
|
||||
const CodecJpeg = "jpeg"
|
||||
const CodecHeic = "heic"
|
||||
const CodecXMP = "xmp"
|
||||
|
|
|
@ -18,6 +18,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
// MediaFile indexes a single media file.
|
||||
|
@ -52,7 +53,7 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
file, primaryFile := entity.File{}, entity.File{}
|
||||
|
||||
photo := entity.NewUserPhoto(o.Stack, userUID)
|
||||
metaData := meta.New()
|
||||
metaData := meta.NewData()
|
||||
labels := classify.Labels{}
|
||||
stripSequence := Config().Settings().StackSequences() && o.Stack
|
||||
|
||||
|
@ -362,141 +363,166 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
// Handle file types.
|
||||
switch {
|
||||
case m.IsPreviewImage():
|
||||
// Color information
|
||||
if p, err := m.Colors(Config().ThumbCachePath()); err != nil {
|
||||
log.Debugf("%s while detecting colors", err.Error())
|
||||
file.FileError = err.Error()
|
||||
// Update color information, if available.
|
||||
if color, colorErr := m.Colors(Config().ThumbCachePath()); colorErr != nil {
|
||||
log.Debugf("%s while detecting colors", colorErr.Error())
|
||||
file.FileError = colorErr.Error()
|
||||
file.FilePrimary = false
|
||||
} else {
|
||||
file.FileMainColor = p.MainColor.Name()
|
||||
file.FileColors = p.Colors.Hex()
|
||||
file.FileLuminance = p.Luminance.Hex()
|
||||
file.FileDiff = p.Luminance.Diff()
|
||||
file.FileChroma = p.Chroma.Percent()
|
||||
file.FileMainColor = color.MainColor.Name()
|
||||
file.FileColors = color.Colors.Hex()
|
||||
file.FileLuminance = color.Luminance.Hex()
|
||||
file.FileDiff = color.Luminance.Diff()
|
||||
file.FileChroma = color.Chroma.Percent()
|
||||
|
||||
if file.FilePrimary {
|
||||
photo.PhotoColor = p.MainColor.ID()
|
||||
photo.PhotoColor = color.MainColor.ID()
|
||||
}
|
||||
}
|
||||
|
||||
// Update resolution and aspect ratio.
|
||||
if m.Width() > 0 && m.Height() > 0 {
|
||||
file.FileWidth = m.Width()
|
||||
file.FileHeight = m.Height()
|
||||
file.FileAspectRatio = m.AspectRatio()
|
||||
file.FilePortrait = m.Portrait()
|
||||
|
||||
megapixels := m.Megapixels()
|
||||
|
||||
if megapixels > photo.PhotoResolution {
|
||||
photo.PhotoResolution = megapixels
|
||||
// Set photo resolution based on the largest media file.
|
||||
if res := m.Megapixels(); res > photo.PhotoResolution {
|
||||
photo.PhotoResolution = res
|
||||
}
|
||||
}
|
||||
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
file.FileCodec = metaData.Codec
|
||||
file.SetMediaUTC(metaData.TakenAt)
|
||||
file.SetDuration(metaData.Duration)
|
||||
file.SetFPS(metaData.FPS)
|
||||
file.SetFrames(metaData.Frames)
|
||||
file.SetProjection(metaData.Projection)
|
||||
file.SetHDR(metaData.IsHDR())
|
||||
file.SetColorProfile(metaData.ColorProfile)
|
||||
file.SetSoftware(metaData.Software)
|
||||
// Update file metadata.
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
file.FileCodec = data.Codec
|
||||
file.SetMediaUTC(data.TakenAt)
|
||||
file.SetProjection(data.Projection)
|
||||
file.SetHDR(data.IsHDR())
|
||||
file.SetColorProfile(data.ColorProfile)
|
||||
file.SetSoftware(data.Software)
|
||||
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != metaData.FileName {
|
||||
file.OriginalName = metaData.FileName
|
||||
// Get video metadata from embedded file?
|
||||
if !data.EmbeddedVideo {
|
||||
file.SetDuration(data.Duration)
|
||||
file.SetFPS(data.FPS)
|
||||
file.SetFrames(data.Frames)
|
||||
} else if info := m.VideoInfo(); info.Compatible {
|
||||
file.SetDuration(info.Duration)
|
||||
file.SetFPS(info.FPS)
|
||||
file.SetFrames(info.Frames)
|
||||
}
|
||||
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != data.FileName {
|
||||
file.OriginalName = data.FileName
|
||||
if photo.OriginalName == "" {
|
||||
photo.OriginalName = fs.StripKnownExt(metaData.FileName)
|
||||
photo.OriginalName = fs.StripKnownExt(data.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
if metaData.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
|
||||
if data.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(data.InstanceID))
|
||||
|
||||
file.InstanceID = metaData.InstanceID
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Set the photo type to animated if it is an animated PNG.
|
||||
// Change the photo type to animated if it is an animated PNG.
|
||||
if photo.TypeSrc == entity.SrcAuto && photo.PhotoType == entity.MediaImage && m.IsAnimatedImage() {
|
||||
photo.PhotoType = entity.MediaAnimated
|
||||
}
|
||||
case m.IsXMP():
|
||||
if metaData, err := meta.XMP(m.FileName()); err == nil {
|
||||
if data, dataErr := meta.XMP(m.FileName()); dataErr == nil {
|
||||
// Update basic metadata.
|
||||
photo.SetTitle(metaData.Title, entity.SrcXmp)
|
||||
photo.SetDescription(metaData.Description, entity.SrcXmp)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcXmp)
|
||||
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcXmp)
|
||||
photo.SetTitle(data.Title, entity.SrcXmp)
|
||||
photo.SetDescription(data.Description, entity.SrcXmp)
|
||||
photo.SetTakenAt(data.TakenAt, data.TakenAtLocal, data.TimeZone, entity.SrcXmp)
|
||||
photo.SetCoordinates(data.Lat, data.Lng, data.Altitude, entity.SrcXmp)
|
||||
|
||||
// Update metadata details.
|
||||
details.SetKeywords(metaData.Keywords.String(), entity.SrcXmp)
|
||||
details.SetNotes(metaData.Notes, entity.SrcXmp)
|
||||
details.SetSubject(metaData.Subject, entity.SrcXmp)
|
||||
details.SetArtist(metaData.Artist, entity.SrcXmp)
|
||||
details.SetCopyright(metaData.Copyright, entity.SrcXmp)
|
||||
details.SetLicense(metaData.License, entity.SrcXmp)
|
||||
details.SetSoftware(metaData.Software, entity.SrcXmp)
|
||||
details.SetKeywords(data.Keywords.String(), entity.SrcXmp)
|
||||
details.SetNotes(data.Notes, entity.SrcXmp)
|
||||
details.SetSubject(data.Subject, entity.SrcXmp)
|
||||
details.SetArtist(data.Artist, entity.SrcXmp)
|
||||
details.SetCopyright(data.Copyright, entity.SrcXmp)
|
||||
details.SetLicense(data.License, entity.SrcXmp)
|
||||
details.SetSoftware(data.Software, entity.SrcXmp)
|
||||
|
||||
// Update externally marked as favorite.
|
||||
if metaData.Favorite {
|
||||
photo.SetFavorite(metaData.Favorite)
|
||||
if data.Favorite {
|
||||
_ = photo.SetFavorite(data.Favorite)
|
||||
}
|
||||
} else {
|
||||
log.Warn(err.Error())
|
||||
file.FileError = err.Error()
|
||||
log.Warn(dataErr.Error())
|
||||
file.FileError = dataErr.Error()
|
||||
}
|
||||
case m.IsRaw(), m.IsImage():
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
// Update basic metadata.
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
photo.SetDescription(metaData.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(metaData.CameraSerial)
|
||||
photo.SetTitle(data.Title, entity.SrcMeta)
|
||||
photo.SetDescription(data.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(data.TakenAt, data.TakenAtLocal, data.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(data.Lat, data.Lng, data.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(data.CameraSerial)
|
||||
|
||||
// Update metadata details.
|
||||
details.SetKeywords(metaData.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(metaData.Notes, entity.SrcMeta)
|
||||
details.SetSubject(metaData.Subject, entity.SrcMeta)
|
||||
details.SetArtist(metaData.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(metaData.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(metaData.License, entity.SrcMeta)
|
||||
details.SetSoftware(metaData.Software, entity.SrcMeta)
|
||||
details.SetKeywords(data.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(data.Notes, entity.SrcMeta)
|
||||
details.SetSubject(data.Subject, entity.SrcMeta)
|
||||
details.SetArtist(data.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(data.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(data.License, entity.SrcMeta)
|
||||
details.SetSoftware(data.Software, entity.SrcMeta)
|
||||
|
||||
if metaData.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
|
||||
if data.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(data.DocumentID))
|
||||
|
||||
photo.UUID = metaData.DocumentID
|
||||
photo.UUID = data.DocumentID
|
||||
}
|
||||
|
||||
if metaData.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
|
||||
if data.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(data.InstanceID))
|
||||
|
||||
file.InstanceID = metaData.InstanceID
|
||||
file.InstanceID = data.InstanceID
|
||||
}
|
||||
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != metaData.FileName {
|
||||
file.OriginalName = metaData.FileName
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != data.FileName {
|
||||
file.OriginalName = data.FileName
|
||||
if photo.OriginalName == "" {
|
||||
photo.OriginalName = fs.StripKnownExt(metaData.FileName)
|
||||
photo.OriginalName = fs.StripKnownExt(data.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
file.FileCodec = metaData.Codec
|
||||
file.FileCodec = data.Codec
|
||||
file.FileWidth = m.Width()
|
||||
file.FileHeight = m.Height()
|
||||
file.FileAspectRatio = m.AspectRatio()
|
||||
file.FilePortrait = m.Portrait()
|
||||
file.SetMediaUTC(metaData.TakenAt)
|
||||
file.SetDuration(metaData.Duration)
|
||||
file.SetFPS(metaData.FPS)
|
||||
file.SetFrames(metaData.Frames)
|
||||
file.SetProjection(metaData.Projection)
|
||||
file.SetHDR(metaData.IsHDR())
|
||||
file.SetColorProfile(metaData.ColorProfile)
|
||||
file.SetSoftware(metaData.Software)
|
||||
file.SetMediaUTC(data.TakenAt)
|
||||
file.SetProjection(data.Projection)
|
||||
file.SetHDR(data.IsHDR())
|
||||
file.SetColorProfile(data.ColorProfile)
|
||||
file.SetSoftware(data.Software)
|
||||
|
||||
// Get video metadata from embedded file?
|
||||
if !m.IsHEIC() || !data.EmbeddedVideo {
|
||||
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)
|
||||
file.FileVideo = true
|
||||
}
|
||||
|
||||
// Set photo resolution based on the largest media file.
|
||||
if res := m.Megapixels(); res > photo.PhotoResolution {
|
||||
photo.PhotoResolution = res
|
||||
}
|
||||
|
@ -519,54 +545,55 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
}
|
||||
}
|
||||
case m.IsVector():
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
// Update basic metadata.
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
photo.SetDescription(metaData.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
|
||||
photo.SetTitle(data.Title, entity.SrcMeta)
|
||||
photo.SetDescription(data.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(data.TakenAt, data.TakenAtLocal, data.TimeZone, entity.SrcMeta)
|
||||
|
||||
// Update metadata details.
|
||||
details.SetKeywords(metaData.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(metaData.Notes, entity.SrcMeta)
|
||||
details.SetSubject(metaData.Subject, entity.SrcMeta)
|
||||
details.SetArtist(metaData.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(metaData.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(metaData.License, entity.SrcMeta)
|
||||
details.SetSoftware(metaData.Software, entity.SrcMeta)
|
||||
details.SetKeywords(data.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(data.Notes, entity.SrcMeta)
|
||||
details.SetSubject(data.Subject, entity.SrcMeta)
|
||||
details.SetArtist(data.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(data.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(data.License, entity.SrcMeta)
|
||||
details.SetSoftware(data.Software, entity.SrcMeta)
|
||||
|
||||
if metaData.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
|
||||
if data.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(data.DocumentID))
|
||||
|
||||
photo.UUID = metaData.DocumentID
|
||||
photo.UUID = data.DocumentID
|
||||
}
|
||||
|
||||
if metaData.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
|
||||
if data.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(data.InstanceID))
|
||||
|
||||
file.InstanceID = metaData.InstanceID
|
||||
file.InstanceID = data.InstanceID
|
||||
}
|
||||
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != metaData.FileName {
|
||||
file.OriginalName = metaData.FileName
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != data.FileName {
|
||||
file.OriginalName = data.FileName
|
||||
if photo.OriginalName == "" {
|
||||
photo.OriginalName = fs.StripKnownExt(metaData.FileName)
|
||||
photo.OriginalName = fs.StripKnownExt(data.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
file.FileCodec = metaData.Codec
|
||||
file.FileCodec = data.Codec
|
||||
file.FileWidth = m.Width()
|
||||
file.FileHeight = m.Height()
|
||||
file.FileAspectRatio = m.AspectRatio()
|
||||
file.FilePortrait = m.Portrait()
|
||||
file.SetMediaUTC(metaData.TakenAt)
|
||||
file.SetDuration(metaData.Duration)
|
||||
file.SetFPS(metaData.FPS)
|
||||
file.SetFrames(metaData.Frames)
|
||||
file.SetProjection(metaData.Projection)
|
||||
file.SetHDR(metaData.IsHDR())
|
||||
file.SetColorProfile(metaData.ColorProfile)
|
||||
file.SetSoftware(metaData.Software)
|
||||
file.SetMediaUTC(data.TakenAt)
|
||||
file.SetDuration(data.Duration)
|
||||
file.SetFPS(data.FPS)
|
||||
file.SetFrames(data.Frames)
|
||||
file.SetProjection(data.Projection)
|
||||
file.SetHDR(data.IsHDR())
|
||||
file.SetColorProfile(data.ColorProfile)
|
||||
file.SetSoftware(data.Software)
|
||||
|
||||
// Set photo resolution based on the largest media file.
|
||||
if res := m.Megapixels(); res > photo.PhotoResolution {
|
||||
photo.PhotoResolution = res
|
||||
}
|
||||
|
@ -577,55 +604,56 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
photo.PhotoType = entity.MediaVector
|
||||
}
|
||||
case m.IsVideo():
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
photo.SetDescription(metaData.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(metaData.CameraSerial)
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
photo.SetTitle(data.Title, entity.SrcMeta)
|
||||
photo.SetDescription(data.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(data.TakenAt, data.TakenAtLocal, data.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(data.Lat, data.Lng, data.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(data.CameraSerial)
|
||||
|
||||
// Update metadata details.
|
||||
details.SetKeywords(metaData.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(metaData.Notes, entity.SrcMeta)
|
||||
details.SetSubject(metaData.Subject, entity.SrcMeta)
|
||||
details.SetArtist(metaData.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(metaData.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(metaData.License, entity.SrcMeta)
|
||||
details.SetSoftware(metaData.Software, entity.SrcMeta)
|
||||
details.SetKeywords(data.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(data.Notes, entity.SrcMeta)
|
||||
details.SetSubject(data.Subject, entity.SrcMeta)
|
||||
details.SetArtist(data.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(data.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(data.License, entity.SrcMeta)
|
||||
details.SetSoftware(data.Software, entity.SrcMeta)
|
||||
|
||||
if metaData.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
|
||||
if data.HasDocumentID() && photo.UUID == "" {
|
||||
log.Infof("index: %s has document_id %s", logName, clean.Log(data.DocumentID))
|
||||
|
||||
photo.UUID = metaData.DocumentID
|
||||
photo.UUID = data.DocumentID
|
||||
}
|
||||
|
||||
if metaData.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(metaData.InstanceID))
|
||||
if data.HasInstanceID() {
|
||||
log.Infof("index: %s has instance_id %s", logName, clean.Log(data.InstanceID))
|
||||
|
||||
file.InstanceID = metaData.InstanceID
|
||||
file.InstanceID = data.InstanceID
|
||||
}
|
||||
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != metaData.FileName {
|
||||
file.OriginalName = metaData.FileName
|
||||
if file.OriginalName == "" && filepath.Base(file.FileName) != data.FileName {
|
||||
file.OriginalName = data.FileName
|
||||
if photo.OriginalName == "" {
|
||||
photo.OriginalName = fs.StripKnownExt(metaData.FileName)
|
||||
photo.OriginalName = fs.StripKnownExt(data.FileName)
|
||||
}
|
||||
}
|
||||
|
||||
file.FileCodec = metaData.Codec
|
||||
file.FileCodec = data.Codec
|
||||
file.FileWidth = m.Width()
|
||||
file.FileHeight = m.Height()
|
||||
file.FileAspectRatio = m.AspectRatio()
|
||||
file.FilePortrait = m.Portrait()
|
||||
file.SetMediaUTC(metaData.TakenAt)
|
||||
file.SetDuration(metaData.Duration)
|
||||
file.SetFPS(metaData.FPS)
|
||||
file.SetFrames(metaData.Frames)
|
||||
file.SetProjection(metaData.Projection)
|
||||
file.SetHDR(metaData.IsHDR())
|
||||
file.SetColorProfile(metaData.ColorProfile)
|
||||
file.SetSoftware(metaData.Software)
|
||||
file.SetMediaUTC(data.TakenAt)
|
||||
file.SetDuration(data.Duration)
|
||||
file.SetFPS(data.FPS)
|
||||
file.SetFrames(data.Frames)
|
||||
file.SetProjection(data.Projection)
|
||||
file.SetHDR(data.IsHDR())
|
||||
file.SetColorProfile(data.ColorProfile)
|
||||
file.SetSoftware(data.Software)
|
||||
|
||||
// Set photo resolution based on the largest media file.
|
||||
if res := m.Megapixels(); res > photo.PhotoResolution {
|
||||
photo.PhotoResolution = res
|
||||
}
|
||||
|
@ -641,7 +669,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 > LivePhotoDurationLimit {
|
||||
if file.FileDuration == 0 || file.FileDuration > video.LiveDuration {
|
||||
photo.PhotoType = entity.MediaVideo
|
||||
} else {
|
||||
photo.PhotoType = entity.MediaLive
|
||||
|
@ -704,27 +732,27 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
}
|
||||
|
||||
// Read metadata from embedded Exif and JSON sidecar file, if exists.
|
||||
if metaData := m.MetaData(); metaData.Error == nil {
|
||||
if data := m.MetaData(); data.Error == nil {
|
||||
// Update basic metadata.
|
||||
photo.SetTitle(metaData.Title, entity.SrcMeta)
|
||||
photo.SetDescription(metaData.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(metaData.TakenAt, metaData.TakenAtLocal, metaData.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(metaData.Lat, metaData.Lng, metaData.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(metaData.CameraSerial)
|
||||
photo.SetTitle(data.Title, entity.SrcMeta)
|
||||
photo.SetDescription(data.Description, entity.SrcMeta)
|
||||
photo.SetTakenAt(data.TakenAt, data.TakenAtLocal, data.TimeZone, entity.SrcMeta)
|
||||
photo.SetCoordinates(data.Lat, data.Lng, data.Altitude, entity.SrcMeta)
|
||||
photo.SetCameraSerial(data.CameraSerial)
|
||||
|
||||
// Update metadata details.
|
||||
details.SetKeywords(metaData.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(metaData.Notes, entity.SrcMeta)
|
||||
details.SetSubject(metaData.Subject, entity.SrcMeta)
|
||||
details.SetArtist(metaData.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(metaData.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(metaData.License, entity.SrcMeta)
|
||||
details.SetSoftware(metaData.Software, entity.SrcMeta)
|
||||
details.SetKeywords(data.Keywords.String(), entity.SrcMeta)
|
||||
details.SetNotes(data.Notes, entity.SrcMeta)
|
||||
details.SetSubject(data.Subject, entity.SrcMeta)
|
||||
details.SetArtist(data.Artist, entity.SrcMeta)
|
||||
details.SetCopyright(data.Copyright, entity.SrcMeta)
|
||||
details.SetLicense(data.License, entity.SrcMeta)
|
||||
details.SetSoftware(data.Software, entity.SrcMeta)
|
||||
|
||||
if metaData.HasDocumentID() && photo.UUID == "" {
|
||||
log.Debugf("index: %s has document_id %s", logName, clean.Log(metaData.DocumentID))
|
||||
if data.HasDocumentID() && photo.UUID == "" {
|
||||
log.Debugf("index: %s has document_id %s", logName, clean.Log(data.DocumentID))
|
||||
|
||||
photo.UUID = metaData.DocumentID
|
||||
photo.UUID = data.DocumentID
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -755,15 +783,21 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot
|
|||
photo.PhotoPanorama = true
|
||||
}
|
||||
|
||||
// Set remaining file properties.
|
||||
// Update file properties.
|
||||
file.FileSidecar = m.IsSidecar()
|
||||
file.FileVideo = m.IsVideo()
|
||||
file.FileVideo = m.IsVideo() || m.MetaData().EmbeddedVideo
|
||||
file.FileType = m.FileType().String()
|
||||
file.MediaType = m.Media().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())
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
package photoprism
|
||||
|
||||
import "time"
|
||||
|
||||
// LivePhotoDurationLimit is the maximum duration of a live photo.
|
||||
var LivePhotoDurationLimit = time.Millisecond * 3100
|
|
@ -1,8 +1,6 @@
|
|||
package photoprism
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/gif"
|
||||
|
@ -11,7 +9,6 @@ import (
|
|||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
|
@ -34,6 +31,7 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
// MediaFile represents a single photo, video, sidecar, or other supported media file.
|
||||
|
@ -57,6 +55,8 @@ type MediaFile struct {
|
|||
height int
|
||||
metaData meta.Data
|
||||
metaOnce sync.Once
|
||||
videoInfo video.Info
|
||||
videoOnce sync.Once
|
||||
fileMutex sync.Mutex
|
||||
location *entity.Cell
|
||||
imageConfig *image.Config
|
||||
|
@ -80,8 +80,9 @@ func NewMediaFileSkipResolve(fileName string, fileNameResolved string) (*MediaFi
|
|||
fileName: fileName,
|
||||
fileNameResolved: fileNameResolved,
|
||||
fileRoot: entity.RootUnknown,
|
||||
fileType: fs.UnknownType,
|
||||
metaData: meta.New(),
|
||||
fileType: fs.TypeUnknown,
|
||||
metaData: meta.NewData(),
|
||||
videoInfo: video.NewInfo(),
|
||||
width: -1,
|
||||
height: -1,
|
||||
}
|
||||
|
@ -317,72 +318,6 @@ func (m *MediaFile) EditedName() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
// ExtractEmbeddedVideo extracts an embedded video file and returns its filename, if any.
|
||||
func (m *MediaFile) ExtractEmbeddedVideo() (string, error) {
|
||||
if m == nil {
|
||||
return "", fmt.Errorf("mediafile: file is nil - you may have found a bug")
|
||||
}
|
||||
|
||||
// Abort if the source media file does not exist.
|
||||
if !m.Exists() {
|
||||
return "", fmt.Errorf("mediafile: %s not found", clean.Log(m.RootRelName()))
|
||||
} else if m.Empty() {
|
||||
return "", fmt.Errorf("mediafile: %s is empty", clean.Log(m.RootRelName()))
|
||||
}
|
||||
|
||||
// Get the embedded video field name from the file metadata.
|
||||
if metaData := m.MetaData(); metaData.Error == nil && metaData.EmbeddedVideo != "" {
|
||||
outputPath := filepath.Join(Config().TempPath(), m.RootRelPath(), "%f")
|
||||
cmd := exec.Command(Config().ExifToolBin(),
|
||||
fmt.Sprintf("-%s", metaData.EmbeddedVideo), // TODO: Is this safe?
|
||||
"-b", "-w",
|
||||
outputPath, m.FileName())
|
||||
|
||||
var out bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &stderr
|
||||
cmd.Env = []string{fmt.Sprintf("HOME=%s", Config().TempPath())}
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Debugf("Error running exiftool on video file: ", err)
|
||||
|
||||
if stderr.String() != "" {
|
||||
return "", errors.New(stderr.String())
|
||||
} else {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
// Find the extracted video file.
|
||||
outputPath = strings.Replace(outputPath, "%f", m.BasePrefix(false), 1)
|
||||
|
||||
// Detect mime type of the extracted video file.
|
||||
mimeType := fs.MimeType(outputPath)
|
||||
|
||||
if l := len(strings.Split(mimeType, "/")); l <= 1 {
|
||||
log.Debugf("Error detecting the mime type of video file at %s", outputPath)
|
||||
|
||||
return "", nil
|
||||
} else if extension := strings.Split(mimeType, "/")[l-1]; extension != "" {
|
||||
// Rename the extracted video file with the correct extension and move it to the sidecar path.
|
||||
_, file := filepath.Split(outputPath)
|
||||
newFileName := fmt.Sprintf("%s.%s", file, extension)
|
||||
dstPath := filepath.Join(Config().SidecarPath(), m.RootRelPath(), newFileName)
|
||||
|
||||
if err := fs.Move(outputPath, dstPath); err != nil {
|
||||
log.Debugf("failed to move extracted video file to %s", outputPath)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dstPath, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// PathNameInfo returns file name infos for indexing.
|
||||
func (m *MediaFile) PathNameInfo(stripSequence bool) (fileRoot, fileBase, relativePath, relativeName string) {
|
||||
fileRoot = m.Root()
|
||||
|
@ -804,12 +739,8 @@ func (m *MediaFile) IsWebP() bool {
|
|||
return m.MimeType() == fs.MimeTypeWebP
|
||||
}
|
||||
|
||||
// Duration returns the duration if the file is a video.
|
||||
// Duration returns the duration is the media content is playable.
|
||||
func (m *MediaFile) Duration() time.Duration {
|
||||
if !m.IsVideo() {
|
||||
return 0
|
||||
}
|
||||
|
||||
return m.MetaData().Duration
|
||||
}
|
||||
|
||||
|
@ -956,6 +887,10 @@ 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) != ""
|
||||
}
|
||||
|
@ -989,13 +924,15 @@ func (m *MediaFile) PreviewImage() (*MediaFile, error) {
|
|||
return nil, fmt.Errorf("%s is empty", m.RootRelName())
|
||||
}
|
||||
|
||||
jpegName := fs.ImageJPEG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
jpegName := fs.ImageJPEG.FindFirst(m.FileName(),
|
||||
[]string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if jpegName != "" {
|
||||
return NewMediaFile(jpegName)
|
||||
}
|
||||
|
||||
pngName := fs.ImagePNG.FindFirst(m.FileName(), []string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
pngName := fs.ImagePNG.FindFirst(m.FileName(),
|
||||
[]string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if pngName != "" {
|
||||
return NewMediaFile(pngName)
|
||||
|
@ -1015,25 +952,15 @@ func (m *MediaFile) HasPreviewImage() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
jpegName := fs.ImageJPEG.FindFirst(
|
||||
m.FileName(),
|
||||
[]string{
|
||||
Config().SidecarPath(),
|
||||
fs.HiddenPath,
|
||||
},
|
||||
Config().OriginalsPath(), false,
|
||||
)
|
||||
jpegName := fs.ImageJPEG.FindFirst(m.FileName(),
|
||||
[]string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if m.hasPreviewImage = fs.MimeType(jpegName) == fs.MimeTypeJPEG; m.hasPreviewImage {
|
||||
return true
|
||||
}
|
||||
|
||||
pngName := fs.ImagePNG.FindFirst(
|
||||
m.FileName(),
|
||||
[]string{
|
||||
Config().SidecarPath(), fs.HiddenPath,
|
||||
}, Config().OriginalsPath(), false,
|
||||
)
|
||||
pngName := fs.ImagePNG.FindFirst(m.FileName(),
|
||||
[]string{Config().SidecarPath(), fs.HiddenPath}, Config().OriginalsPath(), false)
|
||||
|
||||
if m.hasPreviewImage = fs.MimeType(pngName) == fs.MimeTypePNG; m.hasPreviewImage {
|
||||
return true
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/meta"
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
// HasSidecarJson returns true if this file has or is a json sidecar file.
|
||||
|
@ -81,10 +82,11 @@ func (m *MediaFile) ReadExifToolJson() error {
|
|||
// MetaData returns exif meta data of a media file.
|
||||
func (m *MediaFile) MetaData() (result meta.Data) {
|
||||
if !m.Ok() || !m.IsMedia() {
|
||||
// No valid media file.
|
||||
// Not a main media file.
|
||||
return m.metaData
|
||||
}
|
||||
|
||||
// Gather the data once and cache it.
|
||||
m.metaOnce.Do(func() {
|
||||
var err error
|
||||
|
||||
|
@ -125,3 +127,22 @@ func (m *MediaFile) MetaData() (result meta.Data) {
|
|||
|
||||
return m.metaData
|
||||
}
|
||||
|
||||
// VideoInfo returns video information if this is a video file or has a video embedded.
|
||||
func (m *MediaFile) VideoInfo() video.Info {
|
||||
if !m.Ok() || !m.IsMedia() {
|
||||
// Not a main media file.
|
||||
return m.videoInfo
|
||||
}
|
||||
|
||||
// Gather the data once and cache it.
|
||||
m.videoOnce.Do(func() {
|
||||
if info, err := video.ProbeFile(m.FileName()); err != nil {
|
||||
log.Debugf("video: %s in %s", err, clean.Log(m.BaseName()))
|
||||
} else {
|
||||
m.videoInfo = info
|
||||
}
|
||||
})
|
||||
|
||||
return m.videoInfo
|
||||
}
|
||||
|
|
|
@ -11,7 +11,9 @@ import (
|
|||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/meta"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
"github.com/photoprism/photoprism/pkg/projection"
|
||||
"github.com/photoprism/photoprism/pkg/video"
|
||||
)
|
||||
|
||||
func TestMediaFile_HasSidecarJson(t *testing.T) {
|
||||
|
@ -435,3 +437,52 @@ func TestMediaFile_Exif_HEIC(t *testing.T) {
|
|||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMediaFile_VideoInfo(t *testing.T) {
|
||||
c := config.TestConfig()
|
||||
t.Run(
|
||||
"samsung-motion-photo.jpg", func(t *testing.T) {
|
||||
fileName := filepath.Join(c.ExamplesPath(), "samsung-motion-photo.jpg")
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info := mf.VideoInfo()
|
||||
|
||||
assert.Equal(t, video.MP4, info.VideoType)
|
||||
assert.Equal(t, video.CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, 1440, info.VideoWidth)
|
||||
assert.Equal(t, 1080, info.VideoHeight)
|
||||
assert.Equal(t, int64(2685814), info.VideoOffset)
|
||||
assert.Equal(t, int64(0), info.ThumbOffset)
|
||||
assert.Equal(t, "2.933s", info.Duration.String())
|
||||
assert.Equal(t, fs.ImageJPEG, info.FileType)
|
||||
assert.Equal(t, media.Live, info.MediaType)
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"beach_sand.jpg", func(t *testing.T) {
|
||||
fileName := filepath.Join(conf.ExamplesPath(), "beach_sand.jpg")
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info := mf.VideoInfo()
|
||||
|
||||
assert.Equal(t, video.Unknown, info.VideoType)
|
||||
assert.Equal(t, video.CodecUnknown, info.VideoCodec)
|
||||
assert.Equal(t, 0, info.VideoWidth)
|
||||
assert.Equal(t, 0, info.VideoHeight)
|
||||
assert.Equal(t, int64(-1), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, time.Duration(0), info.Duration)
|
||||
assert.Equal(t, fs.ImageJPEG, info.FileType)
|
||||
assert.Equal(t, media.Image, info.MediaType)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -56,13 +56,6 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
matches = append(matches, name)
|
||||
}
|
||||
|
||||
// Extract an embedded video file and add it to the list of files, if successful.
|
||||
if videoName, videoErr := m.ExtractEmbeddedVideo(); videoErr != nil {
|
||||
log.Warnf("media: %s om %s (extract embedded video)", clean.Error(videoErr), clean.Log(m.RootRelName()))
|
||||
} else if videoName != "" {
|
||||
matches = append(matches, videoName)
|
||||
}
|
||||
|
||||
isHEIC := false
|
||||
|
||||
for _, fileName := range matches {
|
||||
|
|
|
@ -2610,66 +2610,3 @@ func TestMediaFile_Duration(t *testing.T) {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMediaFile_ExtractEmbeddedVideo(t *testing.T) {
|
||||
conf := config.TestConfig()
|
||||
t.Run(
|
||||
"samsung-motion-photo.jpg", func(t *testing.T) {
|
||||
// input image file
|
||||
fileName := filepath.Join(
|
||||
conf.ExamplesPath(),
|
||||
"samsung-motion-photo.jpg",
|
||||
)
|
||||
// expected output video file
|
||||
outputName := filepath.Join(
|
||||
conf.SidecarPath(), "samsung-motion-photo.mp4",
|
||||
)
|
||||
|
||||
_ = os.Remove(outputName)
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// extract the video
|
||||
if embeddedVideoName, err := mf.ExtractEmbeddedVideo(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if embeddedVideoName == "" {
|
||||
t.Errorf("embeddedVideoName should not be empty")
|
||||
} else {
|
||||
t.Logf(embeddedVideoName)
|
||||
assert.Equal(t, embeddedVideoName, outputName)
|
||||
assert.Truef(
|
||||
t, fs.FileExists(embeddedVideoName),
|
||||
"output file does not exist: %s", embeddedVideoName,
|
||||
)
|
||||
|
||||
_ = os.Remove(outputName)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"beach_sand.jpg", func(t *testing.T) {
|
||||
// input image file
|
||||
fileName := filepath.Join(
|
||||
conf.ExamplesPath(),
|
||||
"beach_sand.jpg",
|
||||
)
|
||||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if embeddedVideoName, err := mf.ExtractEmbeddedVideo(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if embeddedVideoName != "" {
|
||||
t.Errorf("expected embeddedVideoName to be empty")
|
||||
} else {
|
||||
t.Logf(embeddedVideoName)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ func FileName(s string) string {
|
|||
s = strings.TrimSpace(s)
|
||||
|
||||
// Remove non-printable and other potentially problematic characters.
|
||||
// The following characters must never be used in a filename: / \ > < | : &
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if !unicode.IsPrint(r) {
|
||||
return -1
|
||||
|
|
|
@ -16,6 +16,7 @@ func Path(s string) string {
|
|||
s = strings.TrimSpace(s)
|
||||
|
||||
// Remove non-printable and other potentially problematic characters.
|
||||
// The following characters must never be used in a filepath: \ > < | : &
|
||||
s = strings.Map(func(r rune) rune {
|
||||
if !unicode.IsPrint(r) {
|
||||
return -1
|
||||
|
@ -38,6 +39,7 @@ func UserPath(dir string) string {
|
|||
return dir
|
||||
}
|
||||
|
||||
// The following characters must never be used in a filepath: \ > < | : &
|
||||
dir = strings.Trim(path.Clean(Path(strings.ReplaceAll(dir, "\\", "/"))), " ~./\\|?*%<>")
|
||||
|
||||
if strings.Contains(dir, "/.") || strings.Contains(dir, "..") || strings.Contains(dir, "//") {
|
||||
|
|
|
@ -53,5 +53,5 @@ var TypeInfo = TypeMap{
|
|||
SidecarYAML: "Serialized YAML Data (Config, Metadata)",
|
||||
SidecarText: "Plain Text",
|
||||
SidecarMarkdown: "Markdown Formatted Text",
|
||||
UnknownType: "Other",
|
||||
TypeUnknown: "Other",
|
||||
}
|
||||
|
|
|
@ -8,13 +8,13 @@ import (
|
|||
)
|
||||
|
||||
// FileType returns the type associated with the specified filename,
|
||||
// and UnknownType if it could not be matched.
|
||||
// and TypeUnknown if it could not be matched.
|
||||
func FileType(fileName string) Type {
|
||||
if t, found := Extensions[LowerExt(fileName)]; found {
|
||||
return t
|
||||
}
|
||||
|
||||
return UnknownType
|
||||
return TypeUnknown
|
||||
}
|
||||
|
||||
// IsAnimatedImage checks if the type associated with the specified filename may be animated.
|
||||
|
|
|
@ -167,7 +167,7 @@ func TestType_FindAll(t *testing.T) {
|
|||
func TestFileType(t *testing.T) {
|
||||
t.Run("Empty", func(t *testing.T) {
|
||||
result := FileType("")
|
||||
assert.Equal(t, UnknownType, result)
|
||||
assert.Equal(t, TypeUnknown, result)
|
||||
})
|
||||
t.Run("JPEG", func(t *testing.T) {
|
||||
result := FileType("testdata/test.jpg")
|
||||
|
|
|
@ -61,7 +61,7 @@ const (
|
|||
SidecarJSON Type = "json" // JSON metadata / config / sidecar file
|
||||
SidecarText Type = "txt" // Text config / sidecar file
|
||||
SidecarMarkdown Type = "md" // Markdown text sidecar file
|
||||
UnknownType Type = "" // Unknown file
|
||||
TypeUnknown Type = "" // Unknown file
|
||||
)
|
||||
|
||||
// TypeAnimated maps animated file types to their mime type.
|
||||
|
|
|
@ -37,6 +37,6 @@ func ContainsAny(l, s []string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// Nothing found.
|
||||
// Not found.
|
||||
return false
|
||||
}
|
||||
|
|
50
pkg/live/README.md
Normal file
50
pkg/live/README.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
Hybrid Photo/Video File Support
|
||||
===============================
|
||||
|
||||
- Apple iOS: Live Photos consist of a JPEG/HEIC image and a QuickTime AVC/HEVC video, which are both required for viewing.
|
||||
- Android: Samsung and Google Pixel smartphones support taking "Motion Photos" with the included Photos app. Motion Photos are JPEG/HEIC image with a short MP4 video embedded after the image data. The image part of these files can be opened in any image viewer that supports JPEG/HEIC, but the video part cannot. However, since the MP4 video is simply appended at the end of the image file, it can be easily read by our software and streamed through the API as needed.
|
||||
|
||||
Introductory Tutorials
|
||||
----------------------
|
||||
|
||||
| Title | Date | URL |
|
||||
|---------------------------------------------------------|----------|------------------------------------------------------------------------------------|
|
||||
| How to detect Android motion photos in Flutter | May 2023 | https://ente.io/blog/tech/android-motion-photos-flutter/ |
|
||||
| Stripping Embedded MP4s out of Android 12 Motion Photos | Oct 2021 | https://mjanja.ch/2021/10/stripping-embedded-mp4s-out-of-android-12-motion-photos/ |
|
||||
| Google Pixel "Motion Photo" Howto | Mar 2021 | https://linuxreviews.org/Google_Pixel_%22Motion_Photo%22 |
|
||||
| go-mp4: Golang Library and CLI Tool for MP4 | Jul 2020 | https://dev.to/sunfishshogi/go-mp4-golang-library-and-cli-tool-for-mp4-52o1 |
|
||||
| Working with Motion Photos | Jan 2019 | https://medium.com/android-news/working-with-motion-photos-da0aa49b50c |
|
||||
| Google: Behind the Motion Photos Technology in Pixel 2 | Mar 2018 | https://blog.research.google/2018/03/behind-motion-photos-technology-in.html |
|
||||
|
||||
Software Libraries and References
|
||||
---------------------------------
|
||||
|
||||
| Title | URL |
|
||||
|------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| Web Video Codec Guide | https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs |
|
||||
| Media Container Formats | https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers |
|
||||
| MP4 Signature Format | https://www.file-recovery.com/mp4-signature-format.htm |
|
||||
| List of file signatures (Wikipedia) | https://en.wikipedia.org/wiki/List_of_file_signatures |
|
||||
| Go library for reading and writing MP4 files | https://github.com/abema/go-mp4 |
|
||||
| Go library for buffered I/O with io.Seeker interface | https://github.com/sunfish-shogi/bufseekio |
|
||||
| How to use the io.Reader interface | https://yourbasic.org/golang/io-reader-interface-explained/ |
|
||||
| AV1 Codec ISO Media File Format | https://aomediacodec.github.io/av1-isobmff |
|
||||
|
||||
Related GitHub Issues
|
||||
---------------------
|
||||
|
||||
- https://github.com/photoprism/photoprism/issues/439 (Samsung: Initial support for Motion Photos)
|
||||
- https://github.com/photoprism/photoprism/issues/1739 (Google: Initial support for Motion Photos)
|
||||
- https://github.com/photoprism/photoprism/issues/2788 (Metadata: Flag Samsung/Google Motion Photos as Live Photos)
|
||||
- https://github.com/cliveontoast/GoMoPho/issues/23 (Google Motion Photos Video Extractor: Add Android 12 Support)
|
||||
|
||||
Related Pull Requests
|
||||
---------------------
|
||||
|
||||
- https://github.com/photoprism/photoprism/pull/3709 (Google: Initial support for Motion Photos)
|
||||
- https://github.com/photoprism/photoprism/pull/3722 (Google: Add support for Motion Photos)
|
||||
- https://github.com/photoprism/photoprism/pull/3660 (Samsung: Improved support for Motion Photos)
|
||||
|
||||
----
|
||||
|
||||
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), [Privacy Policy](https://www.photoprism.app/privacy), and [Code of Conduct](https://www.photoprism.app/code-of-conduct). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*
|
25
pkg/live/live.go
Normal file
25
pkg/live/live.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
Package live provides types and abstractions for hybrid photo/video file formats.
|
||||
|
||||
Copyright (c) 2018 - 2023 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"):
|
||||
<https://docs.photoprism.app/license/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:
|
||||
<https://www.photoprism.app/trademark>
|
||||
|
||||
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:
|
||||
<https://docs.photoprism.app/developer-guide/>
|
||||
*/
|
||||
package live
|
|
@ -52,5 +52,5 @@ var Formats = map[fs.Type]Type{
|
|||
fs.SidecarText: Sidecar,
|
||||
fs.SidecarJSON: Sidecar,
|
||||
fs.SidecarMarkdown: Sidecar,
|
||||
fs.UnknownType: Other,
|
||||
fs.TypeUnknown: Other,
|
||||
}
|
||||
|
|
7
pkg/media/headers.go
Normal file
7
pkg/media/headers.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package media
|
||||
|
||||
// Standard media file headers.
|
||||
var (
|
||||
JpegImageHeader = []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01}
|
||||
ISOBaseMediaHeader = []byte{0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6F, 0x6D}
|
||||
)
|
23
pkg/video/README.md
Normal file
23
pkg/video/README.md
Normal file
|
@ -0,0 +1,23 @@
|
|||
Video File Support
|
||||
==================
|
||||
|
||||
Technical References and Tutorials
|
||||
----------------------------------
|
||||
|
||||
| Title | URL |
|
||||
|------------------------------------------------------|-------------------------------------------------------------------------|
|
||||
| Web Video Codec Guide | https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_codecs |
|
||||
| Media Container Formats | https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers |
|
||||
| MP4 Signature Format | https://www.file-recovery.com/mp4-signature-format.htm |
|
||||
| List of file signatures (Wikipedia) | https://en.wikipedia.org/wiki/List_of_file_signatures |
|
||||
| How to use the io.Reader interface | https://yourbasic.org/golang/io-reader-interface-explained/ |
|
||||
| AV1 Codec ISO Media File Format | https://aomediacodec.github.io/av1-isobmff |
|
||||
|
||||
Hybrid Photo/Video Formats
|
||||
--------------------------
|
||||
|
||||
For more information on hybrid photo/video file formats, e.g. Apple Live Photos and Samsung/Google Motion Photos, see [github.com/photoprism/photoprism/tree/develop/pkg/live](https://github.com/photoprism/photoprism/tree/develop/pkg/live).
|
||||
|
||||
----
|
||||
|
||||
*PhotoPrism® is a [registered trademark](https://www.photoprism.app/trademark). By using the software and services we provide, you agree to our [Terms of Service](https://www.photoprism.app/terms), [Privacy Policy](https://www.photoprism.app/privacy), and [Code of Conduct](https://www.photoprism.app/code-of-conduct). Docs are [available](https://link.photoprism.app/github-docs) under the [CC BY-NC-SA 4.0 License](https://creativecommons.org/licenses/by-nc-sa/4.0/); [additional terms](https://github.com/photoprism/photoprism/blob/develop/assets/README.md) may apply.*
|
109
pkg/video/chunk.go
Normal file
109
pkg/video/chunk.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
|
||||
"github.com/sunfish-shogi/bufseekio"
|
||||
)
|
||||
|
||||
// Chunk represents a fixed length file chunk.
|
||||
type Chunk [4]byte
|
||||
|
||||
// Get returns the chunk as byte array.
|
||||
func (c Chunk) Get() [4]byte {
|
||||
return c
|
||||
}
|
||||
|
||||
// Hex returns the chunk as hex formatted string.
|
||||
func (c Chunk) Hex() string {
|
||||
return fmt.Sprintf("0x%x", c[:])
|
||||
}
|
||||
|
||||
// String returns the chunk as string.
|
||||
func (c Chunk) String() string {
|
||||
return string(c[:])
|
||||
}
|
||||
|
||||
// Bytes returns the chunk as byte slice.
|
||||
func (c Chunk) Bytes() []byte {
|
||||
return c[:]
|
||||
}
|
||||
|
||||
// Uint32 returns the chunk as unsigned integer.
|
||||
func (c Chunk) Uint32() uint32 {
|
||||
return binary.BigEndian.Uint32(c.Bytes())
|
||||
}
|
||||
|
||||
// Equal compares the chunk with a byte slice.
|
||||
func (c Chunk) Equal(b []byte) bool {
|
||||
return bytes.Equal(c.Bytes(), b)
|
||||
}
|
||||
|
||||
// FileOffset returns the index of the chunk in the specified file, or -1 if it was not found.
|
||||
func (c Chunk) FileOffset(fileName string) (int, error) {
|
||||
if !fs.FileExists(fileName) {
|
||||
return -1, errors.New("file not found")
|
||||
}
|
||||
|
||||
file, err := os.Open(fileName)
|
||||
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
index, err := c.DataOffset(file)
|
||||
|
||||
return index, err
|
||||
}
|
||||
|
||||
// DataOffset returns the index of the chunk in f, or -1 if it was not found.
|
||||
func (c Chunk) DataOffset(file io.ReadSeeker) (int, error) {
|
||||
if file == nil {
|
||||
return -1, errors.New("file is nil")
|
||||
}
|
||||
|
||||
data := c.Bytes()
|
||||
dataSize := len(data)
|
||||
blockSize := 128 * 1024
|
||||
buffer := make([]byte, blockSize)
|
||||
|
||||
// Create buffered read seeker.
|
||||
r := bufseekio.NewReadSeeker(file, blockSize, dataSize)
|
||||
|
||||
// Index offset.
|
||||
var offset int
|
||||
|
||||
// Search in batches.
|
||||
for {
|
||||
n, err := r.Read(buffer)
|
||||
buffer = buffer[:n]
|
||||
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
break
|
||||
} else if n == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
// Return data index, if found.
|
||||
if i := bytes.Index(buffer, data); i >= 0 {
|
||||
return offset + i, nil
|
||||
}
|
||||
|
||||
offset += n
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
23
pkg/video/chunks.go
Normal file
23
pkg/video/chunks.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package video
|
||||
|
||||
// Chunks represents a list of file chunks.
|
||||
type Chunks []Chunk
|
||||
|
||||
// ContainsAny checks if at least one common chunk exists.
|
||||
func (c Chunks) ContainsAny(b [][4]byte) bool {
|
||||
if len(c) == 0 || len(b) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Find matches.
|
||||
for i := range c {
|
||||
for j := range b {
|
||||
if b[j] == c[i] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Not found.
|
||||
return false
|
||||
}
|
52
pkg/video/chunks_mp4.go
Normal file
52
pkg/video/chunks_mp4.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package video
|
||||
|
||||
// ChunkMP4 specifies the start chunk of MP4 video files,
|
||||
// it must be followed by a valid subtype chunk.
|
||||
var (
|
||||
ChunkMP4 = Chunk{'f', 't', 'y', 'p'}
|
||||
ChunkQT = Chunk{'q', 't', ' ', ' '}
|
||||
ChunkISOM = Chunk{'i', 's', 'o', 'm'}
|
||||
ChunkISO2 = Chunk{'i', 's', 'o', '2'}
|
||||
ChunkISO3 = Chunk{'i', 's', 'o', '3'}
|
||||
ChunkISO4 = Chunk{'i', 's', 'o', '4'}
|
||||
ChunkISO5 = Chunk{'i', 's', 'o', '5'}
|
||||
ChunkISO6 = Chunk{'i', 's', 'o', '6'}
|
||||
ChunkISO7 = Chunk{'i', 's', 'o', '7'}
|
||||
ChunkISO8 = Chunk{'i', 's', 'o', '8'}
|
||||
ChunkISO9 = Chunk{'i', 's', 'o', '9'}
|
||||
ChunkAVC1 = Chunk{'a', 'v', 'c', '1'}
|
||||
ChunkHEV1 = Chunk{'h', 'e', 'v', '1'}
|
||||
ChunkHVC1 = Chunk{'h', 'v', 'c', '1'}
|
||||
ChunkAV01 = Chunk{'a', 'v', '0', '1'}
|
||||
ChunkAV1C = Chunk{'a', 'v', '1', 'C'}
|
||||
ChunkMMP4 = Chunk{'m', 'm', 'p', '4'}
|
||||
ChunkMP4V = Chunk{'m', 'p', '4', 'v'}
|
||||
ChunkMP41 = Chunk{'m', 'p', '4', '1'}
|
||||
ChunkMP42 = Chunk{'m', 'p', '4', '2'}
|
||||
ChunkMP71 = Chunk{'m', 'p', '7', '1'}
|
||||
ChunkHEIC = Chunk{'h', 'e', 'i', 'c'}
|
||||
)
|
||||
|
||||
// CompatibleBrands contains compatible subtypes chunks.
|
||||
var CompatibleBrands = Chunks{
|
||||
ChunkQT,
|
||||
ChunkISOM,
|
||||
ChunkISO2,
|
||||
ChunkISO3,
|
||||
ChunkISO4,
|
||||
ChunkISO5,
|
||||
ChunkISO6,
|
||||
ChunkISO7,
|
||||
ChunkISO8,
|
||||
ChunkISO9,
|
||||
ChunkAVC1,
|
||||
ChunkHEV1,
|
||||
ChunkHVC1,
|
||||
ChunkAV01,
|
||||
ChunkAV1C,
|
||||
ChunkMMP4,
|
||||
ChunkMP4V,
|
||||
ChunkMP41,
|
||||
ChunkMP42,
|
||||
ChunkMP71,
|
||||
}
|
120
pkg/video/chunks_test.go
Normal file
120
pkg/video/chunks_test.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/sunfish-shogi/bufseekio"
|
||||
)
|
||||
|
||||
func TestChunk_TypeCast(t *testing.T) {
|
||||
t.Run("String", func(t *testing.T) {
|
||||
assert.Equal(t, "ftyp", ChunkMP4.String())
|
||||
})
|
||||
t.Run("Hex", func(t *testing.T) {
|
||||
assert.Equal(t, "0x66747970", ChunkMP4.Hex())
|
||||
})
|
||||
t.Run("Uint32", func(t *testing.T) {
|
||||
assert.Equal(t, uint32(0x66747970), ChunkMP4.Uint32())
|
||||
})
|
||||
}
|
||||
|
||||
func TestChunk_FileOffset(t *testing.T) {
|
||||
t.Run("mp4v-avc1.mp4", func(t *testing.T) {
|
||||
index, err := ChunkMP4.FileOffset("testdata/mp4v-avc1.mp4")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, index)
|
||||
})
|
||||
t.Run("isom-avc1.mp4", func(t *testing.T) {
|
||||
index, err := ChunkMP4.FileOffset("testdata/isom-avc1.mp4")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 4, index)
|
||||
})
|
||||
t.Run("image-isom-avc1.jpg", func(t *testing.T) {
|
||||
index, err := ChunkMP4.FileOffset("testdata/image-isom-avc1.jpg")
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 23213, index)
|
||||
})
|
||||
}
|
||||
|
||||
func TestChunks(t *testing.T) {
|
||||
t.Run("mp4v-avc1.mp4", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/mp4v-avc1.mp4")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
r := bufseekio.NewReadSeeker(f, 8, 4)
|
||||
|
||||
var startChunk = make([]byte, 4)
|
||||
var subType = make([]byte, 4)
|
||||
|
||||
if _, err := r.Seek(4, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Read first 4-byte chunk.
|
||||
if n, err := r.Read(startChunk); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 4 {
|
||||
t.Fatal("expected to read 4 bytes")
|
||||
}
|
||||
|
||||
// Read second 4-byte chunk.
|
||||
if n, err := r.Read(subType); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 4 {
|
||||
t.Fatal("expected to read 4 bytes")
|
||||
}
|
||||
|
||||
assert.Equal(t, ChunkMP4.Bytes(), startChunk[:4])
|
||||
assert.Equal(t, ChunkMP4V.Bytes(), subType[:4])
|
||||
})
|
||||
t.Run("isom-avc1.mp4", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/isom-avc1.mp4")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
b := make([]byte, 12)
|
||||
|
||||
// Read first 12 bytes from video file.
|
||||
if n, err := f.Read(b); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 12 {
|
||||
t.Fatalf("expected to read 12 bytes instead of %d", n)
|
||||
}
|
||||
|
||||
assert.Equal(t, ChunkMP4[:], b[4:8])
|
||||
assert.Equal(t, ChunkISOM[:], b[8:12])
|
||||
})
|
||||
t.Run("image-isom-avc1.jpg", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/image-isom-avc1.jpg")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
b := make([]byte, 12)
|
||||
|
||||
// Read first 12 bytes from video file.
|
||||
if n, err := f.Read(b); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if n != 12 {
|
||||
t.Fatalf("expected to read 12 bytes instead of %d", n)
|
||||
}
|
||||
|
||||
assert.NotEqual(t, ChunkMP4, *(*[4]byte)(b[4:8]))
|
||||
assert.NotEqual(t, ChunkISOM, *(*[4]byte)(b[8:12]))
|
||||
})
|
||||
}
|
||||
|
||||
func TestChunks_ContainsAny(t *testing.T) {
|
||||
t.Run("Found", func(t *testing.T) {
|
||||
chunks := [][4]byte{ChunkMP41, ChunkMP42}
|
||||
assert.True(t, CompatibleBrands.ContainsAny(chunks))
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
chunks := [][4]byte{ChunkMP4}
|
||||
assert.False(t, CompatibleBrands.ContainsAny(chunks))
|
||||
})
|
||||
}
|
|
@ -5,9 +5,9 @@ type Codec string
|
|||
// Check browser support: https://cconcolato.github.io/media-mime-support/
|
||||
|
||||
const (
|
||||
UnknownCodec Codec = ""
|
||||
CodecUnknown Codec = ""
|
||||
CodecAVC Codec = "avc1"
|
||||
CodecHEVC Codec = "hvc1"
|
||||
CodecHVC Codec = "hvc1"
|
||||
CodecVVC Codec = "vvc"
|
||||
CodecAV1 Codec = "av01"
|
||||
CodecVP8 Codec = "vp8"
|
||||
|
@ -18,18 +18,18 @@ const (
|
|||
|
||||
// Codecs maps identifiers to codecs.
|
||||
var Codecs = StandardCodecs{
|
||||
"": UnknownCodec,
|
||||
"a_opus": UnknownCodec,
|
||||
"a_vorbis": UnknownCodec,
|
||||
"": CodecUnknown,
|
||||
"a_opus": CodecUnknown,
|
||||
"a_vorbis": CodecUnknown,
|
||||
"avc": CodecAVC,
|
||||
"avc1": CodecAVC,
|
||||
"v_avc": CodecAVC,
|
||||
"v_avc1": CodecAVC,
|
||||
"hevc": CodecHEVC,
|
||||
"hvc": CodecHEVC,
|
||||
"hvc1": CodecHEVC,
|
||||
"v_hvc": CodecHEVC,
|
||||
"v_hvc1": CodecHEVC,
|
||||
"hevc": CodecHVC,
|
||||
"hvc": CodecHVC,
|
||||
"hvc1": CodecHVC,
|
||||
"v_hvc": CodecHVC,
|
||||
"v_hvc1": CodecHVC,
|
||||
"vvc": CodecVVC,
|
||||
"v_vvc": CodecVVC,
|
||||
"av1": CodecAV1,
|
||||
|
|
|
@ -3,8 +3,8 @@ package video
|
|||
import "testing"
|
||||
|
||||
func TestCodecs(t *testing.T) {
|
||||
if val := Codecs[""]; val != UnknownCodec {
|
||||
t.Fatal("default codec should be UnknownCodec")
|
||||
if val := Codecs[""]; val != CodecUnknown {
|
||||
t.Fatal("default codec should be CodecUnknown")
|
||||
}
|
||||
|
||||
if val := Codecs["avc"]; val != CodecAVC {
|
||||
|
|
25
pkg/video/content_type.go
Normal file
25
pkg/video/content_type.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// Standard content types.
|
||||
const (
|
||||
ContentTypeDefault = "application/octet-stream"
|
||||
ContentTypeAVC = fs.MimeTypeMP4 + `; codecs="avc1"`
|
||||
ContentTypeMOV = fs.MimeTypeMOV
|
||||
)
|
||||
|
||||
// ContentType composes the video content type from the given mime type and codec.
|
||||
func ContentType(mimeType string, codec Codec) string {
|
||||
if codec == CodecUnknown {
|
||||
return mimeType
|
||||
} else if mimeType == "" {
|
||||
return ContentTypeDefault
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s; codecs=\"%s\"", mimeType, codec)
|
||||
}
|
27
pkg/video/content_type_test.go
Normal file
27
pkg/video/content_type_test.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestContentType(t *testing.T) {
|
||||
t.Run("QuickTime", func(t *testing.T) {
|
||||
assert.Equal(t, fs.MimeTypeMOV, ContentType(fs.MimeTypeMOV, ""))
|
||||
})
|
||||
t.Run("QuickTime_HVC", func(t *testing.T) {
|
||||
assert.Equal(t, `video/quicktime; codecs="hvc1"`, ContentType(fs.MimeTypeMOV, CodecHVC))
|
||||
})
|
||||
t.Run("MP4", func(t *testing.T) {
|
||||
assert.Equal(t, fs.MimeTypeMP4, ContentType(fs.MimeTypeMP4, ""))
|
||||
})
|
||||
t.Run("MP4_AVC", func(t *testing.T) {
|
||||
assert.Equal(t, ContentTypeAVC, ContentType(fs.MimeTypeMP4, CodecAVC))
|
||||
})
|
||||
t.Run("MP4_HVC", func(t *testing.T) {
|
||||
assert.Equal(t, `video/mp4; codecs="hvc1"`, ContentType(fs.MimeTypeMP4, CodecHVC))
|
||||
})
|
||||
}
|
61
pkg/video/info.go
Normal file
61
pkg/video/info.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
// Info represents video file information.
|
||||
type Info struct {
|
||||
FileName string
|
||||
FileSize int64
|
||||
FileType fs.Type
|
||||
MediaType media.Type
|
||||
ThumbOffset int64
|
||||
ThumbMimeType string
|
||||
VideoOffset int64
|
||||
VideoMimeType string
|
||||
VideoType Type
|
||||
VideoCodec Codec
|
||||
VideoWidth int
|
||||
VideoHeight int
|
||||
Duration time.Duration
|
||||
Frames int
|
||||
FPS float64
|
||||
Tracks int
|
||||
Encrypted bool
|
||||
FastStart bool
|
||||
Compatible bool
|
||||
}
|
||||
|
||||
// NewInfo returns a new Info struct with default values.
|
||||
func NewInfo() Info {
|
||||
return Info{
|
||||
FileType: fs.TypeUnknown,
|
||||
FileSize: -1,
|
||||
MediaType: media.Unknown,
|
||||
ThumbOffset: -1,
|
||||
VideoOffset: -1,
|
||||
VideoType: Unknown,
|
||||
VideoCodec: CodecUnknown,
|
||||
FPS: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
// VideoSize returns the size of the embedded video, if possible.
|
||||
func (info Info) VideoSize() int64 {
|
||||
if info.FileSize < 0 || info.VideoOffset < 0 {
|
||||
return 0
|
||||
}
|
||||
return info.FileSize - info.VideoOffset
|
||||
}
|
||||
|
||||
// VideoContentType composes the video content type from its mime type and codec.
|
||||
func (info Info) VideoContentType() string {
|
||||
if info.VideoMimeType == "" {
|
||||
return ""
|
||||
}
|
||||
return ContentType(info.VideoMimeType, info.VideoCodec)
|
||||
}
|
6
pkg/video/live.go
Normal file
6
pkg/video/live.go
Normal file
|
@ -0,0 +1,6 @@
|
|||
package video
|
||||
|
||||
import "time"
|
||||
|
||||
// LiveDuration is the maximum duration for a video to play inline like a live photo.
|
||||
var LiveDuration = time.Millisecond * 3100
|
165
pkg/video/probe.go
Normal file
165
pkg/video/probe.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/abema/go-mp4"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
// ProbeFile returns information for the given filename.
|
||||
func ProbeFile(fileName string) (info Info, err error) {
|
||||
if fileName == "" {
|
||||
return info, errors.New("filename missing")
|
||||
}
|
||||
|
||||
var stat os.FileInfo
|
||||
var file *os.File
|
||||
|
||||
// Ensure the file exists and is not a directory.
|
||||
if stat, err = os.Stat(fileName); err != nil {
|
||||
return info, err
|
||||
} else if stat.IsDir() {
|
||||
return info, errors.New("invalid filename")
|
||||
}
|
||||
|
||||
// Open the file for reading.
|
||||
if file, err = os.Open(fileName); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
// Get video information.
|
||||
info, err = Probe(file)
|
||||
|
||||
// Add file name.
|
||||
info.FileName = fileName
|
||||
info.FileSize = stat.Size()
|
||||
|
||||
// Set file type based on filename?
|
||||
if info.FileType == fs.TypeUnknown {
|
||||
info.FileType = fs.FileType(fileName)
|
||||
}
|
||||
|
||||
// Set media type based on filename?
|
||||
if info.MediaType == media.Unknown {
|
||||
info.MediaType = media.Formats[info.FileType]
|
||||
}
|
||||
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Probe returns information on the provided video file.
|
||||
// see https://pkg.go.dev/github.com/abema/go-mp4#ProbeInfo
|
||||
func Probe(file io.ReadSeeker) (info Info, err error) {
|
||||
// Set defaults.
|
||||
info = NewInfo()
|
||||
|
||||
if file == nil {
|
||||
return info, errors.New("file is nil")
|
||||
}
|
||||
|
||||
// Probe file from the beginning.
|
||||
if _, err = file.Seek(0, io.SeekStart); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Find start chunk.
|
||||
if offset, findErr := ChunkMP4.DataOffset(file); findErr != nil {
|
||||
return info, findErr
|
||||
} else if offset < 4 {
|
||||
return info, nil
|
||||
} else {
|
||||
info.VideoOffset = int64(offset) - 4
|
||||
}
|
||||
|
||||
// Ignore any data before the video offset.
|
||||
videoFile := NewReadSeeker(file, info.VideoOffset)
|
||||
|
||||
var video *mp4.ProbeInfo
|
||||
|
||||
// Detect MP4 video metadata.
|
||||
if video, err = mp4.Probe(videoFile); err != nil {
|
||||
return info, err
|
||||
}
|
||||
|
||||
// Check compatibility.
|
||||
if CompatibleBrands.ContainsAny(video.CompatibleBrands) {
|
||||
info.Compatible = true
|
||||
info.VideoType = MP4
|
||||
info.FPS = 30.0 // TODO: Detect actual frames per second!
|
||||
|
||||
if info.VideoOffset > 0 {
|
||||
info.MediaType = media.Live
|
||||
info.ThumbOffset = 0
|
||||
} else {
|
||||
info.MediaType = media.Video
|
||||
info.FileType = fs.VideoMP4
|
||||
}
|
||||
}
|
||||
|
||||
// Check major brand.
|
||||
switch video.MajorBrand {
|
||||
case ChunkMP41.Get(), ChunkMP42.Get():
|
||||
info.VideoMimeType = fs.MimeTypeMP4
|
||||
case ChunkQT.Get():
|
||||
info.VideoType = MOV
|
||||
info.VideoMimeType = fs.MimeTypeMOV
|
||||
if info.MediaType == media.Video {
|
||||
info.FileType = fs.VideoMOV
|
||||
}
|
||||
}
|
||||
|
||||
// Additional properties.
|
||||
info.FastStart = video.FastStart
|
||||
info.Tracks = len(video.Tracks)
|
||||
|
||||
// Check tracks for codec, resolution and encryption.
|
||||
for _, track := range video.Tracks {
|
||||
if track.Encrypted && !info.Encrypted {
|
||||
info.Encrypted = track.Encrypted
|
||||
}
|
||||
|
||||
switch track.Codec {
|
||||
case mp4.CodecAVC1:
|
||||
info.VideoCodec = CodecAVC
|
||||
}
|
||||
|
||||
if avc := track.AVC; avc != nil {
|
||||
if info.VideoMimeType == "" {
|
||||
info.VideoMimeType = fs.MimeTypeMP4
|
||||
}
|
||||
if w := int(avc.Width); w > info.VideoWidth {
|
||||
info.VideoWidth = w
|
||||
}
|
||||
if h := int(avc.Height); h > info.VideoHeight {
|
||||
info.VideoHeight = h
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Detect codec by searching for matching chunks.
|
||||
if info.VideoCodec == "" {
|
||||
if found, _ := ChunkHVC1.DataOffset(file); found > 0 {
|
||||
info.VideoCodec = CodecHVC
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate video duration in seconds.
|
||||
if video.Duration > 0 {
|
||||
s := float64(video.Duration) / float64(video.Timescale)
|
||||
info.Duration = time.Duration(s * float64(time.Second))
|
||||
if info.FPS > 0 {
|
||||
info.Frames = int(math.Round(info.FPS * s))
|
||||
}
|
||||
}
|
||||
|
||||
return info, err
|
||||
}
|
281
pkg/video/probe_test.go
Normal file
281
pkg/video/probe_test.go
Normal file
|
@ -0,0 +1,281 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/media"
|
||||
)
|
||||
|
||||
func TestProbeFile(t *testing.T) {
|
||||
t.Run("PathName", func(t *testing.T) {
|
||||
fileName := "testdata"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, info)
|
||||
})
|
||||
t.Run("NotFound", func(t *testing.T) {
|
||||
fileName := "testdata/invalid"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.Error(t, err)
|
||||
require.NotNil(t, info)
|
||||
})
|
||||
/*t.Run("motion-photo.heif", func(t *testing.T) {
|
||||
fileName := "testdata/motion-photo.heif"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(3366300), info.FileSize)
|
||||
assert.Equal(t, fs.ImageHEIC, info.FileType)
|
||||
assert.Equal(t, MOV, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Image, info.MediaType)
|
||||
assert.Equal(t, CodecHVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMOV, info.VideoMimeType)
|
||||
assert.Equal(t, "", info.VideoContentType())
|
||||
assert.Equal(t, "1.166666666s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.166, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 5, info.Tracks)
|
||||
assert.Equal(t, 0, info.VideoWidth)
|
||||
assert.Equal(t, 0, info.VideoHeight)
|
||||
assert.Equal(t, 35, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})*/
|
||||
t.Run("mp4v-avc1.mp4", func(t *testing.T) {
|
||||
fileName := "testdata/mp4v-avc1.mp4"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(55061), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMP4, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "810.0081ms", info.Duration.String())
|
||||
assert.InEpsilon(t, 0.81, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 1, info.Tracks)
|
||||
assert.Equal(t, 640, info.VideoWidth)
|
||||
assert.Equal(t, 416, info.VideoHeight)
|
||||
assert.Equal(t, 24, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, true, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("mp42-hvc1.mp4", func(t *testing.T) {
|
||||
fileName := "testdata/mp42-hvc1.mp4"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(217963), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMP4, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "1.066666666s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.066, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 2, info.Tracks)
|
||||
assert.Equal(t, 464, info.VideoWidth)
|
||||
assert.Equal(t, 848, info.VideoHeight)
|
||||
assert.Equal(t, 32, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("quicktime-hvc1.mov", func(t *testing.T) {
|
||||
fileName := "testdata/quicktime-hvc1.mov"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(710953), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMOV, info.FileType)
|
||||
assert.Equal(t, MOV, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecHVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMOV, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeMOV+`; codecs="hvc1"`, info.VideoContentType())
|
||||
assert.Equal(t, "1.166666666s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.166, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 5, info.Tracks)
|
||||
assert.Equal(t, 0, info.VideoWidth)
|
||||
assert.Equal(t, 0, info.VideoHeight)
|
||||
assert.Equal(t, 35, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("quicktime-jpeg.mov", func(t *testing.T) {
|
||||
fileName := "testdata/quicktime-jpeg.mov"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(475190), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMOV, info.FileType)
|
||||
assert.Equal(t, Unknown, info.VideoType)
|
||||
assert.Equal(t, int64(-1), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecUnknown, info.VideoCodec)
|
||||
assert.Equal(t, "", info.VideoContentType())
|
||||
assert.Equal(t, "", info.VideoMimeType)
|
||||
assert.Equal(t, "0s", info.Duration.String())
|
||||
assert.Equal(t, 0, info.Tracks)
|
||||
assert.Equal(t, 0, info.VideoWidth)
|
||||
assert.Equal(t, 0, info.VideoHeight)
|
||||
assert.Equal(t, 0, info.Frames)
|
||||
assert.Equal(t, 0.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, false, info.Compatible)
|
||||
})
|
||||
t.Run("image-isom-avc1.jpg", func(t *testing.T) {
|
||||
fileName := "testdata/image-isom-avc1.jpg"
|
||||
info, err := ProbeFile(fileName)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, fileName, info.FileName)
|
||||
assert.Equal(t, int64(31487), info.FileSize)
|
||||
assert.Equal(t, fs.ImageJPEG, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(23209), info.VideoOffset)
|
||||
assert.Equal(t, int64(0), info.ThumbOffset)
|
||||
assert.Equal(t, media.Live, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "1.024s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.024, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 2, info.Tracks)
|
||||
assert.Equal(t, 320, info.VideoWidth)
|
||||
assert.Equal(t, 180, info.VideoHeight)
|
||||
assert.Equal(t, 31, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProbe(t *testing.T) {
|
||||
t.Run("mp4v-avc1.mp4", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/mp4v-avc1.mp4")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
info, err := Probe(f)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, "", info.FileName)
|
||||
assert.Equal(t, int64(-1), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMP4, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "810.0081ms", info.Duration.String())
|
||||
assert.InEpsilon(t, 0.81, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 1, info.Tracks)
|
||||
assert.Equal(t, 640, info.VideoWidth)
|
||||
assert.Equal(t, 416, info.VideoHeight)
|
||||
assert.Equal(t, 24, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, true, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("isom-avc1.mp4", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/isom-avc1.mp4")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
info, err := Probe(f)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, "", info.FileName)
|
||||
assert.Equal(t, int64(-1), info.FileSize)
|
||||
assert.Equal(t, fs.VideoMP4, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(0), info.VideoOffset)
|
||||
assert.Equal(t, int64(-1), info.ThumbOffset)
|
||||
assert.Equal(t, media.Video, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "1.024s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.024, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 2, info.Tracks)
|
||||
assert.Equal(t, 320, info.VideoWidth)
|
||||
assert.Equal(t, 180, info.VideoHeight)
|
||||
assert.Equal(t, 31, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
t.Run("image-isom-avc1.jpg", func(t *testing.T) {
|
||||
f, fileErr := os.Open("testdata/image-isom-avc1.jpg")
|
||||
require.NoError(t, fileErr)
|
||||
defer f.Close()
|
||||
|
||||
info, err := Probe(f)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, info)
|
||||
|
||||
assert.Equal(t, "", info.FileName)
|
||||
assert.Equal(t, int64(-1), info.FileSize)
|
||||
assert.Equal(t, fs.TypeUnknown, info.FileType)
|
||||
assert.Equal(t, MP4, info.VideoType)
|
||||
assert.Equal(t, int64(23209), info.VideoOffset)
|
||||
assert.Equal(t, int64(0), info.ThumbOffset)
|
||||
assert.Equal(t, media.Live, info.MediaType)
|
||||
assert.Equal(t, CodecAVC, info.VideoCodec)
|
||||
assert.Equal(t, fs.MimeTypeMP4, info.VideoMimeType)
|
||||
assert.Equal(t, ContentTypeAVC, info.VideoContentType())
|
||||
assert.Equal(t, "1.024s", info.Duration.String())
|
||||
assert.InEpsilon(t, 1.024, info.Duration.Seconds(), 0.01)
|
||||
assert.Equal(t, 2, info.Tracks)
|
||||
assert.Equal(t, 320, info.VideoWidth)
|
||||
assert.Equal(t, 180, info.VideoHeight)
|
||||
assert.Equal(t, 31, info.Frames)
|
||||
assert.Equal(t, 30.0, info.FPS)
|
||||
assert.Equal(t, false, info.Encrypted)
|
||||
assert.Equal(t, false, info.FastStart)
|
||||
assert.Equal(t, true, info.Compatible)
|
||||
})
|
||||
}
|
100
pkg/video/reader.go
Normal file
100
pkg/video/reader.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// Reader reads an existing file from an offset until the end.
|
||||
type Reader struct {
|
||||
fileName string
|
||||
file *os.File
|
||||
offset int64
|
||||
}
|
||||
|
||||
// NewReader creates a new Reader.
|
||||
func NewReader(fileName string, offset int64) (*Reader, error) {
|
||||
// Check if the file exists.
|
||||
if !fs.FileExists(fileName) {
|
||||
return nil, errors.New("file not found")
|
||||
}
|
||||
|
||||
// Open file for reading.
|
||||
file, err := os.Open(fileName)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure that the offset is positive and we are starting at the right position.
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
} else if offset > 0 {
|
||||
if _, seekErr := file.Seek(offset, io.SeekStart); seekErr != nil {
|
||||
_ = file.Close()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Create new reader and return it.
|
||||
return &Reader{fileName: fileName, file: file, offset: int64(offset)}, nil
|
||||
}
|
||||
|
||||
// Read reads up to len(p) bytes into p. It returns the number of bytes read and any error encountered.
|
||||
func (r *Reader) Read(p []byte) (n int, err error) {
|
||||
return r.file.Read(p)
|
||||
}
|
||||
|
||||
// Close closes the file after reading.
|
||||
func (r *Reader) Close() error {
|
||||
r.fileName = ""
|
||||
r.offset = 0
|
||||
return r.file.Close()
|
||||
}
|
||||
|
||||
// ReadSeeker reads an existing file from an offset until the end.
|
||||
type ReadSeeker struct {
|
||||
file io.ReadSeeker
|
||||
offset int64
|
||||
}
|
||||
|
||||
// NewReadSeeker creates a new ReadSeeker.
|
||||
func NewReadSeeker(file io.ReadSeeker, offset int64) *ReadSeeker {
|
||||
// Ensure that the offset is positive.
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
return &ReadSeeker{
|
||||
file: file,
|
||||
offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
// Read reads up to len(p) bytes into p. It returns the number of bytes read and any error encountered.
|
||||
func (r *ReadSeeker) Read(p []byte) (n int, err error) {
|
||||
return r.file.Read(p)
|
||||
}
|
||||
|
||||
// Seek sets the offset for the next Read or Write to offset, interpreted according to whence:
|
||||
// - SeekStart means relative to the start of the file
|
||||
// - SeekCurrent means relative to the current offset
|
||||
// - SeekEnd means relative to the end (for example, offset = -2 specifies the penultimate byte of the file)
|
||||
//
|
||||
// Seek returns the new offset relative to the start of the file or an error, if any.
|
||||
// Seeking to an offset before the start of the file is an error.
|
||||
func (r *ReadSeeker) Seek(offset int64, whence int) (pos int64, err error) {
|
||||
switch whence {
|
||||
case io.SeekStart:
|
||||
pos, err = r.file.Seek(offset+r.offset, whence)
|
||||
case io.SeekCurrent, io.SeekEnd:
|
||||
pos, err = r.file.Seek(offset, whence)
|
||||
default:
|
||||
return 0, errors.New("unknown whence")
|
||||
}
|
||||
|
||||
return pos - r.offset, err
|
||||
}
|
50
pkg/video/reader_test.go
Normal file
50
pkg/video/reader_test.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package video
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
)
|
||||
|
||||
func TestReader(t *testing.T) {
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
info, probeErr := ProbeFile("testdata/image-isom-avc1.jpg")
|
||||
|
||||
if probeErr != nil {
|
||||
t.Fatal(probeErr)
|
||||
}
|
||||
|
||||
require.NotNil(t, info)
|
||||
|
||||
reader, readerErr := NewReader(info.FileName, info.VideoOffset)
|
||||
|
||||
if readerErr != nil {
|
||||
t.Fatal(probeErr)
|
||||
}
|
||||
|
||||
defer reader.Close()
|
||||
|
||||
require.NotNil(t, reader)
|
||||
|
||||
videoData, ioErr := io.ReadAll(reader)
|
||||
|
||||
if ioErr != nil {
|
||||
t.Fatal(probeErr)
|
||||
}
|
||||
|
||||
stat, statErr := os.Stat(info.FileName)
|
||||
|
||||
if statErr != nil {
|
||||
t.Fatal(probeErr)
|
||||
}
|
||||
|
||||
assert.True(t, int(stat.Size()) > len(videoData))
|
||||
assert.Equal(t, int(stat.Size()-info.VideoOffset), len(videoData))
|
||||
assert.Equal(t, info.VideoMimeType, mimetype.Detect(videoData).String())
|
||||
})
|
||||
}
|
BIN
pkg/video/testdata/image-isom-avc1.jpg
vendored
Normal file
BIN
pkg/video/testdata/image-isom-avc1.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 31 KiB |
BIN
pkg/video/testdata/image.jpg
vendored
Normal file
BIN
pkg/video/testdata/image.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 23 KiB |
BIN
pkg/video/testdata/isom-avc1.mp4
vendored
Normal file
BIN
pkg/video/testdata/isom-avc1.mp4
vendored
Normal file
Binary file not shown.
78
pkg/video/testdata/isom-avc1.mp4.json
vendored
Normal file
78
pkg/video/testdata/isom-avc1.mp4.json
vendored
Normal file
|
@ -0,0 +1,78 @@
|
|||
[{
|
||||
"SourceFile": "isom-avc1.mp4",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "isom-avc1.mp4",
|
||||
"Directory": ".",
|
||||
"FileSize": 8278,
|
||||
"FileModifyDate": "2023:09:21 13:26:12+00:00",
|
||||
"FileAccessDate": "2023:09:21 14:31:07+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 14:31:07+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "isom",
|
||||
"MinorVersion": "0.2.0",
|
||||
"CompatibleBrands": ["isom","iso2","avc1","mp41"],
|
||||
"MediaDataSize": 6394,
|
||||
"MediaDataOffset": 48,
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "0000:00:00 00:00:00",
|
||||
"ModifyDate": "0000:00:00 00:00:00",
|
||||
"TimeScale": 1000,
|
||||
"Duration": 1.024,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 3,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "0000:00:00 00:00:00",
|
||||
"TrackModifyDate": "0000:00:00 00:00:00",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 1,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"ImageWidth": 320,
|
||||
"ImageHeight": 180,
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 320,
|
||||
"SourceImageHeight": 180,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"BitDepth": 24,
|
||||
"PixelAspectRatio": "1:1",
|
||||
"VideoFrameRate": 10,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "0000:00:00 00:00:00",
|
||||
"MediaModifyDate": "0000:00:00 00:00:00",
|
||||
"MediaTimeScale": 44100,
|
||||
"MediaDuration": 1.02321995464853,
|
||||
"MediaLanguageCode": "eng",
|
||||
"HandlerDescription": "SoundHandle",
|
||||
"Balance": 0,
|
||||
"AudioFormat": "mp4a",
|
||||
"AudioChannels": 2,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 44100,
|
||||
"HandlerType": "mdir",
|
||||
"HandlerVendorID": "appl",
|
||||
"Encoder": "Lavf58.29.100",
|
||||
"LocationInformation": "(none) Role=shooting Lat=0.00000 Lon=0.00000 Alt=0.00 Body=earth Notes=",
|
||||
"ImageSize": "320 180",
|
||||
"Megapixels": 0.0576,
|
||||
"AvgBitrate": 49953,
|
||||
"GPSAltitude": 0,
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSLatitude": 0.00000,
|
||||
"GPSLongitude": 0.00000,
|
||||
"Rotation": 0,
|
||||
"GPSPosition": "0.00000 0.00000"
|
||||
}]
|
BIN
pkg/video/testdata/mp42-hvc1.mp4
vendored
Normal file
BIN
pkg/video/testdata/mp42-hvc1.mp4
vendored
Normal file
Binary file not shown.
71
pkg/video/testdata/mp42-hvc1.mp4.json
vendored
Normal file
71
pkg/video/testdata/mp42-hvc1.mp4.json
vendored
Normal file
|
@ -0,0 +1,71 @@
|
|||
[{
|
||||
"SourceFile": "mp42-hvc1.mp4",
|
||||
"ExifToolVersion": 12.40,
|
||||
"FileName": "mp42-hvc1.mp4",
|
||||
"Directory": ".",
|
||||
"FileSize": 217963,
|
||||
"FileModifyDate": "2023:09:22 18:36:10+02:00",
|
||||
"FileAccessDate": "2023:09:22 18:36:12+02:00",
|
||||
"FileInodeChangeDate": "2023:09:22 18:36:10+02:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "mp42",
|
||||
"MinorVersion": "0.0.1",
|
||||
"CompatibleBrands": ["isom","mp41","mp42"],
|
||||
"MediaDataSize": 216002,
|
||||
"MediaDataOffset": 44,
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2023:09:22 16:35:02",
|
||||
"ModifyDate": "2023:09:22 16:35:03",
|
||||
"TimeScale": 44100,
|
||||
"Duration": 1.06666666666667,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 3,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2023:09:22 16:35:02",
|
||||
"TrackModifyDate": "2023:09:22 16:35:03",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 1.06666666666667,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"ImageWidth": 464,
|
||||
"ImageHeight": 848,
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 464,
|
||||
"SourceImageHeight": 848,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"BitDepth": 24,
|
||||
"ColorRepresentation": "nclx 1 1 1",
|
||||
"PixelAspectRatio": "3:3",
|
||||
"VideoFrameRate": 29.0625,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2023:09:22 16:35:02",
|
||||
"MediaModifyDate": "2023:09:22 16:35:03",
|
||||
"MediaTimeScale": 44100,
|
||||
"MediaDuration": 1.11455782312925,
|
||||
"MediaLanguageCode": "und",
|
||||
"HandlerType": "soun",
|
||||
"HandlerDescription": "Core Media Audio",
|
||||
"Balance": 0,
|
||||
"AudioFormat": "mp4a",
|
||||
"AudioChannels": 2,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 44100,
|
||||
"ImageSize": "464 848",
|
||||
"Megapixels": 0.393472,
|
||||
"AvgBitrate": 1620015,
|
||||
"Rotation": 0
|
||||
}]
|
BIN
pkg/video/testdata/mp4v-avc1.mp4
vendored
Normal file
BIN
pkg/video/testdata/mp4v-avc1.mp4
vendored
Normal file
Binary file not shown.
63
pkg/video/testdata/mp4v-avc1.mp4.json
vendored
Normal file
63
pkg/video/testdata/mp4v-avc1.mp4.json
vendored
Normal file
|
@ -0,0 +1,63 @@
|
|||
[{
|
||||
"SourceFile": "mp4v-avc1.mp4",
|
||||
"ExifToolVersion": 12.56,
|
||||
"FileName": "mp4v-avc1.mp4",
|
||||
"Directory": ".",
|
||||
"FileSize": 55061,
|
||||
"FileModifyDate": "2021:11:01 14:39:36+00:00",
|
||||
"FileAccessDate": "2023:09:21 14:30:55+00:00",
|
||||
"FileInodeChangeDate": "2023:09:21 14:30:55+00:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MP4",
|
||||
"FileTypeExtension": "MP4",
|
||||
"MIMEType": "video/mp4",
|
||||
"MajorBrand": "mp4v",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["mp4v","mp42","isom"],
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "0000:00:00 00:00:00",
|
||||
"ModifyDate": "0000:00:00 00:00:00",
|
||||
"TimeScale": 11111,
|
||||
"Duration": 0.810008100081001,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 2,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "0000:00:00 00:00:00",
|
||||
"TrackModifyDate": "0000:00:00 00:00:00",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 0.810008100081001,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 1,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"ImageWidth": 640,
|
||||
"ImageHeight": 416,
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "0000:00:00 00:00:00",
|
||||
"MediaModifyDate": "0000:00:00 00:00:00",
|
||||
"MediaTimeScale": 11111,
|
||||
"MediaDuration": 0.810008100081001,
|
||||
"MediaLanguageCode": "und",
|
||||
"HandlerType": "vide",
|
||||
"GraphicsMode": 0,
|
||||
"OpColor": "0 0 0",
|
||||
"CompressorID": "avc1",
|
||||
"SourceImageWidth": 640,
|
||||
"SourceImageHeight": 416,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"BitDepth": 24,
|
||||
"VideoFrameRate": 11.111,
|
||||
"MediaDataSize": 54336,
|
||||
"MediaDataOffset": 725,
|
||||
"ImageSize": "640 416",
|
||||
"Megapixels": 0.26624,
|
||||
"AvgBitrate": 536646,
|
||||
"Rotation": 0
|
||||
}]
|
BIN
pkg/video/testdata/quicktime-hvc1.mov
vendored
Normal file
BIN
pkg/video/testdata/quicktime-hvc1.mov
vendored
Normal file
Binary file not shown.
99
pkg/video/testdata/quicktime-hvc1.mov.json
vendored
Normal file
99
pkg/video/testdata/quicktime-hvc1.mov.json
vendored
Normal file
|
@ -0,0 +1,99 @@
|
|||
[{
|
||||
"SourceFile": "quicktime-hvc1.mov",
|
||||
"ExifToolVersion": 12.40,
|
||||
"FileName": "quicktime-hvc1.mov",
|
||||
"Directory": ".",
|
||||
"FileSize": 710953,
|
||||
"FileModifyDate": "2023:09:22 18:41:14+02:00",
|
||||
"FileAccessDate": "2023:09:22 18:43:09+02:00",
|
||||
"FileInodeChangeDate": "2023:09:22 18:44:53+02:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MOV",
|
||||
"FileTypeExtension": "MOV",
|
||||
"MIMEType": "video/quicktime",
|
||||
"MajorBrand": "qt ",
|
||||
"MinorVersion": "0.0.0",
|
||||
"CompatibleBrands": ["qt "],
|
||||
"MediaDataSize": 702641,
|
||||
"MediaDataOffset": 36,
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2023:09:22 16:34:05",
|
||||
"ModifyDate": "2023:09:22 16:34:07",
|
||||
"TimeScale": 600,
|
||||
"Duration": 1.16666666666667,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 1,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 6,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2023:09:22 16:34:05",
|
||||
"TrackModifyDate": "2023:09:22 16:34:07",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 1.16666666666667,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"ImageWidth": 1280,
|
||||
"ImageHeight": 720,
|
||||
"CleanApertureDimensions": "1280 720",
|
||||
"ProductionApertureDimensions": "1280 720",
|
||||
"EncodedPixelsDimensions": "1280 720",
|
||||
"GraphicsMode": 64,
|
||||
"OpColor": "32768 32768 32768",
|
||||
"CompressorID": "hvc1",
|
||||
"SourceImageWidth": 1280,
|
||||
"SourceImageHeight": 720,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"CompressorName": "HEVC",
|
||||
"BitDepth": 24,
|
||||
"VideoFrameRate": 29.9572039942939,
|
||||
"CameraLensModel-und-DE": "iPad mini back camera 3mm f/1.8",
|
||||
"CameraFocalLength35mmEquivalent-und-DE": 30,
|
||||
"Balance": 0,
|
||||
"AudioFormat": "mp4a",
|
||||
"AudioChannels": 1,
|
||||
"AudioBitsPerSample": 16,
|
||||
"AudioSampleRate": 44100,
|
||||
"PurchaseFileFormat": "mp4a",
|
||||
"Warning": "[minor] The ExtractEmbedded option may find more tags in the media data",
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"ContentDescribes": 1,
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2023:09:22 16:34:05",
|
||||
"MediaModifyDate": "2023:09:22 16:34:07",
|
||||
"MediaTimeScale": 600,
|
||||
"MediaDuration": 1.16833333333333,
|
||||
"MediaLanguageCode": "und",
|
||||
"GenMediaVersion": 0,
|
||||
"GenFlags": "0 0 0",
|
||||
"GenGraphicsMode": 64,
|
||||
"GenOpColor": "32768 32768 32768",
|
||||
"GenBalance": 0,
|
||||
"HandlerClass": "dhlr",
|
||||
"HandlerVendorID": "appl",
|
||||
"HandlerDescription": "Core Media Data Handler",
|
||||
"MetaFormat": "mebx",
|
||||
"HandlerType": "mdta",
|
||||
"LocationAccuracyHorizontal": 35.000000,
|
||||
"GPSCoordinates": "52.4597 13.322 51.178",
|
||||
"Make": "Apple",
|
||||
"Model": "iPad mini",
|
||||
"Software": 17.0,
|
||||
"CreationDate": "2023:09:22 18:34:05+02:00",
|
||||
"CameraLensModel": "iPad mini back camera 3mm f/1.8",
|
||||
"CameraFocalLength35mmEquivalent": 30,
|
||||
"ImageSize": "1280 720",
|
||||
"Megapixels": 0.9216,
|
||||
"AvgBitrate": 4818110,
|
||||
"GPSAltitude": 51.178,
|
||||
"GPSAltitudeRef": 0,
|
||||
"GPSLatitude": 52.4597,
|
||||
"GPSLongitude": 13.322,
|
||||
"Rotation": 90,
|
||||
"GPSPosition": "52.4597 13.322"
|
||||
}]
|
BIN
pkg/video/testdata/quicktime-jpeg.mov
vendored
Normal file
BIN
pkg/video/testdata/quicktime-jpeg.mov
vendored
Normal file
Binary file not shown.
62
pkg/video/testdata/quicktime-jpeg.mov.json
vendored
Normal file
62
pkg/video/testdata/quicktime-jpeg.mov.json
vendored
Normal file
|
@ -0,0 +1,62 @@
|
|||
[{
|
||||
"SourceFile": "quicktime-jpeg.mov",
|
||||
"ExifToolVersion": 12.40,
|
||||
"FileName": "quicktime-jpeg.mov",
|
||||
"Directory": ".",
|
||||
"FileSize": 475190,
|
||||
"FileModifyDate": "2023:09:22 18:30:07+02:00",
|
||||
"FileAccessDate": "2023:09:22 18:46:27+02:00",
|
||||
"FileInodeChangeDate": "2023:09:22 18:46:27+02:00",
|
||||
"FilePermissions": 100664,
|
||||
"FileType": "MOV",
|
||||
"FileTypeExtension": "MOV",
|
||||
"MIMEType": "video/quicktime",
|
||||
"MovieHeaderVersion": 0,
|
||||
"CreateDate": "2003:06:29 17:17:17",
|
||||
"ModifyDate": "2003:06:29 17:18:19",
|
||||
"TimeScale": 600,
|
||||
"Duration": 41.6666666666667,
|
||||
"PreferredRate": 1,
|
||||
"PreferredVolume": 0.99609375,
|
||||
"PreviewTime": 0,
|
||||
"PreviewDuration": 0,
|
||||
"PosterTime": 0,
|
||||
"SelectionTime": 0,
|
||||
"SelectionDuration": 0,
|
||||
"CurrentTime": 0,
|
||||
"NextTrackID": 2,
|
||||
"TrackHeaderVersion": 0,
|
||||
"TrackCreateDate": "2003:06:29 17:17:17",
|
||||
"TrackModifyDate": "2003:06:29 17:17:40",
|
||||
"TrackID": 1,
|
||||
"TrackDuration": 41.6666666666667,
|
||||
"TrackLayer": 0,
|
||||
"TrackVolume": 0,
|
||||
"MatrixStructure": "1 0 0 0 1 0 0 0 1",
|
||||
"ImageWidth": 434,
|
||||
"ImageHeight": 343,
|
||||
"MediaHeaderVersion": 0,
|
||||
"MediaCreateDate": "2003:06:29 17:17:17",
|
||||
"MediaModifyDate": "2003:06:29 17:17:40",
|
||||
"MediaTimeScale": 600,
|
||||
"MediaDuration": 41.6666666666667,
|
||||
"GraphicsMode": 64,
|
||||
"OpColor": "32768 32768 32768",
|
||||
"HandlerClass": "dhlr",
|
||||
"HandlerType": "alis",
|
||||
"HandlerVendorID": "appl",
|
||||
"HandlerDescription": "Apple Alias Data Handler",
|
||||
"CompressorID": "jpeg",
|
||||
"VendorID": "appl",
|
||||
"SourceImageWidth": 434,
|
||||
"SourceImageHeight": 343,
|
||||
"XResolution": 72,
|
||||
"YResolution": 72,
|
||||
"CompressorName": "Photo - JPEG",
|
||||
"BitDepth": 24,
|
||||
"VideoFrameRate": 12,
|
||||
"WindowLocation": "88 32",
|
||||
"ImageSize": "434 343",
|
||||
"Megapixels": 0.148862,
|
||||
"Rotation": 0
|
||||
}]
|
|
@ -6,9 +6,9 @@ import (
|
|||
|
||||
// Type represents a video format type.
|
||||
type Type struct {
|
||||
File fs.Type
|
||||
Codec Codec
|
||||
Width int
|
||||
Height int
|
||||
Public bool
|
||||
Codec Codec
|
||||
FileType fs.Type
|
||||
WidthLimit int
|
||||
HeightLimit int
|
||||
Public bool
|
||||
}
|
||||
|
|
|
@ -4,83 +4,98 @@ import (
|
|||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
// Unknown represents an unknown video file type.
|
||||
var Unknown = Type{
|
||||
Codec: CodecUnknown,
|
||||
FileType: fs.TypeUnknown,
|
||||
}
|
||||
|
||||
// MP4 is a Multimedia Container (MPEG-4 Part 14).
|
||||
var MP4 = Type{
|
||||
File: fs.VideoMP4,
|
||||
Codec: CodecAVC,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: true,
|
||||
Codec: CodecAVC,
|
||||
FileType: fs.VideoMP4,
|
||||
WidthLimit: 8192,
|
||||
HeightLimit: 4320,
|
||||
Public: true,
|
||||
}
|
||||
|
||||
// MOV are QuickTime videos based on the MPEG-4 format,
|
||||
var MOV = Type{
|
||||
Codec: CodecAVC,
|
||||
FileType: fs.VideoMOV,
|
||||
WidthLimit: 8192,
|
||||
HeightLimit: 4320,
|
||||
Public: true,
|
||||
}
|
||||
|
||||
// AVC aka Advanced Video Coding (H.264).
|
||||
var AVC = Type{
|
||||
File: fs.VideoAVC,
|
||||
Codec: CodecAVC,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: true,
|
||||
Codec: CodecAVC,
|
||||
FileType: fs.VideoAVC,
|
||||
WidthLimit: 8192,
|
||||
HeightLimit: 4320,
|
||||
Public: true,
|
||||
}
|
||||
|
||||
// HEVC aka High Efficiency Video Coding (H.265).
|
||||
var HEVC = Type{
|
||||
File: fs.VideoHEVC,
|
||||
Codec: CodecHEVC,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecHVC,
|
||||
FileType: fs.VideoHEVC,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// VVC aka Versatile Video Coding (H.266).
|
||||
var VVC = Type{
|
||||
File: fs.VideoVVC,
|
||||
Codec: CodecVVC,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecVVC,
|
||||
FileType: fs.VideoVVC,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// VP8 + Google WebM.
|
||||
var VP8 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecVP8,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecVP8,
|
||||
FileType: fs.VideoWebM,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// VP9 + Google WebM.
|
||||
var VP9 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecVP9,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecVP9,
|
||||
FileType: fs.VideoWebM,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// AV1 + Google WebM.
|
||||
var AV1 = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: CodecAV1,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecAV1,
|
||||
FileType: fs.VideoWebM,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// OGV aka Ogg/Theora.
|
||||
var OGV = Type{
|
||||
File: fs.VideoOGV,
|
||||
Codec: CodecOGV,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecOGV,
|
||||
FileType: fs.VideoOGV,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
||||
// WebM Container.
|
||||
var WebM = Type{
|
||||
File: fs.VideoWebM,
|
||||
Codec: UnknownCodec,
|
||||
Width: 0,
|
||||
Height: 0,
|
||||
Public: false,
|
||||
Codec: CodecUnknown,
|
||||
FileType: fs.VideoWebM,
|
||||
WidthLimit: 0,
|
||||
HeightLimit: 0,
|
||||
Public: false,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Package video provides video file related types and functions.
|
||||
Package video provides video file related types and abstractions.
|
||||
|
||||
Copyright (c) 2018 - 2023 PhotoPrism UG. All rights reserved.
|
||||
|
||||
|
|
Loading…
Reference in a new issue