"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThumbnailService = void 0; const child_process_1 = require("child_process"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const logger_1 = require("@/utils/logger"); class ThumbnailService { static THUMBNAIL_DIR = '/media/local/thumbnails'; static FFMPEG_TIMEOUT = 30000; // 30 seconds /** * Generate thumbnail for a video * Extracts frame at 10% duration (minimum 1 second) */ static async generateThumbnail(options) { const { videoPath, videoId, duration, orientation, outputDir } = options; const thumbnailDir = outputDir || this.THUMBNAIL_DIR; const thumbnailPath = path_1.default.join(thumbnailDir, `${videoId}.jpg`); // Calculate timestamp (10% of duration, minimum 1 second) const timestamp = Math.max(1, Math.floor(duration * 0.1)); // Determine thumbnail size based on orientation const isPortrait = orientation === 'portrait'; const scale = isPortrait ? '180:320' : '320:180'; try { // Ensure thumbnail directory exists await promises_1.default.mkdir(thumbnailDir, { recursive: true }); // Extract frame using FFmpeg await this.runFFmpegCommand([ '-ss', timestamp.toString(), // Seek to timestamp '-i', videoPath, // Input video '-vframes', '1', // Extract 1 frame '-vf', `scale=${scale}`, // Scale to target size '-q:v', '2', // Quality (1-31, lower is better) '-y', // Overwrite existing thumbnailPath // Output path ]); logger_1.logger.info(`Generated thumbnail for video ${videoId} at ${thumbnailPath}`); return thumbnailPath; } catch (error) { logger_1.logger.error(`Failed to generate thumbnail for video ${videoId}:`, error); throw new Error(`Thumbnail generation failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } /** * Delete thumbnail file */ static async deleteThumbnail(thumbnailPath) { try { await promises_1.default.unlink(thumbnailPath); logger_1.logger.info(`Deleted thumbnail: ${thumbnailPath}`); } catch (error) { // Ignore errors if file doesn't exist if (error.code !== 'ENOENT') { logger_1.logger.error(`Failed to delete thumbnail ${thumbnailPath}:`, error); } } } /** * Check if thumbnail exists */ static async thumbnailExists(videoId, outputDir) { const thumbnailDir = outputDir || this.THUMBNAIL_DIR; const thumbnailPath = path_1.default.join(thumbnailDir, `${videoId}.jpg`); try { await promises_1.default.access(thumbnailPath); return true; } catch { return false; } } /** * Run FFmpeg command with timeout */ static runFFmpegCommand(args) { return new Promise((resolve, reject) => { const ffmpeg = (0, child_process_1.spawn)('ffmpeg', args); let stderr = ''; const timeout = setTimeout(() => { ffmpeg.kill('SIGKILL'); reject(new Error('FFmpeg timeout exceeded')); }, this.FFMPEG_TIMEOUT); ffmpeg.stderr.on('data', (data) => { stderr += data.toString(); }); ffmpeg.on('close', (code) => { clearTimeout(timeout); if (code === 0) { resolve(); } else { reject(new Error(`FFmpeg exited with code ${code}: ${stderr}`)); } }); ffmpeg.on('error', (error) => { clearTimeout(timeout); reject(error); }); }); } } exports.ThumbnailService = ThumbnailService; //# sourceMappingURL=thumbnail.service.js.map