├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── input.mp4 └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .idea 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Razvan 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 | # Video editor using NodeJS 2 | 3 | This repository is associated with [this YouTube video](https://youtu.be/4hJLIMp51Cg). 4 | 5 | In [this](https://youtu.be/4hJLIMp51Cg) video we made a video editor from scratch using NodeJS. We useed ffmpeg and jimp to help us with the editing (link for those in the video description). I recently started an [Instagram account](http://instagram.com/the.dev.guy/) so I wanted to make a quick video editor that will add some padding to the video to match the aspect ratio for IGTV and a watermark. 6 | 7 | ## Install application 8 | 9 | In order to install the application you just have to clone the repository and run `npm install` in the project root folder. 10 | 11 | ## Start the application 12 | 13 | To start the app run `npm start` in the root folder of the app. 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Import dependencies 2 | const Jimp = require("jimp"); 3 | const fs = require("fs-extra"); 4 | const pathToFfmpeg = require("ffmpeg-static"); 5 | const util = require('util'); 6 | 7 | const exec = util.promisify(require('child_process').exec); 8 | 9 | // Video editor settings 10 | const videoEncoder = 'h264'; 11 | const inputFile = 'input.mp4'; 12 | const outputFile = 'output.mp4'; 13 | 14 | const inputFolder = 'temp/raw-frames'; 15 | const outputFolder = 'temp/edited-frames'; 16 | 17 | let currentProgress = 0; 18 | 19 | (async function () { 20 | try { 21 | // Create temporary folders 22 | console.log('Initialize temp files'); 23 | await fs.mkdir('temp'); 24 | await fs.mkdir(inputFolder); 25 | await fs.mkdir(outputFolder); 26 | 27 | // Decode MP4 video and resize it to width 1080 and height auto (to keep the aspect ratio) 28 | console.log('Decoding'); 29 | await exec(`"${pathToFfmpeg}" -i ${inputFile} -vf scale=1080:-1 ${inputFolder}/%d.png`); 30 | 31 | // Edit each frame 32 | console.log('Rendering'); 33 | const frames = fs.readdirSync(inputFolder); 34 | 35 | for (let frameCount = 1; frameCount <= frames.length; frameCount++) { 36 | 37 | // Check and log progress 38 | checkProgress(frameCount, frames.length); 39 | 40 | // Read the current frame 41 | let frame = await Jimp.read(`${inputFolder}/${frameCount}.png`); 42 | 43 | // Modify frame 44 | frame = await modifyFrame(frame); 45 | 46 | // Save the frame 47 | await frame.writeAsync(`${outputFolder}/${frameCount}.png`); 48 | } 49 | 50 | // Encode video from PNG frames to MP4 (no audio) 51 | console.log('Encoding'); 52 | await exec(`"${pathToFfmpeg}" -start_number 1 -i ${outputFolder}/%d.png -vcodec ${videoEncoder} -pix_fmt yuv420p temp/no-audio.mp4`); 53 | 54 | // Copy audio from original video 55 | console.log('Adding audio'); 56 | await exec(`"${pathToFfmpeg}" -i temp/no-audio.mp4 -i ${inputFile} -c copy -map 0:v:0 -map 1:a:0? ${outputFile}`); 57 | 58 | // Remove temp folder 59 | console.log('Cleaning up'); 60 | await fs.remove('temp'); 61 | 62 | } catch (e) { 63 | console.log("An error occurred:", e); 64 | 65 | // Remove temp folder 66 | console.log('Cleaning up'); 67 | await fs.remove('temp'); 68 | } 69 | })(); 70 | 71 | /** 72 | * Edit frame 73 | * Add padding to change the aspect ratio to 9:16 (for IGTV) 74 | * Add watermark to frame corner 75 | * @param frame 76 | */ 77 | const modifyFrame = async (frame) => { 78 | 79 | // Calculate the new height for 9:16 aspect ratio based on the current video width 80 | let newHeight = 16 * frame.bitmap.width / 9; 81 | // Video height must be an even number 82 | newHeight = newHeight % 2 === 0 ? newHeight : (newHeight + 1); 83 | 84 | // Create new image width current width, new height and white background 85 | const newImage = new Jimp(frame.bitmap.width, newHeight, 'white'); 86 | 87 | // Add watermark 88 | const font = await Jimp.loadFont(Jimp.FONT_SANS_64_BLACK); 89 | newImage.print(font, 20, newImage.bitmap.height - 100, '@the.dev.guy'); 90 | 91 | // Center the video in the current 9:16 image 92 | newImage.composite(frame, 0, (newHeight / 2) - (frame.bitmap.height / 2)); 93 | 94 | return newImage; 95 | }; 96 | 97 | /** 98 | * Calculate the processing progress based on the current frame number and the total number of frames 99 | * @param currentFrame 100 | * @param totalFrames 101 | */ 102 | const checkProgress = (currentFrame, totalFrames) => { 103 | const progress = currentFrame / totalFrames * 100; 104 | if (progress > (currentProgress + 10)) { 105 | const displayProgress = Math.floor(progress); 106 | console.log(`Progress: ${displayProgress}%`); 107 | currentProgress = displayProgress; 108 | } 109 | }; 110 | -------------------------------------------------------------------------------- /input.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/razvanstatescu/nodejs-video-editor/8970109bd56c18316e8b59862ea25439c9ec7f86/input.mp4 -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "video-editor", 3 | "version": "1.0.0", 4 | "description": "A simple video editor using jimp and ffmpeg", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js" 9 | }, 10 | "author": "Statescu Razvan", 11 | "license": "MIT", 12 | "dependencies": { 13 | "ffmpeg-static": "^4.2.1", 14 | "fs-extra": "^9.0.0", 15 | "jimp": "^0.10.3" 16 | } 17 | } 18 | --------------------------------------------------------------------------------