import { exec } from 'child_process'; import { promisify } from 'util'; import * as fs from 'fs'; import * as path from 'path'; const execAsync = promisify(exec); export interface ThumbnailOptions { videoId: string; timeStamp?: string; // Format: "00:01:30" or "90" (seconds) width?: number; height?: number; quality?: number; } export class ThumbnailGenerator { private thumbnailDir: string; constructor() { this.thumbnailDir = path.join(process.cwd(), 'thumbnails'); this.ensureThumbnailDir(); } private ensureThumbnailDir() { if (!fs.existsSync(this.thumbnailDir)) { fs.mkdirSync(this.thumbnailDir, { recursive: true }); } } async generateThumbnail(options: ThumbnailOptions): Promise { const { videoId, timeStamp = "5", width = 400, height = 225, quality = 85 } = options; // Create unique filename based on parameters const filename = `${videoId}_${timeStamp.replace(/:/g, '-')}_${width}x${height}.jpg`; const outputPath = path.join(this.thumbnailDir, filename); // Check if thumbnail already exists if (fs.existsSync(outputPath)) { return outputPath; } try { // Use Bunny.net direct video URL for FFmpeg const videoUrl = `https://vz-7982dfc4-cc8.b-cdn.net/${videoId}/playlist.m3u8`; // FFmpeg command to extract frame at specific time const ffmpegCommand = [ 'ffmpeg', '-i', `"${videoUrl}"`, '-ss', timeStamp, '-vframes', '1', '-vf', `scale=${width}:${height}`, '-q:v', quality.toString(), `"${outputPath}"`, '-y' ].join(' '); console.log(`Generating thumbnail: ${ffmpegCommand}`); const { stdout, stderr } = await execAsync(ffmpegCommand); if (fs.existsSync(outputPath)) { console.log(`Thumbnail generated successfully: ${outputPath}`); return outputPath; } else { console.error('Thumbnail file was not created'); return null; } } catch (error) { console.error('Error generating thumbnail:', error); return null; } } async generateMultipleThumbnails(videoId: string, timestamps: string[]): Promise { const results: string[] = []; for (const timestamp of timestamps) { const thumbnailPath = await this.generateThumbnail({ videoId, timeStamp: timestamp }); if (thumbnailPath) { results.push(thumbnailPath); } } return results; } async enhanceThumbnail(inputPath: string, outputPath: string): Promise { try { // Use ImageMagick to enhance the thumbnail const magickCommand = [ 'convert', `"${inputPath}"`, '-auto-level', '-enhance', '-unsharp', '0x1', `"${outputPath}"` ].join(' '); await execAsync(magickCommand); if (fs.existsSync(outputPath)) { return outputPath; } return null; } catch (error) { console.error('Error enhancing thumbnail:', error); return null; } } listThumbnails(videoId: string): string[] { try { const files = fs.readdirSync(this.thumbnailDir); return files .filter(file => file.startsWith(videoId) && file.endsWith('.jpg')) .map(file => path.join(this.thumbnailDir, file)); } catch (error) { console.error('Error listing thumbnails:', error); return []; } } deleteThumbnail(videoId: string, timestamp?: string): boolean { try { if (timestamp) { // Delete specific thumbnail const filename = `${videoId}_${timestamp.replace(/:/g, '-')}_400x225.jpg`; const filePath = path.join(this.thumbnailDir, filename); if (fs.existsSync(filePath)) { fs.unlinkSync(filePath); return true; } } else { // Delete all thumbnails for video const files = fs.readdirSync(this.thumbnailDir); const videoThumbnails = files.filter(file => file.startsWith(videoId)); for (const file of videoThumbnails) { fs.unlinkSync(path.join(this.thumbnailDir, file)); } return videoThumbnails.length > 0; } return false; } catch (error) { console.error('Error deleting thumbnail:', error); return false; } } }