Skip to content

Ffmpeg on NextJS do not work!? #875

@R4Ajeti

Description

@R4Ajeti

Describe the bug
I am trying to get those 5 params: width: number; height: number; fps: number; totalFrame: number; sizeMb: number; durationSecond: number;

  1. The user uploads the video using upload from antd (package version "^5.25.4")
  2. [email protected], [email protected], ffmpeg/[email protected]", ffmpeg/util@^0.12.2".
  3. I don't have the user's full file path because of a security issue in the browser.
  4. The method "getVideoDetail" is working, but when the video is cropped, it gives me the wrong fps
  5. The method "getVideoDetailFfmpeg" is giving me this error: "FFmpeg error: ErrnoError: FS error"

PS: I'm new to Javascript and not sure what I'm doing wrong, or is it possible to do this on Javascript, please help me

To Reproduce
`// --- test-page-view.tsx ---

import React, { useRef, useState } from 'react';
import { Button, Card, Flex, Upload, Modal } from 'antd';
import { CloudUploadOutlined } from '@ant-design/icons';

import { FFmpeg } from '@ffmpeg/ffmpeg';
import { fetchFile, toBlobURL } from '@ffmpeg/util';

const ffmpeg = new FFmpeg();

export const getVideoDetailFfmpeg = async (
file: File
): Promise<{
width: number;
height: number;
fps: number;
totalFrame: number;
sizeMb: number;
durationSecond: number;
}> => {

await ffmpeg.load();

const fileName = file.name;
// 2) write file into MEMFS
await ffmpeg.writeFile(fileName, await fetchFile(file));

try {
    // 3) exec ffprobe‐style JSON dump
    await ffmpeg.exec([
        '-i', fileName,
        '-hide_banner',
        '-loglevel', 'error',
        '-select_streams', 'v:0',
        '-show_entries', 'stream=width,height,r_frame_rate,nb_frames',
        '-show_entries', 'format=duration,size',
        '-of', 'json',
        'metadata.json',
    ]);

    // 4) read + parse
    const rawData = await ffmpeg.readFile('metadata.json');
    const rawBytes = typeof rawData === 'string'
        ? new TextEncoder().encode(rawData)
        : rawData;
    const { streams, format } = JSON.parse(new TextDecoder().decode(rawBytes));
    const stream = streams[0];

    // 5) compute
    const safeDiv = (ratio: string) => {
        const [n, d] = ratio.split('/').map(Number);
        return n / (d || 1);
    };
    const fps = safeDiv(stream.r_frame_rate);
    const durationSecond = Math.floor(Number(format.duration));
    const sizeMb = Number(format.size) / 1_000_000;
    const totalFrame = stream.nb_frames
        ? Number(stream.nb_frames)
        : Math.floor(durationSecond * fps);

    return {
        width: stream.width,
        height: stream.height,
        fps: parseFloat(fps.toFixed(2)),
        totalFrame,
        sizeMb: parseFloat(sizeMb.toFixed(2)),
        durationSecond,
    };
} catch (err) {
    console.error('FFmpeg error:', err);
    throw err;
}

};

const getVideoDetail = (file: File): Promise<{
width: number;
height: number;
fps: number;
totalFrame: number;
sizeMb: number;
durationSecond: number;
}> => {

return new Promise((resolve, reject) => {
    const video = document.createElement('video');
    const url = URL.createObjectURL(file);

    console.log('Video URL:', url);

    video.src = url;
    video.muted = true;
    video.preload = 'metadata';

    video.addEventListener('loadedmetadata', () => {
        const width = video.videoWidth;
        const height = video.videoHeight;
        const durationSecond = Math.ceil(video.duration);
        const sizeMb = parseFloat((file.size / (1024 * 1024)).toFixed(2));

        const frameTimes: number[] = [];
        const maxFrames = 60;

        const onFrame = (_: number, metadata: VideoFrameCallbackMetadata) => {
            frameTimes.push(metadata.mediaTime);

            if (frameTimes.length < maxFrames) {
                video.requestVideoFrameCallback(onFrame);
                return;
            }

            const intervals = frameTimes.slice(1).map((t, i) => t - frameTimes[i]);
            const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
            const fps = parseFloat((1 / avgInterval).toFixed(2));
            const totalFrame = Math.round(durationSecond * fps);
            console.log('FPS frame interval:', fps, intervals.length, durationSecond);

            resolve({
                width,
                height,
                fps,
                totalFrame,
                sizeMb: Number(sizeMb),
                durationSecond: parseFloat(durationSecond.toFixed(2))
            });

            URL.revokeObjectURL(url);
        };

        video.play().then(() => video.requestVideoFrameCallback(onFrame)).catch(reject);
    });

    video.addEventListener('error', reject);
});

};

export const TestPageView: React.FC = () => {
const [open, setOpen] = useState(false);
const [fileList, setFileList] = useState<File[]>([]);
const [videoDetails, setVideoDetails] = useState<{
width: number;
height: number;
fps: number;
totalFrame: number;
sizeMb: number;
durationSecond: number;
} | null>(null);

const fileInputRef = useRef<HTMLInputElement>(null);
const uploadRef = useRef<any>(null);

const handleChange = async (info: any) => {
    const file = info.file;
    if (!file?.type?.startsWith('video/')) {
        setFileList([file]);
        setOpen(true);
        return;
    }

    getVideoDetailFfmpeg(file)
        .then(setVideoDetails)
        .catch(console.error)
        .finally(() => {
            setFileList([file]);
            setOpen(true);
        });
};

const closeModal = () => {
    setOpen(false);
    setFileList([]);
    setVideoDetails(null);
    if (fileInputRef.current) fileInputRef.current.value = '';
    if (uploadRef.current) uploadRef.current.upload.uploader.fileInput.value = '';
};

return (
    <Card>
        <Flex gap="middle" justify='center'>
            <Flex vertical gap="large" style={{ padding: 50 }}>
                <Flex justify='center'>
                    <Upload
                        ref={uploadRef}
                        name="file"
                        listType="picture-circle"
                        showUploadList={false}
                        accept="video/*"
                        onChange={handleChange}
                    >
                        <button style={{ border: 0, background: 'none' }} type="button">
                            {/* Fixed by adding rev prop */}
                            <CloudUploadOutlined rev={undefined} style={{ fontSize: '32px' }} />
                        </button>
                    </Upload>
                </Flex>
                <Flex justify='center' gap="middle">
                    <Button onClick={() => fileInputRef.current?.click()}>Browse File</Button>
                    <input
                        type="file"
                        ref={fileInputRef}
                        hidden
                        accept="video/*"
                        onChange={e => e.target.files?.[0] && handleChange({ file: e.target.files[0] })}
                    />
                </Flex>
            </Flex>
        </Flex>

        <Modal
            title="Upload Video"
            open={open}
            onCancel={closeModal}
            onOk={() => {
                console.log('Uploading:', fileList);
                closeModal();
            }}
        >
            {fileList[0] ? (
                <div>
                    <p>Selected file: {fileList[0].name}</p>
                    {videoDetails ? (
                        <>
                            <p>Dimensions: {videoDetails.width} × {videoDetails.height}</p>
                            <p>FPS: {videoDetails.fps}</p>
                            <p>Total frames: {videoDetails.totalFrame}</p>
                            <p>Size: {videoDetails.sizeMb} MB</p>
                            <p>Duration: {videoDetails.durationSecond} seconds</p>
                        </>
                    ) : (
                        <p>Loading video details...</p>
                    )}
                </div>
            ) : (
                <p>No file selected</p>
            )}
        </Modal>
    </Card>
);

};`

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions