feat: generate multiple thumbs and posters
This commit is contained in:
9
apps/worker/src/__tests__/variants-sizes.test.ts
Normal file
9
apps/worker/src/__tests__/variants-sizes.test.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { test, expect } from "bun:test";
|
||||||
|
import { computeImageVariantPlan } from "../variants";
|
||||||
|
|
||||||
|
test("computeImageVariantPlan includes 256 and 768 thumbs", () => {
|
||||||
|
expect(computeImageVariantPlan()).toEqual([
|
||||||
|
{ kind: "thumb", size: 256 },
|
||||||
|
{ kind: "thumb", size: 768 },
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -7,6 +7,8 @@ import { Readable } from "stream";
|
|||||||
|
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
|
|
||||||
|
import { computeImageVariantPlan, computeVideoPosterPlan } from "./variants";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
CopyObjectCommand,
|
CopyObjectCommand,
|
||||||
GetObjectCommand,
|
GetObjectCommand,
|
||||||
@@ -426,53 +428,37 @@ export async function handleProcessAsset(raw: unknown) {
|
|||||||
if (updates.width === null && imgMeta.width) updates.width = imgMeta.width;
|
if (updates.width === null && imgMeta.width) updates.width = imgMeta.width;
|
||||||
if (updates.height === null && imgMeta.height) updates.height = imgMeta.height;
|
if (updates.height === null && imgMeta.height) updates.height = imgMeta.height;
|
||||||
|
|
||||||
const thumb256Path = join(tempDir, "thumb_256.jpg");
|
const imagePlan = computeImageVariantPlan();
|
||||||
const thumb768Path = join(tempDir, "thumb_768.jpg");
|
const thumbKeys: Record<number, string> = {};
|
||||||
|
for (const item of imagePlan) {
|
||||||
|
const size = item.size;
|
||||||
|
const thumbPath = join(tempDir, `thumb_${size}.jpg`);
|
||||||
await sharp(inputPath)
|
await sharp(inputPath)
|
||||||
.rotate()
|
.rotate()
|
||||||
.resize(256, 256, { fit: "inside", withoutEnlargement: true })
|
.resize(size, size, { fit: "inside", withoutEnlargement: true })
|
||||||
.jpeg({ quality: 80 })
|
.jpeg({ quality: 80 })
|
||||||
.toFile(thumb256Path);
|
.toFile(thumbPath);
|
||||||
await sharp(inputPath)
|
|
||||||
.rotate()
|
|
||||||
.resize(768, 768, { fit: "inside", withoutEnlargement: true })
|
|
||||||
.jpeg({ quality: 80 })
|
|
||||||
.toFile(thumb768Path);
|
|
||||||
|
|
||||||
const thumb256Key = `thumbs/${asset.id}/image_256.jpg`;
|
const thumbKey = `thumbs/${asset.id}/image_${size}.jpg`;
|
||||||
const thumb768Key = `thumbs/${asset.id}/image_768.jpg`;
|
|
||||||
await uploadObject({
|
await uploadObject({
|
||||||
bucket: asset.bucket,
|
bucket: asset.bucket,
|
||||||
key: thumb256Key,
|
key: thumbKey,
|
||||||
filePath: thumb256Path,
|
filePath: thumbPath,
|
||||||
contentType: "image/jpeg",
|
|
||||||
});
|
|
||||||
await uploadObject({
|
|
||||||
bucket: asset.bucket,
|
|
||||||
key: thumb768Key,
|
|
||||||
filePath: thumb768Path,
|
|
||||||
contentType: "image/jpeg",
|
contentType: "image/jpeg",
|
||||||
});
|
});
|
||||||
await upsertVariant({
|
await upsertVariant({
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
kind: "thumb",
|
kind: "thumb",
|
||||||
size: 256,
|
size,
|
||||||
key: thumb256Key,
|
key: thumbKey,
|
||||||
mimeType: "image/jpeg",
|
mimeType: "image/jpeg",
|
||||||
width: typeof updates.width === "number" ? updates.width : null,
|
width: typeof updates.width === "number" ? updates.width : null,
|
||||||
height: typeof updates.height === "number" ? updates.height : null,
|
height: typeof updates.height === "number" ? updates.height : null,
|
||||||
});
|
});
|
||||||
await upsertVariant({
|
thumbKeys[size] = thumbKey;
|
||||||
assetId: asset.id,
|
}
|
||||||
kind: "thumb",
|
updates.thumb_small_key = thumbKeys[256] ?? null;
|
||||||
size: 768,
|
updates.thumb_med_key = thumbKeys[768] ?? null;
|
||||||
key: thumb768Key,
|
|
||||||
mimeType: "image/jpeg",
|
|
||||||
width: typeof updates.width === "number" ? updates.width : null,
|
|
||||||
height: typeof updates.height === "number" ? updates.height : null,
|
|
||||||
});
|
|
||||||
updates.thumb_small_key = thumb256Key;
|
|
||||||
updates.thumb_med_key = thumb768Key;
|
|
||||||
} else if (asset.media_type === "video") {
|
} else if (asset.media_type === "video") {
|
||||||
rawTags = await tryReadExifTags();
|
rawTags = await tryReadExifTags();
|
||||||
maybeSetCaptureDateFromTags(rawTags);
|
maybeSetCaptureDateFromTags(rawTags);
|
||||||
@@ -512,12 +498,16 @@ export async function handleProcessAsset(raw: unknown) {
|
|||||||
|
|
||||||
rawTags = { ...rawTags, ffprobe: ffprobeData };
|
rawTags = { ...rawTags, ffprobe: ffprobeData };
|
||||||
|
|
||||||
const posterPath = join(tempDir, "poster_256.jpg");
|
const posterPlan = computeVideoPosterPlan();
|
||||||
|
const posterKeys: Record<number, string> = {};
|
||||||
|
for (const item of posterPlan) {
|
||||||
|
const size = item.size;
|
||||||
|
const posterPath = join(tempDir, `poster_${size}.jpg`);
|
||||||
await runCommand("ffmpeg", [
|
await runCommand("ffmpeg", [
|
||||||
"-i",
|
"-i",
|
||||||
inputPath,
|
inputPath,
|
||||||
"-vf",
|
"-vf",
|
||||||
"scale=256:256:force_original_aspect_ratio=decrease",
|
`scale=${size}:${size}:force_original_aspect_ratio=decrease`,
|
||||||
"-vframes",
|
"-vframes",
|
||||||
"1",
|
"1",
|
||||||
"-q:v",
|
"-q:v",
|
||||||
@@ -525,7 +515,7 @@ export async function handleProcessAsset(raw: unknown) {
|
|||||||
"-y",
|
"-y",
|
||||||
posterPath
|
posterPath
|
||||||
]);
|
]);
|
||||||
const posterKey = `thumbs/${asset.id}/poster_256.jpg`;
|
const posterKey = `thumbs/${asset.id}/poster_${size}.jpg`;
|
||||||
await uploadObject({
|
await uploadObject({
|
||||||
bucket: asset.bucket,
|
bucket: asset.bucket,
|
||||||
key: posterKey,
|
key: posterKey,
|
||||||
@@ -535,13 +525,15 @@ export async function handleProcessAsset(raw: unknown) {
|
|||||||
await upsertVariant({
|
await upsertVariant({
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
kind: "poster",
|
kind: "poster",
|
||||||
size: 256,
|
size,
|
||||||
key: posterKey,
|
key: posterKey,
|
||||||
mimeType: "image/jpeg",
|
mimeType: "image/jpeg",
|
||||||
width: typeof updates.width === "number" ? updates.width : null,
|
width: typeof updates.width === "number" ? updates.width : null,
|
||||||
height: typeof updates.height === "number" ? updates.height : null,
|
height: typeof updates.height === "number" ? updates.height : null,
|
||||||
});
|
});
|
||||||
updates.poster_key = posterKey;
|
posterKeys[size] = posterKey;
|
||||||
|
}
|
||||||
|
updates.poster_key = posterKeys[256] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset.media_type === "video" && typeof updates.poster_key !== "string") {
|
if (asset.media_type === "video" && typeof updates.poster_key !== "string") {
|
||||||
|
|||||||
18
apps/worker/src/variants.ts
Normal file
18
apps/worker/src/variants.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export type VariantPlanItem = {
|
||||||
|
kind: "thumb" | "poster";
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function computeImageVariantPlan(): VariantPlanItem[] {
|
||||||
|
return [
|
||||||
|
{ kind: "thumb", size: 256 },
|
||||||
|
{ kind: "thumb", size: 768 },
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function computeVideoPosterPlan(): VariantPlanItem[] {
|
||||||
|
return [
|
||||||
|
{ kind: "poster", size: 256 },
|
||||||
|
{ kind: "poster", size: 768 },
|
||||||
|
];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user