update
This commit is contained in:
@@ -73,6 +73,31 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
return color || '0xffffff';
|
||||
};
|
||||
|
||||
// Helper function to properly escape shell arguments
|
||||
const escapeShellArg = (arg) => {
|
||||
// If argument contains spaces, brackets, or other special characters, quote it
|
||||
if (/[\s\[\]()$`"'\\|&;<>*?~]/.test(arg)) {
|
||||
return `"${arg.replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return arg;
|
||||
};
|
||||
|
||||
// Better text escaping function for FFmpeg drawtext filter
|
||||
const escapeTextForDrawtext = (text) => {
|
||||
return text
|
||||
.replace(/\\/g, '\\\\\\\\') // Escape backslashes - needs 4 backslashes for proper escaping
|
||||
.replace(/'/g, "\\'") // Escape single quotes
|
||||
.replace(/:/g, '\\:') // Escape colons
|
||||
.replace(/\[/g, '\\[') // Escape square brackets
|
||||
.replace(/\]/g, '\\]')
|
||||
.replace(/,/g, '\\,') // Escape commas
|
||||
.replace(/;/g, '\\;') // Escape semicolons
|
||||
.replace(/\|/g, '\\|') // Escape pipes
|
||||
.replace(/\n/g, ' ') // Replace newlines with spaces
|
||||
.replace(/\r/g, ' ') // Replace carriage returns with spaces
|
||||
.replace(/\t/g, ' '); // Replace tabs with spaces
|
||||
};
|
||||
|
||||
const generateFFmpegCommand = useCallback(
|
||||
(is_string = true, useLocalFiles = false) => {
|
||||
showConsoleLogs && console.log('🎬 STARTING FFmpeg generation');
|
||||
@@ -102,9 +127,9 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
if (videos.length === 0 && images.length === 0) {
|
||||
if (is_string) {
|
||||
return 'ffmpeg -f lavfi -i color=black:size=450x800:duration=1 -c:v libx264 -t 1 output.mp4';
|
||||
return 'ffmpeg -f lavfi -i color=black:size=450x800:duration=1 -c:v libvpx-vp9 -t 1 output.mp4';
|
||||
} else {
|
||||
return ['-f', 'lavfi', '-i', 'color=black:size=450x800:duration=1', '-c:v', 'libx264', '-t', '1', 'output.mp4'];
|
||||
return ['-f', 'lavfi', '-i', 'color=black:size=450x800:duration=1', '-c:v', 'libvpx-vp9', '-t', '1', 'output.mp4'];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,7 +155,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
let videoLayer = 'base';
|
||||
|
||||
// FIXED: Sort all visual elements by layer, then process in layer order
|
||||
// Sort all visual elements by layer, then process in layer order
|
||||
const allVisualElements = [
|
||||
...videos.map((v, i) => ({ ...v, elementType: 'video', originalIndex: i })),
|
||||
...images.map((img, i) => ({ ...img, elementType: 'image', originalIndex: i })),
|
||||
@@ -226,14 +251,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
console.log(`📝 Text ${i} (Layer ${t.layer || 0}) - Position: (${t.x}, ${t.y}) Text: "${t.text.substring(0, 30)}..."`);
|
||||
|
||||
// Better text escaping for FFmpeg
|
||||
const escapedText = t.text
|
||||
.replace(/\\/g, '\\\\') // Escape backslashes first
|
||||
.replace(/'/g, is_string ? "\\'" : "'") // Escape quotes
|
||||
.replace(/:/g, '\\:') // Escape colons
|
||||
.replace(/\[/g, '\\[') // Escape square brackets
|
||||
.replace(/\]/g, '\\]')
|
||||
.replace(/,/g, '\\,') // Escape commas
|
||||
.replace(/;/g, '\\;'); // Escape semicolons
|
||||
const escapedText = escapeTextForDrawtext(t.text);
|
||||
|
||||
// Get the appropriate font file path
|
||||
const fontFilePath = getFontFilePath(t.fontFamily, t.fontWeight, t.fontStyle);
|
||||
@@ -248,7 +266,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
const borderWidth = Math.max(0, t.strokeWidth || 0); // Ensure non-negative
|
||||
|
||||
// Build drawtext filter with proper border handling
|
||||
// For centering: use (w-tw)/2 for x and adjust y as needed
|
||||
// FIXED: Wrap enable parameter without quotes to avoid truncation
|
||||
let drawTextFilter = `[${videoLayer}]drawtext=fontfile=/${fontFileName}:text='${escapedText}':x=(w-tw)/2:y=${centerY}:fontsize=${t.fontSize}:fontcolor=${fontColor}`;
|
||||
|
||||
// Only add border if strokeWidth > 0
|
||||
@@ -256,7 +274,8 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
drawTextFilter += `:borderw=${borderWidth}:bordercolor=${borderColor}`;
|
||||
}
|
||||
|
||||
drawTextFilter += `:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`;
|
||||
// FIXED: Don't wrap enable parameter in quotes - this was causing the truncation
|
||||
drawTextFilter += `:enable=between(t\\,${t.startTime}\\,${t.startTime + t.duration})[t${i}]`;
|
||||
|
||||
showConsoleLogs && console.log(`Text filter ${i}:`, drawTextFilter);
|
||||
filters.push(drawTextFilter);
|
||||
@@ -302,7 +321,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
`[${videoLayer}]`,
|
||||
...audioArgs,
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'libvpx-vp9',
|
||||
'-pix_fmt',
|
||||
'yuv420p',
|
||||
'-r',
|
||||
@@ -316,27 +335,32 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
let inputStrings = [];
|
||||
|
||||
videos.forEach((v, i) => {
|
||||
inputStrings.push(`-i "${useLocalFiles ? `input_video_${i}.webm` : v.source_webm}"`);
|
||||
inputStrings.push(`-i ${escapeShellArg(useLocalFiles ? `input_video_${i}.webm` : v.source_webm)}`);
|
||||
});
|
||||
|
||||
images.forEach((img, i) => {
|
||||
inputStrings.push(`-loop 1 -t ${img.duration} -i "${useLocalFiles ? `input_image_${i}.jpg` : img.source}"`);
|
||||
inputStrings.push(`-loop 1 -t ${img.duration} -i ${escapeShellArg(useLocalFiles ? `input_image_${i}.jpg` : img.source)}`);
|
||||
});
|
||||
|
||||
const inputs = inputStrings.join(' ');
|
||||
const audioMap = audioArgs.length > 0 ? ` ${audioArgs.join(' ')}` : '';
|
||||
const command = `ffmpeg ${inputs} -filter_complex "${filterComplex}" -map "[${videoLayer}]"${audioMap} -c:v libx264 -pix_fmt yuv420p -r 30 -t ${totalDuration} output.mp4`;
|
||||
const audioMap = audioArgs.length > 0 ? ` ${audioArgs.map((arg) => escapeShellArg(arg)).join(' ')}` : '';
|
||||
const command = `ffmpeg ${inputs} -filter_complex ${escapeShellArg(filterComplex)} -map ${escapeShellArg(`[${videoLayer}]`)}${audioMap} -c:v libvpx-vp9 -pix_fmt yuv420p -r 30 -t ${totalDuration} output.mp4`;
|
||||
|
||||
showConsoleLogs && console.log('🎵 FINAL COMMAND HAS AUDIO:', command.includes('atrim') && command.includes('audio_final'));
|
||||
|
||||
return command;
|
||||
} else {
|
||||
showConsoleLogs && console.log('🎵 FINAL ARGS HAVE AUDIO:', finalArgs.includes('atrim') && finalArgs.includes('audio_final'));
|
||||
showConsoleLogs &&
|
||||
console.log(
|
||||
'🎵 FINAL ARGS HAVE AUDIO:',
|
||||
finalArgs.some((arg) => typeof arg === 'string' && arg.includes('atrim')) &&
|
||||
finalArgs.some((arg) => typeof arg === 'string' && arg.includes('audio_final')),
|
||||
);
|
||||
|
||||
return finalArgs;
|
||||
}
|
||||
},
|
||||
[timelineElements, dimensions, totalDuration],
|
||||
[timelineElements, dimensions, totalDuration, showConsoleLogs],
|
||||
);
|
||||
|
||||
const ffmpegCommand = useMemo(() => {
|
||||
@@ -347,7 +371,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
showConsoleLogs && console.log('🎬 FFMPEG COMMAND GENERATED:');
|
||||
showConsoleLogs && console.log('Command:', ffmpegCommand);
|
||||
navigator.clipboard.writeText(ffmpegCommand);
|
||||
}, [ffmpegCommand]);
|
||||
}, [ffmpegCommand, showConsoleLogs]);
|
||||
|
||||
const exportVideo = async () => {
|
||||
setIsExporting(true);
|
||||
@@ -367,7 +391,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
showConsoleLogs && console.log('FFmpeg Log:', message);
|
||||
});
|
||||
|
||||
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm';
|
||||
const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.15/dist/esm';
|
||||
const coreURL = `${baseURL}/ffmpeg-core.js`;
|
||||
const wasmURL = `${baseURL}/ffmpeg-core.wasm`;
|
||||
|
||||
@@ -395,7 +419,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
// Add Arial font (fallback)
|
||||
fontsToLoad.set('arial.ttf', 'https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf');
|
||||
|
||||
// Add fonts used by text elements - FIXED: use the actual font paths from getFontFilePath
|
||||
// Add fonts used by text elements
|
||||
timelineElements
|
||||
.filter((el) => el.type === 'text')
|
||||
.forEach((text) => {
|
||||
@@ -404,7 +428,7 @@ const useVideoExport = ({ timelineElements, dimensions, totalDuration }) => {
|
||||
|
||||
// Only add if not already in map and not arial.ttf
|
||||
if (fontFileName !== 'arial.ttf' && !fontsToLoad.has(fontFileName)) {
|
||||
fontsToLoad.set(fontFileName, fontFilePath); // Use the actual path, not reconstructed
|
||||
fontsToLoad.set(fontFileName, fontFilePath);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user