diff --git a/public/js/showcase.js b/public/js/showcase.js index 7e9c47f2..772052ad 100644 --- a/public/js/showcase.js +++ b/public/js/showcase.js @@ -52,19 +52,30 @@ function makeShowcaseItem(timelineItem) { break; case TimelineMediaTypes.VIDEO: addThumbnailFunc = () => { - const video = document.createElement('video'); - video.src = timelineItem.asset_url; // TODO: Use image thumbnails - video.controls = false; - video.classList.add('h-100'); - video.preload = 'metadata'; - itemEl.thumbnail.appendChild(video); + let thumbEl; + if (timelineItem.thumbnail_url) { + thumbEl = document.createElement('img'); + thumbEl.src = timelineItem.thumbnail_url; + } else { + thumbEl = document.createElement('video'); + thumbEl.src = timelineItem.asset_url; + thumbEl.controls = false; + thumbEl.preload = 'metadata'; + } + thumbEl.classList.add('h-100'); + itemEl.thumbnail.appendChild(thumbEl); }; createModalContentFunc = () => { const modalVideo = document.createElement('video'); modalVideo.src = timelineItem.asset_url; + if (timelineItem.thumbnail_url) { + modalVideo.poster = timelineItem.thumbnail_url; + modalVideo.preload = 'none'; + } else { + modalVideo.preload = 'metadata'; + } modalVideo.controls = true; - modalVideo.preload = 'metadata'; modalVideo.classList.add('mw-100', 'mh-60vh'); return modalVideo; }; diff --git a/src/assets/assets.go b/src/assets/assets.go index 167557de..12ada70e 100644 --- a/src/assets/assets.go +++ b/src/assets/assets.go @@ -6,6 +6,7 @@ import ( "crypto/sha1" "errors" "fmt" + "image" "io" "net/http" "os" @@ -129,8 +130,9 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As var thumbnailKey *string - previewBytes, err := ExtractPreview(ctx, in.ContentType, in.Content) - if err != nil { + width := in.Width + height := in.Height + if previewBytes, thumbWidth, thumbHeight, err := ExtractPreview(ctx, in.ContentType, in.Content); err != nil { logging.Error().Err(err).Msg("Failed to generate preview for asset") } else if len(previewBytes) > 0 { keyStr := AssetKey(id.String(), fmt.Sprintf("%s_thumb.png", id.String())) @@ -147,6 +149,11 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As } else { thumbnailKey = &keyStr } + + if width == 0 || height == 0 { + width = thumbWidth + height = thumbHeight + } } // Save a record in our database @@ -163,8 +170,8 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As len(in.Content), in.ContentType, checksum, - in.Width, - in.Height, + width, + height, in.UploaderID, ) if err != nil { @@ -187,31 +194,46 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As return asset, nil } -func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byte, error) { - if config.Config.PreviewGeneration.FFMpegPath == "" { - return nil, nil +func getFFMpegPath() string { + path := config.Config.PreviewGeneration.FFMpegPath + if path != "" { + return path + } + var err error + path, err = exec.LookPath("ffmpeg") + if err == nil { + return path + } + return "" +} + +func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byte, int, int, error) { + log := logging.ExtractLogger(ctx) + + execPath := getFFMpegPath() + if execPath == "" { + return nil, 0, 0, nil } if !strings.HasPrefix(mimeType, "video") { - return nil, nil + return nil, 0, 0, nil } file, err := os.CreateTemp("", "hmnasset") if err != nil { - return nil, oops.New(err, "Failed to create temp file for preview generation") + return nil, 0, 0, oops.New(err, "Failed to create temp file for preview generation") } defer os.Remove(file.Name()) _, err = file.Write(inBytes) if err != nil { - return nil, oops.New(err, "Failed to write to temp file for preview generation") + return nil, 0, 0, oops.New(err, "Failed to write to temp file for preview generation") } err = file.Close() if err != nil { - return nil, oops.New(err, "Failed to close temp file for preview generation") + return nil, 0, 0, oops.New(err, "Failed to close temp file for preview generation") } args := fmt.Sprintf("-i %s -filter_complex [0]select=gte(n\\,1)[s0] -map [s0] -f image2 -vcodec png -vframes 1 pipe:1", file.Name()) - execPath := config.Config.PreviewGeneration.FFMpegPath if config.Config.PreviewGeneration.CPULimitPath != "" { args = fmt.Sprintf("-l 10 -- %s %s", execPath, args) execPath = config.Config.PreviewGeneration.CPULimitPath @@ -224,11 +246,17 @@ func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byt ffmpegCmd.Stderr = &errorOut err = ffmpegCmd.Run() if err != nil { - logging.Error().Str("ffmpeg output", string(errorOut.Bytes())).Msg("FFMpeg returned error while generating preview thumbnail") - return nil, oops.New(err, "FFMpeg failed for preview generation") + log.Error().Str("ffmpeg output", errorOut.String()).Msg("FFMpeg returned error while generating preview thumbnail") + return nil, 0, 0, oops.New(err, "FFMpeg failed for preview generation") } - return output.Bytes(), nil + imageBytes := output.Bytes() + cfg, _, err := image.DecodeConfig(bytes.NewBuffer(imageBytes)) + if err != nil { + log.Error().Err(err).Msg("failed to get width/height from video thumbnail") + } + + return imageBytes, cfg.Width, cfg.Height, nil } func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.Job { @@ -238,11 +266,24 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J go func() { defer job.Done() log.Debug().Msg("Starting preview gen job") + + if getFFMpegPath() == "" { + log.Warn().Msg("Couldn't find ffmpeg! No thumbnails will be generated.") + return + } + assets, err := db.Query[models.Asset](ctx, conn, ` SELECT $columns FROM asset - WHERE mime_type LIKE 'video%' AND (thumbnail_s3_key IS NULL OR thumbnail_s3_key = '') + WHERE + mime_type LIKE 'video%' + AND ( + thumbnail_s3_key IS NULL + OR thumbnail_s3_key = '' + OR width = 0 + OR height = 0 + ) `, ) if err != nil { @@ -258,7 +299,11 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J return default: } - log.Debug().Str("AssetID", asset.ID.String()).Msg("Generating preview") + + log := log.With().Str("AssetID", asset.ID.String()).Logger() + ctx := logging.AttachLoggerToContext(&log, ctx) + + log.Debug().Msg("Generating preview") assetUrl := hmnurl.BuildS3Asset(asset.S3Key) resp, err := http.Get(assetUrl) if err != nil || resp.StatusCode != 200 { @@ -271,7 +316,7 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J log.Error().Err(err).Msg("Failed to read asset body for preview generation") continue } - thumbBytes, err := ExtractPreview(ctx, asset.MimeType, body) + thumbBytes, width, height, err := ExtractPreview(ctx, asset.MimeType, body) if err != nil { log.Error().Err(err).Msg("Failed to run extraction for preview generation") continue @@ -293,17 +338,24 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J _, err = conn.Exec(ctx, ` UPDATE asset - SET thumbnail_s3_key = $1 - WHERE asset.id = $2 + SET + thumbnail_s3_key = $1, + width = $2, + height = $3 + WHERE asset.id = $4 `, keyStr, + width, + height, asset.ID, ) if err != nil { log.Error().Err(err).Msg("Failed to update asset for preview generation") continue } - log.Debug().Str("AssetID", asset.ID.String()).Msg("Generated preview successfully!") + log.Debug().Msg("Generated preview successfully!") + } else { + log.Debug().Msg("No error, but no thumbnail was generated, skipping") } } log.Debug().Msg("No more previews to generate") diff --git a/src/models/asset.go b/src/models/asset.go index 6badc109..1ae6fe48 100644 --- a/src/models/asset.go +++ b/src/models/asset.go @@ -9,7 +9,7 @@ type Asset struct { UploaderID *int `db:"uploader_id"` S3Key string `db:"s3_key"` - ThumbnailS3Key string `db:"thumbnail_s3_key'` + ThumbnailS3Key string `db:"thumbnail_s3_key"` Filename string `db:"filename"` Size int `db:"size"` MimeType string `db:"mime_type"` diff --git a/src/templates/src/include/timeline_item.html b/src/templates/src/include/timeline_item.html index 801be0ba..dd565019 100644 --- a/src/templates/src/include/timeline_item.html +++ b/src/templates/src/include/timeline_item.html @@ -55,7 +55,11 @@ {{ if eq .Type mediaimage }} {{ else if eq .Type mediavideo }} -