|
|
|
'use strict'; |
|
|
|
var fs = require('fs'); |
|
var path = require('path'); |
|
var PassThrough = require('stream').PassThrough; |
|
var async = require('async'); |
|
var utils = require('./utils'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = function recipes(proto) { |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proto.saveToFile = |
|
proto.save = function(output) { |
|
this.output(output).run(); |
|
return this; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proto.writeToStream = |
|
proto.pipe = |
|
proto.stream = function(stream, options) { |
|
if (stream && !('writable' in stream)) { |
|
options = stream; |
|
stream = undefined; |
|
} |
|
|
|
if (!stream) { |
|
if (process.version.match(/v0\.8\./)) { |
|
throw new Error('PassThrough stream is not supported on node v0.8'); |
|
} |
|
|
|
stream = new PassThrough(); |
|
} |
|
|
|
this.output(stream, options).run(); |
|
return stream; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proto.takeScreenshots = |
|
proto.thumbnail = |
|
proto.thumbnails = |
|
proto.screenshot = |
|
proto.screenshots = function(config, folder) { |
|
var self = this; |
|
var source = this._currentInput.source; |
|
config = config || { count: 1 }; |
|
|
|
|
|
if (typeof config === 'number') { |
|
config = { |
|
count: config |
|
}; |
|
} |
|
|
|
|
|
if (!('folder' in config)) { |
|
config.folder = folder || '.'; |
|
} |
|
|
|
|
|
if ('timestamps' in config) { |
|
config.timemarks = config.timestamps; |
|
} |
|
|
|
|
|
if (!('timemarks' in config)) { |
|
if (!config.count) { |
|
throw new Error('Cannot take screenshots: neither a count nor a timemark list are specified'); |
|
} |
|
|
|
var interval = 100 / (1 + config.count); |
|
config.timemarks = []; |
|
for (var i = 0; i < config.count; i++) { |
|
config.timemarks.push((interval * (i + 1)) + '%'); |
|
} |
|
} |
|
|
|
|
|
if ('size' in config) { |
|
var fixedSize = config.size.match(/^(\d+)x(\d+)$/); |
|
var fixedWidth = config.size.match(/^(\d+)x\?$/); |
|
var fixedHeight = config.size.match(/^\?x(\d+)$/); |
|
var percentSize = config.size.match(/^(\d+)%$/); |
|
|
|
if (!fixedSize && !fixedWidth && !fixedHeight && !percentSize) { |
|
throw new Error('Invalid size parameter: ' + config.size); |
|
} |
|
} |
|
|
|
|
|
var metadata; |
|
function getMetadata(cb) { |
|
if (metadata) { |
|
cb(null, metadata); |
|
} else { |
|
self.ffprobe(function(err, meta) { |
|
metadata = meta; |
|
cb(err, meta); |
|
}); |
|
} |
|
} |
|
|
|
async.waterfall([ |
|
|
|
function computeTimemarks(next) { |
|
if (config.timemarks.some(function(t) { return ('' + t).match(/^[\d.]+%$/); })) { |
|
if (typeof source !== 'string') { |
|
return next(new Error('Cannot compute screenshot timemarks with an input stream, please specify fixed timemarks')); |
|
} |
|
|
|
getMetadata(function(err, meta) { |
|
if (err) { |
|
next(err); |
|
} else { |
|
|
|
var vstream = meta.streams.reduce(function(biggest, stream) { |
|
if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) { |
|
return stream; |
|
} else { |
|
return biggest; |
|
} |
|
}, { width: 0, height: 0 }); |
|
|
|
if (vstream.width === 0) { |
|
return next(new Error('No video stream in input, cannot take screenshots')); |
|
} |
|
|
|
var duration = Number(vstream.duration); |
|
if (isNaN(duration)) { |
|
duration = Number(meta.format.duration); |
|
} |
|
|
|
if (isNaN(duration)) { |
|
return next(new Error('Could not get input duration, please specify fixed timemarks')); |
|
} |
|
|
|
config.timemarks = config.timemarks.map(function(mark) { |
|
if (('' + mark).match(/^([\d.]+)%$/)) { |
|
return duration * parseFloat(mark) / 100; |
|
} else { |
|
return mark; |
|
} |
|
}); |
|
|
|
next(); |
|
} |
|
}); |
|
} else { |
|
next(); |
|
} |
|
}, |
|
|
|
|
|
function normalizeTimemarks(next) { |
|
config.timemarks = config.timemarks.map(function(mark) { |
|
return utils.timemarkToSeconds(mark); |
|
}).sort(function(a, b) { return a - b; }); |
|
|
|
next(); |
|
}, |
|
|
|
|
|
function fixPattern(next) { |
|
var pattern = config.filename || 'tn.png'; |
|
|
|
if (pattern.indexOf('.') === -1) { |
|
pattern += '.png'; |
|
} |
|
|
|
if (config.timemarks.length > 1 && !pattern.match(/%(s|0*i)/)) { |
|
var ext = path.extname(pattern); |
|
pattern = path.join(path.dirname(pattern), path.basename(pattern, ext) + '_%i' + ext); |
|
} |
|
|
|
next(null, pattern); |
|
}, |
|
|
|
|
|
function replaceFilenameTokens(pattern, next) { |
|
if (pattern.match(/%[bf]/)) { |
|
if (typeof source !== 'string') { |
|
return next(new Error('Cannot replace %f or %b when using an input stream')); |
|
} |
|
|
|
pattern = pattern |
|
.replace(/%f/g, path.basename(source)) |
|
.replace(/%b/g, path.basename(source, path.extname(source))); |
|
} |
|
|
|
next(null, pattern); |
|
}, |
|
|
|
|
|
function getSize(pattern, next) { |
|
if (pattern.match(/%[whr]/)) { |
|
if (fixedSize) { |
|
return next(null, pattern, fixedSize[1], fixedSize[2]); |
|
} |
|
|
|
getMetadata(function(err, meta) { |
|
if (err) { |
|
return next(new Error('Could not determine video resolution to replace %w, %h or %r')); |
|
} |
|
|
|
var vstream = meta.streams.reduce(function(biggest, stream) { |
|
if (stream.codec_type === 'video' && stream.width * stream.height > biggest.width * biggest.height) { |
|
return stream; |
|
} else { |
|
return biggest; |
|
} |
|
}, { width: 0, height: 0 }); |
|
|
|
if (vstream.width === 0) { |
|
return next(new Error('No video stream in input, cannot replace %w, %h or %r')); |
|
} |
|
|
|
var width = vstream.width; |
|
var height = vstream.height; |
|
|
|
if (fixedWidth) { |
|
height = height * Number(fixedWidth[1]) / width; |
|
width = Number(fixedWidth[1]); |
|
} else if (fixedHeight) { |
|
width = width * Number(fixedHeight[1]) / height; |
|
height = Number(fixedHeight[1]); |
|
} else if (percentSize) { |
|
width = width * Number(percentSize[1]) / 100; |
|
height = height * Number(percentSize[1]) / 100; |
|
} |
|
|
|
next(null, pattern, Math.round(width / 2) * 2, Math.round(height / 2) * 2); |
|
}); |
|
} else { |
|
next(null, pattern, -1, -1); |
|
} |
|
}, |
|
|
|
|
|
function replaceSizeTokens(pattern, width, height, next) { |
|
pattern = pattern |
|
.replace(/%r/g, '%wx%h') |
|
.replace(/%w/g, width) |
|
.replace(/%h/g, height); |
|
|
|
next(null, pattern); |
|
}, |
|
|
|
|
|
function replaceVariableTokens(pattern, next) { |
|
var filenames = config.timemarks.map(function(t, i) { |
|
return pattern |
|
.replace(/%s/g, utils.timemarkToSeconds(t)) |
|
.replace(/%(0*)i/g, function(match, padding) { |
|
var idx = '' + (i + 1); |
|
return padding.substr(0, Math.max(0, padding.length + 1 - idx.length)) + idx; |
|
}); |
|
}); |
|
|
|
self.emit('filenames', filenames); |
|
next(null, filenames); |
|
}, |
|
|
|
|
|
function createDirectory(filenames, next) { |
|
fs.exists(config.folder, function(exists) { |
|
if (!exists) { |
|
fs.mkdir(config.folder, function(err) { |
|
if (err) { |
|
next(err); |
|
} else { |
|
next(null, filenames); |
|
} |
|
}); |
|
} else { |
|
next(null, filenames); |
|
} |
|
}); |
|
} |
|
], function runCommand(err, filenames) { |
|
if (err) { |
|
return self.emit('error', err); |
|
} |
|
|
|
var count = config.timemarks.length; |
|
var split; |
|
var filters = [split = { |
|
filter: 'split', |
|
options: count, |
|
outputs: [] |
|
}]; |
|
|
|
if ('size' in config) { |
|
|
|
self.size(config.size); |
|
|
|
|
|
var sizeFilters = self._currentOutput.sizeFilters.get().map(function(f, i) { |
|
if (i > 0) { |
|
f.inputs = 'size' + (i - 1); |
|
} |
|
|
|
f.outputs = 'size' + i; |
|
|
|
return f; |
|
}); |
|
|
|
|
|
split.inputs = 'size' + (sizeFilters.length - 1); |
|
|
|
|
|
filters = sizeFilters.concat(filters); |
|
|
|
|
|
self._currentOutput.sizeFilters.clear(); |
|
} |
|
|
|
var first = 0; |
|
for (var i = 0; i < count; i++) { |
|
var stream = 'screen' + i; |
|
split.outputs.push(stream); |
|
|
|
if (i === 0) { |
|
first = config.timemarks[i]; |
|
self.seekInput(first); |
|
} |
|
|
|
self.output(path.join(config.folder, filenames[i])) |
|
.frames(1) |
|
.map(stream); |
|
|
|
if (i > 0) { |
|
self.seek(config.timemarks[i] - first); |
|
} |
|
} |
|
|
|
self.complexFilter(filters); |
|
self.run(); |
|
}); |
|
|
|
return this; |
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
proto.mergeToFile = |
|
proto.concatenate = |
|
proto.concat = function(target, options) { |
|
|
|
var fileInput = this._inputs.filter(function(input) { |
|
return !input.isStream; |
|
})[0]; |
|
|
|
var self = this; |
|
this.ffprobe(this._inputs.indexOf(fileInput), function(err, data) { |
|
if (err) { |
|
return self.emit('error', err); |
|
} |
|
|
|
var hasAudioStreams = data.streams.some(function(stream) { |
|
return stream.codec_type === 'audio'; |
|
}); |
|
|
|
var hasVideoStreams = data.streams.some(function(stream) { |
|
return stream.codec_type === 'video'; |
|
}); |
|
|
|
|
|
self.output(target, options) |
|
.complexFilter({ |
|
filter: 'concat', |
|
options: { |
|
n: self._inputs.length, |
|
v: hasVideoStreams ? 1 : 0, |
|
a: hasAudioStreams ? 1 : 0 |
|
} |
|
}) |
|
.run(); |
|
}); |
|
|
|
return this; |
|
}; |
|
}; |
|
|