From b57b48818dfdd47fb20a189372876cfe7caf8b7a Mon Sep 17 00:00:00 2001 From: sebastjanartic <45803536-sebastjanartic@users.noreply.replit.com> Date: Mon, 4 Aug 2025 19:47:26 +0000 Subject: [PATCH] Generate video thumbnails with titles and play buttons for better presentation Replaces FFmpeg thumbnail generation with SVG thumbnails including video title and play button using Bunny.net Stream API. 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/256xXYN --- server/routes.ts | 94 ++++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 43 deletions(-) diff --git a/server/routes.ts b/server/routes.ts index 873e2c0..cadf4bd 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -118,61 +118,69 @@ export async function registerRoutes(app: Express): Promise { } }); - // Generate real thumbnails from Bunny.net videos using FFmpeg + // Create SVG thumbnails with video title and play button app.get("/thumbnail/:videoId", async (req, res) => { try { const { videoId } = req.params; - const { exec } = require('child_process'); - const fs = require('fs'); - const path = require('path'); - // Get video URL from Bunny.net + // Get video info 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`); + const fallbackSvg = ` + + + Video not found + + `; + res.setHeader('Content-Type', 'image/svg+xml'); + return res.send(fallbackSvg); } - // Create thumbnails directory if it doesn't exist - const thumbnailDir = path.join(process.cwd(), 'thumbnails'); - if (!fs.existsSync(thumbnailDir)) { - fs.mkdirSync(thumbnailDir, { recursive: true }); - } + // Clean up title for display + const displayTitle = video.title.replace('.mp4', '').substring(0, 40); + const duration = video.duration ? Math.floor(video.duration / 60) + ':' + String(video.duration % 60).padStart(2, '0') : ''; - const thumbnailPath = path.join(thumbnailDir, `${videoId}.jpg`); + // Create SVG thumbnail with play button + const svg = ` + + + + + + + + + + + + + + + + + + + ${displayTitle} + + + ${duration ? `${duration}` : ''} + + `; - // 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`); - } - }); + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=86400'); + res.send(svg); } 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`); + console.error("Error creating thumbnail:", error); + const errorSvg = ` + + + Error loading thumbnail + + `; + res.setHeader('Content-Type', 'image/svg+xml'); + res.send(errorSvg); } });