diff --git a/src/pages/tools/video/merge-video/index.tsx b/src/pages/tools/video/merge-video/index.tsx index 3866b32..8a7de65 100644 --- a/src/pages/tools/video/merge-video/index.tsx +++ b/src/pages/tools/video/merge-video/index.tsx @@ -19,32 +19,22 @@ export default function MergeVideo({ const [result, setResult] = useState(null); const [loading, setLoading] = useState(false); - console.log('MergeVideo component rendering, input:', input); - const compute = async ( _values: InitialValuesType, input: MultiVideoInput[] ) => { - console.log('Compute called with input:', input); if (!input || input.length < 2) { - console.log('Not enough files to merge'); return; } setLoading(true); try { const files = input.map((item) => item.file); - console.log( - 'Files to merge:', - files.map((f) => f.name) - ); const mergedBlob = await mergeVideos(files, initialValues); const mergedFile = new File([mergedBlob], 'merged-video.mp4', { type: 'video/mp4' }); setResult(mergedFile); - console.log('Merge successful'); } catch (err) { - console.error(`Failed to merge video: ${err}`); setResult(null); } finally { setLoading(false); @@ -59,7 +49,6 @@ export default function MergeVideo({ { - console.log('Input changed:', newInput); setInput(newInput); }} accept={['video/*', '.mp4', '.avi', '.mov', '.mkv']} diff --git a/src/pages/tools/video/merge-video/merge-video.service.test.ts b/src/pages/tools/video/merge-video/merge-video.service.test.ts index ccc81e6..311c067 100644 --- a/src/pages/tools/video/merge-video/merge-video.service.test.ts +++ b/src/pages/tools/video/merge-video/merge-video.service.test.ts @@ -49,4 +49,14 @@ describe('merge-video', () => { expect(result).toBeInstanceOf(Blob); expect(result.type).toBe('video/mp4'); }); + + it('handles different video formats by re-encoding', async () => { + const mockFile1 = createMockFile('video1.avi', 'video/x-msvideo'); + const mockFile2 = createMockFile('video2.mov', 'video/quicktime'); + + const result = await mergeVideos([mockFile1, mockFile2], {}); + + expect(result).toBeInstanceOf(Blob); + expect(result.type).toBe('video/mp4'); + }); }); diff --git a/src/pages/tools/video/merge-video/service.ts b/src/pages/tools/video/merge-video/service.ts index c376681..026c2b1 100644 --- a/src/pages/tools/video/merge-video/service.ts +++ b/src/pages/tools/video/merge-video/service.ts @@ -2,8 +2,6 @@ import { InitialValuesType, MergeVideoInput, MergeVideoOutput } from './types'; import { FFmpeg } from '@ffmpeg/ffmpeg'; import { fetchFile } from '@ffmpeg/util'; -// This function will use ffmpeg.wasm to merge multiple video files in the browser. -// Returns a Promise that resolves to a Blob of the merged video. export async function mergeVideos( input: MergeVideoInput, options: InitialValuesType @@ -19,55 +17,118 @@ export async function mergeVideos( const outputName = 'output.mp4'; try { - // Load FFmpeg + // Load FFmpeg with proper error handling if (!ffmpeg.loaded) { await ffmpeg.load({ wasmURL: - 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm' + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.wasm', + workerURL: + 'https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.9/dist/esm/ffmpeg-core.worker.js' }); } - // Write all input files to ffmpeg FS - fileNames.push(...input.map((file, idx) => `input${idx}.mp4`)); + // Write all input files to ffmpeg FS with better error handling for (let i = 0; i < input.length; i++) { - await ffmpeg.writeFile(fileNames[i], await fetchFile(input[i])); + const fileName = `input${i}.mp4`; + fileNames.push(fileName); + + try { + const fileData = await fetchFile(input[i]); + await ffmpeg.writeFile(fileName, fileData); + console.log(`Successfully wrote ${fileName}`); + } catch (fileError) { + console.error(`Failed to write ${fileName}:`, fileError); + throw new Error(`Failed to process input file ${i + 1}: ${fileError}`); + } } - // Create concat list file - const concatList = fileNames.map((name) => `file '${name}'`).join('\n'); - await ffmpeg.writeFile( - 'concat_list.txt', - new TextEncoder().encode(concatList) - ); + // Build the filter_complex string for concat filter + const videoInputs = fileNames.map((_, idx) => `[${idx}:v]`).join(' '); + const audioInputs = fileNames.map((_, idx) => `[${idx}:a]`).join(' '); + const filterComplex = `${videoInputs} ${audioInputs} concat=n=${input.length}:v=1:a=1 [v] [a]`; - // Run ffmpeg concat demuxer - await ffmpeg.exec([ - '-f', - 'concat', - '-safe', - '0', - '-i', - 'concat_list.txt', - '-c', - 'copy', - outputName - ]); + // Build input arguments + const inputArgs = []; + for (const fileName of fileNames) { + inputArgs.push('-i', fileName); + } + + console.log('Starting FFmpeg processing...'); + console.log('Filter complex:', filterComplex); + + // Method 2: Fallback to concat demuxer + try { + console.log('Trying concat demuxer method...'); + + const concatList = fileNames.map((name) => `file '${name}'`).join('\n'); + await ffmpeg.writeFile( + 'concat_list.txt', + new TextEncoder().encode(concatList) + ); + + await ffmpeg.exec([ + '-f', + 'concat', + '-safe', + '0', + '-i', + 'concat_list.txt', + '-c:v', + 'libx264', + '-preset', + 'ultrafast', + '-crf', + '30', + '-threads', + '0', + '-y', + outputName + ]); + + // Check if output was created + try { + const testRead = await ffmpeg.readFile(outputName); + if (testRead && testRead.length > 0) { + console.log('Concat demuxer method succeeded'); + } + } catch (readError) { + console.log('Concat demuxer method failed to produce output'); + } + } catch (execError) { + console.error('Concat demuxer method failed:', execError); + } + + // Check if output file exists and read it + let mergedData; + try { + mergedData = await ffmpeg.readFile(outputName); + console.log('Successfully read output file'); + } catch (readError) { + console.error('Failed to read output file:', readError); + throw new Error('Failed to read merged video file'); + } + + if (!mergedData || mergedData.length === 0) { + throw new Error('Output file is empty or corrupted'); + } - const mergedData = await ffmpeg.readFile(outputName); return new Blob([mergedData], { type: 'video/mp4' }); } catch (error) { console.error('Error merging videos:', error); - throw new Error(`Failed to merge videos: ${error}`); + throw error instanceof Error + ? error + : new Error('Unknown error occurred during video merge'); } finally { - // Clean up temporary files - try { - for (const fileName of fileNames) { + // Clean up temporary files with better error handling + const filesToClean = [...fileNames, outputName, 'concat_list.txt']; + + for (const fileName of filesToClean) { + try { await ffmpeg.deleteFile(fileName); + } catch (cleanupError) { + // Ignore cleanup errors - they're not critical + console.warn(`Could not delete ${fileName}:`, cleanupError); } - await ffmpeg.deleteFile('concat_list.txt'); - await ffmpeg.deleteFile(outputName); - } catch (cleanupError) { - console.warn('Error cleaning up temporary files:', cleanupError); } } }