Open In App

Create a Video Editor using React

Last Updated : 09 Feb, 2024
Improve
Improve
Like Article
Like
Save
Share
Report

Video Editor is one of the useful apps in day-to-day life. In this article, we’ll walk you through the process of building a basic video editing app using React Native. The application enables users to upload, trim, and convert specific scenes to GIFs and then download the final edited file directly from their browser.

Output Preview: Let us have a look at how the final output will look like.
Screenshot_85_1080x608

Prerequisites:

Approach to create Video Editor

  1. We’ll begin with setting up the project environment.
  2. To simplify, every component of the video editor is written and explained separately, such as: Video upload, Video player, Video editor, Video to GIF converter.
  3. Showcased how to use ffmpeg.wasm to launch an ffmpeg command that clips and converts the video to GIF in the browser.
  4. At last, all the components will be imported together in a VideoEditor.js and rendered in App.js file for the execution.

Functionalities of Video Editor:

  • Video import and playback.
  • Trim and cut video clips.
  • convert video to gif.
  • Preview and download edited video in real-time.

Steps to initialize a React project:

Step 1: Initialize a new Create React App React project called video-editor-wasm-react with the command below:

npx create-react-app video-editor-wasm-react
cd video-editor-wasm-react

Step 2: Create a file in src folder- setupProxy.js. This is required to enable SharedArrayBuffer in your browser so that it will make fmmpeg.wasm to function.

Javascript




// src/setupProxy.js
 
module.exports = function (app) {
    app.use(function (req, res, next) {
        res.setHeader("Cross-Origin-Opener-Policy", "same-origin")
        res.setHeader("Cross-Origin-Embedder-Policy", "require-corp")
        next()
    })
}


Step 3: Install the required dependencies.

npm install @ffmpeg/ffmpeg@0.10.0 antd video-react redux

Project Structure:

Screenshot-2024-02-02-180337

Dependencies:

"dependencies": {
"@ffmpeg/ffmpeg": "^0.10.0",
"@testing-library/jest-dom": "^5.17.0",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0",
"antd": "^5.14.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "5.0.1",
"redux": "^5.0.1",
"video-react": "^0.16.0",
"web-vitals": "^2.1.4"
}

Example Code: Create the necessary files and add the following code.

Javascript




// src/components/VideoConversionButton.js
 
import { Button } from "antd"
import { fetchFile } from "@ffmpeg/ffmpeg"
import { sliderValueToVideoTime } from "../utils/utils"
 
function VideoConversionButton({
    videoPlayerState,
    sliderValues,
    videoFile,
    ffmpeg,
    onConversionStart = () => { },
    onConversionEnd = () => { },
    onGifCreated = () => { },
}) {
    const convertToGif = async () => {
        // starting the conversion process
        onConversionStart(true)
 
        const inputFileName = "gif.mp4"
        const outputFileName = "output.gif"
 
        // writing the video file to memory
        ffmpeg.FS("writeFile", inputFileName, await fetchFile(videoFile))
 
        const [min, max] = sliderValues
        const minTime = sliderValueToVideoTime(videoPlayerState.duration, min)
        const maxTime = sliderValueToVideoTime(videoPlayerState.duration, max)
 
        // cutting the video and converting it to GIF with an FFMpeg command
        await ffmpeg
                   .run("-i", inputFileName, "-ss", `${minTime}`,
                           "-to", `${maxTime}`, "-f", "gif", outputFileName)
 
        // reading the resulting file
        const data = ffmpeg.FS("readFile", outputFileName)
 
        // converting the GIF file created by FFmpeg to a valid image URL
        const gifUrl = URL.createObjectURL(new Blob([data.buffer],
                                            { type: "image/gif" }))
        onGifCreated(gifUrl)
 
        // ending the conversion process
        onConversionEnd(false)
    }
 
    return <Button onClick={() => convertToGif()}>Convert to GIF</Button>
}
 
export default VideoConversionButton


Javascript




// src/components/VideoEditor.js
 
import { createFFmpeg } from "@ffmpeg/ffmpeg"
import { useEffect, useState } from "react"
import { Slider, Spin } from "antd"
import { VideoPlayer } from "./VideoPlayer"
import { sliderValueToVideoTime } from "../utils/utils"
import VideoUpload from "./VideoUpload"
import VideoConversionButton from "./VideoConversionButton"
const ffmpeg = createFFmpeg({ log: true })
 
function VideoEditor() {
    const [ffmpegLoaded, setFFmpegLoaded] = useState(false)
    const [videoFile, setVideoFile] = useState()
    const [videoPlayerState, setVideoPlayerState] = useState()
    const [videoPlayer, setVideoPlayer] = useState()
    const [gifUrl, setGifUrl] = useState()
    const [sliderValues, setSliderValues] = useState([0, 100])
    const [processing, setProcessing] = useState(false)
 
    useEffect(() => {
        // loading ffmpeg on startup
        ffmpeg.load().then(() => {
            setFFmpegLoaded(true)
        })
    }, [])
 
    useEffect(() => {
        const min = sliderValues[0]
        // when the slider values are updated, updating the
        // video time
        if (min !== undefined && videoPlayerState && videoPlayer) {
            videoPlayer.seek(sliderValueToVideoTime(
                videoPlayerState.duration, min
            ))
        }
    }, [sliderValues])
 
    useEffect(() => {
        if (videoPlayer && videoPlayerState) {
            // allowing users to watch only the portion of
            // the video selected by the slider
            const [min, max] = sliderValues
 
            const minTime =sliderValueToVideoTime(videoPlayerState.duration,min)
            const maxTime =sliderValueToVideoTime(videoPlayerState.duration,max)
 
            if (videoPlayerState.currentTime < minTime) {
                videoPlayer.seek(minTime)
            }
            if (videoPlayerState.currentTime > maxTime) {
                // looping logic
                videoPlayer.seek(minTime)
            }
        }
    }, [videoPlayerState])
 
    useEffect(() => {
        // when the current videoFile is removed,
        // restoring the default state
        if (!videoFile) {
            setVideoPlayerState(undefined)
            setSliderValues([0, 100])
            setVideoPlayerState(undefined)
            setGifUrl(undefined)
        }
    }, [videoFile])
 
    return (
        <div>
            <Spin
                spinning={processing || !ffmpegLoaded}
                tip={!ffmpegLoaded ? "Waiting for FFmpeg to load..."
                                     :"Processing..."}
            >
                <div>
                    {videoFile ? (
                        <VideoPlayer
                            src={URL.createObjectURL(videoFile)}
                            onPlayerChange={(videoPlayer) => {
                                setVideoPlayer(videoPlayer)
                            }}
                            onChange={(videoPlayerState) => {
                                setVideoPlayerState(videoPlayerState)
                            }}
                        />
                    ) : (
                        <h1>Upload a video</h1>
                    )}
                </div>
                <div className={"upload-div"}>
                    <VideoUpload
                        disabled={!!videoFile}
                        onChange={(videoFile) => {
                            setVideoFile(videoFile)
                        }}
                        onRemove={() => {
                            setVideoFile(undefined)
                        }}
                    />
                </div>
                <div className={"slider-div"}>
                    <h3>Cut Video</h3>
                    <Slider
                        disabled={!videoPlayerState}
                        value={sliderValues}
                        range={true}
                        onChange={(values) => {
                            setSliderValues(values)
                        }}
                        tooltip={{
                            formatter: null,
                        }}
                    />
                </div>
                <div className={"conversion-div"}>
                    <VideoConversionButton
                        onConversionStart={() => {
                            setProcessing(true)
                        }}
                        onConversionEnd={() => {
                            setProcessing(false)
                        }}
                        ffmpeg={ffmpeg}
                        videoPlayerState={videoPlayerState}
                        sliderValues={sliderValues}
                        videoFile={videoFile}
                        onGifCreated={(girUrl) => {
                            setGifUrl(girUrl)
                        }}
                    />
                </div>
                {gifUrl && (
                    <div className={"gif-div"}>
                        <h3>Resulting GIF</h3>
                        <img src={gifUrl}
                             className={"gif"}
                             alt={"GIF file generated in the client side"}/>
                        <a href={gifUrl}
                           download={"test.gif"}
                           className={"ant-btn ant-btn-default"}>
                            Download
                        </a>
                    </div>
                )}
            </Spin>
        </div>
    )
}
 
export default VideoEditor


Javascript




// src/components/VideoPlayer.js
 
import { BigPlayButton, ControlBar,
         LoadingSpinner, Player, PlayToggle } from "video-react"
import "video-react/dist/video-react.css"
import { useEffect, useState } from "react"
 
export function VideoPlayer({
    src,
    onPlayerChange = () => { },
    onChange = () => { },
    startTime = undefined,
}) {
    const [player, setPlayer] = useState(undefined)
    const [playerState, setPlayerState] = useState(undefined)
 
    useEffect(() => {
        if (playerState) {
            onChange(playerState)
        }
    }, [playerState])
 
    useEffect(() => {
        onPlayerChange(player)
 
        if (player) {
            player.subscribeToStateChange(setPlayerState)
        }
    }, [player])
 
    return (
        <div className={"video-player"}>
            <Player
                ref={(player) => {
                    setPlayer(player)
                }}
                startTime={startTime}
            >
                <source src={src} />
                <BigPlayButton position="center" />
                <LoadingSpinner />
                <ControlBar autoHide={false} disableDefaultControls={true}>
                    <PlayToggle />
                </ControlBar>
            </Player>
        </div>
    )
}


Javascript




// src/components/VideoUpload.js
 
import { Button, Upload } from "antd"
 
function VideoUpload({ disabled, onChange = () => { }, onRemove = () => { } }) {
    return (
        <>
            <Upload
                disabled={disabled}
                beforeUpload={() => {
                    return false
                }}
                accept="video/*"
                onChange={(info) => {
                    if (info.fileList && info.fileList.length > 0) {
                        onChange(info.fileList[0].originFileObj)
                    }
                }}
                showUploadList={false}
            >
                <Button>Upload Video</Button>
            </Upload>
            <Button
                danger={true}
                disabled={!disabled}
                onClick={() => {
                    onRemove(undefined)
                }}
            >
                Remove
            </Button>
        </>
    )
}
 
export default VideoUpload


Javascript




// src/utils/utils.js
 
export function sliderValueToVideoTime(duration, sliderValue) {
    return Math.round(duration * sliderValue / 100)
}


Javascript




// App.js
 
import "./App.css"
import VideoEditor from "./components/VideoEditor"
 
function App() {
    return (
        <div className={"app"}>
            <VideoEditor />
        </div>
    )
}
 
export default App


CSS




/* App.css */
 
.App {
    text-align: center;
}
 
.App-logo {
    height: 40vmin;
    pointer-events: none;
}
 
@media (prefers-reduced-motion: no-preference) {
    .App-logo {
        animation: App-logo-spin infinite 20s linear;
    }
}
 
.App-header {
    background-color: #282c34;
    min-height: 100vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-size: calc(10px + 2vmin);
    color: white;
}
 
.App-link {
    color: #61dafb;
}
 
@keyframes App-logo-spin {
    from {
        transform: rotate(0deg);
    }
 
    to {
        transform: rotate(360deg);
    }
}


Step 4: To start the application run the following command.

npm start

Output:
test



Like Article
Suggest improvement
Share your thoughts in the comments

Similar Reads