diff --git a/.gitignore b/.gitignore index c08673e..7eb40b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.png *.mp3 *.gif +*.webm converted-ss/ tmp-ss/ node_modules/ diff --git a/addAudio.js b/addAudio.js new file mode 100644 index 0000000..156143d --- /dev/null +++ b/addAudio.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function addAudio(inputVideoFile = __dirname + '/input.mp4', inputAudioFile = __dirname + '/input.mp3', outputFile = __dirname + '/output.mp4', callback) { + console.log('adding audio to video file'); + exec(`ffmpeg -i ${inputVideoFile} -i ${inputAudioFile} -c:v copy -c:a aac ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/convertToAscii.js b/convertToAscii.js index 1add1a3..8e63e4b 100644 --- a/convertToAscii.js +++ b/convertToAscii.js @@ -1,4 +1,5 @@ -module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModifier = 4, output = __dirname + '/tmp.img.png') => { +module.exports = convertToAscii = async (file = __dirname + 'input.png', output = __dirname + '/output.png', dotSize = 24, verticalSpacinModifier = 4, callback) => { + console.log('converting image to ascii'); const hobbitImageFile = file; const density = 'Ñ@#W$9876543210?!abc;:+=-,._ '; @@ -33,6 +34,11 @@ module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModif ctx.fillStyle = 'black'; ctx.fillRect(0, 0, hobbit.width * dotSize, hobbit.height * dotSize); + + ctx.font = (dotSize + verticalSpacinModifier) + "px Courier"; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = `white`; for(let x = 0; x < hobbit.width; x++) { for(let y = 0; y < hobbit.height; y++) { @@ -42,22 +48,16 @@ module.exports = convertToAscii = async (file, dotSize = 24, verticalSpacinModif const b = hobbit.pixels[pixelPosition + 2]; const a = hobbit.pixels[pixelPosition + 3]; - const avg = Math.floor((r+g+b)/ 3); + const avg = Math.floor((r+g+b)/ 3) * (a/255); const gray = map(255 - avg, 0, 255, 0, density.length); - - ctx.font = (dotSize + verticalSpacinModifier) + "px Courier"; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.fillStyle = `white`; ctx.fillText(density.charAt(gray), x*dotSize + dotSize/2, y*dotSize + dotSize/2); } } ImageDataURI.outputFile(canvas.toDataURL(), output); - console.log(output, 'converted'); res(1); }); - }); + callback(output); } diff --git a/extractAudio.js b/extractAudio.js new file mode 100644 index 0000000..bf4dc5d --- /dev/null +++ b/extractAudio.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function extractAudio(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.mp4', callback) { + console.log('extracting audio'); + exec(`ffmpeg -i ${inputFile} ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/extractFrames.js b/extractFrames.js new file mode 100644 index 0000000..92d00da --- /dev/null +++ b/extractFrames.js @@ -0,0 +1,14 @@ +const fs = require('fs'); +module.exports = async function extractFrames(inputFile = __dirname + '/input.mp4', outputFolder = __dirname + '/tmp-ss', callback) { + console.log('extracting frames'); + fs.mkdirSync(outputFolder); + + const extractFrames = require('ffmpeg-extract-frames') + + await extractFrames({ + input: inputFile, + output: outputFolder + '/%d.png', + }); + + callback(); +} diff --git a/gifToMp4.js b/gifToMp4.js new file mode 100644 index 0000000..370b6d0 --- /dev/null +++ b/gifToMp4.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function gifToMp4(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.gif', callback) { + console.log('converting gif to mp4'); + exec(`ffmpeg -i ${inputFile} ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/index.js b/index.js index b508036..d678c01 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,4 @@ /* - PREREQUISITES: - ffmpeg installed and added to the PATH @@ -16,55 +15,302 @@ COMMANDS TO BUILD VIDEO AFTER CONVERSION: - convert video to gif (no audio) ffmpeg -i .\converted-video.mp4 -f gif .\converted-video.gif - */ -const extractFrames = require('ffmpeg-extract-frames') -const convertToAscii = require('./convertToAscii'); +const convertToAscii = require("./convertToAscii"); +const extractFrames = require("./extractFrames"); +const gifToMp4 = require("./gifToMp4"); +const scaleVideo = require("./scaleVideo"); const fs = require('fs'); -const { exec } = require("child_process"); -const fsExtra = require('fs-extra') +const makeVideoFromImages = require("./makeVideoFromImages"); +const mp4ToGif = require("./mp4ToGif"); +const fsExtra = require('fs-extra'); -fsExtra.emptyDirSync(__dirname + '/converted-ss') -fsExtra.emptyDirSync(__dirname + '/tmp-ss') +args = process.argv; -if (!fs.existsSync(__dirname + '/converted-ss')) { - fs.mkdirSync(__dirname + '/converted-ss'); -} -if (!fs.existsSync(__dirname + '/tmp-ss')) { - fs.mkdirSync(__dirname + '/tmp-ss'); -} +args.shift(); +args.shift(); -const PATH_TO_VIDEO_TO_BE_CONVERTED = __dirname + '/sponge-bob-mrs-puff-scaled.mp4'; +const base = 10; -async function run() { - await extractFrames({ - input: PATH_TO_VIDEO_TO_BE_CONVERTED, - output: __dirname + '/tmp-ss/%d.png' - }); +const randomName = (Math.random() + 1).toString(36).substring(2); - let files = fs.readdirSync(__dirname + '/tmp-ss'); +const action = args.shift(); - files = files.filter(name => name.endsWith('.png')); +let usage = ''; +let example = ''; - for (let file of files) { - await convertToAscii(__dirname + '/tmp-ss/' + file, 12, 0, __dirname + '/converted-ss/' + file); - fs.rmSync(__dirname + '/tmp-ss/' + file); - } - exec('ffmpeg -framerate 10 -i ./converted-ss/%d.png -c:v libx264 -r 30 -pix_fmt yuv420p output.mp4 -y', (error, stdout, stderr) => { - if (error) { - console.log(`error: ${error.message}`); - return; +let input; +let output; +let pixelSize; +let pixelModifyer; +let scaledSize; +let fps; + + + +const _scaleVideo = (inputFile, pixelSize, pixelModifyer, scaledSize, fps, output, convertToGif = false, deleteInputFile = false) => { + scaleVideo(inputFile, 'mp4-scaled-' + randomName + '.mp4', scaledSize, fps, (scaledFile) => { + if (deleteInputFile) { + fs.rmSync(inputFile); } - if (stderr) { - console.log(`stderr: ${stderr}`); - return; - } - console.log(`stdout: ${stdout}`); + const outputFolder = __dirname + '/extracted-frames-'+randomName; + extractFrames(scaledFile, outputFolder, async () => { + fs.rmSync(scaledFile); + let files = fs.readdirSync(outputFolder); + const convertedFolder = __dirname + '/converted-frames-'+randomName; + let index = 0; + for (let file of files) { + index++; + await convertToAscii(outputFolder + '/' + file, convertedFolder + '/' + file, pixelSize, pixelModifyer, () => { + console.log(index, '/', files.length); + fs.rmSync(outputFolder + '/' + file); + }); + } + fsExtra.emptyDirSync(outputFolder); + fs.rmdirSync(outputFolder); + makeVideoFromImages(convertedFolder + '/%d.png', __dirname + 'generated-video-' + randomName + '.mp4', fps, fps, (generatedVideoFIle) => { + fsExtra.emptyDirSync(convertedFolder); + fs.rmdirSync(convertedFolder); + if (convertToGif) { + mp4ToGif(generatedVideoFIle, output, (gif) => { + console.log('gif generated in: ' + gif); + }); + } else { + fs.renameSync(generatedVideoFIle, output); + console.log('video generated in: ' + output); + } + }); + }) }); - - console.log('done'); } -run(); +switch (action) { + case 'img': + + usage = `Usage: npm run convert:img input-image-path.png[optional, default:./input.png] output-image-path.png[optional, default:./output.png] pixel-size[optional, default:24] font-size-modifier[optional, degault:4]`; + example = `Example: npm run convert:img input.png output.png 24 0`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 4) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + convertToAscii(args[0], args[1], args[2], args[3], () => { + console.log(`Image converted and saved in ${args[1]}`); + }); + + break; + case 'gif:gif': + + usage = `Usage: npm run convert:gif:gif input-image-path.png[optional, default:./input.gif] output-image-path.png[optional, default:./output.gif] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:gif:gif input.gif output.gif 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + gifToMp4(input, 'gif-to-mp4-'+randomName+'.mp4', (convertedFile) => { + _scaleVideo(convertedFile, pixelSize, pixelModifyer, scaledSize, fps, output, true, true); + }); + + break; + case 'gif:mp4': + + usage = `Usage: npm run convert:gif:mp4 input-image-path.gif[optional, default:./input.gif] output-image-path.mp4[optional, default:./output.mp4] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:gif:mp4 input.gif output.mp4 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + gifToMp4(input, 'gif-to-mp4-'+randomName+'.mp4', (convertedFile) => { + _scaleVideo(convertedFile, pixelSize, pixelModifyer, scaledSize, fps, output, false, true); + }); + + break; + case 'mp4:gif': + + usage = `Usage: npm run convert:mp4:gif input-image-path.mp4[optional, default:./input.mp4] output-image-path.gif[optional, default:./output.gif] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:mp4:gif input.mp4 output.gif 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + _scaleVideo(input, pixelSize, pixelModifyer, scaledSize, fps, output, true, false); + + break; + case 'mp4:mp4': + + usage = `Usage: npm run convert:mp4:mp4 input-image-path.mp4[optional, default:./input.mp4] output-image-path.mp4[optional, default:./output.mp4] pixel-size[optional, default:24] font-size-modifier[optional, degault:4] scaled-size[optional, default=120] fps[optional, default=10]`; + example = `Example: npm run convert:mp4:mp4 input.mp4 output.mp4 24 4 120 10`; + + if (args[0] === 'help') { + console.log(`${usage}\n${example}`); + process.exit(0); + } + + if (args.length > 7) { + console.error(`Invalid parameter count!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[2] && isNaN(parseInt(args[2], base))) { + console.error(`Pixel size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[3] && isNaN(parseInt(args[3], base))) { + console.error(`Font size modifier must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[4] && isNaN(parseInt(args[4], base))) { + console.error(`Scaled size must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + if (args[5] && isNaN(parseInt(args[5], base))) { + console.error(`Original FPS must be a number!\n${usage}\n${example}`); + process.exit(0); + } + + input = args[0]; + output = args[1]; + pixelSize = parseInt(args[2], base); + pixelModifyer = parseInt(args[3], base); + scaledSize = parseInt(args[4], base); + fps = parseInt(args[5], base); + + + _scaleVideo(input, pixelSize, pixelModifyer, scaledSize, fps, output, false, false); + + break; + + default: +console.log(`Available commands: +convert:gif:gif - converts regular gif to ascii gif +convert:gif:mp4 - converts regular gif to ascii mp4 +convert:mp4:mp4 - converts regular mp4 to ascii mp4 +convert:mp4:gif - converts regular mp4 to ascii gif +convert:img - converts regular image to ascii image\n`); +} diff --git a/makeVideoFromImages.js b/makeVideoFromImages.js new file mode 100644 index 0000000..e763945 --- /dev/null +++ b/makeVideoFromImages.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function makeVideoFromImages(inputFilesPattern = './converted-ss/%d.png', outputFile = __dirname + '/output.mp4', originalFramerate = 10, outputFramerate = 10, callback) { + console.log('combining images to video'); + exec(`ffmpeg -framerate ${originalFramerate} -i ${inputFilesPattern} -c:v libx264 -r ${outputFramerate} -pix_fmt yuv420p ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/mp4ToGif.js b/mp4ToGif.js new file mode 100644 index 0000000..381a496 --- /dev/null +++ b/mp4ToGif.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function mp4ToGif(inputFile = __dirname + '/input.gif', outputFile = __dirname + '/output.gif', callback) { + console.log('converting mp4 to gif'); + exec(`ffmpeg -i ${inputFile} -f gif ${outputFile} -y`, () => { + callback(outputFile); + }); + +} diff --git a/package.json b/package.json index 48803c5..e647e90 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,12 @@ "description": "", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "convert:gif:gif": "node index gif:gif", + "convert:gif:mp4": "node index gif:mp4", + "convert:mp4:mp4": "node index mp4:mp4", + "convert:mp4:gif": "node index mp4:gif", + "convert:img": "node index img", + "help": "node index" }, "author": "", "license": "ISC", diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ae29d17 --- /dev/null +++ b/readme.md @@ -0,0 +1,55 @@ +# ASCII Video Converter + +## Prerequisites + +``` + - nodejs: https://nodejs.org/en/download/ + - ffmpeg: https://www.ffmpeg.org/download.html +``` + +## Commands + +### Options + - input-file + - file to be converted (original is kept) + - default: input.mp4/input.gif/input.png depending on command used + + - output-file + - file to save the converted file + - default: input.mp4/input.gif/input.png depending on command used + + - pixel-size + - size of ASCII character (output resolution will be multipied by this amount, downscaling is recomended) + - default: 24 + + - font-size-modifier + - increase ASCII character size by set pixes without increasing space it takes. Could be useful when using bigger pixel-size setting + - default: 4 + + - scaled-size + - pixels in width (result will be scaled-size * pixel-size of width) + - default: 120 + + - fps + - frames per second of generated file + - default: 10 + +### Convert GIF to ASCII GIF + +`npm run convert:gif:gif [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert GIF to ASCII MP4 + +`npm run convert:gif:mp4 [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert MP4 to ASCII MP4 + +`npm run convert:mp4:mp4 [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert MP4 to ASCII GIF + +`npm run convert:mp4:gif [input-file] [output-file] [pixel-size] [font-size-modifier] [scaled-size] [fps]` + +### Convert PNG to ASCII PNG + +`npm run convert:img [input-gif-file] [output-gif-file] [pixel-size] [font-size-modifier]` diff --git a/scaleVideo.js b/scaleVideo.js new file mode 100644 index 0000000..ad495c7 --- /dev/null +++ b/scaleVideo.js @@ -0,0 +1,9 @@ +const { exec } = require("child_process"); + +module.exports = function scaleVideo(inputFile = __dirname + '/input.mp4', outputFile = __dirname + '/output.mp4', height = 120, fps = 10, callback) { + console.log('scaling video'); + exec(`ffmpeg.exe -i ${inputFile} -vf "scale=${height}:-2, fps=${fps} " ${outputFile} -y`, () => { + callback(outputFile); + }); + +}