Generate video thumbnails dynamically using the video stream

Implement FFmpeg to generate video thumbnails and add thumbnail caching.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 50814a1e-92e4-4968-856f-7bc7eedf5e8f
Replit-Commit-Checkpoint-Type: full_checkpoint
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/8cc42625-c1f5-4e43-99bd-77f2c4dedee2/50814a1e-92e4-4968-856f-7bc7eedf5e8f/1majdC5
This commit is contained in:
sebastjanartic
2025-08-04 19:43:01 +00:00
parent 801309a47e
commit 8949236543
3 changed files with 75 additions and 2 deletions
+55 -2
View File
@@ -118,9 +118,62 @@ export async function registerRoutes(app: Express): Promise<Server> {
}
});
// Temporary placeholder - waiting for user preference on thumbnail style
// Generate real thumbnails from Bunny.net videos using FFmpeg
app.get("/thumbnail/:videoId", async (req, res) => {
res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
try {
const { videoId } = req.params;
const { exec } = require('child_process');
const fs = require('fs');
const path = require('path');
// Get video URL from Bunny.net
const video = await storage.getVideo(videoId);
if (!video) {
return res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
}
// Create thumbnails directory if it doesn't exist
const thumbnailDir = path.join(process.cwd(), 'thumbnails');
if (!fs.existsSync(thumbnailDir)) {
fs.mkdirSync(thumbnailDir, { recursive: true });
}
const thumbnailPath = path.join(thumbnailDir, `${videoId}.jpg`);
// Check if thumbnail already exists
if (fs.existsSync(thumbnailPath)) {
res.setHeader('Content-Type', 'image/jpeg');
res.setHeader('Cache-Control', 'public, max-age=86400');
return res.sendFile(thumbnailPath);
}
// Generate video URL for FFmpeg (use direct CDN URL without iframe)
const directVideoUrl = `https://vz-7982dfc4-cc8.b-cdn.net/${videoId}/playlist.m3u8`;
// Generate thumbnail using FFmpeg
const ffmpegCommand = `ffmpeg -i "${directVideoUrl}" -ss 00:00:05 -vframes 1 -vf "scale=400:225" "${thumbnailPath}" -y`;
exec(ffmpegCommand, (error: any, stdout: any, stderr: any) => {
if (error) {
console.error(`FFmpeg error: ${error.message}`);
// Fallback to placeholder
return res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
}
if (fs.existsSync(thumbnailPath)) {
res.setHeader('Content-Type', 'image/jpeg');
res.setHeader('Cache-Control', 'public, max-age=86400');
res.sendFile(thumbnailPath);
} else {
// Fallback if thumbnail generation failed
res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
}
});
} catch (error) {
console.error("Error generating thumbnail:", error);
res.redirect(`https://images.unsplash.com/photo-1536240478700-b869070f9279?ixlib=rb-4.0.3&auto=format&fit=crop&w=400&h=225`);
}
});
const httpServer = createServer(app);