first commit
This commit is contained in:
53
commands/config.js
Normal file
53
commands/config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* Config
|
||||
* Generate a config file in the current directory
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
*/
|
||||
function done() {
|
||||
|
||||
console.log(di.chalk.green('Successfully Saved'));
|
||||
console.log('The config file is saved into the file:');
|
||||
console.log(di.chalk.magenta('config.yml'));
|
||||
|
||||
// Terminate the app
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
di.fs.copy(di.path.join(ROOT_PATH, 'config.yml'), 'config.yml', done);
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'config';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Generate a config file in the current directory';
|
||||
|
||||
/**
|
||||
* Command's handler function
|
||||
* @type {Function}
|
||||
*/
|
||||
module.exports.handler = command;
|
||||
65
commands/generate.js
Normal file
65
commands/generate.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Generate
|
||||
* Generate a web player for a recording file
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
*/
|
||||
function done() {
|
||||
|
||||
// Terminate the app
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
console.log('This command is not implemented yet. It will be avalible in the next versions');
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'generate <recordingFile>';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Generate a web player for a recording file';
|
||||
|
||||
/**
|
||||
* Command's handler function
|
||||
* @type {Function}
|
||||
*/
|
||||
module.exports.handler = command;
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* @param {Object} yargs
|
||||
*/
|
||||
module.exports.builder = function(yargs) {
|
||||
|
||||
// Define the recordingFile argument
|
||||
yargs.positional('recordingFile', {
|
||||
describe: 'the recording file',
|
||||
type: 'string',
|
||||
coerce: di.utility.loadYAML
|
||||
});
|
||||
|
||||
};
|
||||
231
commands/play.js
Normal file
231
commands/play.js
Normal file
@@ -0,0 +1,231 @@
|
||||
/**
|
||||
* Play
|
||||
* Play a recording file on your terminal
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Print the passed content
|
||||
*
|
||||
* @param {String} content
|
||||
* @param {Function} callback
|
||||
*/
|
||||
function playCallback(content, callback) {
|
||||
|
||||
process.stdout.write(content);
|
||||
callback();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
*/
|
||||
function done() {
|
||||
|
||||
// Full reset for the terminal
|
||||
process.stdout.write('\033c');
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
process.stdin.pause();
|
||||
|
||||
// Playing optinos
|
||||
var options = {
|
||||
frameDelay: argv.recordingFile.json.config.frameDelay,
|
||||
maxIdleTime: argv.recordingFile.json.config.maxIdleTime
|
||||
};
|
||||
|
||||
// Use the actual delays between frames as recorded
|
||||
if (argv.realTiming) {
|
||||
|
||||
options = {
|
||||
frameDelay: 'auto',
|
||||
maxIdleTime: 'auto'
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
// When app is closing
|
||||
di.death(done);
|
||||
|
||||
// Add the speedFactor option
|
||||
options.speedFactor = argv.speedFactor;
|
||||
|
||||
// Adjust frames delays
|
||||
adjustFramesDelays(argv.recordingFile.json.records, options);
|
||||
|
||||
// Play the recording records
|
||||
play(argv.recordingFile.json.records, playCallback, null, options);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust frames delays
|
||||
*
|
||||
* Options:
|
||||
*
|
||||
* - frameDelay (default: auto)
|
||||
* - Delay between frames in ms
|
||||
* - If the value is `auto` use the actual recording delays
|
||||
*
|
||||
* - maxIdleTime (default: 2000)
|
||||
* - Maximum delay between frames in ms
|
||||
* - Ignored if the `frameDelay` isn't set to `auto`
|
||||
* - Set to `auto` to prevnt limiting the max idle time
|
||||
*
|
||||
* - speedFactor (default: 1)
|
||||
* - Multiply the frames delays by this factor
|
||||
*
|
||||
* @param {Array} records
|
||||
* @param {Object} options (optional)
|
||||
*/
|
||||
function adjustFramesDelays(records, options) {
|
||||
|
||||
// Default value for options
|
||||
if (typeof options === 'undefined') {
|
||||
options = {};
|
||||
}
|
||||
|
||||
// Default value for options.frameDelay
|
||||
if (typeof options.frameDelay === 'undefined') {
|
||||
options.frameDelay = 'auto';
|
||||
}
|
||||
|
||||
// Default value for options.maxIdleTime
|
||||
if (typeof options.maxIdleTime === 'undefined') {
|
||||
options.maxIdleTime = 2000;
|
||||
}
|
||||
|
||||
// Default value for options.speedFactor
|
||||
if (typeof options.speedFactor === 'undefined') {
|
||||
options.speedFactor = 1;
|
||||
}
|
||||
|
||||
// Foreach record
|
||||
records.forEach(function(record) {
|
||||
|
||||
// Adjust the delay according to the options
|
||||
if (options.frameDelay != 'auto') {
|
||||
record.delay = options.frameDelay;
|
||||
} else if (options.maxIdleTime != 'auto' && record.delay > options.maxIdleTime) {
|
||||
record.delay = options.maxIdleTime;
|
||||
}
|
||||
|
||||
// Apply speedFactor
|
||||
record.delay = record.delay * options.speedFactor;
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Play recording records
|
||||
*
|
||||
* @param {Array} records
|
||||
* @param {Function} playCallback
|
||||
* @param {Function|Null} doneCallback
|
||||
*/
|
||||
function play(records, playCallback, doneCallback) {
|
||||
|
||||
var tasks = [];
|
||||
|
||||
// Default value for doneCallback
|
||||
if (typeof doneCallback === 'undefined') {
|
||||
doneCallback = null;
|
||||
}
|
||||
|
||||
// Foreach record
|
||||
records.forEach(function(record) {
|
||||
|
||||
tasks.push(function(callback) {
|
||||
|
||||
setTimeout(function() {
|
||||
playCallback(record.content, callback);
|
||||
}, record.delay);
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
di.async.series(tasks, function(error, results) {
|
||||
|
||||
if (doneCallback) {
|
||||
doneCallback();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'play <recordingFile>';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Play a recording file on your terminal';
|
||||
|
||||
/**
|
||||
* Command's handler function
|
||||
* @type {Function}
|
||||
*/
|
||||
module.exports.handler = command;
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* @param {Object} yargs
|
||||
*/
|
||||
module.exports.builder = function(yargs) {
|
||||
|
||||
// Define the recordingFile argument
|
||||
yargs.positional('recordingFile', {
|
||||
describe: 'The recording file',
|
||||
type: 'string',
|
||||
coerce: di.utility.loadYAML
|
||||
});
|
||||
|
||||
// Define the real-timing option
|
||||
yargs.option('r', {
|
||||
alias: 'real-timing',
|
||||
describe: 'Use the actual delays between frames as recorded',
|
||||
type: 'boolean',
|
||||
default: false
|
||||
});
|
||||
|
||||
// Define the speed-factor option
|
||||
yargs.option('s', {
|
||||
alias: 'speed-factor',
|
||||
describe: 'Speed factor, multiply the frames delays by this factor',
|
||||
type: 'number',
|
||||
default: 1.0
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Module //////////////////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
// Play recording records
|
||||
module.exports.play = play;
|
||||
|
||||
// Adjust frames delays
|
||||
module.exports.adjustFramesDelays = adjustFramesDelays;
|
||||
273
commands/record.js
Normal file
273
commands/record.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* Record
|
||||
* Record your terminal and create a recording file
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* The path of the recording file
|
||||
* @type {String}
|
||||
*/
|
||||
var recordingFile = null;
|
||||
|
||||
/**
|
||||
* The normalized configurations
|
||||
* @type {Object} {json, raw}
|
||||
*/
|
||||
var config = {};
|
||||
|
||||
/**
|
||||
* To keep tracking of the timestamp
|
||||
* of the last inserted record
|
||||
* @type {Number}
|
||||
*/
|
||||
var lastRecordTimestamp = null;
|
||||
|
||||
/**
|
||||
* To store the records
|
||||
* @type {Array}
|
||||
*/
|
||||
var records = [];
|
||||
|
||||
/**
|
||||
* Normalize the config file
|
||||
*
|
||||
* - Set default values in the json and raw
|
||||
* - Change the formatting of the values in the json and raw
|
||||
*
|
||||
* @param {Object} config {json, raw}
|
||||
* @return {Object} {json, raw}
|
||||
*/
|
||||
function normalizeConfig(config) {
|
||||
|
||||
// Default value for command
|
||||
if (!config.json.command) {
|
||||
|
||||
// Windows OS
|
||||
if (di.os.platform() === 'win32') {
|
||||
di.utility.changeYAMLValue(config, 'command', 'powershell.exe');
|
||||
} else {
|
||||
di.utility.changeYAMLValue(config, 'command', 'bash');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Default value for cwd
|
||||
if (!config.json.cwd) {
|
||||
di.utility.changeYAMLValue(config, 'cwd', process.cwd());
|
||||
} else {
|
||||
di.utility.changeYAMLValue(config, 'cwd', di.path.resolve(config.json.cwd));
|
||||
}
|
||||
|
||||
// Default value for cols
|
||||
if (di.is.not.number(config.json.cols)) {
|
||||
di.utility.changeYAMLValue(config, 'cols', process.stdout.columns);
|
||||
}
|
||||
|
||||
// Default value for rows
|
||||
if (di.is.not.number(config.json.rows)) {
|
||||
di.utility.changeYAMLValue(config, 'rows', process.stdout.rows);
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the duration from the last inserted record in ms,
|
||||
* and update lastRecordTimestamp
|
||||
*
|
||||
* @return {Number}
|
||||
*/
|
||||
function getDuration() {
|
||||
|
||||
// Calculate the duration from the last inserted record
|
||||
var duration = di.now().toFixed() - lastRecordTimestamp;
|
||||
|
||||
// Update the lastRecordTimestamp
|
||||
lastRecordTimestamp = di.now().toFixed();
|
||||
|
||||
return duration;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* When an input or output is received from the PTY instance
|
||||
*
|
||||
* @param {Buffer} content
|
||||
*/
|
||||
function onData(content) {
|
||||
|
||||
process.stdout.write(content);
|
||||
|
||||
var duration = getDuration();
|
||||
|
||||
if (duration < 5) {
|
||||
var lastRecord = records[records.length - 1];
|
||||
lastRecord.content += content;
|
||||
return;
|
||||
}
|
||||
|
||||
records.push({
|
||||
delay: duration,
|
||||
content: content
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
* Store the output file with reserving the comments
|
||||
*/
|
||||
function done() {
|
||||
|
||||
var outputYAML = '';
|
||||
|
||||
// Add config parent element
|
||||
outputYAML += '# The configurations that used for the recording, feel free to edit them\n';
|
||||
outputYAML += 'config:\n\n';
|
||||
|
||||
// Add the configurations with indentation
|
||||
outputYAML += config.raw.replace(/^/gm, ' ');
|
||||
|
||||
// Add the records
|
||||
outputYAML += '\n# Records, feel free to edit them\n';
|
||||
outputYAML += di.yaml.dump({records: records});
|
||||
|
||||
// Store the data into the recording file
|
||||
try {
|
||||
di.fs.writeFileSync(recordingFile, outputYAML, 'utf8');
|
||||
} catch (error) {
|
||||
di.errorHandler(error.message);
|
||||
process.exit();
|
||||
}
|
||||
|
||||
console.log(di.chalk.green('Successfully Recorded'));
|
||||
console.log('The recording data is saved into the file:');
|
||||
console.log(di.chalk.magenta(recordingFile));
|
||||
console.log('You can edit the file and even change the configurations.');
|
||||
|
||||
// Terminate the app
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
// Normalize the configurations
|
||||
config = normalizeConfig(argv.config);
|
||||
|
||||
// Store the path of the recordingFile
|
||||
recordingFile = argv.recordingFile;
|
||||
|
||||
// Overwrite the command to be executed
|
||||
if (argv.command) {
|
||||
di.utility.changeYAMLValue(config, 'command', argv.command);
|
||||
}
|
||||
|
||||
// Split the command and its arguments
|
||||
var args = di.stringArgv(config.json.command);
|
||||
var command = args[0];
|
||||
var commandArguments = args.slice(1);
|
||||
|
||||
// PTY instance
|
||||
var ptyProcess = di.pty.spawn(command, commandArguments, {
|
||||
name: 'xterm-color',
|
||||
cols: config.json.cols,
|
||||
rows: config.json.rows,
|
||||
cwd: config.json.pwd,
|
||||
env: di.deepmerge(process.env, config.json.env)
|
||||
});
|
||||
|
||||
// Input and output capturing and redirection
|
||||
ptyProcess.on('data', onData);
|
||||
ptyProcess.on('exit', done);
|
||||
process.stdin.on('data', ptyProcess.write.bind(ptyProcess));
|
||||
|
||||
// Input and output normalization
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdout.setDefaultEncoding('utf8');
|
||||
process.stdin.setRawMode(true);
|
||||
process.stdin.resume();
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'record <recordingFile>';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Record your terminal and create a recording file';
|
||||
|
||||
/**
|
||||
* Handler
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
module.exports.handler = function(argv) {
|
||||
|
||||
// The default configurations
|
||||
var defaultConfig = di.utility.getDefaultConfig();
|
||||
|
||||
// Default value for the config option
|
||||
if (typeof argv.config == 'undefined') {
|
||||
argv.config = di.utility.getDefaultConfig();
|
||||
}
|
||||
|
||||
// Execute the command
|
||||
command(argv);
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* @param {Object} yargs
|
||||
*/
|
||||
module.exports.builder = function(yargs) {
|
||||
|
||||
// Define the recordingFile argument
|
||||
yargs.positional('recordingFile', {
|
||||
describe: 'A name for the recording file',
|
||||
type: 'string',
|
||||
coerce: di._.partial(di.utility.resolveFilePath, di._, 'yml')
|
||||
});
|
||||
|
||||
// Define the config option
|
||||
yargs.option('c', {
|
||||
alias: 'config',
|
||||
type: 'string',
|
||||
describe: 'Overwrite the default configurations',
|
||||
requiresArg: true,
|
||||
coerce: di.utility.loadYAML
|
||||
});
|
||||
|
||||
// Define the config option
|
||||
yargs.option('d', {
|
||||
alias: 'command',
|
||||
type: 'string',
|
||||
describe: 'The command to be executed',
|
||||
requiresArg: true,
|
||||
default: null
|
||||
});
|
||||
|
||||
// Add examples
|
||||
yargs.example('$0 record foo', 'Start recording and create a recording file called foo.yml');
|
||||
yargs.example('$0 record foo --config config.yml', 'Start recording with with your own configurations');
|
||||
|
||||
};
|
||||
381
commands/render.js
Normal file
381
commands/render.js
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* Render
|
||||
* Render a recording file as an animated gif image
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create a progress bar for processing frames
|
||||
*
|
||||
* @param {String} operation a name for the operation
|
||||
* @param {Number} framesCount
|
||||
* @return {ProgressBar}
|
||||
*/
|
||||
function getProgressBar(operation, framesCount) {
|
||||
|
||||
return new di.ProgressBar(operation + ' ' + di.chalk.magenta('frame :current/:total') + ' :percent [:bar] :etas', {
|
||||
width: 30,
|
||||
total: framesCount
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the recording data into render/data.json
|
||||
*
|
||||
* @param {Object} recordingFile
|
||||
* @return {Promise}
|
||||
*/
|
||||
function writeRecordingData(recordingFile) {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
// Write the data into data.json file in the root path of the app
|
||||
di.fs.writeFile(di.path.join(ROOT_PATH, 'render/data.json'), JSON.stringify(recordingFile.json), 'utf8', function(error) {
|
||||
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the dimensions of the first rendered frame
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function getFrameDimensions() {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
// The path of the first rendered frame
|
||||
var framePath = di.path.join(ROOT_PATH, 'render/frames/0.png');
|
||||
|
||||
// Read and parse the image
|
||||
di.fs.createReadStream(framePath).pipe(new di.PNG()).on('parsed', function() {
|
||||
|
||||
resolve({
|
||||
width: this.width,
|
||||
height: this.height
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the frames into PNG images
|
||||
*
|
||||
* @param {Array} records [{delay, content}, ...]
|
||||
* @param {Object} options {step}
|
||||
* @return {Promise}
|
||||
*/
|
||||
function renderFrames(records, options) {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
// The number of frames
|
||||
var framesCount = records.length;
|
||||
|
||||
// Create a progress bar
|
||||
var progressBar = getProgressBar('Rendering', Math.ceil(framesCount / options.step));
|
||||
|
||||
// Execute the rendering process
|
||||
var render = di.spawn(di.electron, [di.path.join(ROOT_PATH, 'render/index.js'), options.step], {detached: false});
|
||||
|
||||
render.stderr.on('data', function(error) {
|
||||
render.kill();
|
||||
reject(new Error(di._.trim(error)));
|
||||
});
|
||||
|
||||
render.stdout.on('data', function(data) {
|
||||
|
||||
progressBar.tick();
|
||||
|
||||
// Rendering is completed
|
||||
if (progressBar.complete) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the rendered frames into an animated GIF image
|
||||
*
|
||||
* @param {Array} records [{delay, content}, ...]
|
||||
* @param {Object} options {quality, repeat, step, outputFile}
|
||||
* @param {Object} frameDimensions {width, height}
|
||||
* @return {Promise}
|
||||
*/
|
||||
function mergeFrames(records, options, frameDimensions) {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
// The number of frames
|
||||
var framesCount = records.length;
|
||||
|
||||
// Used for the step option
|
||||
var stepsCounter = 0;
|
||||
|
||||
// Create a progress bar
|
||||
var progressBar = getProgressBar('Merging', Math.ceil(framesCount / options.step));
|
||||
|
||||
// The gif image
|
||||
var gif = new di.GIFEncoder(frameDimensions.width, frameDimensions.height, {
|
||||
highWaterMark: 5 * 1024 * 1024
|
||||
});
|
||||
|
||||
// Pipe
|
||||
gif.pipe(di.fs.createWriteStream(options.outputFile));
|
||||
|
||||
// Quality
|
||||
gif.setQuality(101 - options.quality);
|
||||
|
||||
// Repeat
|
||||
gif.setRepeat(options.repeat);
|
||||
|
||||
// Write the headers
|
||||
gif.writeHeader();
|
||||
|
||||
// Foreach frame
|
||||
di.async.eachOfSeries(records, function(frame, index, callback) {
|
||||
|
||||
if (stepsCounter != 0) {
|
||||
stepsCounter = (stepsCounter + 1) % options.step;
|
||||
return callback();
|
||||
}
|
||||
|
||||
stepsCounter = (stepsCounter + 1) % options.step;
|
||||
|
||||
// The path of the rendered frame
|
||||
var framePath = di.path.join(ROOT_PATH, 'render/frames', index + '.png');
|
||||
|
||||
// Read and parse the rendered frame
|
||||
di.fs.createReadStream(framePath).pipe(new di.PNG()).on('parsed', function() {
|
||||
|
||||
progressBar.tick();
|
||||
|
||||
// Set delay
|
||||
gif.setDelay(frame.delay);
|
||||
|
||||
// Add frames
|
||||
gif.addFrame(this.data);
|
||||
|
||||
// Next
|
||||
callback();
|
||||
|
||||
});
|
||||
|
||||
}, function(error) {
|
||||
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
// Write the footer
|
||||
gif.finish();
|
||||
resolve();
|
||||
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the temporary rendered PNG images
|
||||
*
|
||||
* @return {Promise}
|
||||
*/
|
||||
function cleanup() {
|
||||
|
||||
return new Promise(function(resolve, reject) {
|
||||
|
||||
di.fs.emptyDir(di.path.join(ROOT_PATH, 'render/frames'), function(error) {
|
||||
|
||||
if (error) {
|
||||
return reject(error);
|
||||
}
|
||||
|
||||
resolve();
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
*
|
||||
* @param {String} outputFile the path of the rendered image
|
||||
*/
|
||||
function done(outputFile) {
|
||||
|
||||
console.log('\n' + di.chalk.green('Successfully Rendered'));
|
||||
console.log('The animated GIF image is saved into the file:');
|
||||
console.log(di.chalk.magenta(outputFile));
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
// Frames
|
||||
var records = argv.recordingFile.json.records;
|
||||
var config = argv.recordingFile.json.config;
|
||||
|
||||
// Number of frames in the recording file
|
||||
var framesCount = records.length;
|
||||
|
||||
// The path of the output file
|
||||
var outputFile = di.utility.resolveFilePath('render' + (new Date()).getTime(), 'gif');
|
||||
|
||||
// For adjusting (calculating) the frames delays
|
||||
var adjustFramesDelaysOptions = {
|
||||
frameDelay: config.frameDelay,
|
||||
maxIdleTime: config.maxIdleTime
|
||||
};
|
||||
|
||||
// For rendering the frames into PMG images
|
||||
var renderingOptions = {
|
||||
step: argv.step
|
||||
};
|
||||
|
||||
// For merging the rendered frames into an animated GIF image
|
||||
var mergingOptions = {
|
||||
quality: config.quality,
|
||||
repeat: config.repeat,
|
||||
step: argv.step,
|
||||
outputFile: outputFile
|
||||
};
|
||||
|
||||
// Overwrite the quality of the rendered image
|
||||
if (argv.quality) {
|
||||
mergingOptions.quality = argv.quality;
|
||||
}
|
||||
|
||||
// Overwrite the outputFile of the rendered image
|
||||
if (argv.output) {
|
||||
outputFile = argv.output;
|
||||
mergingOptions.outputFile = argv.output;
|
||||
}
|
||||
|
||||
// Tasks
|
||||
di.asyncPromises.waterfall([
|
||||
|
||||
// Remove all previously rendered frames
|
||||
cleanup,
|
||||
|
||||
// Write the recording data into render/data.json
|
||||
di._.partial(writeRecordingData, argv.recordingFile),
|
||||
|
||||
// Render the frames into PNG images
|
||||
di._.partial(renderFrames, records, renderingOptions),
|
||||
|
||||
// Adjust frames delays
|
||||
di._.partial(di.play.adjustFramesDelays, records, adjustFramesDelaysOptions),
|
||||
|
||||
// Get the dimensions of the first rendered frame
|
||||
di._.partial(getFrameDimensions),
|
||||
|
||||
// Merge the rendered frames into an animated GIF image
|
||||
di._.partial(mergeFrames, records, mergingOptions),
|
||||
|
||||
// Delete the temporary rendered PNG images
|
||||
cleanup
|
||||
|
||||
]).then(function() {
|
||||
|
||||
done(outputFile);
|
||||
|
||||
}).catch(function(error) {
|
||||
|
||||
di.errorHandler(error.message);
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'render <recordingFile>';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Render a recording file as an animated gif image';
|
||||
|
||||
/**
|
||||
* Command's handler function
|
||||
* @type {Function}
|
||||
*/
|
||||
module.exports.handler = command;
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* @param {Object} yargs
|
||||
*/
|
||||
module.exports.builder = function(yargs) {
|
||||
|
||||
// Define the recordingFile argument
|
||||
yargs.positional('recordingFile', {
|
||||
describe: 'The recording file',
|
||||
type: 'string',
|
||||
coerce: di.utility.loadYAML
|
||||
});
|
||||
|
||||
// Define the output option
|
||||
yargs.option('o', {
|
||||
alias: 'output',
|
||||
type: 'string',
|
||||
describe: 'A name for the output file',
|
||||
requiresArg: true,
|
||||
coerce: di._.partial(di.utility.resolveFilePath, di._, 'gif')
|
||||
});
|
||||
|
||||
// Define the quality option
|
||||
yargs.option('q', {
|
||||
alias: 'quality',
|
||||
type: 'number',
|
||||
describe: 'The quality of the rendered image (1 - 100)',
|
||||
requiresArg: true
|
||||
});
|
||||
|
||||
// Define the quality option
|
||||
yargs.option('s', {
|
||||
alias: 'step',
|
||||
type: 'number',
|
||||
describe: 'To reduce the number of rendered frames (step > 1)',
|
||||
requiresArg: true,
|
||||
default: 1
|
||||
});
|
||||
|
||||
};
|
||||
65
commands/share.js
Normal file
65
commands/share.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Share
|
||||
* Upload a recording file and get a link for an online player
|
||||
*
|
||||
* @author Mohammad Fares <faressoft.com@gmail.com>
|
||||
*/
|
||||
|
||||
/**
|
||||
* Executed after the command completes its task
|
||||
*/
|
||||
function done() {
|
||||
|
||||
// Terminate the app
|
||||
process.exit();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* The command's main function
|
||||
*
|
||||
* @param {Object} argv
|
||||
*/
|
||||
function command(argv) {
|
||||
|
||||
console.log('This command is not implemented yet. It will be avalible in the next versions');
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////
|
||||
// Command Definition //////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* Command's usage
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.command = 'share <recordingFile>';
|
||||
|
||||
/**
|
||||
* Command's description
|
||||
* @type {String}
|
||||
*/
|
||||
module.exports.describe = 'Upload a recording file and get a link for an online player';
|
||||
|
||||
/**
|
||||
* Command's handler function
|
||||
* @type {Function}
|
||||
*/
|
||||
module.exports.handler = command;
|
||||
|
||||
/**
|
||||
* Builder
|
||||
*
|
||||
* @param {Object} yargs
|
||||
*/
|
||||
module.exports.builder = function(yargs) {
|
||||
|
||||
// Define the recordingFile argument
|
||||
yargs.positional('recordingFile', {
|
||||
describe: 'the recording file',
|
||||
type: 'string',
|
||||
coerce: di.utility.loadYAML
|
||||
});
|
||||
|
||||
};
|
||||
Reference in New Issue
Block a user