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
This commit is contained in:
sebastjanartic 2025-08-04 19:47:26 +00:00
parent 8949236543
commit b57b48818d

View File

@ -118,61 +118,69 @@ export async function registerRoutes(app: Express): Promise<Server> {
} }
}); });
// 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) => { app.get("/thumbnail/:videoId", async (req, res) => {
try { try {
const { videoId } = req.params; 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); const video = await storage.getVideo(videoId);
if (!video) { 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 = `
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="225" fill="#1a1a1a"/>
<text x="200" y="112" text-anchor="middle" fill="white" font-family="Arial" font-size="16">Video not found</text>
</svg>
`;
res.setHeader('Content-Type', 'image/svg+xml');
return res.send(fallbackSvg);
} }
// Create thumbnails directory if it doesn't exist // Clean up title for display
const thumbnailDir = path.join(process.cwd(), 'thumbnails'); const displayTitle = video.title.replace('.mp4', '').substring(0, 40);
if (!fs.existsSync(thumbnailDir)) { const duration = video.duration ? Math.floor(video.duration / 60) + ':' + String(video.duration % 60).padStart(2, '0') : '';
fs.mkdirSync(thumbnailDir, { recursive: true });
}
const thumbnailPath = path.join(thumbnailDir, `${videoId}.jpg`); // Create SVG thumbnail with play button
const svg = `
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#1e40af;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1e3a8a;stop-opacity:1" />
</linearGradient>
</defs>
<rect width="400" height="225" fill="url(#grad)" />
// Check if thumbnail already exists <!-- Play button circle -->
if (fs.existsSync(thumbnailPath)) { <circle cx="200" cy="112" r="35" fill="rgba(255,255,255,0.9)" />
res.setHeader('Content-Type', 'image/jpeg'); <!-- Play button triangle -->
res.setHeader('Cache-Control', 'public, max-age=86400'); <polygon points="188,97 188,127 218,112" fill="#1e40af" />
return res.sendFile(thumbnailPath);
}
// Generate video URL for FFmpeg (use direct CDN URL without iframe) <!-- Title background -->
const directVideoUrl = `https://vz-7982dfc4-cc8.b-cdn.net/${videoId}/playlist.m3u8`; <rect x="0" y="175" width="400" height="50" fill="rgba(0,0,0,0.7)" />
// Generate thumbnail using FFmpeg <!-- Title text -->
const ffmpegCommand = `ffmpeg -i "${directVideoUrl}" -ss 00:00:05 -vframes 1 -vf "scale=400:225" "${thumbnailPath}" -y`; <text x="20" y="200" fill="white" font-family="Arial, sans-serif" font-size="14" font-weight="bold">${displayTitle}</text>
exec(ffmpegCommand, (error: any, stdout: any, stderr: any) => { <!-- Duration -->
if (error) { ${duration ? `<text x="370" y="200" fill="white" font-family="Arial, sans-serif" font-size="12" text-anchor="end">${duration}</text>` : ''}
console.error(`FFmpeg error: ${error.message}`); </svg>
// 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/svg+xml');
res.setHeader('Content-Type', 'image/jpeg'); res.setHeader('Cache-Control', 'public, max-age=86400');
res.setHeader('Cache-Control', 'public, max-age=86400'); res.send(svg);
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) { } catch (error) {
console.error("Error generating thumbnail:", error); console.error("Error creating 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 errorSvg = `
<svg width="400" height="225" xmlns="http://www.w3.org/2000/svg">
<rect width="400" height="225" fill="#dc2626"/>
<text x="200" y="112" text-anchor="middle" fill="white" font-family="Arial" font-size="16">Error loading thumbnail</text>
</svg>
`;
res.setHeader('Content-Type', 'image/svg+xml');
res.send(errorSvg);
} }
}); });