├── .eslintrc.json ├── app ├── globals.css ├── favicon.ico ├── favicon-16x16.png ├── favicon-32x32.png ├── apple-touch-icon.png ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── page.tsx └── layout.tsx ├── next.config.mjs ├── postcss.config.mjs ├── .gitignore ├── tailwind.config.ts ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── package.json ├── types └── types.ts ├── LICENSE.md ├── components ├── banner.tsx └── react-video-editor.tsx └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /app/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/favicon-16x16.png -------------------------------------------------------------------------------- /app/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/favicon-32x32.png -------------------------------------------------------------------------------- /app/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/apple-touch-icon.png -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /app/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sambowenhughes/a-react-video-editor/HEAD/app/android-chrome-512x512.png -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import Banner from "@/components/banner"; 2 | import ReactVideoEditor from "@/components/react-video-editor"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 | 8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "React Video Editor", 9 | description: "A react video editor component.", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "a-react-video-editor", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@remotion/player": "^4.0.208", 13 | "lucide-react": "^0.438.0", 14 | "next": "14.2.7", 15 | "react": "^18.3.1", 16 | "react-dom": "^18", 17 | "remotion": "^4.0.208" 18 | }, 19 | "devDependencies": { 20 | "@types/node": "^20", 21 | "@types/react": "^18", 22 | "@types/react-dom": "^18", 23 | "eslint": "^8", 24 | "eslint-config-next": "14.2.7", 25 | "postcss": "^8", 26 | "tailwindcss": "^3.4.1", 27 | "typescript": "^5" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /types/types.ts: -------------------------------------------------------------------------------- 1 | export interface Clip { 2 | id: string; 3 | start: number; 4 | duration: number; 5 | src: string; 6 | row: number; 7 | } 8 | 9 | export interface TextOverlay { 10 | id: string; 11 | start: number; 12 | duration: number; 13 | text: string; 14 | row: number; 15 | } 16 | 17 | export interface Sound { 18 | id: string; 19 | start: number; 20 | duration: number; 21 | content: string; 22 | row: number; 23 | file: string; // Add this line 24 | } 25 | 26 | export interface PexelsMedia { 27 | id: number; 28 | width: number; 29 | height: number; 30 | url: string; 31 | image?: string; 32 | duration?: number; 33 | video_files?: { link: string; quality: string }[]; 34 | } 35 | 36 | export interface Effect { 37 | id: string; 38 | type: string; 39 | start: number; 40 | duration: number; 41 | row: number; 42 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 [Your Name or Your Organization's Name] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /components/banner.tsx: -------------------------------------------------------------------------------- 1 | import { Sparkles } from "lucide-react"; 2 | 3 | export default function Banner() { 4 | return ( 5 |
9 |
10 |

11 | 12 | 13 | Sparkles icon 14 | 15 | 16 | Upgrade to the Pro version of{" "} 17 | 22 | React Video Editor 23 | {" "} 24 | for advanced features and support! 25 | 26 |

27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Video Editor (Open Source Edition) 2 | 3 | > **Important Notice:** 4 | > The active repository for this project has been moved to 5 | > 👉 **[reactvideoeditor/free-react-video-editor](https://github.com/reactvideoeditor/free-react-video-editor)** 👈 6 | > Please visit the new repository for the **latest updates**, **contributions**, and **issues**. 7 | 8 | --- 9 | 10 | A free, open-source basic video editor example that runs directly in your web browser. This project serves as a foundation for video editing capabilities and is a simplified version of the full-featured [React Video Editor](https://www.reactvideoeditor.com/). 11 | 12 | Built with: 13 | - [Next.js](https://nextjs.org/) - React framework for server-side rendering and static site generation 14 | - [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework 15 | - [Remotion](https://www.remotion.dev/) - Framework for programmatically creating videos 16 | - [React](https://reactjs.org/) - JavaScript library for building user interfaces 17 | 18 | ## Demo 19 | 20 | Try out the live demo: [React Video Editor (Open Source Edition)](https://www.reactvideoeditor.com/open-source) 21 | 22 | ## About the Project 23 | 24 | This React Video Editor serves as a foundational tool for understanding and interacting with the core building blocks of Remotion. It offers a user-friendly, browser-based interface that allows users to compose videos by arranging clips and adding text overlays seamlessly. While the current version provides a smooth and interactive experience, it's designed to be a stepping stone toward building a fully-fledged video editor 25 | 26 | Key features include: 27 | 28 | - Arranging video clips on a timeline 29 | - Adding static text overlays to videos 30 | - Real-time preview of composition 31 | 32 | While this open-source version provides basic video composition functionality, it does not include advanced editing features. For a more comprehensive video editing solution, check out the [pro version](https://www.reactvideoeditor.com/) which offers additional capabilities and integration options for React applications. 33 | 34 | **Important Note:** This project uses the Remotion video player. If you intend to use this project, please be aware that you may need a Remotion license depending on your use case. Check out the [Remotion Licensing](https://www.remotion.dev/docs/licensing) page for more information and ensure you comply with their licensing terms. 35 | 36 | ## Getting Started 37 | 38 | To get started with this project, follow these steps: 39 | 40 | 1. Clone the repository to your local machine. 41 | 42 | 2. Install the dependencies: 43 | 44 | ```bash 45 | npm install 46 | ``` 47 | 48 | 3. Run the development server: 49 | 50 | ```bash 51 | npm run dev 52 | ``` 53 | 54 | 4. Open [http://localhost:3000](http://localhost:3000) in your browser to see the video editor in action. 55 | 56 | You can start editing the project by modifying the files in the `app` directory. The page will auto-update as you make changes. 57 | 58 | 59 | ## License 60 | 61 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. 62 | -------------------------------------------------------------------------------- /components/react-video-editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useRef, useCallback, useMemo, useEffect } from "react"; 4 | import { Player, PlayerRef } from "@remotion/player"; 5 | import { Sequence, Video, interpolate, useCurrentFrame } from "remotion"; 6 | import { LetterText, Plus, Text } from "lucide-react"; 7 | 8 | import { Clip, TextOverlay } from "@/types/types"; 9 | 10 | /** 11 | * TimelineMarker Component 12 | * Renders a marker on the timeline to indicate the current frame position 13 | */ 14 | const TimelineMarker: React.FC<{ 15 | currentFrame: number; 16 | totalDuration: number; 17 | }> = React.memo(({ currentFrame, totalDuration }) => { 18 | const markerPosition = useMemo(() => { 19 | return `${(currentFrame / totalDuration) * 100}%`; 20 | }, [currentFrame, totalDuration]); 21 | 22 | return ( 23 |
32 |
33 |
34 | ); 35 | }); 36 | 37 | TimelineMarker.displayName = "TimelineMarker"; 38 | 39 | /** 40 | * ReactVideoEditor Component 41 | * Main component for the video editor interface 42 | */ 43 | const ReactVideoEditor: React.FC = () => { 44 | // State management 45 | const [clips, setClips] = useState([]); 46 | const [textOverlays, setTextOverlays] = useState([]); 47 | const [totalDuration, setTotalDuration] = useState(1); 48 | const [currentFrame, setCurrentFrame] = useState(0); 49 | const [isMobile, setIsMobile] = useState(false); 50 | 51 | // Refs 52 | const playerRef = useRef(null); 53 | const timelineRef = useRef(null); 54 | 55 | /** 56 | * Adds a new clip to the timeline 57 | */ 58 | const addClip = () => { 59 | const lastItem = [...clips, ...textOverlays].reduce( 60 | (latest, item) => 61 | item.start + item.duration > latest.start + latest.duration 62 | ? item 63 | : latest, 64 | { start: 0, duration: 0 } 65 | ); 66 | 67 | const newClip: Clip = { 68 | id: `clip-${clips.length + 1}`, 69 | start: lastItem.start + lastItem.duration, 70 | duration: 300, 71 | src: "https://hgwavsootdmvmjdvfiwc.supabase.co/storage/v1/object/public/clips/reactvideoeditor-quality.mp4?t=2024-09-03T02%3A09%3A02.395Z", 72 | row: 0, 73 | }; 74 | 75 | setClips([...clips, newClip]); 76 | updateTotalDuration([...clips, newClip], textOverlays); 77 | }; 78 | 79 | /** 80 | * Adds a new text overlay to the timeline 81 | */ 82 | const addTextOverlay = () => { 83 | const lastItem = [...clips, ...textOverlays].reduce( 84 | (latest, item) => 85 | item.start + item.duration > latest.start + latest.duration 86 | ? item 87 | : latest, 88 | { start: 0, duration: 0 } 89 | ); 90 | 91 | const newOverlay: TextOverlay = { 92 | id: `text-${textOverlays.length + 1}`, 93 | start: lastItem.start + lastItem.duration, 94 | duration: 100, 95 | text: `Text ${textOverlays.length + 1}`, 96 | row: 0, 97 | }; 98 | 99 | setTextOverlays([...textOverlays, newOverlay]); 100 | updateTotalDuration(clips, [...textOverlays, newOverlay]); 101 | }; 102 | 103 | /** 104 | * Updates the total duration of the composition based on clips and text overlays 105 | */ 106 | const updateTotalDuration = ( 107 | updatedClips: Clip[], 108 | updatedTextOverlays: TextOverlay[] 109 | ) => { 110 | const lastClipEnd = updatedClips.reduce( 111 | (max, clip) => Math.max(max, clip.start + clip.duration), 112 | 0 113 | ); 114 | const lastTextOverlayEnd = updatedTextOverlays.reduce( 115 | (max, overlay) => Math.max(max, overlay.start + overlay.duration), 116 | 0 117 | ); 118 | 119 | const newTotalDuration = Math.max(lastClipEnd, lastTextOverlayEnd); 120 | setTotalDuration(newTotalDuration); 121 | }; 122 | 123 | /** 124 | * Composition component for Remotion Player 125 | */ 126 | const Composition = useCallback( 127 | () => ( 128 | <> 129 | {[...clips, ...textOverlays] 130 | .sort((a, b) => a.start - b.start) 131 | .map((item) => ( 132 | 137 | {"src" in item ? ( 138 | 143 | ))} 144 | 145 | ), 146 | [clips, textOverlays] 147 | ); 148 | 149 | /** 150 | * TimelineItem component for rendering clips and text overlays on the timeline 151 | */ 152 | const TimelineItem: React.FC<{ 153 | item: Clip | TextOverlay; 154 | type: "clip" | "text"; 155 | index: number; 156 | }> = ({ item, type, index }) => { 157 | const bgColor = 158 | type === "clip" 159 | ? "bg-indigo-500 to-indigo-400" 160 | : type === "text" 161 | ? "bg-purple-500 to-purple-400" 162 | : "bg-green-500 to-green-400"; 163 | 164 | return ( 165 |
174 |
175 | {type.charAt(0).toUpperCase() + type.slice(1)} {index + 1} 176 |
177 |
178 |
179 |
180 | ); 181 | }; 182 | 183 | // Effect for updating current frame 184 | useEffect(() => { 185 | const interval = setInterval(() => { 186 | if (playerRef.current) { 187 | const frame = playerRef.current.getCurrentFrame(); 188 | if (frame !== null) { 189 | setCurrentFrame(frame); 190 | } 191 | } 192 | }, 1000 / 30); 193 | 194 | return () => clearInterval(interval); 195 | }, []); 196 | 197 | // Effect for checking mobile view 198 | useEffect(() => { 199 | const checkMobile = () => { 200 | setIsMobile(window.innerWidth <= 768); 201 | }; 202 | 203 | checkMobile(); 204 | window.addEventListener('resize', checkMobile); 205 | 206 | return () => window.removeEventListener('resize', checkMobile); 207 | }, []); 208 | 209 | // Render mobile view message if on a mobile device 210 | if (isMobile) { 211 | return ( 212 |
213 |
214 |

Mobile View Not Supported

215 |

This video editor is only available on desktop or laptop devices.

216 |
217 |
218 | ); 219 | } 220 | 221 | // Main render 222 | return ( 223 |
224 | {/* Player section */} 225 |
226 |
227 |
228 |
235 |
Loading...
} 248 | inputProps={{}} 249 | /> 250 |
251 |
252 |
253 |
254 | 255 | {/* Timeline section */} 256 |
257 | {/* Timeline controls */} 258 |
259 |
260 | 267 | 274 |
275 |
276 | 277 | {/* Timeline items */} 278 |
282 |
283 |
284 |
292 |
293 |
294 |
295 |
296 |
297 | {clips.map((clip, index) => ( 298 | 304 | ))} 305 | {textOverlays.map((overlay, index) => ( 306 | 312 | ))} 313 |
314 |
315 |
316 | 320 |
321 |
322 |
323 | ); 324 | }; 325 | 326 | /** 327 | * TextOverlayComponent 328 | * Renders a text overlay with a fade-in effect 329 | */ 330 | const TextOverlayComponent: React.FC<{ text: string }> = ({ text }) => { 331 | const frame = useCurrentFrame(); 332 | const opacity = interpolate(frame, [0, 30], [0, 1], { 333 | extrapolateRight: "clamp", 334 | }); 335 | 336 | return ( 337 |
350 | {text} 351 |
352 | ); 353 | }; 354 | 355 | export default ReactVideoEditor; 356 | --------------------------------------------------------------------------------