From 8949236543138fb807d73fd89b9b699356234997 Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Mon, 4 Aug 2025 19:43:01 +0000 Subject: [PATCH] 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 --- .replit | 1 + server/routes.ts | 57 ++++++++++++++++++++++++++++++++++++++++++++-- test_thumbnail.jpg | 19 ++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 test_thumbnail.jpg diff --git a/.replit b/.replit index adcfadc..d2607c4 100644 --- a/.replit +++ b/.replit @@ -4,6 +4,7 @@ hidden = [".config", ".git", "generated-icon.png", "node_modules", "dist"] [nix] channel = "stable-24_05" +packages = ["ffmpeg"] [deployment] deploymentTarget = "autoscale" diff --git a/server/routes.ts b/server/routes.ts index 43d0dc3..873e2c0 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -118,9 +118,62 @@ export async function registerRoutes(app: Express): Promise { } }); - // 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); diff --git a/test_thumbnail.jpg b/test_thumbnail.jpg new file mode 100644 index 0000000..264fce4 --- /dev/null +++ b/test_thumbnail.jpg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + AlexReichinger-Ciaomiabella + + + 3:02 + + + \ No newline at end of file