import ffmpeg from 'fluent-ffmpeg'; import ffprobe from '@ffprobe-installer/ffprobe'; import path from 'path'; import fs from 'fs-extra'; import { exec } from 'child_process'; import { promisify } from 'util'; // Promisify exec const execPromise = promisify(exec); // Configure ffmpeg to use ffprobe path ffmpeg.setFfprobePath(ffprobe.path); /** * Video processing service */ export class VideoProcessor { /** * Get video duration in seconds * @param videoPath Path to video file * @returns Duration in seconds */ static async getVideoDuration(videoPath: string): Promise { return new Promise((resolve, reject) => { ffmpeg.ffprobe(videoPath, (err, metadata) => { if (err) { return reject(err); } // Get duration from metadata const duration = metadata.format.duration || 0; resolve(duration); }); }); } /** * Generate thumbnail at specified timestamp * @param videoPath Path to video file * @param outputDir Path to output directory * @param timestamp Timestamp in seconds (default: 10) * @returns Path to generated thumbnail */ static async generateThumbnail( videoPath: string, outputDir: string, timestamp: number = 10 ): Promise { console.log('Generating thumbnail for video:', videoPath); console.log('Output directory:', outputDir); console.log('Timestamp:', timestamp); // Create unique filename const filename = `thumbnail-${Date.now()}.jpg`; const outputPath = path.join(outputDir, filename); console.log('Output path:', outputPath); // Check if video file exists try { const videoExists = await fs.pathExists(videoPath); console.log('Video file exists:', videoExists); if (!videoExists) { throw new Error(`Video file does not exist: ${videoPath}`); } // Check if output directory exists const dirExists = await fs.pathExists(outputDir); console.log('Output directory exists:', dirExists); if (!dirExists) { console.log('Creating output directory'); await fs.ensureDir(outputDir); } } catch (err) { console.error('Error checking files:', err); throw err; } // Use fluent-ffmpeg instead of command line return new Promise((resolve, reject) => { console.log('Using fluent-ffmpeg to generate thumbnail'); ffmpeg(videoPath) .on('error', (err) => { console.error('fluent-ffmpeg error:', err); reject(err); }) .on('end', () => { console.log('fluent-ffmpeg finished'); resolve(outputPath); }) .screenshots({ timestamps: [timestamp], filename: path.basename(outputPath), folder: outputDir, size: '640x360' }); }); } /** * Check if video has subtitles * @param videoPath Path to video file * @returns Boolean indicating if subtitles are present */ static async hasSubtitles(videoPath: string): Promise { return new Promise((resolve, reject) => { ffmpeg.ffprobe(videoPath, (err, metadata) => { if (err) { return reject(err); } // Check if any stream is a subtitle const hasSubtitleStream = metadata.streams.some( stream => stream.codec_type === 'subtitle' ); resolve(hasSubtitleStream); }); }); } /** * Get relative path from absolute path * @param absolutePath Absolute file path * @returns Relative path for storage in database */ static getRelativePath(absolutePath: string): string { // Convert absolute path to relative path for storage const relativePath = absolutePath.replace(process.cwd(), ''); return relativePath.startsWith('/') ? relativePath.substring(1) : relativePath; } /** * Get absolute path from relative path * @param relativePath Relative file path * @returns Absolute path for file operations */ static getAbsolutePath(relativePath: string): string { return path.join(process.cwd(), relativePath); } /** * Delete file if it exists * @param filePath Path to file */ static async deleteFile(filePath: string): Promise { if (!filePath) return; try { const exists = await fs.pathExists(filePath); if (exists) { await fs.unlink(filePath); } } catch (error) { console.error(`Error deleting file ${filePath}:`, error); } } }