media-exporter-processor
Version:
Media processing API with thumbnail generation and cloud storage
230 lines • 9.6 kB
JavaScript
;
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