raiyan khan
raiyan khan

Reputation: 3

FFmpeg WASM writeFile Stalls and Doesn't Complete in React App with Ant Design

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:

Debugging Steps I've Tried:

Expected Behavior

Actual Behavior

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

Answers (0)

Related Questions