├── .gitignore ├── LICENSE.md ├── README.md ├── images ├── Reactive.png ├── dark.png ├── drag&drop.png └── editor.png ├── package-lock.json ├── package.json ├── public ├── index.html ├── manifest.json └── robots.txt ├── src ├── App.css ├── App.js ├── Checkbox.js ├── VideoEditor │ ├── Editor.js │ └── VideoEditor.js ├── checkbox.css ├── editor.css ├── index.css ├── index.js ├── reportWebVitals.js ├── setupProxy.js └── setupTests.js └── yarn.lock /.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 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [year] [fullname] 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Reactive: React Video Editor 4 | Reactive is a react based video editor made with the mission to build the simplest yet powerful video editing software. So, buckle up & let's get started! 5 | 6 | ## Demo 7 | 1. Enjoy reactive here: https://prakshal-jain.github.io/Reactive/ 8 | 2. To try & tweak source code instantly, visit: https://codesandbox.io/s/reactive-e8suc 9 | 10 | ## Install NPM package: 11 | To install reactive NPM package, run ```npm i react-video-editor``` 12 | URL: https://www.npmjs.com/package/react-video-editor 13 | 14 | ## Getting started: 15 | 1. Please make sure you have npm installed 16 | 2. Clone the repository, and run ```npm install```. This downloads all the dependencies required. 17 | 3. Now, it's the time to fire up our server 🚀. Run ```npm start``` to do so. 18 | 4. Congratulations! You are all set to experience the simplicity of the great in-browser video editing software! 19 | 20 | ## How reactive is Reactive? 21 | 1. Drag and drop your videos to edit. 22 |
23 | 24 |
25 | 26 | 2. Edit based on your mood: Light & Dark themes makes reactive even more user friendly. 27 |
28 | 29 |
30 | 31 | 3. Edit seamlessly 32 |
33 | 34 |
35 | -------------------------------------------------------------------------------- /images/Reactive.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prakshal-Jain/Reactive/8c8693b9de39e890e2ad0853fc67504e756b8d82/images/Reactive.png -------------------------------------------------------------------------------- /images/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prakshal-Jain/Reactive/8c8693b9de39e890e2ad0853fc67504e756b8d82/images/dark.png -------------------------------------------------------------------------------- /images/drag&drop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prakshal-Jain/Reactive/8c8693b9de39e890e2ad0853fc67504e756b8d82/images/drag&drop.png -------------------------------------------------------------------------------- /images/editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prakshal-Jain/Reactive/8c8693b9de39e890e2ad0853fc67504e756b8d82/images/editor.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reactive-collab", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.8.1", 7 | "@emotion/styled": "^11.8.1", 8 | "@ffmpeg-installer/ffmpeg": "^1.1.0", 9 | "@ffmpeg/core": "^0.10.0", 10 | "@ffmpeg/ffmpeg": "^0.10.1", 11 | "@fortawesome/fontawesome-svg-core": "^1.2.35", 12 | "@fortawesome/free-solid-svg-icons": "^5.15.3", 13 | "@fortawesome/react-fontawesome": "^0.1.14", 14 | "@material-ui/core": "^4.12.3", 15 | "@mui/material": "^5.4.3", 16 | "@mui/styles": "^5.4.2", 17 | "@reduxjs/toolkit": "^1.6.0", 18 | "@testing-library/jest-dom": "^5.11.4", 19 | "@testing-library/react": "^11.1.0", 20 | "@testing-library/user-event": "^12.1.10", 21 | "body-parser": "^1.20.0", 22 | "browserify": "^17.0.0", 23 | "coi-serviceworker": "^0.1.6", 24 | "debounce": "^1.2.1", 25 | "eslint": "^8.15.0", 26 | "ffmpeg-on-progress": "^1.0.0", 27 | "ffmpeg-progress-wrapper": "^2.0.1", 28 | "gh-pages": "^3.2.3", 29 | "inobounce": "^0.2.0", 30 | "js": "^0.1.0", 31 | "lodash.debounce": "^4.0.8", 32 | "lottie-web": "^5.9.4", 33 | "node": "^17.7.2", 34 | "progress-bar-react-ui": "^1.0.2", 35 | "react": "^17.0.2", 36 | "react-dom": "^17.0.2", 37 | "react-file-drop": "^3.1.5", 38 | "react-scripts": "^5.0.1", 39 | "react-video-editor": "^1.0.2", 40 | "shaneFFMPEG": "git+https://github.com/Shane-Geary/shaneFFMPEG.git", 41 | "styled-components": "^5.3.5", 42 | "tss-react": "^3.4.1", 43 | "use-debounce": "^7.0.1", 44 | "web-vitals": "^1.0.1" 45 | }, 46 | "scripts": { 47 | "start": "react-scripts start", 48 | "build": "react-scripts build", 49 | "test": "react-scripts test", 50 | "eject": "react-scripts eject" 51 | }, 52 | "eslintConfig": { 53 | "extends": [ 54 | "react-app", 55 | "react-app/jest" 56 | ] 57 | }, 58 | "browserslist": { 59 | "production": [ 60 | ">0.2%", 61 | "not dead", 62 | "not op_mini all" 63 | ], 64 | "development": [ 65 | "last 1 chrome version", 66 | "last 1 firefox version", 67 | "last 1 safari version" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 | 32 | 35 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Prakshal-Jain/Reactive/8c8693b9de39e890e2ad0853fc67504e756b8d82/src/App.css -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import './App.css'; 2 | import VideoEditor from './VideoEditor/VideoEditor' 3 | 4 | function App() { 5 | return ( 6 |
7 | 8 |
9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /src/Checkbox.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import "./checkbox.css"; 3 | 4 | class Checkbox extends React.Component { 5 | clickHandle = () => { 6 | if(this.props.checked){ 7 | this.props.onUncheck() 8 | } 9 | else{ 10 | this.props.onCheck() 11 | } 12 | } 13 | render() { 14 | if(this.props.isActive){ 15 | return( 16 |
17 |
18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | else{ 25 | return( 26 |
27 |
28 | 29 |
31 |
32 | ) 33 | } 34 | } 35 | } 36 | export default Checkbox -------------------------------------------------------------------------------- /src/VideoEditor/Editor.js: -------------------------------------------------------------------------------- 1 | // /* eslint-disable func-names */ 2 | import {useState, useRef, useEffect} from 'react' 3 | import '../editor.css' 4 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' // https://fontawesome.com/v5/docs/web/use-with/react 5 | import {faVolumeMute, faVolumeUp, faPause, faPlay, faGripLinesVertical, faSync, faStepBackward, faStepForward, faCamera, faDownload, faEraser} from '@fortawesome/free-solid-svg-icons' // https://fontawesome.com/v5/docs/web/use-with/react 6 | 7 | import {createFFmpeg, fetchFile} from '@ffmpeg/ffmpeg' // https://github.com/ffmpegwasm/ffmpeg.wasm/blob/master/docs/api.md 8 | 9 | 10 | function Editor({videoUrl, timings, setTimings}) { 11 | 12 | //Boolean state to handle video mute 13 | const [isMuted, setIsMuted] = useState(false) 14 | 15 | //Boolean state to handle whether video is playing or not 16 | const [playing, setPlaying] = useState(false) 17 | 18 | //Float integer state to help with trimming duration logic 19 | const [difference, setDifference] = useState(0.2) 20 | 21 | //Boolean state to handle deleting grabber functionality 22 | const [deletingGrabber, setDeletingGrabber] = useState(false) 23 | 24 | //State for error handling 25 | const [currentWarning, setCurrentWarning] = useState(null) 26 | 27 | //State for imageUrl 28 | const [imageUrl, setImageUrl] = useState('') 29 | 30 | //Boolean state handling trimmed video 31 | const [trimmingDone, setTrimmingDone] = useState(false) 32 | 33 | //Integer state to blue progress bar as video plays 34 | const [seekerBar, setSeekerBar] = useState(0) 35 | 36 | 37 | //Ref handling metadata needed for trim markers 38 | const currentlyGrabbedRef = useRef({'index': 0, 'type': 'none'}) 39 | 40 | //Ref handling the trimmed video element 41 | const trimmedVidRef = useRef() 42 | 43 | //Ref handling the initial video element for trimming 44 | const playVideoRef = useRef() 45 | 46 | //Ref handling the progress bar element 47 | const progressBarRef = useRef() 48 | 49 | //Ref handling the element of the current play time 50 | const playBackBarRef = useRef() 51 | 52 | //Variable for error handling on the delete grabber functionality 53 | const warnings = {'delete_grabber': (
Please click on the grabber (either start or end) to delete it
)} 54 | 55 | //State handling storing of the trimmed video 56 | const [trimmedVideo, setTrimmedVideo] = useState() 57 | 58 | //Integer state to handle the progress bars numerical incremation 59 | const [progress, setProgress] = useState(0) 60 | 61 | //Boolean state handling whether ffmpeg has loaded or not 62 | const [ready, setReady] = useState(false) 63 | 64 | //Ref to handle the current instance of ffmpeg when loaded 65 | const ffmpeg = useRef(null) 66 | 67 | 68 | //Function handling loading in ffmpeg 69 | const load = async () => { 70 | try{ 71 | await ffmpeg.current.load() 72 | 73 | setReady(true) 74 | } 75 | catch(error) { 76 | console.log(error) 77 | } 78 | } 79 | 80 | //Loading in ffmpeg when this component renders 81 | useEffect(() => { 82 | ffmpeg.current = createFFmpeg({ 83 | log: true, 84 | corePath: 'https://unpkg.com/@ffmpeg/core@0.10.0/dist/ffmpeg-core.js' 85 | }) 86 | load() 87 | // eslint-disable-next-line react-hooks/exhaustive-deps 88 | }, []) 89 | 90 | 91 | //Lifecycle handling the logic needed for the progress bar - displays the blue bar that grows as the video plays 92 | // eslint-disable-next-line react-hooks/exhaustive-deps 93 | useEffect(() => { 94 | if(playVideoRef.current.onloadedmetadata) { 95 | const currentIndex = currentlyGrabbedRef.current.index 96 | const seek = (playVideoRef.current.currentTime - timings[0].start) / playVideoRef.current.duration * 100 97 | setSeekerBar(seek) 98 | progressBarRef.current.style.width = `${seekerBar}%` 99 | if((playVideoRef.current.currentTime >= timings[0].end)) { 100 | playVideoRef.current.pause() 101 | setPlaying(false) 102 | currentlyGrabbedRef.current = ({'index': currentIndex + 1, 'type': 'start'}) 103 | progressBarRef.current.style.width = '0%' 104 | progressBarRef.current.style.left = `${timings[0].start / playVideoRef.current.duration * 100}%` 105 | playVideoRef.current.currentTime = timings[0].start 106 | } 107 | } 108 | 109 | window.addEventListener('keyup', (event) => { 110 | if(event.key === ' ') { 111 | playPause() 112 | } 113 | }) 114 | 115 | //Handles the start and end metadata for the timings state 116 | const time = timings 117 | playVideoRef.current.onloadedmetadata = () => { 118 | if(time.length === 0) { 119 | time.push({'start': 0, 'end': playVideoRef.current.duration}) 120 | setTimings(time) 121 | addActiveSegments() 122 | } 123 | else{ 124 | addActiveSegments() 125 | } 126 | } 127 | }) 128 | 129 | //Lifecycle that handles removing event listener from the mouse event on trimmer - Desktop browser 130 | useEffect(() => { 131 | return window.removeEventListener('mouseup', removeMouseMoveEventListener) 132 | // eslint-disable-next-line react-hooks/exhaustive-deps 133 | }, []) 134 | 135 | //Lifecycle that handles removing event listener from the touch/pointer event on trimmer - mobile browser 136 | useEffect(() => { 137 | return window.removeEventListener('pointerup', removePointerMoveEventListener) 138 | // eslint-disable-next-line react-hooks/exhaustive-deps 139 | }, []) 140 | 141 | //Function handling the trimmer movement logic 142 | const handleMouseMoveWhenGrabbed = (event) => { 143 | playVideoRef.current.pause() 144 | addActiveSegments() 145 | let playbackRect = playBackBarRef.current.getBoundingClientRect() 146 | let seekRatio = (event.clientX - playbackRect.left) / playbackRect.width 147 | const index = currentlyGrabbedRef.current.index 148 | const type = currentlyGrabbedRef.current.type 149 | let time = timings 150 | let seek = playVideoRef.current.duration * seekRatio 151 | if((type === 'start') && (seek > ((index !== 0) ? (time[index - 1].end + difference + 0.2) : 0)) && seek < time[index].end - difference){ 152 | progressBarRef.current.style.left = `${seekRatio * 100}%` 153 | playVideoRef.current.currentTime = seek 154 | time[index]['start'] = seek 155 | setPlaying(false) 156 | setTimings(time) 157 | } 158 | else if((type === 'end') && (seek > time[index].start + difference) && (seek < (index !== (timings.length - 1) ? time[index].start - difference - 0.2 : playVideoRef.current.duration))){ 159 | progressBarRef.current.style.left = `${time[index].start / playVideoRef.current.duration * 100}%` 160 | playVideoRef.current.currentTime = time[index].start 161 | time[index]['end'] = seek 162 | setPlaying(false) 163 | setTimings(time) 164 | } 165 | progressBarRef.current.style.width = '0%' 166 | } 167 | 168 | //Function that handles removing event listener from the mouse event on trimmer - Desktop browser 169 | const removeMouseMoveEventListener = () => { 170 | window.removeEventListener('mousemove', handleMouseMoveWhenGrabbed) 171 | } 172 | 173 | //Lifecycle that handles removing event listener from the mouse event on trimmer - Mobile browser 174 | const removePointerMoveEventListener = () => { 175 | window.removeEventListener('pointermove', handleMouseMoveWhenGrabbed) 176 | } 177 | 178 | //Function handling reset logic 179 | const reset = () => { 180 | playVideoRef.current.pause() 181 | 182 | setIsMuted(false) 183 | setPlaying(false) 184 | currentlyGrabbedRef.current = {'index': 0, 'type': 'none'} 185 | setDifference(0.2) 186 | setDeletingGrabber(false) 187 | setCurrentWarning(false) 188 | setImageUrl('') 189 | 190 | setTimings([{'start': 0, 'end': playVideoRef.current.duration}]) 191 | playVideoRef.current.currentTime = timings[0].start 192 | progressBarRef.current.style.left = `${timings[0].start / playVideoRef.current.duration * 100}%` 193 | progressBarRef.current.style.width = '0%' 194 | addActiveSegments() 195 | } 196 | 197 | //Function handling thumbnail logic 198 | const captureSnapshot = () => { 199 | let video = playVideoRef.current 200 | const canvas = document.createElement('canvas') 201 | // scale the canvas accordingly 202 | canvas.width = video.videoWidth 203 | canvas.height = video.videoHeight 204 | // draw the video at that frame 205 | canvas.getContext('2d') 206 | .drawImage(video, 0, 0, canvas.width, canvas.height) 207 | // convert it to a usable data URL 208 | const dataURL = canvas.toDataURL() 209 | setImageUrl({imageUrl: dataURL}) 210 | } 211 | 212 | //Function handling download of thumbnail logic 213 | const downloadSnapshot = () => { 214 | let a = document.createElement('a') //Create 215 | a.href = imageUrl //Image Base64 Goes here 216 | a.download = 'Thumbnail.png' //File name Here 217 | a.click() //Downloaded file 218 | } 219 | 220 | //Function handling skip to previous logic 221 | const skipPrevious = () => { 222 | if(playing){ 223 | playVideoRef.current.pause() 224 | } 225 | // let previousIndex = (currentlyGrabbed.index !== 0) ? (currentlyGrabbed.index - 1) : (timings.length - 1) 226 | // setCurrentlyGrabbed({currentlyGrabbed: {'index': previousIndex, 'type': 'start'}, playing: false}) 227 | // currentlyGrabbedRef.current = {'index': previousIndex, 'type': 'start'} 228 | // progressBarRef.current.style.left = `${timings[previousIndex].start / playVideoRef.current.duration * 100}%` 229 | // progressBarRef.current.style.width = '0%' 230 | // playVideoRef.current.currentTime = timings[previousIndex].start 231 | } 232 | 233 | //Function handling play and pause logic 234 | const playPause = () => { 235 | if(playing){ 236 | playVideoRef.current.pause() 237 | } 238 | else{ 239 | if((playVideoRef.current.currentTime >= timings[0].end)) { 240 | playVideoRef.current.pause() 241 | setPlaying(false) 242 | currentlyGrabbedRef.current = {'index': 0, 'type': 'start'} 243 | playVideoRef.current.currentTime = timings[0].start 244 | progressBarRef.current.style.left = `${timings[0].start / playVideoRef.current.duration * 100}%` 245 | progressBarRef.current.style.width = '0%' 246 | } 247 | playVideoRef.current.play() 248 | } 249 | setPlaying(!playing) 250 | } 251 | 252 | //Function handling skip to next logic 253 | const skipNext = () => { 254 | if(playing){ 255 | playVideoRef.current.pause() 256 | } 257 | // let nextIndex = (currentlyGrabbed.index !== (timings.length - 1)) ? (currentlyGrabbed.index + 1) : 0 258 | // setCurrentlyGrabbed({currentlyGrabbed: {'index': nextIndex, 'type': 'start'}, playing: false}) 259 | // currentlyGrabbedRef.current = {'index': nextIndex, 'type': 'start'} 260 | // progressBarRef.current.style.left = `${timings[nextIndex].start / playVideoRef.current.duration * 100}%` 261 | // progressBarRef.current.style.width = '0%' 262 | // playVideoRef.current.currentTime = timings[nextIndex].start 263 | } 264 | 265 | //Function handling updating progress logic (clicking on progress bar to jump to different time durations) 266 | const updateProgress = (event) => { 267 | let playbackRect = playBackBarRef.current.getBoundingClientRect() 268 | let seekTime = ((event.clientX - playbackRect.left) / playbackRect.width) * playVideoRef.current.duration 269 | playVideoRef.current.pause() 270 | // find where seekTime is in the segment 271 | let index = -1 272 | let counter = 0 273 | for(let times of timings){ 274 | if(seekTime >= times.start && seekTime <= times.end){ 275 | index = counter 276 | } 277 | counter += 1 278 | } 279 | if(index === -1) { 280 | return 281 | } 282 | setPlaying(false) 283 | currentlyGrabbedRef.current = {'index': index, 'type': 'start'} 284 | progressBarRef.current.style.width = '0%' // Since the width is set later, this is necessary to hide weird UI 285 | progressBarRef.current.style.left = `${timings[index].start / playVideoRef.current.duration * 100}%` 286 | playVideoRef.current.currentTime = seekTime 287 | } 288 | 289 | //Function handling adding new trim markers logic 290 | const addGrabber = () => { 291 | const time = timings 292 | const end = time[time.length - 1].end + difference 293 | setDeletingGrabber({deletingGrabber: false, currentWarning: null}) 294 | if(end >= playVideoRef.current.duration){ 295 | return 296 | } 297 | time.push({'start': end + 0.2, 'end': playVideoRef.current.duration}) 298 | setTimings(time) 299 | addActiveSegments() 300 | } 301 | 302 | //Function handling first step of deleting trimmer 303 | const preDeleteGrabber = () => { 304 | if(deletingGrabber){ 305 | setDeletingGrabber({deletingGrabber: false, currentWarning: null}) 306 | } 307 | else{ 308 | setDeletingGrabber({deletingGrabber: true, currentWarning: 'delete_grabber'}) 309 | } 310 | } 311 | 312 | //Function handling deletion of trimmers logic 313 | const deleteGrabber = (index) => { 314 | let time = timings 315 | setDeletingGrabber({deletingGrabber: false, currentWarning: null, currentlyGrabbed: {'index': 0, 'type': 'start'}}) 316 | setDeletingGrabber({deletingGrabber: false, currentWarning: null, currentlyGrabbed: {'index': 0, 'type': 'start'}}) 317 | if(time.length === 1){ 318 | return 319 | } 320 | time.splice(index, 1) 321 | progressBarRef.current.style.left = `${time[0].start / playVideoRef.current.duration * 100}%` 322 | playVideoRef.current.currentTime = time[0].start 323 | progressBarRef.current.style.width = '0%' 324 | addActiveSegments() 325 | } 326 | 327 | //Function handling logic of time segments throughout videos duration 328 | const addActiveSegments = () => { 329 | let colors = '' 330 | let counter = 0 331 | colors += `, rgb(240, 240, 240) 0%, rgb(240, 240, 240) ${timings[0].start / playVideoRef.current.duration * 100}%` 332 | for(let times of timings) { 333 | if(counter > 0) { 334 | colors += `, rgb(240, 240, 240) ${timings[counter].end / playVideoRef.current.duration * 100}%, rgb(240, 240, 240) ${times.start / playVideoRef.current.duration * 100}%` 335 | } 336 | colors += `, #ccc ${times.start / playVideoRef.current.duration * 100}%, #ccc ${times.end / playVideoRef.current.duration * 100}%` 337 | counter += 1 338 | } 339 | colors += `, rgb(240, 240, 240) ${timings[counter - 1].end / playVideoRef.current.duration * 100}%, rgb(240, 240, 240) 100%` 340 | playBackBarRef.current.style.background = `linear-gradient(to right${colors})` 341 | } 342 | 343 | // Function handling logic for post trimmed video 344 | const saveVideo = async(fileInput) => { 345 | let metadata = { 346 | 'trim_times': timings, 347 | 'mute': isMuted 348 | } 349 | console.log(metadata.trim_times) 350 | const trimStart = metadata.trim_times[0].start 351 | const trimEnd = metadata.trim_times[0].end 352 | 353 | const trimmedVideo = trimEnd - trimStart 354 | 355 | console.log('Trimmed Duration: ', trimmedVideo) 356 | console.log('Trim End: ', trimEnd) 357 | 358 | try{ 359 | //Disabling new-cap for FS function 360 | // eslint-disable-next-line new-cap 361 | ffmpeg.current.FS('writeFile', 'myFile.mp4', await fetchFile(videoUrl)) 362 | 363 | ffmpeg.current.setProgress(({ratio}) => { 364 | console.log('ffmpeg progress: ', ratio) 365 | if(ratio < 0) { 366 | setProgress(0) 367 | } 368 | setProgress(Math.round(ratio * 100)) 369 | }) 370 | 371 | await ffmpeg.current.run('-ss', `${trimStart}`, '-accurate_seek', '-i', 'myFile.mp4', '-to', `${trimmedVideo}`, '-codec', 'copy', 'output.mp4') 372 | 373 | //Disabling new-cap for FS function 374 | // eslint-disable-next-line new-cap 375 | const data = ffmpeg.current.FS('readFile', 'output.mp4') 376 | 377 | const url = URL.createObjectURL(new Blob([data.buffer], {type: 'video/mp4'})) 378 | 379 | setTrimmedVideo(url) 380 | setTrimmingDone(true) 381 | // setLottiePlaying(false) 382 | } 383 | catch(error) { 384 | console.log(error) 385 | } 386 | } 387 | 388 | return ( 389 |
390 | {/* Video element for the trimmed video */} 391 | {trimmingDone ? 392 |
397 | 411 |
412 | : null 413 | } 414 | {/* Main video element for the video editor */} 415 | 432 |
433 | {/* If there is an instance of the playVideoRef, render the trimmer markers */} 434 | {playVideoRef.current ? 435 | Array.from(timings).map((timing, index) => ( 436 |
438 |
439 | {/* Markup and logic for the start trim marker */} 440 |
{ 444 | if(deletingGrabber){ 445 | deleteGrabber(index) 446 | } 447 | else{ 448 | currentlyGrabbedRef.current = {'index': index, 'type': 'start'} 449 | window.addEventListener('mousemove', handleMouseMoveWhenGrabbed) 450 | window.addEventListener('mouseup', removeMouseMoveEventListener) 451 | } 452 | }} 453 | //Events for mobile - Start marker 454 | onPointerDown={() => { 455 | if(deletingGrabber){ 456 | deleteGrabber(index) 457 | } 458 | else{ 459 | currentlyGrabbedRef.current = {'index': index, 'type': 'start'} 460 | window.addEventListener('pointermove', handleMouseMoveWhenGrabbed) 461 | window.addEventListener('pointerup', removePointerMoveEventListener) 462 | } 463 | }} 464 | > 465 | 466 | 467 | 468 |
469 | {/* Markup and logic for the end trim marker */} 470 |
{ 474 | if(deletingGrabber){ 475 | deleteGrabber(index) 476 | } 477 | else{ 478 | currentlyGrabbedRef.current = {'index': index, 'type': 'end'} 479 | window.addEventListener('mousemove', handleMouseMoveWhenGrabbed) 480 | window.addEventListener('mouseup', removeMouseMoveEventListener) 481 | } 482 | }} 483 | //Events for mobile - End marker 484 | onPointerDown={() => { 485 | if(deletingGrabber){ 486 | deleteGrabber(index) 487 | } 488 | else{ 489 | currentlyGrabbedRef.current = {'index': index, 'type': 'end'} 490 | window.addEventListener('pointermove', handleMouseMoveWhenGrabbed) 491 | window.addEventListener('pointerup', removePointerMoveEventListener) 492 | } 493 | }} 494 | > 495 | 496 | 497 | 498 |
499 |
500 |
501 | )) 502 | : []} 503 |
504 |
505 |
506 | 507 |
508 |
509 | 510 | 511 | 512 |
513 |
514 | 515 | 516 | 517 |
518 |
519 | 520 | 521 | 522 |
523 |
524 | {ready ? 525 |
526 | : 527 |
Loading...
528 | } 529 | {currentWarning != null ?
{warnings[currentWarning]}
: ''} 530 | {(imageUrl !== '') ? 531 |
532 | Photos 533 |
534 |
535 | 536 | 539 |
540 |
541 |
542 | : ''} 543 |
544 | ) 545 | } 546 | 547 | export default Editor -------------------------------------------------------------------------------- /src/VideoEditor/VideoEditor.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | import {useState, useEffect} from 'react' 3 | import {FileDrop} from 'react-file-drop' // https://github.com/sarink/react-file-drop 4 | import '../editor.css' 5 | import Editor from './Editor' 6 | import {FontAwesomeIcon} from '@fortawesome/react-fontawesome' // https://fontawesome.com/v5/docs/web/use-with/react 7 | import {faLightbulb, faMoon} from '@fortawesome/free-solid-svg-icons' // https://fontawesome.com/v5/docs/web/use-with/react 8 | 9 | function VideoEditor() { 10 | 11 | //Boolean state handling whether upload has occured or not 12 | const [isUpload, setIsUpload] = useState(true) 13 | 14 | //State handling storing of the video 15 | const [videoUrl, setVideoUrl] = useState('') 16 | const [videoBlob, setVideoBlob] = useState('') 17 | 18 | //Boolean state handling whether light or dark mode has been chosen 19 | const [isDarkMode, setIsDarkMode] = useState(false) 20 | 21 | //Stateful array handling storage of the start and end times of videos 22 | const [timings, setTimings] = useState([]) 23 | 24 | 25 | //Lifecycle handling light and dark themes 26 | useEffect(() => { 27 | toggleThemes() 28 | document.addEventListener('drop', function(e) { 29 | e.preventDefault() 30 | e.stopPropagation() 31 | }) 32 | // eslint-disable-next-line react-hooks/exhaustive-deps 33 | }, []) 34 | 35 | //Function handling file input as well as file drop library and rendering of those elements 36 | const renderUploader = () => { 37 | return ( 38 |
39 | uploadFile(e.target.files)} 41 | type='file' 42 | className='hidden' 43 | id='up_file' 44 | /> 45 | uploadFile(e)} 47 | onTargetClick={() => document.getElementById('up_file').click()} 48 | > 49 | Click or drop your video here to edit! 50 | 51 |
52 | ) 53 | } 54 | 55 | //Function handling rendering the Editor component and passing props to that child component 56 | const renderEditor = () => { 57 | return ( 58 | // videoUrl --> URL of uploaded video 59 | 66 | ) 67 | } 68 | 69 | //Function handling the light and dark themes logic 70 | const toggleThemes = () => { 71 | if(isDarkMode) { 72 | document.body.style.backgroundColor = '#1f242a' 73 | document.body.style.color = '#fff' 74 | } 75 | if(!isDarkMode){ 76 | document.body.style.backgroundColor = '#fff' 77 | document.body.style.color = '#1f242a' 78 | } 79 | setIsDarkMode(!isDarkMode) 80 | } 81 | 82 | //Function handling the file upload file logic 83 | const uploadFile = async (fileInput) => { 84 | console.log(fileInput[0]) 85 | let fileUrl = URL.createObjectURL(fileInput[0]) 86 | setIsUpload(false) 87 | setVideoUrl(fileUrl) 88 | setVideoBlob(fileInput[0]) 89 | } 90 | 91 | return ( 92 |
93 | {/* Boolean to handle whether to render the file uploader or the video editor */} 94 | {isUpload ? renderUploader() : renderEditor()} 95 |
96 | {isDarkMode ? 97 | () 99 | : 100 | } 101 |
102 |
103 | ) 104 | } 105 | 106 | export default VideoEditor -------------------------------------------------------------------------------- /src/checkbox.css: -------------------------------------------------------------------------------- 1 | .square-icon { 2 | background-color: transparent; 3 | width: 1.5em; 4 | height: 1.5em; 5 | border-radius: 0.25em; 6 | text-align: center; 7 | line-height: 1.2em; 8 | vertical-align: middle; 9 | color: #F04E26; 10 | margin: 0.5em; 11 | } 12 | 13 | .check_label{ 14 | margin-left: 0.5em; 15 | } 16 | 17 | .active{ 18 | border: 0.15em solid #F04E26; 19 | } 20 | 21 | .inactive{ 22 | border: 0.15em solid #6c757d; 23 | } -------------------------------------------------------------------------------- /src/editor.css: -------------------------------------------------------------------------------- 1 | body{ 2 | transition: background-color .3s ease, color .3s ease; 3 | } 4 | 5 | .file-drop{ 6 | width: 100%; 7 | height: fit-content; 8 | font-weight: bold; 9 | font-size: large; 10 | } 11 | 12 | .file-drop > .file-drop-target{ 13 | transition: border-color .3s ease; 14 | border: 0.2em solid #f04e26; 15 | border-radius: 1em; 16 | text-align: center; 17 | padding: 8em 0em; 18 | cursor: pointer; 19 | } 20 | 21 | .file-drop > .file-drop-target.file-drop-dragging-over-target{ 22 | border-color: #90ee90; 23 | transition: border-color .3s ease; 24 | color: #cacaca 25 | } 26 | 27 | .hidden{ 28 | display: none; 29 | } 30 | 31 | .toolBar{ 32 | padding: 1em; 33 | margin: 2em; 34 | text-align: center; 35 | border-radius: 1em; 36 | box-shadow: rgba(60, 64, 67, 0.3) 0px 1px 2px 0px, rgba(60, 64, 67, 0.15) 0px 1px 3px 1px; 37 | } 38 | 39 | .toggle{ 40 | background-color:rgb(185, 185, 185); 41 | color: yellow; 42 | height: 2em; 43 | width: 2em; 44 | text-align: center; 45 | line-height: 2em; 46 | vertical-align: middle; 47 | border-radius: 100em; 48 | font-size: x-large; 49 | z-index: 100; 50 | position: fixed; 51 | bottom: 1em; 52 | right: 1em; 53 | cursor: pointer; 54 | transition: background-color .2s ease; 55 | -webkit-touch-callout: none; /* iOS Safari */ 56 | -webkit-user-select: none; /* Safari */ 57 | -khtml-user-select: none; /* Konqueror HTML */ 58 | -moz-user-select: none; /* Old versions of Firefox */ 59 | -ms-user-select: none; /* Internet Explorer/Edge */ 60 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ 61 | 62 | } 63 | 64 | .toggle:hover{ 65 | transition: background-color .2s ease; 66 | background-color:rgb(146, 146, 146); 67 | } 68 | 69 | /* Styling for Editing */ 70 | .wrapper { 71 | width: 60%; 72 | max-width: 90%; 73 | padding: 1rem; 74 | border-radius: 4px; 75 | margin: auto; 76 | position: absolute; 77 | top: 50%; 78 | left: 50%; 79 | transform: translate(-50%, -50%); 80 | -webkit-touch-callout: none; /* iOS Safari */ 81 | -webkit-user-select: none; /* Safari */ 82 | -khtml-user-select: none; /* Konqueror HTML */ 83 | -moz-user-select: none; /* Old versions of Firefox */ 84 | -ms-user-select: none; /* Internet Explorer/Edge */ 85 | user-select: none; /* Non-prefixed version, currently supported by Chrome, Edge, Opera and Firefox */ 86 | 87 | } 88 | 89 | .video { 90 | width: 100%; 91 | cursor: pointer; 92 | border-radius: 1em; 93 | } 94 | 95 | .playback { 96 | position: relative; 97 | margin-top: 1rem; 98 | margin-left: 24px; 99 | margin-right: 24px; 100 | height: 10px; 101 | background: #2f3b44; 102 | margin-bottom: 1rem; 103 | border-radius: 1em; 104 | } 105 | .playback .seekable { 106 | position: absolute; 107 | top: 0; 108 | bottom: 0; 109 | left: 0; 110 | right: 0; 111 | background: rgb(240, 240, 240); 112 | cursor: pointer; 113 | } 114 | .playback .grabber { 115 | position: absolute; 116 | top: -4px; 117 | bottom: -4px; 118 | width: 18px; 119 | border-radius: 2px; 120 | z-index: 1; 121 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.4); 122 | cursor: pointer; 123 | transition: transform 0.2s ease; 124 | display: flex; 125 | align-items: center; 126 | justify-content: center; 127 | } 128 | .playback .grabber.start { 129 | background: #a8d736; 130 | } 131 | .playback .grabber.end { 132 | background: #fc4242; 133 | } 134 | .playback .grabber:hover { 135 | transform: scaleY(1.4); 136 | } 137 | .playback .grabber svg { 138 | /* user-drag: none; 139 | -moz-user-select: none; 140 | -webkit-user-drag: none; */ 141 | } 142 | .playback .progress { 143 | background: #0072cf; 144 | position: absolute; 145 | left: 0; 146 | top: 0; 147 | bottom: 0; 148 | cursor: pointer; 149 | pointer-events: none; 150 | } 151 | 152 | .controls { 153 | display: flex; 154 | justify-content: space-between; 155 | text-align: center; 156 | } 157 | .controls .player-controls button { 158 | width: 34px; 159 | margin: 0 0.125rem; 160 | } 161 | .controls .player-controls .play-control { 162 | background: #2f3b44; 163 | border: 0; 164 | color: #a5b0b5; 165 | padding: 0.5rem; 166 | border-radius: 4px; 167 | cursor: pointer; 168 | font-weight: bold; 169 | text-transform: uppercase; 170 | letter-spacing: 0.05em; 171 | } 172 | .controls .player-controls .play-control:hover { 173 | background: #445562; 174 | } 175 | .controls .player-controls .play-control:active { 176 | color: #ffffff; 177 | } 178 | .controls .player-controls .seek-start { 179 | background: #2f3b44; 180 | border: 0; 181 | color: #a5b0b5; 182 | padding: 0.5rem; 183 | border-radius: 4px; 184 | cursor: pointer; 185 | font-weight: bold; 186 | text-transform: uppercase; 187 | letter-spacing: 0.05em; 188 | } 189 | .controls .player-controls .seek-start:hover { 190 | background: #445562; 191 | } 192 | .controls .player-controls .seek-start:active { 193 | color: #ffffff; 194 | } 195 | .controls .player-controls .seek-end { 196 | background: #2f3b44; 197 | border: 0; 198 | color: #a5b0b5; 199 | padding: 0.5rem; 200 | border-radius: 4px; 201 | cursor: pointer; 202 | font-weight: bold; 203 | text-transform: uppercase; 204 | letter-spacing: 0.05em; 205 | } 206 | .controls .player-controls .seek-end:hover { 207 | background: #445562; 208 | } 209 | .controls .player-controls .seek-end:active { 210 | color: #ffffff; 211 | } 212 | .controls .settings-control { 213 | background: #2f3b44; 214 | border: 0; 215 | color: #a5b0b5; 216 | padding: 0.5rem; 217 | border-radius: 4px; 218 | cursor: pointer; 219 | font-weight: bold; 220 | text-transform: uppercase; 221 | letter-spacing: 0.05em; 222 | } 223 | 224 | .controls .settings-control:active { 225 | color: #ffffff; 226 | } 227 | 228 | .controls .settings-control:hover { 229 | background: #445562; 230 | } 231 | .controls .trim-control { 232 | background: #0072cf; 233 | border: 0; 234 | color: #fff; 235 | padding: 0.5rem; 236 | border-radius: 4px; 237 | cursor: pointer; 238 | font-weight: bold; 239 | text-transform: uppercase; 240 | letter-spacing: 0.1em; 241 | } 242 | .controls .trim-control:hover { 243 | background: #038eff; 244 | } 245 | 246 | .margined{ 247 | margin: 0em 0.3em; 248 | } 249 | 250 | .warning{ 251 | padding: 0.8em; 252 | color: #da7f00; 253 | background-color: #ffecd1; 254 | border-radius: 0.3em; 255 | border: 0.1em solid #f0ad4e; 256 | margin: 1em; 257 | justify-content: center; 258 | text-align: center; 259 | } 260 | 261 | .thumbnail{ 262 | width: 20%; 263 | height: auto; 264 | justify-content: center; 265 | align-items: center; 266 | border: 0.2em solid black; 267 | border-radius: 0.3em; 268 | } 269 | 270 | .marginVertical{ 271 | margin: 1.5em 0em; 272 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './App' 4 | import reportWebVitals from './reportWebVitals' 5 | import {CacheProvider} from '@emotion/react' 6 | import createCache from '@emotion/cache' 7 | 8 | export const muiCache = createCache({ 9 | 'key': 'mui', 10 | 'prepend': true 11 | }) 12 | 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | , 19 | document.getElementById('root') 20 | ) 21 | 22 | // If you want your app to work offline and load faster, you can change 23 | // unregister() to register() below. Note this comes with some pitfalls. 24 | // Learn more about service workers: https://cra.link/PWA 25 | // serviceWorkerRegistration.unregister() 26 | 27 | // If you want to start measuring performance in your app, pass a function 28 | // to log results (for example: reportWebVitals(console.log)) 29 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 30 | reportWebVitals() 31 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupProxy.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable func-names */ 2 | module.exports = function (app) { 3 | app.use(function (req, res, next) { 4 | res.setHeader('Cross-Origin-Opener-Policy', 'same-origin') 5 | res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp') 6 | next() 7 | }) 8 | } -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | --------------------------------------------------------------------------------