"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