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;
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;
};

View File

@ -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")

View File

@ -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"`

View File

@ -55,7 +55,11 @@
{{ if eq .Type mediaimage }}
<img src="{{ .AssetUrl }}">
{{ 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 }}
<audio src="{{ .AssetUrl }}" controls>
{{ 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 {
assetUrl := hmnurl.BuildS3Asset(asset.S3Key)
var thumbnailUrl string
if asset.ThumbnailS3Key != "" {
thumbnailUrl = hmnurl.BuildS3Asset(asset.ThumbnailS3Key)
}
return templates.TimelineItemMedia{
Type: templates.TimelineItemMediaTypeVideo,
AssetUrl: assetUrl,
// TODO: Use image thumbnails
MimeType: asset.MimeType,
Width: asset.Width,
Height: asset.Height,
Type: templates.TimelineItemMediaTypeVideo,
AssetUrl: assetUrl,
ThumbnailUrl: thumbnailUrl,
MimeType: asset.MimeType,
Width: asset.Width,
Height: asset.Height,
}
}