Skip to content

Instantly share code, notes, and snippets.

@thomasjbradley
Last active August 29, 2015 14:02
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 thomasjbradley/acc1fd16cc294f9ad154 to your computer and use it in GitHub Desktop.
Save thomasjbradley/acc1fd16cc294f9ad154 to your computer and use it in GitHub Desktop.
Slideshow to DVD: Takes a folder of images and converts them into an MP4 and DVD slideshow.
#!/usr/bin/env node
/**
* # Slideshow to DVD
*
* Takes a folder of images and converts them into an MP4 and DVD slideshow.
* Will resize and crop images to 720p (1280x720).
* Expects music to be AAC M4A, though will likely work with other formats using the -m flag.
*
* ## Installation
*
* 1. Install Homebrew
* 2. brew install node imagemagick ffmpeg dvdauthor
* 3. npm install -g commander rimraf multimeter exec-sync ncp
* 4. Add to .profile
* PATH=/usr/local/share/npm/bin:$PATH
* PATH=/usr/local/sbin:$PATH
* PATH=/usr/local/bin:$PATH
* export PATH
* export NODE_PATH=/usr/local/lib/node_modules:/usr/local/share/npm/lib/node_modules:$NODE_PATH
*/
var
fs = require('fs'),
program = require('commander'),
rimraf = require('rimraf'),
ncp = require('ncp').ncp,
exec = require('child_process').exec,
execSync = require('exec-sync'),
multimeter = require('multimeter'),
debug = false,
verbose = false,
multi = multimeter(process),
progressBar,
totalSteps = 100,
currentStep = 0,
path = '',
out = '/tmp',
createDvd = true,
dvd = '/dvd',
titlecard = false,
files = [],
slides = [],
music = false,
slidetime = 5,
fadetime = 0.7,
finalfile = 'slideshow',
format = '-pix_fmt yuvj420p -c:v libx264 -preset slow',
commands = {
copy: 'cp "{{path}}/{{img}}" "{{path}}{{out}}/{{img}}"',
resize: 'mogrify -quality 92 -resize 1280x720^ -gravity center -extent 1280x720 "{{path}}{{out}}/*.jpg"',
fadein: 'ffmpeg -loop 1 -i "{{path}}{{out}}/{{img}}" -vf fade=in:st=0:d={{fadetime}} -t {{slidetime}} {{format}} -y "{{path}}{{out}}/{{index}}-0000.mp4"',
single: 'ffmpeg -loop 1 -i "{{path}}{{out}}/{{img}}" -t {{slidetime}} {{format}} -y "{{path}}{{out}}/{{index}}-0000.mp4"',
crossfade: 'ffmpeg -loop 1 -i "{{path}}{{out}}/{{img1}}" -loop 1 -i "{{path}}{{out}}/{{img2}}" -filter_complex "[1:v][0:v]blend=all_expr=\'A*(if(gte(T,{{fadetime}}),1,T/{{fadetime}}))+B*(1-(if(gte(T,{{fadetime}}),1,T/{{fadetime}})))\'" -t {{fadetime}} {{format}} -y "{{path}}{{out}}/{{index1}}-{{index2}}.mp4"',
fadeout: 'ffmpeg -loop 1 -i "{{path}}{{out}}/{{img}}" -vf fade=out:st={{st}}:d={{fadetime}} -t {{slidetime}} {{format}} -y "{{path}}{{out}}/{{index}}-0000.mp4"',
fadein_titlecard: 'ffmpeg -loop 1 -i "{{titlecard}}" -vf fade=in:st=0:d={{fadetime}} -t {{slidetime}} {{format}} -y "{{path}}{{out}}/zzzz-0000.mp4"',
fadeout_titlecard: 'ffmpeg -loop 1 -i "{{titlecard}}" -vf fade=out:st={{st}}:d={{fadetime}} -t {{slidetime}} {{format}} -y "{{path}}{{out}}/zzzz-0001.mp4"',
concat: 'ffmpeg -f concat -i "{{path}}{{out}}/list.txt" -c copy -y "{{path}}{{out}}/concat.mp4"',
audiolength: 'ffmpeg -i "{{audio}}"',
audiorepeat: 'ffmpeg {{inputs}} -filter_complex \'{{channels}}concat=n={{total}}:v=0:a=1[out]\' -map \'[out]\' -y "{{path}}{{out}}/audio.m4a"',
audiofade: 'ffmpeg -i "{{audio}}" -af afade=t=out:st={{st}}:d={{fadetime}} -t {{length}} -y "{{path}}{{out}}/audio-final.m4a"',
audiobind: 'ffmpeg -i "{{path}}{{out}}/concat.mp4" -i "{{path}}{{out}}/audio-final.m4a" -map 0:v -map 1:a -codec copy -y "{{path}}/{{finalfile}}.mp4"',
dvdformat: 'ffmpeg -i "{{path}}/{{finalfile}}.mp4" -aspect 16:9 -target ntsc-dvd "{{path}}{{out}}/dvd.mpg"',
dvdadd: 'dvdauthor -o "{{path}}{{dvd}}" -t "{{path}}{{out}}/dvd.mpg" -i post="jump chapter 1;"',
dvdcreate: 'export VIDEO_FORMAT=NTSC && dvdauthor -o "{{path}}{{dvd}}" -T'
}
;
var writeIfVerbose = function (text) {
if (verbose) {
process.stdout.write(text);
}
};
var determineSteps = function () {
var base = 9 + ((slides.length * 2) - 3);
if (music) {
base += 3;
}
if (createDvd) {
base += 3;
}
if (!debug) {
base += 1;
}
totalSteps = base;
};
var updateProgress = function () {
currentStep++;
if (typeof progressBar !== 'undefined') {
progressBar.percent(Math.round((currentStep / totalSteps) * 100));
}
};
var getVideoLength = function () {
return (slides.length * slidetime) + (fadetime * slides.length);
};
var getAudioLength = function () {
var
cmd = getCommand('audiolength').replace('{{audio}}', music),
raw = execSync(cmd, true),
matcher = /Duration\: \d\d\:(\d\d\:\d\d\.\d\d)\,/,
dur = raw.stderr.match(matcher)[1].split(':'),
min = parseInt(dur[0], 10) * 60,
total = min + parseFloat(dur[1])
;
return total;
};
var getCommand = function (cmd, opts) {
var opts = opts || {};
return commands[cmd]
.replace('{{format}}', format)
.replace(/\{\{path\}\}/g, opts.path || path)
.replace(/\{\{out\}\}/g, opts.out || out)
.replace(/\{\{slidetime\}\}/g, opts.slidetime || slidetime)
.replace(/\{\{fadetime\}\}/g, opts.fadetime || fadetime)
.replace('{{finalfile}}', finalfile)
.replace(/\{\{dvd\}\}/g, dvd)
;
}
var prePad = function (text) {
return ('0000000000' + text).substr(-4);
};
var prepareDirectory = function () {
writeIfVerbose('Preparing directory ... ');
rimraf.sync(path + '/' + finalfile + '.mp4')
rimraf.sync(path + out);
fs.mkdirSync(path + out);
rimraf.sync(path + dvd);
if (createDvd) {
fs.mkdirSync(path + dvd);
}
updateProgress();
writeIfVerbose('done.\n');
};
var findFiles = function () {
files = fs.readdirSync(path).sort()
writeIfVerbose('Finding files ... ');
files.forEach(function (item, index) {
if (!music && item.substr(-3) === 'm4a') {
music = path + '/' + item;
}
if (item.substr(-3) === 'jpg') {
slides.push(item);
}
});
updateProgress();
writeIfVerbose('done.\n');
};
var copyImages = function () {
var cmd = getCommand('copy');
writeIfVerbose('Copying files ... ');
slides.forEach(function (item, index) {
execSync(cmd.replace(/\{\{img\}\}/g, item));
});
writeIfVerbose('done.\n');
updateProgress();
resizeImages();
};
var resizeImages = function () {
var cmd = getCommand('resize');
writeIfVerbose('Resizing images ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done\n');
updateProgress();
fadeIn();
});
};
var fadeIn = function () {
var cmd = getCommand('fadein');
cmd = cmd
.replace('{{img}}', slides[0])
.replace('{{index}}', prePad(0))
;
writeIfVerbose('Fading in: ' + slides[0] + ' ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
buildSingle(1);
});
};
var buildSingle = function (index) {
var cmd = getCommand('single');
cmd = cmd
.replace('{{img}}', slides[index])
.replace('{{index}}', prePad(index))
;
writeIfVerbose('Building: ' + slides[index] + ' ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
if (index + 1 > slides.length - 1) {
buildTransition(0);
} else {
buildSingle(index + 1);
}
});
};
var buildTransition = function (index) {
var
next = index + 1,
cmd = getCommand('crossfade')
;
if (next > slides.length - 1) {
fadeOut(index);
return;
}
cmd = cmd
.replace('{{img1}}', slides[index])
.replace('{{img2}}', slides[next])
.replace('{{index1}}', prePad(index))
.replace('{{index2}}', prePad(next))
;
writeIfVerbose('Transitioning: ' + slides[index] + ' to ' + slides[next] + ' ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
buildTransition(next);
});
};
var fadeOut = function (index) {
var cmd = getCommand('fadeout');
cmd = cmd
.replace('{{img}}', slides[index])
.replace('{{index}}', prePad(index))
.replace('{{st}}', slidetime - fadetime)
;
writeIfVerbose('Fading out: ' + slides[index] + ' ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
if (titlecard) {
fadeInTitleCard();
return;
}
concat();
});
};
var fadeInTitleCard = function () {
var cmd = getCommand('fadein_titlecard', {
slidetime: Math.round(slidetime / 2)
});
cmd = cmd
.replace('{{titlecard}}', titlecard)
;
writeIfVerbose('Fading in title card: ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
fadeOutTitleCard();
});
};
var fadeOutTitleCard = function () {
var
newSlideTime = Math.round(slidetime / 2),
cmd = getCommand('fadeout_titlecard', {
slidetime: newSlideTime
});
cmd = cmd
.replace('{{titlecard}}', titlecard)
.replace('{{st}}', newSlideTime - (1 + fadetime))
;
writeIfVerbose('Fading out title card: ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
concat();
});
};
var prepFileForList = function (file) {
return "file '" + path + out + "/" + file + "'";
};
var concat = function () {
var
allFiles = [],
goodFiles = [],
concat = getCommand('concat')
;
writeIfVerbose('Joining ... ');
allFiles = fs.readdirSync(path + out).sort();
allFiles.forEach(function (item) {
if (item.substr(-3) === 'mp4') {
goodFiles.push(prepFileForList(item));
}
});
fs.writeFileSync(path + out + '/list.txt', goodFiles.join('\n'));
exec(concat, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
if (music) {
prepareAudio();
return;
}
if (createDvd) {
dvdFormat();
return;
}
finalize();
});
};
var getInputs = function (total) {
var i, out = '';
for (i = 0; i <= total; i++) {
out += ' -i "' + music + '"';
}
return out;
};
var getChannels = function (total) {
var i, out = '';
for (i = 0; i <= total - 1; i++) {
out += '[' + i + ':0]';
}
return out;
}
var prepareAudio = function () {
var
vidLength = getVideoLength(),
audLength = getAudioLength(),
reps = Math.ceil(vidLength / audLength),
cmd = getCommand('audiorepeat')
;
writeIfVerbose('Repeating audio ... ');
if (reps === 1) {
writeIfVerbose('done.\n');
updateProgress();
fadeAudio(music);
} else {
cmd = cmd
.replace('{{total}}', reps)
.replace('{{channels}}', getChannels(reps))
.replace('{{inputs}}', getInputs(reps))
;
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
fadeAudio(path + out + '/audio.m4a');
});
}
};
var fadeAudio = function (audio) {
var
vidLength = getVideoLength(),
cmd = getCommand('audiofade')
;
cmd = cmd
.replace('{{audio}}', audio)
.replace('{{st}}', (vidLength - fadetime).toFixed(1))
.replace('{{length}}', vidLength)
;
writeIfVerbose('Fading audio ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
bindAudio();
});
};
var bindAudio = function () {
var cmd = getCommand('audiobind');
writeIfVerbose('Binding audio ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
if (createDvd) {
dvdFormat();
return;
}
finalize();
});
};
var dvdFormat = function () {
var cmd = getCommand('dvdformat');
writeIfVerbose('Converting to DVD video ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
dvdAdd();
});
};
var dvdAdd = function () {
var cmd = getCommand('dvdadd');
writeIfVerbose('Adding to DVD ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
dvdCreate();
});
};
var dvdCreate = function () {
var cmd = getCommand('dvdcreate');
writeIfVerbose('Creating DVD ... ');
exec(cmd, function (err, stdout, stderr) {
writeIfVerbose('done.\n');
updateProgress();
finalize();
});
};
var finalize = function () {
if (!debug) {
writeIfVerbose('Deleting temporary folder.\n');
updateProgress();
rimraf.sync(path + out);
}
writeIfVerbose('Done.\n');
multi.write('\n').destroy();
};
program
.version('1.0.0')
.usage('[options] <folder>')
.option('-m, --music <file>', 'location of the music .m4a')
.option('-t, --titlecard <file>', 'location of the title card')
.option('-s, --slidetime <length>', 'the length each slide should play')
.option('-f, --fadetime <length>', 'the length of the fade transitions')
.option('--no-dvd', "don't create the DVD version")
.option('-v, --verbose', 'display constant status messages')
.option('-d, --debug', "debug mode: don't delete temporary files at the end")
program
.command('*')
.description('Convert a series of JPEGs into a video slideshow and DVD.')
.action(function (env) {
path = env;
debug = program.debug || false;
verbose = program.verbose || false;
titlecard = program.titlecard || false;
slidetime = program.slidetime || 5;
fadetime = program.fadetime || 0.7;
music = program.music || false;
createDvd = program.dvd;
prepareDirectory();
findFiles();
determineSteps();
console.log('§ Slides: ' + slides.length + '; Music: ' + music.replace(path + '/', ''));
if (!verbose) {
multi.drop({ width: 40 }, function (bar) {
progressBar = bar;
});
}
copyImages();
})
program.parse(process.argv)
if (program.args.length === 0) {
program.help();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment