157 lines
4.5 KiB
TypeScript
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);
|
|
}
|
|
}
|
|
}
|