Files
overlord/Overlord-Server/src/thumbnails.ts
T

102 lines
2.8 KiB
TypeScript
Raw Normal View History

let _sharp: typeof import("sharp") extends { default: infer D } ? D : never;
async function getSharp() {
if (!_sharp) {
_sharp = (await import("sharp")).default;
}
return _sharp;
}
2026-03-22 23:21:27 -05:00
const THUMBNAIL_WIDTH = Math.max(64, Number(process.env.OVERLORD_THUMBNAIL_WIDTH || 640));
const THUMBNAIL_HEIGHT = Math.max(48, Number(process.env.OVERLORD_THUMBNAIL_HEIGHT || 360));
2026-03-22 23:21:27 -05:00
const THUMBNAIL_QUALITY = Math.min(90, Math.max(40, Number(process.env.OVERLORD_THUMBNAIL_QUALITY || 72)));
const MAX_THUMBNAIL_SOURCE_BYTES = Math.max(
256 * 1024,
Number(process.env.OVERLORD_THUMBNAIL_MAX_SOURCE_BYTES || 16 * 1024 * 1024),
);
2026-03-05 00:04:26 -06:00
const thumbnails = new Map<string, string>();
2026-03-22 23:21:27 -05:00
const latestFrames = new Map<string, { bytes: Uint8Array; format: string; capturedAt: number }>();
2026-03-05 00:04:26 -06:00
const thumbnailRequests = new Map<string, number>();
export function setThumbnail(id: string, dataUrl: string) {
thumbnails.set(id, dataUrl);
}
export function getThumbnail(id: string) {
return thumbnails.get(id) ?? null;
}
export function clearThumbnail(id: string) {
thumbnails.delete(id);
latestFrames.delete(id);
thumbnailRequests.delete(id);
}
export function setLatestFrame(id: string, bytes: Uint8Array, format: string) {
2026-03-22 23:21:27 -05:00
if (!bytes || bytes.byteLength === 0 || bytes.byteLength > MAX_THUMBNAIL_SOURCE_BYTES) {
latestFrames.delete(id);
return;
}
latestFrames.set(id, { bytes, format, capturedAt: Date.now() });
2026-03-05 00:04:26 -06:00
}
2026-03-22 23:21:27 -05:00
async function buildThumbnailDataUrl(bytes: Uint8Array, format: string): Promise<string | null> {
if (!bytes || bytes.byteLength === 0) {
return null;
}
const inputFormat = format === "jpg" ? "jpeg" : format;
if (!["jpeg", "webp"].includes(inputFormat)) {
return null;
}
const sharp = await getSharp();
2026-03-22 23:21:27 -05:00
const output = await sharp(Buffer.from(bytes), { failOn: "none" })
.rotate()
.resize({
width: THUMBNAIL_WIDTH,
height: THUMBNAIL_HEIGHT,
fit: "inside",
withoutEnlargement: true,
fastShrinkOnLoad: true,
})
.webp({ quality: THUMBNAIL_QUALITY })
.toBuffer();
return `data:image/webp;base64,${output.toString("base64")}`;
}
export async function generateThumbnail(id: string): Promise<boolean> {
2026-03-05 00:04:26 -06:00
const frameData = latestFrames.get(id);
if (!frameData) {
return false;
}
2026-03-22 23:21:27 -05:00
try {
const dataUrl = await buildThumbnailDataUrl(frameData.bytes, frameData.format);
if (!dataUrl) {
return false;
}
thumbnails.set(id, dataUrl);
latestFrames.delete(id);
return true;
} catch {
return false;
}
2026-03-05 00:04:26 -06:00
}
export function markThumbnailRequested(id: string) {
thumbnailRequests.set(id, Date.now());
}
export function consumeThumbnailRequest(id: string, windowMs = 5000): boolean {
const ts = thumbnailRequests.get(id);
if (!ts) return false;
if (Date.now() - ts > windowMs) {
thumbnailRequests.delete(id);
return false;
}
thumbnailRequests.delete(id);
return true;
}