Use new thumbnails
This commit is contained in:
parent
65aab39432
commit
cdacc5b3a0
|
@ -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;
|
||||||
modalVideo.controls = true;
|
if (timelineItem.thumbnail_url) {
|
||||||
|
modalVideo.poster = timelineItem.thumbnail_url;
|
||||||
|
modalVideo.preload = 'none';
|
||||||
|
} else {
|
||||||
modalVideo.preload = 'metadata';
|
modalVideo.preload = 'metadata';
|
||||||
|
}
|
||||||
|
modalVideo.controls = true;
|
||||||
modalVideo.classList.add('mw-100', 'mh-60vh');
|
modalVideo.classList.add('mw-100', 'mh-60vh');
|
||||||
return modalVideo;
|
return modalVideo;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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"`
|
||||||
|
|
|
@ -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 }}
|
||||||
|
{{ if .ThumbnailUrl }}
|
||||||
|
<video src="{{ .AssetUrl }}" poster="{{ .ThumbnailUrl }}" preload="none" controls>
|
||||||
|
{{ else }}
|
||||||
<video src="{{ .AssetUrl }}" preload="metadata" controls>
|
<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 }}
|
||||||
|
|
|
@ -158,11 +158,15 @@ 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,
|
||||||
|
|
Loading…
Reference in New Issue