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.
Prerequisites:
Approach to create Video Editor
- We’ll begin with setting up the project environment.
- 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.
- Showcased how to use ffmpeg.wasm to launch an ffmpeg command that clips and converts the video to GIF in the browser.
- 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.
// 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:
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.
// 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
|
// 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
|
// 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>
)
} |
// 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
|
// src/utils/utils.js export function sliderValueToVideoTime(duration, sliderValue) {
return Math.round(duration * sliderValue / 100)
} |
// App.js import "./App.css"
import VideoEditor from "./components/VideoEditor"
function App() {
return (
<div className={ "app" }>
<VideoEditor />
</div>
)
} export default App
|
/* App.css */ .App { text-align : center ;
} .App-logo { height : 40 vmin;
pointer-events: none ;
} @media (prefers-reduced-motion: no-preference) { .App-logo {
animation: App-logo-spin infinite 20 s linear;
}
} .App-header { background-color : #282c34 ;
min-height : 100 vh;
display : flex;
flex- direction : column;
align-items: center ;
justify- content : center ;
font-size : calc( 10px + 2 vmin);
color : white ;
} .App-link { color : #61dafb ;
} @keyframes App-logo-spin { from {
transform: rotate( 0 deg);
}
to {
transform: rotate( 360 deg);
}
} |
Step 4: To start the application run the following command.
npm start