From 342799e7f4f9d15668fcef958f7cba94ab127ee4 Mon Sep 17 00:00:00 2001 From: ct Date: Sun, 15 Jun 2025 09:33:02 +0800 Subject: [PATCH] Update --- .../editor/partials/canvas/video-editor.jsx | 223 +----------------- 1 file changed, 8 insertions(+), 215 deletions(-) diff --git a/resources/js/modules/editor/partials/canvas/video-editor.jsx b/resources/js/modules/editor/partials/canvas/video-editor.jsx index 16fe10f..7e79f88 100644 --- a/resources/js/modules/editor/partials/canvas/video-editor.jsx +++ b/resources/js/modules/editor/partials/canvas/video-editor.jsx @@ -1,8 +1,7 @@ import { useMitt } from '@/plugins/MittContext'; import useVideoEditorStore from '@/stores/VideoEditorStore'; -import { FFmpeg } from '@ffmpeg/ffmpeg'; -import { fetchFile, toBlobURL } from '@ffmpeg/util'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import useVideoExport from './video-export'; import VideoPreview from './video-preview'; const VideoEditor = ({ width, height }) => { @@ -91,13 +90,8 @@ const VideoEditor = ({ width, height }) => { ]); const lastUpdateRef = useRef(0); - const ffmpegRef = useRef(new FFmpeg()); const emitter = useMitt(); - const [isExporting, setIsExporting] = useState(false); - const [exportProgress, setExportProgress] = useState(0); - const [exportStatus, setExportStatus] = useState(''); - const [currentTime, setCurrentTime] = useState(0); const [isPlaying, setIsPlaying] = useState(false); const [videoElements, setVideoElements] = useState({}); @@ -118,125 +112,12 @@ const VideoEditor = ({ width, height }) => { const totalDuration = Math.max(...timelineElements.map((el) => el.startTime + el.duration)); - const generateFFmpegCommand = useCallback( - (is_string = true, useLocalFiles = false) => { - console.log('🎬 STARTING FFmpeg generation'); - - const videos = timelineElements.filter((el) => el.type === 'video'); - const texts = timelineElements.filter((el) => el.type === 'text'); - - console.log('Videos found:', videos.length); - - if (videos.length === 0) { - if (is_string) { - return 'ffmpeg -f lavfi -i color=black:size=450x800:duration=1 -c:v libx264 -t 1 output.mp4'; - } else { - return ['-f', 'lavfi', '-i', 'color=black:size=450x800:duration=1', '-c:v', 'libx264', '-t', '1', 'output.mp4']; - } - } - - let inputArgs = []; - videos.forEach((v, i) => { - inputArgs.push('-i'); - inputArgs.push(useLocalFiles ? `input${i}.webm` : v.source); - }); - - let filters = []; - filters.push(`color=black:size=${dimensions.width}x${dimensions.height}:duration=${totalDuration}[base]`); - - let videoLayer = 'base'; - videos.forEach((v, i) => { - filters.push(`[${i}:v]trim=start=${v.inPoint}:duration=${v.duration},setpts=PTS-STARTPTS[v${i}_trim]`); - filters.push(`[v${i}_trim]scale=${Math.round(v.width)}:${Math.round(v.height)}[v${i}_scale]`); - filters.push( - `[${videoLayer}][v${i}_scale]overlay=${Math.round(v.x)}:${Math.round(v.y)}:enable='between(t,${v.startTime},${ - v.startTime + v.duration - })'[v${i}_out]`, - ); - videoLayer = `v${i}_out`; - }); - - console.log('🎵 PROCESSING AUDIO FOR', videos.length, 'VIDEOS'); - - let audioOutputs = []; - videos.forEach((v, i) => { - const delay = Math.round(v.startTime * 1000); - console.log(`🎵 Audio ${i}: delay=${delay}ms, inPoint=${v.inPoint}, duration=${v.duration}`); - filters.push(`[${i}:a]atrim=start=${v.inPoint}:duration=${v.duration},asetpts=PTS-STARTPTS,adelay=${delay}|${delay}[a${i}]`); - audioOutputs.push(`[a${i}]`); - }); - - let audioArgs = []; - if (audioOutputs.length === 1) { - filters.push(`[a0]apad=pad_dur=${totalDuration}[audio_final]`); - audioArgs = ['-map', '[audio_final]', '-c:a', 'aac']; - } else if (audioOutputs.length > 1) { - filters.push(`${audioOutputs.join('')}amix=inputs=${audioOutputs.length}:duration=longest[audio_final]`); - audioArgs = ['-map', '[audio_final]', '-c:a', 'aac']; - } - - console.log('🎵 Audio args:', audioArgs); - - texts.forEach((t, i) => { - const escapedText = t.text.replace(/'/g, is_string ? "\\'" : "'").replace(/:/g, '\\:'); - - filters.push( - `[${videoLayer}]drawtext=fontfile=/arial.ttf:text='${escapedText}':x=${Math.round( - t.x, - )}:y=${Math.round(t.y)}:fontsize=${t.fontSize}:fontcolor=${t.fill}:borderw=${t.strokeWidth}:bordercolor=${ - t.stroke - }:enable='between(t,${t.startTime},${t.startTime + t.duration})'[t${i}]`, - ); - videoLayer = `t${i}`; - }); - - const filterComplex = filters.join('; '); - console.log('🎵 Filter includes atrim:', filterComplex.includes('atrim')); - - const finalArgs = [ - ...inputArgs, - '-filter_complex', - filterComplex, - '-map', - `[${videoLayer}]`, - ...audioArgs, - '-c:v', - 'libx264', - '-pix_fmt', - 'yuv420p', - '-r', - '30', - '-t', - totalDuration.toString(), - 'output.mp4', - ]; - - if (is_string) { - const inputs = videos.map((v, i) => `-i "${useLocalFiles ? `input${i}.webm` : v.source}"`).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`; - - console.log('🎵 FINAL COMMAND HAS AUDIO:', command.includes('atrim') && command.includes('audio_final')); - - return command; - } else { - console.log('🎵 FINAL ARGS HAVE AUDIO:', finalArgs.includes('atrim') && finalArgs.includes('audio_final')); - - return finalArgs; - } - }, - [timelineElements, dimensions, totalDuration], - ); - - const ffmpegCommand = useMemo(() => { - return generateFFmpegCommand(true, false); - }, [generateFFmpegCommand]); - - const copyFFmpegCommand = useCallback(() => { - console.log('🎬 FFMPEG COMMAND GENERATED:'); - console.log('Command:', ffmpegCommand); - navigator.clipboard.writeText(ffmpegCommand); - }, [ffmpegCommand]); + // Use the FFmpeg hook + const { isExporting, exportProgress, exportStatus, ffmpegCommand, copyFFmpegCommand, exportVideo } = useVideoExport({ + timelineElements, + dimensions, + totalDuration, + }); useEffect(() => { const videoEls = {}; @@ -379,94 +260,6 @@ const VideoEditor = ({ width, height }) => { } }, [isPlaying, videoElements]); - const exportVideo = async () => { - setIsExporting(true); - setExportProgress(0); - setExportStatus('Starting export...'); - - try { - setExportStatus('Loading FFmpeg...'); - - const ffmpeg = new FFmpeg(); - - ffmpeg.on('progress', ({ progress }) => { - setExportProgress(Math.round(progress * 100)); - }); - - ffmpeg.on('log', ({ message }) => { - console.log(message); - }); - - const baseURL = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/esm'; - const coreURL = `${baseURL}/ffmpeg-core.js`; - const wasmURL = `${baseURL}/ffmpeg-core.wasm`; - - console.log('Converting JS coreURL...'); - const coreBlobURL = await toBlobURL(coreURL, 'text/javascript'); - console.log('JS coreURL ready:', coreBlobURL); - - console.log('Converting WASM URL...'); - const wasmBlobURL = await toBlobURL(wasmURL, 'application/wasm'); - console.log('WASM URL ready:', wasmBlobURL); - - console.log('Calling ffmpeg.load...'); - await ffmpeg.load({ - coreURL: coreBlobURL, - wasmURL: wasmBlobURL, - }); - console.log('FFmpeg loaded!'); - setExportProgress(20); - - setExportStatus('Loading font...'); - await ffmpeg.writeFile('arial.ttf', await fetchFile('https://raw.githubusercontent.com/ffmpegwasm/testdata/master/arial.ttf')); - console.log('Font loaded!'); - setExportProgress(30); - - setExportStatus('Downloading videos...'); - const videos = timelineElements.filter((el) => el.type === 'video'); - - for (let i = 0; i < videos.length; i++) { - await ffmpeg.writeFile(`input${i}.webm`, await fetchFile(videos[i].source)); - setExportProgress(30 + Math.round(((i + 1) / videos.length) * 30)); - } - - setExportStatus('Processing video...'); - let args = generateFFmpegCommand(false, true); - - setExportProgress(70); - await ffmpeg.exec(args); - - setExportStatus('Downloading...'); - setExportProgress(90); - - const fileData = await ffmpeg.readFile('output.mp4'); - const data = new Uint8Array(fileData); - - const blob = new Blob([data.buffer], { type: 'video/mp4' }); - const url = URL.createObjectURL(blob); - - const link = document.createElement('a'); - link.href = url; - link.download = 'exported_video.mp4'; - link.click(); - URL.revokeObjectURL(url); - - setExportProgress(100); - setExportStatus('Complete!'); - - ffmpeg.terminate(); - } catch (error) { - console.error('Export error:', error); - setExportStatus(`Failed: ${error.message}`); - } finally { - setTimeout(() => { - setIsExporting(false); - setExportStatus(''); - setExportProgress(0); - }, 3000); - } - }; - const getActiveElements = useCallback( (time) => { return timelineElements.filter((element) => {