Rewrite frontend as single self-contained HTML file — all CSS/JS inline, no external files to fail loading
This commit is contained in:
261
dist/utils/mediaUtils.js
vendored
Normal file
261
dist/utils/mediaUtils.js
vendored
Normal file
@@ -0,0 +1,261 @@
|
||||
"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
|
||||
Reference in New Issue
Block a user