From fbebb13d28eaec67fc25aee6116e5101df4511c4 Mon Sep 17 00:00:00 2001 From: viktorstrate Date: Wed, 27 Jan 2021 18:00:37 +0100 Subject: [PATCH] Don't transcode web compatible video formats. This relates to the following issues #131 #169 #173 --- api/graphql/models/media.go | 4 +-- api/graphql/resolvers/media.go | 2 +- api/scanner/media_type.go | 20 ++++++++++++--- api/scanner/process_photo.go | 44 +++++++++++++++++++------------- api/scanner/process_video.go | 46 ++++++++++++++++++++++++++++++++-- 5 files changed, 91 insertions(+), 25 deletions(-) diff --git a/api/graphql/models/media.go b/api/graphql/models/media.go index 4e79f27b..b297460f 100644 --- a/api/graphql/models/media.go +++ b/api/graphql/models/media.go @@ -17,14 +17,14 @@ type Media struct { AlbumID int `gorm:"not null;index"` Album Album `gorm:"constraint:OnDelete:CASCADE;"` ExifID *int `gorm:"index"` - Exif *MediaEXIF `gorm:"constraint:OnDelete:SET NULL;"` + Exif *MediaEXIF `gorm:"constraint:OnDelete:CASCADE;"` MediaURL []MediaURL `gorm:"constraint:OnDelete:CASCADE;"` DateShot time.Time `gorm:"not null"` DateImported time.Time `gorm:"not null"` // Favorite bool `gorm:"not null, default:false"` Type MediaType `gorm:"not null;index"` VideoMetadataID *int `gorm:"index"` - VideoMetadata *VideoMetadata `gorm:"constraint:OnDelete:SET NULL;"` + VideoMetadata *VideoMetadata `gorm:"constraint:OnDelete:CASCADE;"` SideCarPath *string SideCarHash *string `gorm:"unique"` diff --git a/api/graphql/resolvers/media.go b/api/graphql/resolvers/media.go index be9f8cd1..e6b0b3d4 100644 --- a/api/graphql/resolvers/media.go +++ b/api/graphql/resolvers/media.go @@ -185,7 +185,7 @@ func (r *mediaResolver) VideoWeb(ctx context.Context, media *models.Media) (*mod var url models.MediaURL err := r.Database. Where("media_id = ?", media.ID). - Where("purpose = ?", models.VideoWeb). + Where("purpose = ? OR purpose = ?", models.VideoWeb, models.MediaOriginal). First(&url).Error if err != nil { diff --git a/api/scanner/media_type.go b/api/scanner/media_type.go index 0050065a..bc77a0f8 100644 --- a/api/scanner/media_type.go +++ b/api/scanner/media_type.go @@ -58,7 +58,7 @@ const ( TypeMPEG MediaType = "video/mpeg" Type3GP MediaType = "video/3gpp" Type3G2 MediaType = "video/3gpp2" - TypeOGV MediaType = "video/ogg" + TypeOGG MediaType = "video/ogg" TypeWMV MediaType = "video/x-ms-wmv" TypeAVI MediaType = "video/x-msvideo" TypeWEBM MediaType = "video/webm" @@ -120,7 +120,7 @@ var VideoMimetypes = [...]MediaType{ TypeMPEG, Type3GP, Type3G2, - TypeOGV, + TypeOGG, TypeWMV, TypeAVI, TypeWEBM, @@ -129,6 +129,14 @@ var VideoMimetypes = [...]MediaType{ TypeMTS, } +// WebVideoMimetypes are video types that can be played directly in the browser without transcoding +var WebVideoMimetypes = [...]MediaType{ + TypeMP4, + TypeMPEG, + TypeWEBM, + TypeOGG, +} + var fileExtensions = map[string]MediaType{ ".jpg": TypeJpeg, ".jpeg": TypeJpeg, @@ -167,7 +175,7 @@ var fileExtensions = map[string]MediaType{ ".mpeg": TypeMPEG, ".3gp": Type3GP, ".3g2": Type3G2, - ".ogv": TypeOGV, + ".ogv": TypeOGG, ".wmv": TypeWMV, ".avi": TypeAVI, ".webm": TypeWEBM, @@ -195,6 +203,12 @@ func (imgType *MediaType) isWebCompatible() bool { } } + for _, web_mime := range WebVideoMimetypes { + if web_mime == *imgType { + return true + } + } + return false } diff --git a/api/scanner/process_photo.go b/api/scanner/process_photo.go index 9c4b4db0..5cecead8 100644 --- a/api/scanner/process_photo.go +++ b/api/scanner/process_photo.go @@ -40,6 +40,25 @@ func makePhotoURLChecker(tx *gorm.DB, mediaID int) func(purpose models.MediaPurp } } +func generateUniqueMediaNamePrefixed(prefix string, mediaPath string, extension string) string { + mediaName := fmt.Sprintf("%s_%s_%s", prefix, path.Base(mediaPath), utils.GenerateToken()) + mediaName = models.SanitizeMediaName(mediaName) + mediaName = mediaName + extension + return mediaName +} + +func generateUniqueMediaName(mediaPath string) string { + + filename := path.Base(mediaPath) + baseName := filename[0 : len(filename)-len(path.Ext(filename))] + baseExt := path.Ext(filename) + + mediaName := fmt.Sprintf("%s_%s", baseName, utils.GenerateToken()) + mediaName = models.SanitizeMediaName(mediaName) + baseExt + + return mediaName +} + func ProcessMedia(tx *gorm.DB, media *models.Media) (bool, error) { imageData := EncodeMediaData{ media: media, @@ -110,13 +129,11 @@ func processPhoto(tx *gorm.DB, imageData *EncodeMediaData, photoCachePath *strin if !contentType.isWebCompatible() { didProcess = true - highres_name := fmt.Sprintf("highres_%s_%s", path.Base(photo.Path), utils.GenerateToken()) - highres_name = models.SanitizeMediaName(highres_name) - highres_name = highres_name + ".jpg" + highresName := generateUniqueMediaNamePrefixed("highres", photo.Path, ".jpg") - baseImagePath = path.Join(*photoCachePath, highres_name) + baseImagePath = path.Join(*photoCachePath, highresName) - newHighResURL, err := generateSaveHighResJPEG(tx, photo, imageData, highres_name, baseImagePath, nil) + newHighResURL, err := generateSaveHighResJPEG(tx, photo, imageData, highresName, baseImagePath, nil) if err != nil { return false, err } @@ -159,11 +176,9 @@ func processPhoto(tx *gorm.DB, imageData *EncodeMediaData, photoCachePath *strin if thumbURL == nil { didProcess = true - thumbnail_name := fmt.Sprintf("thumbnail_%s_%s", path.Base(photo.Path), utils.GenerateToken()) - thumbnail_name = models.SanitizeMediaName(thumbnail_name) - thumbnail_name = thumbnail_name + ".jpg" + thumbnailName := generateUniqueMediaNamePrefixed("thumbnail", photo.Path, ".jpg") - newThumbURL, err := generateSaveThumbnailJPEG(tx, photo, thumbnail_name, photoCachePath, baseImagePath, nil) + newThumbURL, err := generateSaveThumbnailJPEG(tx, photo, thumbnailName, photoCachePath, baseImagePath, nil) if err != nil { return false, err } @@ -228,12 +243,7 @@ func makeMediaCacheDir(media *models.Media) (*string, error) { } func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *EncodeMediaData, photoDimensions *PhotoDimensions) error { - photoName := path.Base(photo.Path) - photoBaseName := photoName[0 : len(photoName)-len(path.Ext(photoName))] - photoBaseExt := path.Ext(photoName) - - original_image_name := fmt.Sprintf("%s_%s", photoBaseName, utils.GenerateToken()) - original_image_name = models.SanitizeMediaName(original_image_name) + photoBaseExt + originalImageName := generateUniqueMediaName(photo.Path) contentType, err := imageData.ContentType() if err != nil { @@ -247,7 +257,7 @@ func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *EncodeMe mediaURL := models.MediaURL{ Media: *photo, - MediaName: original_image_name, + MediaName: originalImageName, Width: photoDimensions.Width, Height: photoDimensions.Height, Purpose: models.MediaOriginal, @@ -256,7 +266,7 @@ func saveOriginalPhotoToDB(tx *gorm.DB, photo *models.Media, imageData *EncodeMe } if err := tx.Create(&mediaURL).Error; err != nil { - return errors.Wrapf(err, "inserting original photo url: %d, %s", photo.ID, photoName) + return errors.Wrapf(err, "inserting original photo url: %d, %s", photo.ID, photo.Title) } return nil diff --git a/api/scanner/process_video.go b/api/scanner/process_video.go index 63aa0c53..8f72eab7 100644 --- a/api/scanner/process_video.go +++ b/api/scanner/process_video.go @@ -9,9 +9,9 @@ import ( "strings" "time" - "github.com/pkg/errors" "github.com/photoview/photoview/api/graphql/models" "github.com/photoview/photoview/api/utils" + "github.com/pkg/errors" "gopkg.in/vansante/go-ffprobe.v2" "gorm.io/gorm" ) @@ -24,6 +24,11 @@ func processVideo(tx *gorm.DB, mediaData *EncodeMediaData, videoCachePath *strin mediaURLFromDB := makePhotoURLChecker(tx, video.ID) + videoOriginalURL, err := mediaURLFromDB(models.MediaOriginal) + if err != nil { + return false, errors.Wrap(err, "error processing video original format") + } + videoWebURL, err := mediaURLFromDB(models.VideoWeb) if err != nil { return false, errors.Wrap(err, "error processing video web-format") @@ -34,7 +39,44 @@ func processVideo(tx *gorm.DB, mediaData *EncodeMediaData, videoCachePath *strin return false, errors.Wrap(err, "error processing video thumbnail") } - if videoWebURL == nil { + videoType, err := mediaData.ContentType() + if err != nil { + return false, errors.Wrap(err, "error getting video content type") + } + + if videoOriginalURL == nil && videoType.isWebCompatible() { + didProcess = true + + origVideoPath := video.Path + videoMediaName := generateUniqueMediaName(video.Path) + + webMetadata, err := readVideoStreamMetadata(origVideoPath) + if err != nil { + return false, errors.Wrapf(err, "failed to read metadata for original video (%s)", video.Title) + } + + fileStats, err := os.Stat(origVideoPath) + if err != nil { + return false, errors.Wrap(err, "reading file stats of original video") + } + + mediaURL := models.MediaURL{ + MediaID: video.ID, + MediaName: videoMediaName, + Width: webMetadata.Width, + Height: webMetadata.Height, + Purpose: models.MediaOriginal, + ContentType: string(*videoType), + FileSize: fileStats.Size(), + } + + if err := tx.Create(&mediaURL).Error; err != nil { + return false, errors.Wrapf(err, "failed to insert original video into database (%s)", video.Title) + } + + } + + if videoWebURL == nil && !videoType.isWebCompatible() { didProcess = true web_video_name := fmt.Sprintf("web_video_%s_%s", path.Base(video.Path), utils.GenerateToken())