import express from "express"; import ffmpeg from "fluent-ffmpeg"; import { PassThrough } from "stream"; import NodeCache from "node-cache"; const app = express(); const PORT = process.env.PORT || 3100; const thumbnailCache = new NodeCache({ stdTTL: 3600, checkperiod: 600, useClones: false }); app.get("/thumbnail", (req, res) => { const videoUrl = req.query.url?.trim(); if (!videoUrl) { return res.status(400).json({ error: "Missing 'url' query parameter" }); } const cachedImage = thumbnailCache.get(videoUrl); if (cachedImage) { res.setHeader("Content-Type", "image/jpeg"); res.setHeader("Cache-Control", "public, max-age=86400"); return res.send(cachedImage); } try { const passthrough = new PassThrough(); let imageBuffer = Buffer.alloc(0); passthrough.on("data", (chunk) => { imageBuffer = Buffer.concat([imageBuffer, chunk]); }); passthrough.on("end", () => { if (imageBuffer.length > 0) { thumbnailCache.set(videoUrl, imageBuffer); } }); ffmpeg(videoUrl) .seekInput(1) .frames(1) .format("image2") .outputOptions("-vcodec", "mjpeg") .on("error", (err) => { console.error("ffmpeg error:", err.message); if (!res.headersSent) { res.status(500).json({ error: "Failed to generate thumbnail" }); } }) .pipe(passthrough, { end: true }); res.setHeader("Content-Type", "image/jpeg"); res.setHeader("Cache-Control", "public, max-age=86400"); passthrough.pipe(res); } catch (err) { console.error("Error:", err); if (!res.headersSent) { res.status(500).json({ error: "Failed to process video" }); } } }); app.get("/health", (_req, res) => { res.json({ status: "ok" }); }); app.listen(PORT, () => { console.log(`Video thumbnail service running on port ${PORT}`); });