Skip to content

Instantly share code, notes, and snippets.

@kadamwhite
Last active September 1, 2016 12:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kadamwhite/effcb6f3de78924be6a98f93f72d80da to your computer and use it in GitHub Desktop.
Save kadamwhite/effcb6f3de78924be6a98f93f72d80da to your computer and use it in GitHub Desktop.
Take a directory of images output by https://github.com/jcjohnson/neural-style and convert them to a mp4
/*
RESOURCES
ffmpeg and libvpx with Homebrew https://gist.github.com/clayton/6196167
ffmpeg docs http://ffmpeg.org/ffmpeg.html
http://robotics.usc.edu/~ampereir/wordpress/?p=702
Compat https://developer.mozilla.org/en-US/docs/Web/HTML/Supported_media_formats
https://en.support.wordpress.com/accepted-filetypes/
http://easywpguide.com/wordpress-manual/adding-images-other-media/inserting-video-audio-or-other-file-type/
"Supported A/V formats include M4a, MP4, OGG, WebM, FLV, WMV, MP3, WAV and WMA files."
*/
'use strict';
var spawn = require( 'child_process' ).spawn;
var exec = require( 'child_process' ).exec;
var fs = require( 'fs' );
/**
* Get the list of files in a directory, either as a list of file and subdir
* names or a list of absolute file system paths
*
* @private
* @param {string} inputDir The file system path to the directory to read
* @returns {Promise} A promise to the string array of file names
*/
const ls = ( inputDir, absolute ) => {
return new Promise( ( resolve, reject ) => {
fs.readdir( inputDir, ( err, list ) => {
if ( err ) {
return reject( err );
}
resolve( list );
});
});
};
/**
* Execute a shell command and return a promise that will resolve or exit
* when that command completes
*
* @param {string} command A shell command string e.g. "mv file1 file2"
* @param {boolean} quiet Whether to suppress outputting the command to be run
* @returns {Promise} A promise that completes when the command finishes
*/
const execCommand = ( command, quiet ) => {
return new Promise( ( resolve, reject ) => {
!quiet && console.log( command );
exec( command, ( error, stdout, stderr ) => {
if ( error ) {
return reject( error );
}
resolve();
});
});
}
const execRegardless = command => {
return execCommand( command ).catch( err => console.log( err ) );
}
/**
* Helper function that takes in an array of functions that return promises,
* then executes those functions sequentially to execute each action
*
* @param {function[]} arrOfFnsReturningPromises An array of functions
* @returns {Promise} A promise that will resolve once all the promises
* returned by that function successfully complete
*/
const runInSequence = arrOfFnsReturningPromises => {
return arrOfFnsReturningPromises.reduce(
( lastStep, startNextStep ) => lastStep.then( startNextStep ),
Promise.resolve()
);
};
const pad = ( num ) => {
let numStr = `${num}`;
while (numStr.length < 4) {
numStr = `0${numStr}`;
}
return numStr;
}
// How long should this jazz be?
// 1 repetition is a full forward-back mirrored sequence
const repetitions = 2;
const filename = process.argv[ 2 ] || 'output';
// const delay = process.argv[ 3 ] || 10;
// const framerate = Math.ceil( delay / 60 );
const framerate = process.argv[ 3 ] || 10;
// const imagickArgs = `-quiet -delay ${delay} -compress None -quality 100`;
// const convertFramesToMP4 = `convert ${imagickArgs} ./frames/*.png ${filename}.mp4`;
const convertFramesToMP4 = `ffmpeg -framerate 10 -i ./frames/frame%04d.png -c:v libx264 -r 30 -pix_fmt yuv420p ${filename}.mp4 -y`;
const convertMP4toWebM = `ffmpeg -i ${filename}.mp4 -c:v libvpx -b:v 1M -c:a libvorbis ${filename}.webm -y`;
// Forget the past
execRegardless( 'rm -rf ./frames' )
// imagine the future
.then( () => execRegardless( 'mkdir ./frames' ) )
// Get directory listing
.then( () => ls( __dirname ) )
// Skip non-png's
.then( files => files.filter( file => /png/.test( file ) ) )
// Assemble the list of images into a repeating sequence
.then( files => {
const first = files.find( file => /100/.test( file ) );
const last = files.find( file => /^[^\d]+$/.test( file ) );
const seq = files.filter( file => file !== first && file !== last );
// First frame (x2 for pseudo-easing)
const frames = [ first, first ]
// Forward tween pass
.concat( seq )
// Last frame (x2 for pseudo-easing)
.concat( [ last, last ] )
// Backwards tween pass
.concat( [].concat( seq ).reverse() )
return frames;
})
.then( files => {
console.log( 'Copying frames to temporary folder...' );
const copyCommands = files
.map( ( file, idx ) => {
return `cp ${file} ./frames/frame${pad(idx)}.png`
})
// Convert the command strings to command invoker functions
.map( command => () => execCommand( command, true ) );
// return copyCommands;
return runInSequence( copyCommands )
.then( () => files )
})
// Convert frames to mp4
.then( () => execCommand( convertFramesToMP4 ) )
// And WebM
.then( () => execCommand( convertMP4toWebM ) )
// Clean up temp files
.then( () => execRegardless( 'rm -rf ./frames' ) )
// Done!
.then( () => console.log( 'Done!' ) )
// Error handling
.catch( err => console.error( err ) );
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment