UNPKG

media-exporter-processor

Version:

Media processing API with thumbnail generation and cloud storage

230 lines 9.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ThumbnailService = void 0; const child_process_1 = require("child_process"); const fs_1 = require("fs"); class ThumbnailService { constructor(config = ThumbnailService.DEFAULT_CONFIG) { this.config = config; } // Get FFmpeg path - prioritize layer path, fallback to system getFFmpegPath() { // Check for FFmpeg layer first (common Lambda layer path) const layerPaths = [ "/opt/bin/ffmpeg", // Common layer path "/opt/ffmpeg/bin/ffmpeg", // Alternative layer path "/usr/bin/ffmpeg", // System path "ffmpeg", // Fallback to PATH ]; return layerPaths[0]; // Start with layer path - will be validated at runtime } // Generate thumbnails from images (no frame extraction) async generateImageThumbnails(imagePath) { const results = []; // Generate all thumbnail sizes directly from the original image for (const size of this.config.sizes.sort((a, b) => b - a)) { const thumbnailPath = `${imagePath}-thumb-${size}.${this.config.format}`; try { // Resize image directly (no frame extraction needed) await this.resizeImage(imagePath, thumbnailPath, size); const buffer = await fs_1.promises.readFile(thumbnailPath); results.push({ size, path: thumbnailPath, buffer }); } catch (error) { console.error(`Failed to generate ${size}px thumbnail:`, error); // Continue with other sizes even if one fails } } // Clean up temporary files await this.cleanupFiles(results.map((r) => r.path)); return results; } // Generate thumbnails from videos (original method) async generateThumbnails(videoPath) { const results = []; // Extract frame at 1 second mark and resize to largest size first const largestSize = Math.max(...this.config.sizes); const tempFramePath = `${videoPath}-frame-${largestSize}.${this.config.format}`; try { // Extract and resize to largest size await this.extractFrame(videoPath, tempFramePath, largestSize); // Generate all thumbnail sizes from the largest one for (const size of this.config.sizes.sort((a, b) => b - a)) { const thumbnailPath = `${videoPath}-thumb-${size}.${this.config.format}`; if (size === largestSize) { // Use the already extracted frame const buffer = await fs_1.promises.readFile(tempFramePath); results.push({ size, path: thumbnailPath, buffer }); } else { // Resize from the largest size to maintain quality await this.resizeImage(tempFramePath, thumbnailPath, size); const buffer = await fs_1.promises.readFile(thumbnailPath); results.push({ size, path: thumbnailPath, buffer }); } } return results; } finally { // Clean up temporary files await this.cleanupFiles([tempFramePath, ...results.map((r) => r.path)]); } } async extractFrame(videoPath, outputPath, size) { return new Promise((resolve, reject) => { const fs = require("fs"); const checkFile = (retries) => { if (fs.existsSync(videoPath)) { try { fs.accessSync(videoPath, fs.constants.R_OK); console.log(`Input file verified: ${videoPath}`); startFFmpeg(); return; } catch (error) { console.log(`File exists but not readable, retrying... (${retries} left)`); } } else { console.log(`File does not exist, retrying... (${retries} left)`); } if (retries > 0) { setTimeout(() => checkFile(retries - 1), 200); } else { reject(new Error(`Input video file does not exist or is not readable: ${videoPath}`)); } }; const startFFmpeg = () => { const args = [ "-i", videoPath, "-ss", "00:00:01.000", "-vframes", "1", "-vf", `scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size}`, "-c:v", "libwebp", "-lossless", "1", "-y", outputPath, ]; const ffmpegPath = this.getFFmpegPath(); console.log(`Running FFmpeg: ${ffmpegPath} ${args.join(" ")}`); // Use system or layer FFmpeg binary const ffmpegProcess = (0, child_process_1.spawn)(ffmpegPath, args); let stderr = ""; ffmpegProcess.stderr.on("data", (data) => { stderr += data.toString(); }); ffmpegProcess.on("close", (code) => { if (code === 0) { console.log(`FFmpeg completed successfully for ${outputPath}`); resolve(); } else { console.error(`FFmpeg failed with code ${code}: ${stderr}`); reject(new Error(`FFmpeg failed with code ${code}: ${stderr}`)); } }); ffmpegProcess.on("error", (error) => { console.error(`FFmpeg spawn error: ${error.message}`); reject(new Error(`FFmpeg spawn error: ${error.message}`)); }); }; checkFile(3); }); } async resizeImage(inputPath, outputPath, size) { return new Promise((resolve, reject) => { const args = [ "-i", inputPath, "-vf", `scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size}`, "-c:v", "libwebp", "-lossless", "1", "-y", outputPath, ]; // Use system or layer FFmpeg binary const ffmpegPath = this.getFFmpegPath(); const ffmpegProcess = (0, child_process_1.spawn)(ffmpegPath, args); let stderr = ""; ffmpegProcess.stderr.on("data", (data) => { stderr += data.toString(); }); ffmpegProcess.on("close", (code) => { if (code === 0) { resolve(); } else { reject(new Error(`FFmpeg resize failed with code ${code}: ${stderr}`)); } }); ffmpegProcess.on("error", (error) => { reject(new Error(`FFmpeg resize spawn error: ${error.message}`)); }); }); } // Resize image directly (for static images, not video frames) async resizeImageDirect(inputPath, outputPath, size) { return new Promise((resolve, reject) => { const args = [ "-i", inputPath, "-vf", `scale=${size}:${size}:force_original_aspect_ratio=increase,crop=${size}:${size}`, "-c:v", "libwebp", "-lossless", "1", "-y", outputPath, ]; const ffmpegPath = this.getFFmpegPath(); console.log(`Running FFmpeg for image resize: ${ffmpegPath} ${args.join(" ")}`); const ffmpegProcess = (0, child_process_1.spawn)(ffmpegPath, args); let stderr = ""; ffmpegProcess.stderr.on("data", (data) => { stderr += data.toString(); }); ffmpegProcess.on("close", (code) => { if (code === 0) { console.log(`FFmpeg image resize completed successfully for ${outputPath}`); resolve(); } else { console.error(`FFmpeg image resize failed with code ${code}: ${stderr}`); reject(new Error(`FFmpeg image resize failed with code ${code}: ${stderr}`)); } }); ffmpegProcess.on("error", (error) => { console.error(`FFmpeg image resize spawn error: ${error.message}`); reject(new Error(`FFmpeg image resize spawn error: ${error.message}`)); }); }); } async cleanupFiles(filePaths) { await Promise.allSettled(filePaths.map(async (filePath) => { try { await fs_1.promises.unlink(filePath); } catch (error) { // Ignore cleanup errors } })); } } exports.ThumbnailService = ThumbnailService; ThumbnailService.DEFAULT_CONFIG = { sizes: [512, 256, 128, 64], quality: 100, format: "webp", }; //# sourceMappingURL=ThumbnailService.js.map