lad-website/backend/src/services/videoProcessor.ts

157 lines
4.5 KiB
TypeScript

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<number> {
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<string> {
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<boolean> {
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<void> {
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);
}
}
}