Use new thumbnails

This commit is contained in:
Ben Visness 2023-05-18 22:07:14 -05:00
parent 65aab39432
commit cdacc5b3a0
5 changed files with 107 additions and 36 deletions

View File

@ -52,19 +52,30 @@ function makeShowcaseItem(timelineItem) {
break; break;
case TimelineMediaTypes.VIDEO: case TimelineMediaTypes.VIDEO:
addThumbnailFunc = () => { addThumbnailFunc = () => {
const video = document.createElement('video'); let thumbEl;
video.src = timelineItem.asset_url; // TODO: Use image thumbnails if (timelineItem.thumbnail_url) {
video.controls = false; thumbEl = document.createElement('img');
video.classList.add('h-100'); thumbEl.src = timelineItem.thumbnail_url;
video.preload = 'metadata'; } else {
itemEl.thumbnail.appendChild(video); 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 = () => { createModalContentFunc = () => {
const modalVideo = document.createElement('video'); const modalVideo = document.createElement('video');
modalVideo.src = timelineItem.asset_url; 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.controls = true;
modalVideo.preload = 'metadata';
modalVideo.classList.add('mw-100', 'mh-60vh'); modalVideo.classList.add('mw-100', 'mh-60vh');
return modalVideo; return modalVideo;
}; };

View File

@ -6,6 +6,7 @@ import (
"crypto/sha1" "crypto/sha1"
"errors" "errors"
"fmt" "fmt"
"image"
"io" "io"
"net/http" "net/http"
"os" "os"
@ -129,8 +130,9 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As
var thumbnailKey *string var thumbnailKey *string
previewBytes, err := ExtractPreview(ctx, in.ContentType, in.Content) width := in.Width
if err != nil { 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") logging.Error().Err(err).Msg("Failed to generate preview for asset")
} else if len(previewBytes) > 0 { } else if len(previewBytes) > 0 {
keyStr := AssetKey(id.String(), fmt.Sprintf("%s_thumb.png", id.String())) 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 { } else {
thumbnailKey = &keyStr thumbnailKey = &keyStr
} }
if width == 0 || height == 0 {
width = thumbWidth
height = thumbHeight
}
} }
// Save a record in our database // 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), len(in.Content),
in.ContentType, in.ContentType,
checksum, checksum,
in.Width, width,
in.Height, height,
in.UploaderID, in.UploaderID,
) )
if err != nil { if err != nil {
@ -187,31 +194,46 @@ func Create(ctx context.Context, dbConn db.ConnOrTx, in CreateInput) (*models.As
return asset, nil return asset, nil
} }
func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byte, error) { func getFFMpegPath() string {
if config.Config.PreviewGeneration.FFMpegPath == "" { path := config.Config.PreviewGeneration.FFMpegPath
return nil, nil 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") { if !strings.HasPrefix(mimeType, "video") {
return nil, nil return nil, 0, 0, nil
} }
file, err := os.CreateTemp("", "hmnasset") file, err := os.CreateTemp("", "hmnasset")
if err != nil { 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()) defer os.Remove(file.Name())
_, err = file.Write(inBytes) _, err = file.Write(inBytes)
if err != nil { 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() err = file.Close()
if err != nil { 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()) 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 != "" { if config.Config.PreviewGeneration.CPULimitPath != "" {
args = fmt.Sprintf("-l 10 -- %s %s", execPath, args) args = fmt.Sprintf("-l 10 -- %s %s", execPath, args)
execPath = config.Config.PreviewGeneration.CPULimitPath execPath = config.Config.PreviewGeneration.CPULimitPath
@ -224,11 +246,17 @@ func ExtractPreview(ctx context.Context, mimeType string, inBytes []byte) ([]byt
ffmpegCmd.Stderr = &errorOut ffmpegCmd.Stderr = &errorOut
err = ffmpegCmd.Run() err = ffmpegCmd.Run()
if err != nil { if err != nil {
logging.Error().Str("ffmpeg output", string(errorOut.Bytes())).Msg("FFMpeg returned error while generating preview thumbnail") log.Error().Str("ffmpeg output", errorOut.String()).Msg("FFMpeg returned error while generating preview thumbnail")
return nil, oops.New(err, "FFMpeg failed for preview generation") 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 { 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() { go func() {
defer job.Done() defer job.Done()
log.Debug().Msg("Starting preview gen job") 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, assets, err := db.Query[models.Asset](ctx, conn,
` `
SELECT $columns SELECT $columns
FROM asset 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 { if err != nil {
@ -258,7 +299,11 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J
return return
default: 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) assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
resp, err := http.Get(assetUrl) resp, err := http.Get(assetUrl)
if err != nil || resp.StatusCode != 200 { 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") log.Error().Err(err).Msg("Failed to read asset body for preview generation")
continue continue
} }
thumbBytes, err := ExtractPreview(ctx, asset.MimeType, body) thumbBytes, width, height, err := ExtractPreview(ctx, asset.MimeType, body)
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to run extraction for preview generation") log.Error().Err(err).Msg("Failed to run extraction for preview generation")
continue continue
@ -293,17 +338,24 @@ func BackgroundPreviewGeneration(ctx context.Context, conn *pgxpool.Pool) jobs.J
_, err = conn.Exec(ctx, _, err = conn.Exec(ctx,
` `
UPDATE asset UPDATE asset
SET thumbnail_s3_key = $1 SET
WHERE asset.id = $2 thumbnail_s3_key = $1,
width = $2,
height = $3
WHERE asset.id = $4
`, `,
keyStr, keyStr,
width,
height,
asset.ID, asset.ID,
) )
if err != nil { if err != nil {
log.Error().Err(err).Msg("Failed to update asset for preview generation") log.Error().Err(err).Msg("Failed to update asset for preview generation")
continue 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") log.Debug().Msg("No more previews to generate")

View File

@ -9,7 +9,7 @@ type Asset struct {
UploaderID *int `db:"uploader_id"` UploaderID *int `db:"uploader_id"`
S3Key string `db:"s3_key"` S3Key string `db:"s3_key"`
ThumbnailS3Key string `db:"thumbnail_s3_key'` ThumbnailS3Key string `db:"thumbnail_s3_key"`
Filename string `db:"filename"` Filename string `db:"filename"`
Size int `db:"size"` Size int `db:"size"`
MimeType string `db:"mime_type"` MimeType string `db:"mime_type"`

View File

@ -55,7 +55,11 @@
{{ if eq .Type mediaimage }} {{ if eq .Type mediaimage }}
<img src="{{ .AssetUrl }}"> <img src="{{ .AssetUrl }}">
{{ else if eq .Type mediavideo }} {{ else if eq .Type mediavideo }}
<video src="{{ .AssetUrl }}" preload="metadata" controls> {{ if .ThumbnailUrl }}
<video src="{{ .AssetUrl }}" poster="{{ .ThumbnailUrl }}" preload="none" controls>
{{ else }}
<video src="{{ .AssetUrl }}" preload="metadata" controls>
{{ end }}
{{ else if eq .Type mediaaudio }} {{ else if eq .Type mediaaudio }}
<audio src="{{ .AssetUrl }}" controls> <audio src="{{ .AssetUrl }}" controls>
{{ else if eq .Type mediaembed }} {{ else if eq .Type mediaembed }}

View File

@ -158,14 +158,18 @@ func imageMediaItem(asset *models.Asset) templates.TimelineItemMedia {
func videoMediaItem(asset *models.Asset) templates.TimelineItemMedia { func videoMediaItem(asset *models.Asset) templates.TimelineItemMedia {
assetUrl := hmnurl.BuildS3Asset(asset.S3Key) assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
var thumbnailUrl string
if asset.ThumbnailS3Key != "" {
thumbnailUrl = hmnurl.BuildS3Asset(asset.ThumbnailS3Key)
}
return templates.TimelineItemMedia{ return templates.TimelineItemMedia{
Type: templates.TimelineItemMediaTypeVideo, Type: templates.TimelineItemMediaTypeVideo,
AssetUrl: assetUrl, AssetUrl: assetUrl,
// TODO: Use image thumbnails ThumbnailUrl: thumbnailUrl,
MimeType: asset.MimeType, MimeType: asset.MimeType,
Width: asset.Width, Width: asset.Width,
Height: asset.Height, Height: asset.Height,
} }
} }