mikser
Version:
Real-time static site generator
273 lines (244 loc) • 8.93 kB
JavaScript
let path = require('path');
let ffmpeg = require('fluent-ffmpeg');
let fs = require('fs-extra-promise');
let Promise = require('bluebird');
let extend = require('node.extend');
let _ = require('lodash');
let S = require('string');
module.exports = function (mikser, context) {
let debug = mikser.debug('videos');
let config = {
presets: {
'360p': { width: 640, height: 360 },
'480p': { width: 853, height: 480 },
'720p': { width: 1280, height: 720 },
'1080p': { width: 1920, height: 1080 },
'mp4': { video: 'libx264', audio: 'aac' },
'webm': { video: 'libvpx', videoBitrate: 1000 }
},
transforms: {
convert: (info, videoCodec, audioCodec) => {
if (!info.keepDestination && info.preset) {
info.destination = info.destination.replace(path.extname(info.destination), '.' + info.preset.name);
}
if (info.preset && !videoCodec) {
videoCodec = info.preset.video;
}
if (videoCodec) {
info.video.videoCodec(videoCodec);
}
if (info.preset && !audioCodec) {
audioCodec = info.preset.audio;
}
if (audioCodec) {
info.video.audioCodec(audioCodec);
}
if (info.preset && info.preset.name == 'webm') {
info.video.videoBitrate(info.preset.videoBitrate);
}
},
resize: (info, width, height) => {
width = width || (info.preset ? info.preset.width : '?');
height = height || (info.preset ? info.preset.height : '?');
if (!info.preset) {
let ext = path.extname(info.destination);
let stringArgs = S(`${width}x${height}`).replaceAll('?', '').s;
info.destination = info.destination.replace(ext, `-${stringArgs}${ext}`);
}
info.video.size(`${width}x${height}`);
},
screenshot: (info, timestamp) => {
info.destination = info.destination.replace(path.extname(info.destination), '.jpg');
let options = {
folder: info.outFolder,
filename: path.basename(info.destination),
timestamps: [timestamp || '50%'],
}
info.takeScreenshot = () => {
debug('Screenshot at', options.timestamps[0]);
info.video.screenshots(options);
}
},
},
}
config = _.defaultsDeep(mikser.options.videos || {}, mikser.config.videos || {}, config);
function wrapTransforms(videoInfo) {
for (let action in config.transforms) {
videoInfo[action] = function() {
let args = [videoInfo].concat(Array.from(arguments));
videoInfo.preset = config.presets[args[1]];
if (videoInfo.preset) {
videoInfo.preset.name = args[1];
args.splice(1,1);
}
if (!videoInfo.keepDestination) {
let newName;
if (videoInfo.preset && !_.includes(['webm', 'mp4'], videoInfo.preset.name)){
newName = videoInfo.preset.name;
}
else {
newName = action;
}
let ext = path.extname(videoInfo.destination);
// update destination and url
videoInfo.destination = videoInfo.destination.replace(ext, '-' + newName + ext);
}
config.transforms[action].apply(null, args);
// remove the preset from args
delete videoInfo.preset;
return videoInfo;
}
}
}
function exposeTransforms (videoInfo) {
let notForExpose = ['screenshots', 'save', 'run', 'saveToFile', 'pipe', 'exec', 'execute', 'stream', 'writeToStream', 'ffprobe', 'output', 'addOutput', 'addListener', 'addOutput', 'emit', 'on', 'getAvailableFormats', 'getAvailableCodecs', 'getAvailableEncoders', 'getAvailableFilters'];
let commands = _.functionsIn(videoInfo.video);
_.remove(commands, (command) => {
return command.charAt(0) === '_' || _.includes(notForExpose, command);
});
for (let command of commands) {
if (!config.transforms[command]) {
config.transforms[command] = function (info) {
try {
info.video[command].apply(info.video, Array.from(arguments).slice(1));
} catch (err) {
console.log('Command error:', command, Array.from(arguments).slice(1), err)
}
}
}
}
}
function outputAndSave(videoInfo, next) {
let padding = 0;
let progress = 0;
videoInfo.video.on('error', (err) => {
if (fs.existsSync(videoInfo.destination)) {
fs.unlinkSync(videoInfo.destination);
}
mikser.diagnostics.log(context, 'error', '[videos] ' + err.message);
next(err);
}).on('progress', (data) => {
if (!videoInfo.takeScreenshot) {
let outputInfo = 'Video: ' + videoInfo.destination.replace(mikser.options.workingFolder, '') + ' ' + new Array(++progress%4+1).join('.') + ' ';
if (data.percent) {
let percent = Math.round(data.percent);
outputInfo = 'Video: ' + videoInfo.destination.replace(mikser.options.workingFolder, '') + ' ' + percent + '%';
}
padding = Math.max(outputInfo.length, padding);
process.stdout.write(S(outputInfo).padRight(padding) + '\x1b[0G');
}
}).on('end', () => {
process.stdout.write(S(' ').padRight(padding) + '\x1b[0G');
next();
});
if (videoInfo.takeScreenshot) {videoInfo.takeScreenshot() }
else {
videoInfo.video.save(videoInfo.destination);
}
}
let outputAndSaveAsync = Promise.promisify(outputAndSave);
function transform(source, destination) {
if (!source) {
let err = new Error('Undefined source');
err.origin = 'videos';
throw err;
}
if (!destination && !context) {
let err = new Error('Undefined destination');
err.origin = 'videos';
throw err;
}
let videoInfo = path.parse(source);
if (destination) {
if (destination.indexOf(mikser.options.workingFolder) !== 0) {
if (context) {
videoInfo.destination = mikser.utils.resolveDestination(destination, context.entity.destination);
} else {
videoInfo.destination = path.join(mikser.options.workingFolder, destination);
}
}
else {
videoInfo.destination = destination;
}
if (mikser.utils.isPathToFile(videoInfo.destination)) {
videoInfo.keepDestination = true;
}
} else {
videoInfo.destination = mikser.utils.predictDestination(source);
videoInfo.destination = mikser.utils.resolveDestination(videoInfo.destination, context.entity.destination);
}
if (!mikser.utils.isPathToFile(videoInfo.destination)) {
videoInfo.destination = path.join(videoInfo.destination, videoInfo.base);
}
videoInfo.toString = () => mikser.utils.getUrl(videoInfo.destination);
videoInfo.outFolder = path.dirname(videoInfo.destination);
videoInfo.video = ffmpeg();
videoInfo.on = () => {
videoInfo.overwrite = true;
return videoInfo;
}
videoInfo.off = () => {
videoInfo.overwrite = false;
return videoInfo;
}
videoInfo.skip = (state) => {
videoInfo.skipped = state;
if (state) videoInfo.destination = source;
return videoInfo
}
exposeTransforms(videoInfo);
wrapTransforms(videoInfo);
return {
process: () => {
if (videoInfo.skipped) return Promise.resolve();
let sourceFilePath = mikser.utils.findSource(source);
if (!sourceFilePath) {
return mikser.diagnostics.log(this, 'warning', `[videos] File not found at: ${source}`);
}
if ((sourceFilePath.indexOf(mikser.options.workingFolder) !== 0) && !videoInfo.destination) {
let err = new Error(`Destination is missing for file ${videoInfo.base}`);
err.origin = 'videos';
throw err;
}
return fs.existsAsync(videoInfo.destination).then((exist) => {
let overwrite = Promise.resolve(true);
if (exist && source != videoInfo.destination) {
if (videoInfo.overwrite) {
overwrite = fs.unlinkAsync(videoInfo.destination).return(true);
} else if (videoInfo.overwrite === false) {
overwrite = Promise.resolve(videoInfo.overwrite)
} else {
overwrite = Promise.join(fs.statAsync(sourceFilePath), fs.statAsync(videoInfo.destination), (sourceStats, destinationStats) => {
if (destinationStats.mtime < sourceStats.mtime) {
return fs.unlinkAsync(videoInfo.destination).return(true);
} else {
debug(videoInfo.destination.replace(mikser.options.workingFolder, ''), 'is newer than', sourceFilePath.replace(mikser.options.workingFolder, ''));
return Promise.resolve(false);
}
});
}
}
return overwrite.then((newer) => {
if (!newer) return Promise.resolve();
fs.ensureDirSync(videoInfo.outFolder);
videoInfo.video.input(sourceFilePath);
return outputAndSaveAsync(videoInfo);
});
});
},
videoInfo: videoInfo
}
}
if (context) {
context.video = function(source, destination) {
let videoTransform = transform(source, destination);
context.process(videoTransform.process);
return videoTransform.videoInfo;
}
}
let plugin = {
transform: transform
}
return plugin;
}