Files
aidio-description/dist/utils/mediaUtils.js

261 lines
14 KiB
JavaScript

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getVideoDuration = getVideoDuration;
exports.captureVideoFrame = captureVideoFrame;
exports.getAudioDuration = getAudioDuration;
exports.combineAudioSegments = combineAudioSegments;
exports.cleanupTempFiles = cleanupTempFiles;
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const path_1 = __importDefault(require("path"));
/**
* Get the duration of a video file in seconds
* @param videoFilePath - Path to the video file
* @returns Duration in seconds
*/
function getVideoDuration(videoFilePath) {
const result = (0, child_process_1.execSync)(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${videoFilePath}"`);
return parseFloat(result.toString());
}
/**
* Capture a frame from a video at a specific time position
* @param videoFilePath - Path to the video file
* @param timePosition - Time position in seconds
* @param outputPath - Output path for the captured frame
* @param lowQuality - If true, save screenshot in 360p resolution
*/
function captureVideoFrame(videoFilePath, timePosition, outputPath, lowQuality = true) {
let command = `ffmpeg -v error -ss ${timePosition} -i "${videoFilePath}" -vframes 1 -q:v 2`;
// Add resolution scaling for low quality option
if (lowQuality) {
command += ' -vf scale=-1:360'; // Scale to 360p height while maintaining aspect ratio
}
command += ` "${outputPath}" -y`;
(0, child_process_1.execSync)(command);
}
/**
* Get the duration of an audio file in seconds
* @param audioFilePath - Path to the audio file
* @returns Duration in seconds
*/
function getAudioDuration(audioFilePath) {
const result = (0, child_process_1.execSync)(`ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 "${audioFilePath}"`);
return parseFloat(result.toString());
}
/**
* Combine audio segments into a single audio track using lossless intermediates
* @param segments - Array of audio segment information
* @param outputPath - Output path for the combined audio
* @param videoDuration - Duration of the video in seconds
* @param settings - Configuration settings
*/
function combineAudioSegments(segments, outputPath, videoDuration, settings) {
console.log(`Combining ${segments.length} audio segments using lossless intermediates...`);
try {
// Create a silent base track with the full video duration (always WAV)
const silentBasePath = path_1.default.join(settings.tempDir, 'silent_base.wav');
(0, child_process_1.execSync)(`ffmpeg -v error -f lavfi -i anullsrc=r=44100:cl=stereo -t ${videoDuration} -c:a pcm_s16le "${silentBasePath}" -y`);
// Sort segments by start time to process them in order
const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime);
// Process one segment at a time, building up the audio file
let currentAudioPath = silentBasePath;
for (let i = 0; i < sortedSegments.length; i++) {
const segment = sortedSegments[i];
const outputFile = path_1.default.join(settings.tempDir, `segment_${i}_output.wav`);
// Convert the segment to a standard WAV format first to avoid compatibility issues
// and ensure we're always working with lossless audio
const standardizedSegment = path_1.default.join(settings.tempDir, `segment_${i}_std.wav`);
(0, child_process_1.execSync)(`ffmpeg -v error -i "${segment.audioFile}" -ar 44100 -ac 2 -c:a pcm_s16le "${standardizedSegment}" -y`);
// Calculate the position for this segment
const timestamp = segment.startTime.toFixed(3);
// Create a filter script for this segment
const filterPath = path_1.default.join(settings.tempDir, `filter_${i}.txt`);
// Use a filter that preserves the audio quality and positions correctly
const filterContent = `[1:a]adelay=${Math.round(segment.startTime * 1000)}|${Math.round(segment.startTime * 1000)}[delayed];\n` +
`[0:a][delayed]amix=inputs=2:duration=first:dropout_transition=0:normalize=0[out]`;
fs_1.default.writeFileSync(filterPath, filterContent);
// Execute FFmpeg with the filter script
(0, child_process_1.execSync)(`ffmpeg -v error -i "${currentAudioPath}" -i "${standardizedSegment}" -filter_complex_script "${filterPath}" -map "[out]" -c:a pcm_s16le "${outputFile}" -y`);
// Clean up previous file if not the original
if (currentAudioPath !== silentBasePath) {
fs_1.default.unlinkSync(currentAudioPath);
}
// Clean up standardized segment and filter
fs_1.default.unlinkSync(standardizedSegment);
fs_1.default.unlinkSync(filterPath);
// Update current audio path for next iteration
currentAudioPath = outputFile;
console.log(`Added segment ${i + 1}/${sortedSegments.length} at position ${timestamp}s`);
}
// Only at the very end, convert to the requested output format
if (path_1.default.extname(outputPath).toLowerCase() === '.mp3') {
console.log(`Converting final lossless WAV to MP3: ${outputPath}`);
(0, child_process_1.execSync)(`ffmpeg -v error -i "${currentAudioPath}" -c:a libmp3lame -q:a 2 "${outputPath}" -y`);
}
else {
fs_1.default.copyFileSync(currentAudioPath, outputPath);
}
console.log(`Audio description track created: ${outputPath}`);
// Clean up the last temp file
if (currentAudioPath !== silentBasePath) {
fs_1.default.unlinkSync(currentAudioPath);
}
if (fs_1.default.existsSync(silentBasePath)) {
fs_1.default.unlinkSync(silentBasePath);
}
return outputPath;
}
catch (error) {
console.error("Error in lossless audio combination:", error.message);
try {
console.log("Trying alternative approach with single-step filter...");
// Create a silent base track (always WAV)
const silentBasePath = path_1.default.join(settings.tempDir, 'silent_base.wav');
(0, child_process_1.execSync)(`ffmpeg -v error -f lavfi -i anullsrc=r=44100:cl=stereo -t ${videoDuration} -c:a pcm_s16le "${silentBasePath}" -y`);
// Create a complex filter to overlay all audio files at their specific timestamps
const filterScriptPath = path_1.default.join(settings.tempDir, 'overlay_filter.txt');
let filterScript = '';
// Sort segments by start time
const sortedSegments = [...segments].sort((a, b) => a.startTime - b.startTime);
// Standardize all segments to WAV first
const standardizedSegments = [];
for (let i = 0; i < sortedSegments.length; i++) {
const segment = sortedSegments[i];
const stdPath = path_1.default.join(settings.tempDir, `std_${i}.wav`);
(0, child_process_1.execSync)(`ffmpeg -v error -i "${segment.audioFile}" -ar 44100 -ac 2 -c:a pcm_s16le "${stdPath}" -y`);
standardizedSegments.push({
path: stdPath,
startTime: segment.startTime
});
}
// Build the FFmpeg command with all standardized inputs
let ffmpegCmd = `ffmpeg -v error -i "${silentBasePath}" `;
// Add all standardized segments as inputs and create the filter script
for (let i = 0; i < standardizedSegments.length; i++) {
// Add as input
ffmpegCmd += `-i "${standardizedSegments[i].path}" `;
// Add to filter script - the input index starts at 1 because 0 is the silent base
const inputIndex = i + 1;
const delay = Math.round(standardizedSegments[i].startTime * 1000);
// Add this input to filter script with proper delay
filterScript += `[${inputIndex}:a]adelay=${delay}|${delay}[a${i}];\n`;
}
// Complete the filter script to merge all streams
filterScript += '[0:a]'; // Start with base
for (let i = 0; i < standardizedSegments.length; i++) {
filterScript += `[a${i}]`;
}
// Use amix with normalize=0 to preserve volumes
filterScript += `amix=inputs=${standardizedSegments.length + 1}:normalize=0:duration=first[aout]`;
// Write the filter script
fs_1.default.writeFileSync(filterScriptPath, filterScript);
// Use an intermediate WAV for the output to maintain quality
const intermediatePath = path_1.default.join(settings.tempDir, 'intermediate_output.wav');
// Complete the FFmpeg command - always output to WAV first
ffmpegCmd += `-filter_complex_script "${filterScriptPath}" -map "[aout]" -c:a pcm_s16le "${intermediatePath}" -y`;
// Execute the command
(0, child_process_1.execSync)(ffmpegCmd);
// Convert to the requested format only at the end
if (path_1.default.extname(outputPath).toLowerCase() === '.mp3') {
console.log(`Converting final audio to MP3...`);
(0, child_process_1.execSync)(`ffmpeg -v error -i "${intermediatePath}" -c:a libmp3lame -q:a 2 "${outputPath}" -y`);
}
else {
fs_1.default.copyFileSync(intermediatePath, outputPath);
}
console.log(`Audio description track created with alternative method: ${outputPath}`);
// Clean up temp files
if (fs_1.default.existsSync(filterScriptPath)) {
fs_1.default.unlinkSync(filterScriptPath);
}
if (fs_1.default.existsSync(silentBasePath)) {
fs_1.default.unlinkSync(silentBasePath);
}
if (fs_1.default.existsSync(intermediatePath)) {
fs_1.default.unlinkSync(intermediatePath);
}
// Clean up standardized segments
standardizedSegments.forEach(seg => {
if (fs_1.default.existsSync(seg.path)) {
fs_1.default.unlinkSync(seg.path);
}
});
return outputPath;
}
catch (secondError) {
console.error("Alternative approach failed:", secondError.message);
// Last resort: Generate a command file with the proper syntax
const cmdFilePath = outputPath.replace(/\.\w+$/, '_ffmpeg_cmd.sh');
let cmdContent = `#!/bin/bash\n\n# FFmpeg command to combine audio segments\n\n`;
// Add commands to convert all segments to WAV first
cmdContent += `# First convert all segments to standard WAV format\n`;
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const stdPath = `"${settings.tempDir}/std_${i}.wav"`;
cmdContent += `ffmpeg -i "${segment.audioFile}" -ar 44100 -ac 2 -c:a pcm_s16le ${stdPath} -y\n`;
}
// Create silent base
cmdContent += `\n# Create silent base track\n`;
cmdContent += `ffmpeg -f lavfi -i anullsrc=r=44100:cl=stereo -t ${videoDuration} -c:a pcm_s16le "${settings.tempDir}/silent_base.wav" -y\n\n`;
// Create filter file
cmdContent += `# Create filter file\n`;
cmdContent += `cat > "${settings.tempDir}/filter.txt" << EOL\n`;
// Add delay filters for each segment
for (let i = 0; i < segments.length; i++) {
const segment = segments[i];
const delay = Math.round(segment.startTime * 1000);
cmdContent += `[${i + 1}:a]adelay=${delay}|${delay}[a${i}];\n`;
}
// Mix all streams
cmdContent += `[0:a]`;
for (let i = 0; i < segments.length; i++) {
cmdContent += `[a${i}]`;
}
cmdContent += `amix=inputs=${segments.length + 1}:normalize=0:duration=first[aout]\nEOL\n\n`;
// Final command
cmdContent += `# Run final FFmpeg command\n`;
cmdContent += `ffmpeg -i "${settings.tempDir}/silent_base.wav" `;
// Add all segments as inputs
for (let i = 0; i < segments.length; i++) {
cmdContent += `-i "${settings.tempDir}/std_${i}.wav" `;
}
// Complete command
cmdContent += `-filter_complex_script "${settings.tempDir}/filter.txt" -map "[aout]" `;
if (path_1.default.extname(outputPath).toLowerCase() === '.mp3') {
cmdContent += `-c:a libmp3lame -q:a 2 `;
}
else {
cmdContent += `-c:a pcm_s16le `;
}
cmdContent += `"${outputPath}" -y\n\n`;
// Add cleanup
cmdContent += `# Clean up temp files\n`;
cmdContent += `rm "${settings.tempDir}/silent_base.wav" "${settings.tempDir}/filter.txt"\n`;
for (let i = 0; i < segments.length; i++) {
cmdContent += `rm "${settings.tempDir}/std_${i}.wav"\n`;
}
// Make the file executable
fs_1.default.writeFileSync(cmdFilePath, cmdContent);
(0, child_process_1.execSync)(`chmod +x "${cmdFilePath}"`);
console.log(`\nCreated executable script with proper FFmpeg commands: ${cmdFilePath}`);
console.log(`Run this script to generate the audio file.`);
return {
commandFile: cmdFilePath
};
}
}
}
/**
* Clean up temporary files
* @param tempDir - Directory containing temporary files
*/
function cleanupTempFiles(tempDir) {
const files = fs_1.default.readdirSync(tempDir);
for (const file of files) {
fs_1.default.unlinkSync(path_1.default.join(tempDir, file));
}
}
//# sourceMappingURL=mediaUtils.js.map