Reputation: 3
I'm using FFmpeg WebAssembly (WASM) in a React app to process and convert a video file before uploading it. The goal is to resize the video to 720p using FFmpeg before sending it to the backend.
Problem:
Everything works up to fetching the file and confirming it's loaded into memory, but FFmpeg hangs at ffmpeg.writeFile() and does not proceed further. No errors are thrown.
Code Snippet:
Loading FFmpeg
const loadFFmpeg = async () => {
if (loaded) return; // Avoid reloading if
already loaded
const baseURL = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd';
const ffmpeg = ffmpegRef.current;
ffmpeg.on('log', ({ message }) => {
messageRef.current.innerHTML = message;
console.log(message);
});
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
setLoaded(true);
};
useEffect(() => {
loadFFmpeg()
}, [])
Fetching and Writing File
const convertVideoTo720p = async (videoFile) => {
console.log("Starting video
conversion...");
const { height } = await getVideoMetadata(videoFile);
console.log(`Video height: ${height}`);
if (height <= 720) {
console.log("No conversion needed.");
return videoFile;
}
const ffmpeg = ffmpegRef.current;
console.log("FFmpeg instance loaded. Writing file to memory...");
const fetchedFile = await fetchFile(videoFile);
console.log("File fetched successfully:", fetchedFile);
console.log("Checking FFmpeg memory before writing...");
console.log(`File size: ${fetchedFile.length} bytes (~${(fetchedFile.length / 1024 / 1024).toFixed(2)} MB)`);
if (!ffmpeg.isLoaded()) {
console.error("FFmpeg is not fully loaded yet!");
return;
}
console.log("Memory seems okay. Writing file to FFmpeg...");
await ffmpeg.writeFile('input.mp4', fetchedFile); // ❌ This line hangs, nothing after runs
console.log("File successfully written to FFmpeg memory.");
};
Debugging Steps I've Tried:
writeFile()
✅ ffmpeg.isLoaded()
returns true
.fetchFile(videoFile)
successfully returns a Uint8Array
.video_${Date.now()}.mp4
, but no changeExpected Behavior
ffmpeg.writeFile('input.mp4', fetchedFile);
should complete and allow FFmpeg to process the video.Actual Behavior
writeFile
, and no errors are thrown.Environment:
Question:
Why is FFmpeg's writeFile()
stalling and never completing?
How can I fix or further debug this issue?
Here is my full code:
import { useNavigate } from "react-router-dom";
import { useEffect, useRef, useState } from 'react';
import { Form, Input, Button, Select, Space } from 'antd';
const { Option } = Select;
import { FaAngleLeft } from "react-icons/fa6";
import { message, Upload } from 'antd';
import { CiCamera } from "react-icons/ci";
import { IoVideocamOutline } from "react-icons/io5";
import { useCreateWorkoutVideoMutation } from "../../../redux/features/workoutVideo/workoutVideoApi";
import { convertVideoTo720p } from "../../../utils/ffmpegHelper";
import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';
const AddWorkoutVideo = () => {
const [videoFile, setVideoFile] = useState(null);
const [imageFile, setImageFile] = useState(null);
const [loaded, setLoaded] = useState(false);
const ffmpegRef = useRef(new FFmpeg());
const videoRef = useRef(null);
const messageRef = useRef(null);
const [form] = Form.useForm();
const [createWorkoutVideo, { isLoading }] = useCreateWorkoutVideoMutation()
const navigate = useNavigate();
const videoFileRef = useRef(null); // Use a ref instead of state
// Handle Video Upload
const handleVideoChange = ({ file }) => {
setVideoFile(file.originFileObj);
};
// Handle Image Upload
const handleImageChange = ({ file }) => {
setImageFile(file.originFileObj);
};
// Load FFmpeg core if needed (optional if you want to preload)
const loadFFmpeg = async () => {
if (loaded) return; // Avoid reloading if already loaded
const baseURL = 'https://unpkg.com/@ffmpeg/[email protected]/dist/umd';
const ffmpeg = ffmpegRef.current;
ffmpeg.on('log', ({ message }) => {
messageRef.current.innerHTML = message;
console.log(message);
});
await ffmpeg.load({
coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
});
setLoaded(true);
};
useEffect(() => {
loadFFmpeg()
}, [])
// Helper: Get video metadata (width and height)
const getVideoMetadata = (file) => {
return new Promise((resolve, reject) => {
const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
resolve({ width: video.videoWidth, height: video.videoHeight });
};
video.onerror = () => reject(new Error('Could not load video metadata'));
video.src = URL.createObjectURL(file);
});
};
// Inline conversion helper function
// const convertVideoTo720p = async (videoFile) => {
// // Check the video resolution first
// const { height } = await getVideoMetadata(videoFile);
// if (height <= 720) {
// // No conversion needed
// return videoFile;
// }
// const ffmpeg = ffmpegRef.current;
// // Load ffmpeg if not already loaded
// // await ffmpeg.load({
// // coreURL: await toBlobURL(`${baseURL}/ffmpeg-core.js`, 'text/javascript'),
// // wasmURL: await toBlobURL(`${baseURL}/ffmpeg-core.wasm`, 'application/wasm'),
// // });
// // Write the input file to the ffmpeg virtual FS
// await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
// // Convert video to 720p (scale filter maintains aspect ratio)
// await ffmpeg.exec(['-i', 'input.mp4', '-vf', 'scale=-1:720', 'output.mp4']);
// // Read the output file
// const data = await ffmpeg.readFile('output.mp4');
// console.log(data, 'data from convertVideoTo720p');
// const videoBlob = new Blob([data.buffer], { type: 'video/mp4' });
// return new File([videoBlob], 'output.mp4', { type: 'video/mp4' });
// };
const convertVideoTo720p = async (videoFile) => {
console.log("Starting video conversion...");
// Check the video resolution first
const { height } = await getVideoMetadata(videoFile);
console.log(`Video height: ${height}`);
if (height <= 720) {
console.log("No conversion needed. Returning original file.");
return videoFile;
}
const ffmpeg = ffmpegRef.current;
console.log("FFmpeg instance loaded. Writing file to memory...");
// await ffmpeg.writeFile('input.mp4', await fetchFile(videoFile));
// console.log("File written. Starting conversion...");
console.log("Fetching file for FFmpeg:", videoFile);
const fetchedFile = await fetchFile(videoFile);
console.log("File fetched successfully:", fetchedFile);
console.log("Checking FFmpeg memory before writing...");
console.log(`File size: ${fetchedFile.length} bytes (~${(fetchedFile.length / 1024 / 1024).toFixed(2)} MB)`);
if (fetchedFile.length > 50 * 1024 * 1024) { // 50MB limit
console.error("File is too large for FFmpeg WebAssembly!");
message.error("File too large. Try a smaller video.");
return;
}
console.log("Memory seems okay. Writing file to FFmpeg...");
const fileName = `video_${Date.now()}.mp4`; // Generate a unique name
console.log(`Using filename: ${fileName}`);
await ffmpeg.writeFile(fileName, fetchedFile);
console.log(`File successfully written to FFmpeg memory as ${fileName}.`);
await ffmpeg.exec(['-i', 'input.mp4', '-vf', 'scale=-1:720', 'output.mp4']);
console.log("Conversion completed. Reading output file...");
const data = await ffmpeg.readFile('output.mp4');
console.log("File read successful. Creating new File object.");
const videoBlob = new Blob([data.buffer], { type: 'video/mp4' });
const convertedFile = new File([videoBlob], 'output.mp4', { type: 'video/mp4' });
console.log(convertedFile, "converted video from convertVideoTo720p");
return convertedFile;
};
const onFinish = async (values) => {
// Ensure a video is selected
if (!videoFileRef.current) {
message.error("Please select a video file.");
return;
}
// Create FormData
const formData = new FormData();
if (imageFile) {
formData.append("image", imageFile);
}
try {
message.info("Processing video. Please wait...");
// Convert the video to 720p only if needed
const convertedVideo = await convertVideoTo720p(videoFileRef.current);
console.log(convertedVideo, 'convertedVideo from onFinish');
formData.append("media", videoFileRef.current);
formData.append("data", JSON.stringify(values));
// Upload manually to the backend
const response = await createWorkoutVideo(formData).unwrap();
console.log(response, 'response from add video');
message.success("Video added successfully!");
form.resetFields(); // Reset form
setVideoFile(null); // Clear file
} catch (error) {
message.error(error.data?.message || "Failed to add video.");
}
// if (videoFile) {
// message.info("Processing video. Please wait...");
// try {
// // Convert the video to 720p only if needed
// const convertedVideo = await convertVideoTo720p(videoFile);
// formData.append("media", convertedVideo);
// } catch (conversionError) {
// message.error("Video conversion failed.");
// return;
// }
// }
// formData.append("data", JSON.stringify(values)); // Convert text fields to JSON
// try {
// const response = await createWorkoutVideo(formData).unwrap();
// console.log(response, 'response from add video');
// message.success("Video added successfully!");
// form.resetFields(); // Reset form
// setFile(null); // Clear file
// } catch (error) {
// message.error(error.data?.message || "Failed to add video.");
// }
};
const handleBackButtonClick = () => {
navigate(-1); // This takes the user back to the previous page
};
const videoUploadProps = {
name: 'video',
// action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
// headers: {
// authorization: 'authorization-text',
// },
// beforeUpload: (file) => {
// const isVideo = file.type.startsWith('video/');
// if (!isVideo) {
// message.error('You can only upload video files!');
// }
// return isVideo;
// },
// onChange(info) {
// if (info.file.status === 'done') {
// message.success(`${info.file.name} video uploaded successfully`);
// } else if (info.file.status === 'error') {
// message.error(`${info.file.name} video upload failed.`);
// }
// },
beforeUpload: (file) => {
const isVideo = file.type.startsWith('video/');
if (!isVideo) {
message.error('You can only upload video files!');
return Upload.LIST_IGNORE; // Prevents the file from being added to the list
}
videoFileRef.current = file; // Store file in ref
// setVideoFile(file); // Store the file in state instead of uploading it automatically
return false; // Prevent auto-upload
},
};
const imageUploadProps = {
name: 'image',
action: 'https://660d2bd96ddfa2943b33731c.mockapi.io/api/upload',
headers: {
authorization: 'authorization-text',
},
beforeUpload: (file) => {
const isImage = file.type.startsWith('image/');
if (!isImage) {
message.error('You can only upload image files!');
}
return isImage;
},
onChange(info) {
if (info.file.status === 'done') {
message.success(`${info.file.name} image uploaded successfully`);
} else if (info.file.status === 'error') {
message.error(`${info.file.name} image upload failed.`);
}
},
};
return (
<>
<div className="flex items-center gap-2 text-xl cursor-pointer" onClick={handleBackButtonClick}>
<FaAngleLeft />
<h1 className="font-semibold">Add Video</h1>
</div>
<div className="rounded-lg py-4 border-[#79CDFF] border-2 shadow-lg mt-8 bg-white">
<div className="space-y-[24px] min-h-[83vh] bg-light-gray rounded-2xl">
<h3 className="text-2xl text-[#174C6B] mb-4 border-b border-[#79CDFF]/50 pb-3 pl-16 font-semibold">
Adding Video
</h3>
<div className="w-full px-16">
<Form
form={form}
layout="vertical"
onFinish={onFinish}
// style={{ maxWidth: 600, margin: '0 auto' }}
>
{/* Section 1 */}
{/* <Space direction="vertical" style={{ width: '100%' }}> */}
{/* <Space size="large" direction="horizontal" className="responsive-space"> */}
<div className="grid grid-cols-2 gap-8 mt-8">
<div>
<Space size="large" direction="horizontal" className="responsive-space-section-2">
{/* Video */}
<Form.Item
label={<span style={{ fontSize: '18px', fontWeight: '600', color: '#2D2D2D' }}>Upload Video</span>}
name="media"
className="responsive-form-item"
// rules={[{ required: true, message: 'Please enter the package amount!' }]}
>
<Upload {...videoUploadProps} onChange={handleVideoChange} maxCount={1}>
<Button style={{ width: '440px', height: '40px', border: '1px solid #79CDFF', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#525252', fontSize: '16px', fontWeight: 600 }}>Select a video</span>
<IoVideocamOutline size={20} color="#174C6B" />
</Button>
</Upload>
</Form.Item>
{/* Thumbnail */}
<Form.Item
label={<span style={{ fontSize: '18px', fontWeight: '600', color: '#2D2D2D' }}>Upload Image</span>}
name="image"
className="responsive-form-item"
// rules={[{ required: true, message: 'Please enter the package amount!' }]}
>
<Upload {...imageUploadProps} onChange={handleImageChange} maxCount={1}>
<Button style={{ width: '440px', height: '40px', border: '1px solid #79CDFF', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<span style={{ color: '#525252', fontSize: '16px', fontWeight: 600 }}>Select an image</span>
<CiCamera size={25} color="#174C6B" />
</Button>
</Upload>
</Form.Item>
{/* Title */}
<Form.Item
label={<span style={{ fontSize: '18px', fontWeight: '600', color: '#2D2D2D' }}>Video Title</span>}
name="name"
className="responsive-form-item-section-2"
>
<Input type="text" placeholder="Enter video title" style={{
height: '40px',
border: '1px solid #79CDFF',
fontSize: '16px',
fontWeight: 600,
color: '#525252',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}} />
</Form.Item>
</Space>
</div>
</div>
{/* </Space> */}
{/* </Space> */}
{/* Submit Button */}
<Form.Item>
<div className="p-4 mt-10 text-center mx-auto flex items-center justify-center">
<button
className="w-[500px] bg-[#174C6B] text-white px-10 h-[45px] flex items-center justify-center gap-3 text-lg outline-none rounded-md "
>
<span className="text-white font-semibold">{isLoading ? 'Uploading...' : 'Upload'}</span>
</button>
</div>
</Form.Item>
</Form>
</div>
</div>
</div>
</>
)
}
export default AddWorkoutVideo
Would appreciate any insights or suggestions. Thanks!
Upvotes: 0
Views: 23