Port to typescript. Compile (or transpile?) step enforces semicolon and whitespace rules on ./frameloom file.

This commit is contained in:
mmcwilliams 2019-04-02 12:57:39 -04:00
parent 2cd09f1e05
commit 3f1016d915
3 changed files with 1009 additions and 462 deletions

896
frameloom Executable file → Normal file
View File

@ -1,22 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
'use strict' const execRaw = require('child_process').exec;
const os = require('os');
const execRaw = require('child_process').exec const path = require('path');
const os = require('os') const program = require('commander');
const path = require('path') const fs = require('fs-extra');
const program = require('commander') const pkg = require('./package.json');
const fs = require('fs-extra') const OUTPUT_RE = new RegExp('{{o}}', 'g');
const INPUT_RE = new RegExp('{{i}}', 'g');
const pkg = require('./package.json') let QUIET = false;
let TMPDIR = os.tmpdir() || '/tmp';
const OUTPUT_RE = new RegExp('{{o}}', 'g') let TMPPATH;
const INPUT_RE = new RegExp('{{i}}', 'g')
let QUIET = false
let TMPDIR = os.tmpdir() || '/tmp'
let TMPPATH
/** /**
* Shells out to execute a command with async/await. * Shells out to execute a command with async/await.
* Async wrapper to exec module. * Async wrapper to exec module.
@ -25,13 +19,14 @@ let TMPPATH
* *
* @returns {Promise} Promise containing the complete stdio * @returns {Promise} Promise containing the complete stdio
**/ **/
async function exec (cmd) { async function exec(cmd) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
return execRaw(cmd, (err, stdio, stderr) => { return execRaw(cmd, (err, stdio, stderr) => {
if (err) return reject(err) if (err)
return resolve(stdio) return reject(err);
}) return resolve(stdio);
}) });
});
} }
/** /**
* Delays process for specified amount of time in milliseconds. * Delays process for specified amount of time in milliseconds.
@ -40,23 +35,25 @@ async function exec (cmd) {
* *
* @returns {Promise} Promise that resolves after set time * @returns {Promise} Promise that resolves after set time
**/ **/
async function delay (ms) { async function delay(ms) {
return new Promise((resolve, reject) =>{ return new Promise((resolve, reject) => {
return setTimeout(resolve, ms) return setTimeout(resolve, ms);
}) });
} }
/** /**
* Log function wrapper that can silences logs when * Log function wrapper that can silences logs when
* QUIET == true * QUIET == true
*/ */
function log (msg, err) { function log(msg, err = false) {
if (QUIET) return false if (QUIET)
if (err) { return false;
console.error(msg, err) if (err) {
} else { console.error(msg, err);
console.log(msg) }
} else {
return true console.log(msg);
}
return true;
} }
/** /**
* Pads a numerical value with preceding zeros to make strings same length. * Pads a numerical value with preceding zeros to make strings same length.
@ -66,58 +63,61 @@ function log (msg, err) {
* *
* @returns {string} Padded number as a string * @returns {string} Padded number as a string
**/ **/
function zeroPad (i, max = 5) { function zeroPad(i, max = 5) {
let len = (i + '').length let str = i + '';
let str = i + '' let len = str.length;
for (let x = 0; x < max - len; x++) { for (let x = 0; x < max - len; x++) {
str = '0' + str str = '0' + str;
} }
return str return str;
} }
/** /**
* Shuffles an array into a random state. * Shuffles an array into a random state.
* *
* @param {array} a Array to randomize * @param {array} a Array to randomize
**/ **/
function shuffle (a) { function shuffle(array) {
for (let i = a.length; i; i--) { let j;
let j = Math.floor(Math.random() * i); let temp;
[a[i - 1], a[j]] = [a[j], a[i - 1]] for (let i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
temp = array[i];
array[i] = array[j];
array[j] = temp;
} }
} }
/** /**
* Clears the temporary directory of all files. * Clears the temporary directory of all files.
* Establishes a directory if none exists. * Establishes a directory if none exists.
**/ **/
async function clear () { async function clear() {
let cmd = `rm -r "${TMPPATH}"` let cmd = `rm -r "${TMPPATH}"`;
let exists let exists;
try {
try { exists = await fs.exists(TMPPATH);
exists = await fs.exists(TMPPATH) }
} catch (err) { catch (err) {
log('Error checking if file exists', err) log('Error checking if file exists', err);
} }
if (exists) {
if (exists) { log(`Clearing temp directory "${TMPPATH}"`);
log(`Clearing temp directory "${TMPPATH}"`) try {
try { await exec(cmd);
await exec(cmd) }
} catch (err) { catch (err) {
//suppress error //suppress error
console.dir(err) console.dir(err);
} }
} }
try {
try { await fs.mkdir(TMPPATH);
await fs.mkdir(TMPPATH) }
} catch (err) { catch (err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
log('Error making directory', err) log('Error making directory', err);
} }
} }
return true;
return true
} }
/** /**
* Exports all frames from video. Appends number to the string * Exports all frames from video. Appends number to the string
@ -126,119 +126,120 @@ async function clear () {
* *
* @param {string} video String representing path to video * @param {string} video String representing path to video
* @param {integer} order Integer to be appended to pathname of file * @param {integer} order Integer to be appended to pathname of file
* @param {boolean} avconv Whether or not to use avconv instead of ffmpeg
* *
* @returns {string} String with the export order, not sure why I did this * @returns {string} String with the export order, not sure why I did this
**/ **/
async function frames (video, order, avconv, e) { async function frames(video, order, avconv) {
let ext = 'tif' let ext = 'tif';
let exe = avconv ? 'avconv' : 'ffmpeg' let exe = avconv ? 'avconv' : 'ffmpeg';
let tmpoutput let tmpoutput;
let cmd let cmd;
tmpoutput = path.join(TMPPATH, `export-%05d_${order}.${ext}`);
tmpoutput = path.join(TMPPATH, `export-%05d_${order}.${ext}`) cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`;
log(`Exporting ${video} as single frames...`);
cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"` try {
await exec(cmd);
log(`Exporting ${video} as single frames...`) }
catch (err) {
try { log('Error exporting video', err);
await exec(cmd) return process.exit(3);
} catch (err) { }
log('Error exporting video', err) return path.join(TMPPATH, `export-%05d_${order}`);
return process.exit(3)
}
return path.join(TMPPATH, `export-%05d_${order}`)
} }
/** /**
* Shells out to run a sub command on every frame to perform effects * Shells out to run a sub command on every frame to perform effects
* *
* @param {string} cmd Command to execute on every frame
*
**/ **/
async function subExec (cmd) { async function subExec(cmd) {
let frames let frames;
let frameCmd let frameCmd;
let framePath let framePath;
try {
try { frames = await fs.readdir(TMPPATH);
frames = await fs.readdir(TMPPATH) }
} catch (err) { catch (err) {
log('Error reading tmp directory', err) log('Error reading tmp directory', err);
} }
frames = frames.filter(file => {
frames = frames.filter (file =>{ if (file.indexOf('.tif') !== -1)
if (file.indexOf('.tif') !== -1) return true return true;
}) });
for (let frame of frames) {
for (let frame of frames) { framePath = path.join(TMPPATH, frame);
framePath = path.join(TMPPATH, frame) if (cmd.indexOf('{{i}}') !== -1 || cmd.indexOf('{{o}}')) {
if (cmd.indexOf('{{i}}') !== -1 || cmd.indexOf('{{o}}')) { frameCmd = cmd.replace(INPUT_RE, framePath)
frameCmd = cmd.replace(INPUT_RE, framePath) .replace(OUTPUT_RE, framePath);
.replace(OUTPUT_RE, framePath) }
} else { else {
frameCmd = `${cmd} ${framePath}` frameCmd = `${cmd} ${framePath}`;
} }
try {
try { await exec(frameCmd);
await exec(frameCmd) }
} catch (err) { catch (err) {
log('Error executing sub command on frame', err) log('Error executing sub command on frame', err);
return process.exit(10) return process.exit(10);
} }
} }
} }
/** /**
* Re-arranges the frames into the order specified in the pattern. * Re-arranges the frames into the order specified in the pattern.
* Calls `patternSort()` to perform the rename and unlink actions * Calls `patternSort()` to perform the rename and unlink actions
* *
* @param {array} pattern Pattern of the frames per input * @param {array} pattern Pattern of the frames per input
* @param {boolean} realtime Flag to turn on or off realtime behavior (drop frames / number of vids) * @param {boolean} realtime Flag to turn on or off realtime behavior (drop frames / number of vids)
* @param {boolean} random Whether or not to randomize frames
**/ **/
async function weave (pattern, realtime, random) { async function weave(pattern, realtime, random) {
let frames let frames;
let old let seq;
let seqFile let alt = false;
let seq log('Weaving frames...');
let alt = false try {
frames = await fs.readdir(TMPPATH);
log('Weaving frames...') }
catch (err) {
try { log('Error reading tmp directory', err);
frames = await fs.readdir(TMPPATH) }
} catch (err) { //console.dir(frames)
log('Error reading tmp directory', err) frames = frames.filter(file => {
} if (file.indexOf('.tif') !== -1)
return true;
//console.dir(frames) });
frames = frames.filter (file =>{ for (let el of pattern) {
if (file.indexOf('.tif') !== -1) return true if (el !== 1)
}) alt = true;
}
for (let el of pattern) { if (random) {
if (el !== 1) alt = true try {
} seq = await randomSort(frames, pattern, realtime);
}
if (random){ catch (err) {
try { log('Error sorting frames', err);
seq = await randomSort(frames, realtime) }
} catch (err) { }
log('Error sorting frames', err) else if (!alt) {
} try {
} else if (!alt) { seq = await standardSort(frames, pattern, realtime);
try { }
seq = await standardSort(frames, pattern, realtime) catch (err) {
} catch (err) { log('Error sorting frames', err);
log('Error sorting frames', err) }
} }
} else if (alt) { else if (alt) {
log('This feature is not ready, please check https://github.com/sixteenmillimeter/frameloom.git', {}) log('This feature is not ready, please check https://github.com/sixteenmillimeter/frameloom.git', {});
process.exit(10) process.exit(10);
try { try {
seq = await altSort(frames, pattern, realtime) seq = await altSort(frames, pattern, realtime);
} catch (err) { }
log('Error sorting frames', err) catch (err) {
} log('Error sorting frames', err);
} }
//console.dir(seq) }
//console.dir(seq)
} }
/** /**
* Alternate frame sorting method. * Alternate frame sorting method.
@ -247,43 +248,40 @@ async function weave (pattern, realtime, random) {
* @param {array} pattern Array representing pattern * @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior * @param {boolean} realtime Flag to group with "realtime" behavior
**/ **/
async function altSort (list, pattern, realtime) { async function altSort(list, pattern, realtime) {
let groups = [] let groups = [];
let newList = [] let newList = [];
let frameCount = 0 let frameCount = 0;
let oldPath let oldPath;
let newName let newName;
let newPath let newPath;
let ext = path.extname(list[0]) let ext = path.extname(list[0]);
for (let g of pattern) {
for (let g of pattern) { groups.push([]);
groups.push([]) }
} for (let i = 0; i < list.length; i++) {
for (let i = 0; i < list.length; i++) { groups[i % pattern.length].push(list[i]);
groups[i % pattern.length].push(list[i]) }
} for (let x = 0; x < list.length; x++) {
for (let x = 0; x < list.length; x++) { for (let g of pattern) {
for (let g of pattern) { for (let i = 0; i < g; i++) {
for (let i = 0; i < g; i++) { /*oldPath = path.join(TMPPATH, list[i]);
newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = path.join(TMPPATH, newName);
/*oldPath = path.join(TMPPATH, list[i]); log(`Renaming ${list[i]} -> ${newName}`);
newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = path.join(TMPPATH, newName);
log(`Renaming ${list[i]} -> ${newName}`); try {
//await fs.move(oldPath, newPath, { overwrite: true })
try { newList.push(newName);
//await fs.move(oldPath, newPath, { overwrite: true }) } catch (err) {
newList.push(newName); log(err);
} catch (err) { }*/
log(err); frameCount++;
}*/ }
}
frameCount++ }
} return newList;
}
}
return newList
} }
/** /**
* Standard frame sorting method. * Standard frame sorting method.
@ -292,272 +290,248 @@ async function altSort (list, pattern, realtime) {
* @param {array} pattern Array representing pattern * @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior * @param {boolean} realtime Flag to group with "realtime" behavior
**/ **/
async function standardSort (list, pattern, realtime) { async function standardSort(list, pattern, realtime) {
let frameCount = 0 let frameCount = 0;
let stepCount let stepCount;
let step let step;
let skipCount let skipCount;
let skip let skip;
let ext = path.extname(list[0]) let ext = path.extname(list[0]);
let oldPath let oldPath;
let newName let newName;
let newPath let newPath;
let newList = [] let newList = [];
if (realtime) {
if (realtime) { skip = false;
skip = false skipCount = pattern.length + 1;
skipCount = pattern.length + 1 }
} for (let i = 0; i < list.length; i++) {
if (realtime) {
for (let i = 0; i < list.length; i++) { skipCount--;
if (realtime) { if (skipCount === 0) {
skipCount--; skip = !skip;
if (skipCount === 0) { skipCount = pattern.length;
skip = !skip; }
skipCount = pattern.length }
} oldPath = path.join(TMPPATH, list[i]);
} if (skip) {
log(`Skipping ${list[i]}`);
oldPath = path.join(TMPPATH, list[i]) try {
await fs.unlink(oldPath);
if (skip) { }
log(`Skipping ${list[i]}`) catch (err) {
try { log('Error deleting frame', err);
await fs.unlink(oldPath) }
} catch (err) { continue;
log('Error deleting frame', err) }
} newName = `./render_${zeroPad(frameCount)}${ext}`;
continue newPath = path.join(TMPPATH, newName);
} log(`Renaming ${list[i]} -> ${newName}`);
try {
newName = `./render_${zeroPad(frameCount)}${ext}` await fs.move(oldPath, newPath);
newPath = path.join(TMPPATH, newName) newList.push(newName);
log(`Renaming ${list[i]} -> ${newName}`) frameCount++;
}
try { catch (err) {
await fs.move(oldPath, newPath) log('Error renaming frame', err);
newList.push(newName) return process.exit(10);
frameCount++ }
} catch (err) { }
log('Error renaming frame', err) return newList;
return process.exit(10)
}
}
return newList
} }
/** /**
* Ramdomly sort frames for re-stitching. * Ramdomly sort frames for re-stitching.
* *
* @param {array} list List of frames to group * @param {array} list List of frames to group
* @param {array} pattern Array representing pattern * @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior * @param {boolean} realtime Flag to group with "realtime" behavior
**/ **/
async function randomSort (list, pattern, realtime) { async function randomSort(list, pattern, realtime) {
let frameCount = 0 let frameCount = 0;
let ext = path.extname(list[0]) let ext = path.extname(list[0]);
let oldPath let oldPath;
let newName let newName;
let newPath let newPath;
let newList = [] let newList = [];
let removeLen = 0 let removeLen = 0;
let remove = [] let remove = [];
shuffle(list);
shuffle(list) if (realtime) {
removeLen = Math.floor(list.length / pattern.length);
if (realtime) { remove = list.slice(removeLen, list.length);
removeLen = Math.floor(list.length / pattern.length) list = list.slice(0, removeLen);
remove = list.slice(removeLen, list.length) log(`Skipping extra frames...`);
list = list.slice(0, removeLen) for (let i = 0; i < remove.length; i++) {
oldPath = path.join(TMPPATH, remove[i]);
log(`Skipping extra frames...`) log(`Skipping ${list[i]}`);
for (let i = 0; i < remove.length; i++) { try {
oldPath = path.join(TMPPATH, remove[i]) await fs.unlink(oldPath);
log(`Skipping ${list[i]}`) }
try { catch (err) {
await fs.unlink(oldPath) log('Error deleting frame', err);
} catch (err) { }
log('Error deleting frame', err) }
} }
} for (let i = 0; i < list.length; i++) {
} oldPath = path.join(TMPPATH, list[i]);
newName = `./render_${zeroPad(frameCount)}${ext}`;
for (let i = 0; i < list.length; i++) { newPath = path.join(TMPPATH, newName);
oldPath = path.join(TMPPATH, list[i]) log(`Renaming ${list[i]} -> ${newName}`);
try {
newName = `./render_${zeroPad(frameCount)}${ext}` await fs.move(oldPath, newPath);
newPath = path.join(TMPPATH, newName) newList.push(newName);
log(`Renaming ${list[i]} -> ${newName}`) }
catch (err) {
try { log('Error moving frame', err);
await fs.move(oldPath, newPath) }
newList.push(newName) frameCount++;
} catch (err) { }
log('Error moving frame', err) return newList;
}
frameCount++
}
return newList
} }
/** /**
* Render the frames into a video using ffmpeg. * Render the frames into a video using ffmpeg.
* *
* @param {string} output Path to export the video to * @param {string} output Path to export the video to
* @param {boolean} avconv Whether or not to use avconv in place of ffmpeg
**/ **/
async function render (output, avconv) { async function render(output, avconv) {
//process.exit() //process.exit()
let frames = path.join(TMPPATH, `render_%05d.tif`) let frames = path.join(TMPPATH, `render_%05d.tif`);
let exe = avconv ? 'avconv' : 'ffmpeg' let exe = avconv ? 'avconv' : 'ffmpeg';
let resolution = '1920x1080' let resolution = '1920x1080'; //TODO: make variable/argument
let h264 = `-vcodec libx264 -g 1 -crf 25 -pix_fmt yuv420p` //TODO: make object configurable with shorthand names
let prores = `-c:v prores_ks -profile:v 3` let h264 = `-vcodec libx264 -g 1 -crf 25 -pix_fmt yuv420p`;
let format = (output.indexOf('.mov') !== -1) ? prores : h264 let prores = `-c:v prores_ks -profile:v 3`;
let framerate = `24` //
const cmd = `${exe} -r ${framerate} -f image2 -s ${resolution} -i ${frames} ${format} -y ${output}` let format = (output.indexOf('.mov') !== -1) ? prores : h264;
let framerate = `24`;
log(`Exporting video ${output}`) const cmd = `${exe} -r ${framerate} -f image2 -s ${resolution} -i ${frames} ${format} -y ${output}`;
log(cmd) log(`Exporting video ${output}`);
log(cmd);
/*try { /*try {
await exec(`ls "${TMPPATH}"`) await exec(`ls "${TMPPATH}"`)
} catch (err) { } catch (err) {
log(err) log(err)
}*/ }*/
try {
try { await exec(cmd);
await exec(cmd) }
} catch (err) { catch (err) {
log('Error rendering video with ffmpeg', err) log('Error rendering video with ffmpeg', err);
} }
} }
/** /**
* Parses the arguments and runs the process of exporting, sorting and then * Parses the arguments and runs the process of exporting, sorting and then
* "weaving" the frames back into a video * "weaving" the frames back into a video
* *
* @param {object} arg Object containing all arguments * @param {object} arg Object containing all arguments
**/ **/
async function main (arg) { async function main(arg) {
let input = arg.input.split(':') let input = arg.input.split(':');
let output = arg.output let output = arg.output;
let pattern = [] let pattern = [];
let realtime = false let realtime = false;
let avconv = false let avconv = false;
let random = false let random = false;
let e = false let e = false;
console.time('frameloom') console.time('frameloom');
if (input.length < 2) {
if (input.length < 2) { log('Must provide more than 1 input', {});
log('Must provide more than 1 input', {}) return process.exit(1);
return process.exit(1) }
} if (!output) {
log('Must provide video output path', {});
if (!output) { return process.exit(2);
log('Must provide video output path', {}) }
return process.exit(2) if (arg.random) {
} random = true;
}
if (arg.random) { if (arg.avconv) {
random = true avconv = true;
} }
if (arg.tmp) {
if (arg.avconv) { TMPDIR = arg.tmp;
avconv = true }
} if (arg.exec) {
e = arg.exec;
if (arg.tmp) { }
TMPDIR = arg.tmp if (arg.quiet) {
} QUIET = true;
}
if (arg.exec) { if (arg.pattern) {
e = arg.exec pattern = arg.pattern.split(':');
} pattern = pattern.map(el => {
return parseInt(el);
if (arg.quiet) { });
QUIET = true }
} else {
for (let i = 0; i < input.length; i++) {
if (arg.pattern) { pattern.push(1);
pattern = arg.pattern.split(':') }
pattern = pattern.map(el =>{ }
return parseInt(el); if (arg.realtime)
}) realtime = true;
} else { TMPPATH = path.join(TMPDIR, 'frameloom');
for (let i = 0; i <input.length; i++) { try {
pattern.push(1); await clear();
} }
} catch (err) {
log('Error clearing temp directory', err);
if (arg.realtime) realtime = true; return process.exit(3);
}
TMPPATH = path.join(TMPDIR, 'frameloom'); log(`Processing video files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`);
for (let i = 0; i < input.length; i++) {
try { try {
await clear() await frames(input[i], i, avconv);
} catch (err) { }
log('Error clearing temp directory', err) catch (err) {
return process.exit(3) log('Error exporting video fie to image sequence', err);
} return process.exit(4);
}
log(`Processing video files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`) }
log('Weaving frames');
for (let i = 0; i < input.length; i++) { try {
try { await weave(pattern, realtime, random);
await frames(input[i], i, avconv) }
} catch (err) { catch (err) {
log('Error exporting video fie to image sequence', err) log('Error weaving', err);
return process.exit(4) return process.exit(5);
} }
} if (e) {
try {
log('Weaving frames') await subExec(e);
try { }
await weave(pattern, realtime, random) catch (err) {
} catch (err) { log('Error performing subcommand', err);
log('Error weaving', err) return process.exit(7);
return process.exit(5) }
} }
try {
if (e) { await render(output, avconv);
try { }
await subExec(e) catch (err) {
} catch (err) { log('Error rendering', err);
log('Error performing subcommand', err) return process.exit(6);
return process.exit(7) }
} try {
} await clear();
}
try { catch (err) {
await render(output, avconv) log('Error clearing files', err);
} catch (err) { return process.exit(7);
log('Error rendering', err) }
return process.exit(6) console.timeEnd('frameloom');
}
try {
await clear()
} catch (err) {
log('Error clearing files', err)
return process.exit(7)
}
console.timeEnd('frameloom')
} }
program program
.version(pkg.version) .version(pkg.version)
.option('-i, --input [files]', 'Specify input videos with paths seperated by colon') .option('-i, --input [files]', 'Specify input videos with paths seperated by colon')
.option('-o, --output [file]', 'Specify output path of video') .option('-o, --output [file]', 'Specify output path of video')
.option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard') .option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard')
.option('-r, --realtime', 'Specify if videos should preserve realtime speed') .option('-r, --realtime', 'Specify if videos should preserve realtime speed')
.option('-t, --tmp [dir]', 'Specify tmp directory for exporting frames') .option('-t, --tmp [dir]', 'Specify tmp directory for exporting frames')
.option('-a, --avconv', 'Specify avconv if preferred to ffmpeg') .option('-a, --avconv', 'Specify avconv if preferred to ffmpeg')
.option('-R, --random', 'Randomize frames. Ignores pattern if included') .option('-R, --random', 'Randomize frames. Ignores pattern if included')
.option('-e, --exec', 'Command to execute on every frame. Specify {{i}} and {{o}} if the command requires it, otherwise frame path will be appended to command') .option('-e, --exec', 'Command to execute on every frame. Specify {{i}} and {{o}} if the command requires it, otherwise frame path will be appended to command')
.option('-q, --quiet', 'Suppresses all log messages') .option('-q, --quiet', 'Suppresses all log messages')
.parse(process.argv) .parse(process.argv);
main(program);
main(program)

View File

@ -6,7 +6,7 @@
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",
"version": "npm --no-git-tag-version version patch", "version": "npm --no-git-tag-version version patch",
"compile" : "./node_modules/.bin/tsc src/frameloom.ts --outFile ./frameloom --noImplicitAny -t ES2017 --moduleResolution Node", "compile" : "./node_modules/.bin/tsc src/frameloom.ts --outFile ./frameloom --noImplicitAny --lib ES2017 -t ES2017 --moduleResolution Node",
"build" : "node build.js", "build" : "node build.js",
"docs": "sh ./scripts/docs.sh", "docs": "sh ./scripts/docs.sh",
"examples" : "sh ./scripts/examples.sh", "examples" : "sh ./scripts/examples.sh",

573
src/frameloom.ts Executable file
View File

@ -0,0 +1,573 @@
#!/usr/bin/env node
'use strict'
const execRaw = require('child_process').exec
const os = require('os')
const path = require('path')
const program = require('commander')
const fs = require('fs-extra')
const pkg : any = require('./package.json')
const OUTPUT_RE : RegExp = new RegExp('{{o}}', 'g')
const INPUT_RE : RegExp = new RegExp('{{i}}', 'g')
let QUIET : boolean = false
let TMPDIR : string = os.tmpdir() || '/tmp'
let TMPPATH : string
/**
* Shells out to execute a command with async/await.
* Async wrapper to exec module.
*
* @param {string} cmd Command to execute
*
* @returns {Promise} Promise containing the complete stdio
**/
async function exec (cmd : string) {
return new Promise((resolve : any, reject : any) => {
return execRaw(cmd, (err : any, stdio : string, stderr : string) => {
if (err) return reject(err)
return resolve(stdio)
})
})
}
/**
* Delays process for specified amount of time in milliseconds.
*
* @param {integer} ms Milliseconds to delay for
*
* @returns {Promise} Promise that resolves after set time
**/
async function delay (ms : number) {
return new Promise((resolve : any, reject : any) =>{
return setTimeout(resolve, ms)
})
}
/**
* Log function wrapper that can silences logs when
* QUIET == true
*/
function log (msg : string, err : any = false) {
if (QUIET) return false
if (err) {
console.error(msg, err)
} else {
console.log(msg)
}
return true
}
/**
* Pads a numerical value with preceding zeros to make strings same length.
*
* @param {integer} i Number to pad
* @param {integer} max (optional) Maximum length of string to pad to
*
* @returns {string} Padded number as a string
**/
function zeroPad (i : number, max : number = 5) {
let str : string = i + ''
let len : number = str.length
for (let x : number = 0; x < max - len; x++) {
str = '0' + str
}
return str
}
/**
* Shuffles an array into a random state.
*
* @param {array} a Array to randomize
**/
function shuffle (array : any[]) {
let j : any
let temp : any
for (let i : number = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1))
temp = array[i]
array[i] = array[j]
array[j] = temp
}
}
/**
* Clears the temporary directory of all files.
* Establishes a directory if none exists.
**/
async function clear () {
let cmd : string = `rm -r "${TMPPATH}"`
let exists : boolean
try {
exists = await fs.exists(TMPPATH)
} catch (err) {
log('Error checking if file exists', err)
}
if (exists) {
log(`Clearing temp directory "${TMPPATH}"`)
try {
await exec(cmd)
} catch (err) {
//suppress error
console.dir(err)
}
}
try {
await fs.mkdir(TMPPATH)
} catch (err) {
if (err.code !== 'EEXIST') {
log('Error making directory', err)
}
}
return true
}
/**
* Exports all frames from video. Appends number to the string
* to keep frames in alternating order to be quickly stitched together
* or re-sorted.
*
* @param {string} video String representing path to video
* @param {integer} order Integer to be appended to pathname of file
* @param {boolean} avconv Whether or not to use avconv instead of ffmpeg
*
* @returns {string} String with the export order, not sure why I did this
**/
async function frames (video : string, order : number, avconv : boolean) {
let ext : string = 'tif'
let exe : string = avconv ? 'avconv' : 'ffmpeg'
let tmpoutput : string
let cmd : string
tmpoutput = path.join(TMPPATH, `export-%05d_${order}.${ext}`)
cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`
log(`Exporting ${video} as single frames...`)
try {
await exec(cmd)
} catch (err) {
log('Error exporting video', err)
return process.exit(3)
}
return path.join(TMPPATH, `export-%05d_${order}`)
}
/**
* Shells out to run a sub command on every frame to perform effects
*
* @param {string} cmd Command to execute on every frame
*
**/
async function subExec (cmd : string) {
let frames : string[]
let frameCmd : string
let framePath : string
try {
frames = await fs.readdir(TMPPATH)
} catch (err) {
log('Error reading tmp directory', err)
}
frames = frames.filter (file =>{
if (file.indexOf('.tif') !== -1) return true
})
for (let frame of frames) {
framePath = path.join(TMPPATH, frame)
if (cmd.indexOf('{{i}}') !== -1 || cmd.indexOf('{{o}}')) {
frameCmd = cmd.replace(INPUT_RE, framePath)
.replace(OUTPUT_RE, framePath)
} else {
frameCmd = `${cmd} ${framePath}`
}
try {
await exec(frameCmd)
} catch (err) {
log('Error executing sub command on frame', err)
return process.exit(10)
}
}
}
/**
* Re-arranges the frames into the order specified in the pattern.
* Calls `patternSort()` to perform the rename and unlink actions
*
* @param {array} pattern Pattern of the frames per input
* @param {boolean} realtime Flag to turn on or off realtime behavior (drop frames / number of vids)
* @param {boolean} random Whether or not to randomize frames
**/
async function weave (pattern : number[], realtime : boolean, random : boolean) {
let frames : string[]
let seq : string[]
let alt : boolean = false
log('Weaving frames...')
try {
frames = await fs.readdir(TMPPATH)
} catch (err) {
log('Error reading tmp directory', err)
}
//console.dir(frames)
frames = frames.filter (file =>{
if (file.indexOf('.tif') !== -1) return true
})
for (let el of pattern) {
if (el !== 1) alt = true
}
if (random){
try {
seq = await randomSort(frames, pattern, realtime)
} catch (err) {
log('Error sorting frames', err)
}
} else if (!alt) {
try {
seq = await standardSort(frames, pattern, realtime)
} catch (err) {
log('Error sorting frames', err)
}
} else if (alt) {
log('This feature is not ready, please check https://github.com/sixteenmillimeter/frameloom.git', {})
process.exit(10)
try {
seq = await altSort(frames, pattern, realtime)
} catch (err) {
log('Error sorting frames', err)
}
}
//console.dir(seq)
}
/**
* Alternate frame sorting method.
*
* @param {array} list List of frames to group
* @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior
**/
async function altSort (list : string[], pattern : number[], realtime : boolean) {
let groups : any[] = []
let newList : string[] = []
let frameCount : number = 0
let oldPath : string
let newName : string
let newPath : string
let ext : string = path.extname(list[0])
for (let g of pattern) {
groups.push([])
}
for (let i : number = 0; i < list.length; i++) {
groups[i % pattern.length].push(list[i])
}
for (let x : number = 0; x < list.length; x++) {
for (let g of pattern) {
for (let i : number = 0; i < g; i++) {
/*oldPath = path.join(TMPPATH, list[i]);
newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = path.join(TMPPATH, newName);
log(`Renaming ${list[i]} -> ${newName}`);
try {
//await fs.move(oldPath, newPath, { overwrite: true })
newList.push(newName);
} catch (err) {
log(err);
}*/
frameCount++
}
}
}
return newList
}
/**
* Standard frame sorting method.
*
* @param {array} list List of frames to group
* @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior
**/
async function standardSort (list : string[], pattern : number[], realtime : boolean) {
let frameCount : number = 0
let stepCount : number
let step : any
let skipCount : number
let skip : boolean
let ext : string = path.extname(list[0])
let oldPath : string
let newName : string
let newPath : string
let newList : string[] = []
if (realtime) {
skip = false
skipCount = pattern.length + 1
}
for (let i : number = 0; i < list.length; i++) {
if (realtime) {
skipCount--;
if (skipCount === 0) {
skip = !skip;
skipCount = pattern.length
}
}
oldPath = path.join(TMPPATH, list[i])
if (skip) {
log(`Skipping ${list[i]}`)
try {
await fs.unlink(oldPath)
} catch (err) {
log('Error deleting frame', err)
}
continue
}
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${newName}`)
try {
await fs.move(oldPath, newPath)
newList.push(newName)
frameCount++
} catch (err) {
log('Error renaming frame', err)
return process.exit(10)
}
}
return newList
}
/**
* Ramdomly sort frames for re-stitching.
*
* @param {array} list List of frames to group
* @param {array} pattern Array representing pattern
* @param {boolean} realtime Flag to group with "realtime" behavior
**/
async function randomSort (list : string[], pattern : number[], realtime : boolean) {
let frameCount : number = 0
let ext : string = path.extname(list[0])
let oldPath : string
let newName : string
let newPath : string
let newList : string[] = []
let removeLen : number = 0
let remove : string[] = []
shuffle(list)
if (realtime) {
removeLen = Math.floor(list.length / pattern.length)
remove = list.slice(removeLen, list.length)
list = list.slice(0, removeLen)
log(`Skipping extra frames...`)
for (let i : number = 0; i < remove.length; i++) {
oldPath = path.join(TMPPATH, remove[i])
log(`Skipping ${list[i]}`)
try {
await fs.unlink(oldPath)
} catch (err) {
log('Error deleting frame', err)
}
}
}
for (let i : number = 0; i < list.length; i++) {
oldPath = path.join(TMPPATH, list[i])
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${newName}`)
try {
await fs.move(oldPath, newPath)
newList.push(newName)
} catch (err) {
log('Error moving frame', err)
}
frameCount++
}
return newList
}
/**
* Render the frames into a video using ffmpeg.
*
* @param {string} output Path to export the video to
* @param {boolean} avconv Whether or not to use avconv in place of ffmpeg
**/
async function render (output : string, avconv : boolean) {
//process.exit()
let frames : string = path.join(TMPPATH, `render_%05d.tif`)
let exe : string = avconv ? 'avconv' : 'ffmpeg'
let resolution : string = '1920x1080' //TODO: make variable/argument
//TODO: make object configurable with shorthand names
let h264 : string = `-vcodec libx264 -g 1 -crf 25 -pix_fmt yuv420p`
let prores : string = `-c:v prores_ks -profile:v 3`
//
let format : string = (output.indexOf('.mov') !== -1) ? prores : h264
let framerate : string = `24`
const cmd : string = `${exe} -r ${framerate} -f image2 -s ${resolution} -i ${frames} ${format} -y ${output}`
log(`Exporting video ${output}`)
log(cmd)
/*try {
await exec(`ls "${TMPPATH}"`)
} catch (err) {
log(err)
}*/
try {
await exec(cmd)
} catch (err) {
log('Error rendering video with ffmpeg', err)
}
}
/**
* Parses the arguments and runs the process of exporting, sorting and then
* "weaving" the frames back into a video
*
* @param {object} arg Object containing all arguments
**/
async function main (arg : any) {
let input : string[] = arg.input.split(':')
let output : string = arg.output
let pattern : any[] = []
let realtime : boolean = false
let avconv : boolean = false
let random : boolean = false
let e : any = false
console.time('frameloom')
if (input.length < 2) {
log('Must provide more than 1 input', {})
return process.exit(1)
}
if (!output) {
log('Must provide video output path', {})
return process.exit(2)
}
if (arg.random) {
random = true
}
if (arg.avconv) {
avconv = true
}
if (arg.tmp) {
TMPDIR = arg.tmp
}
if (arg.exec) {
e = arg.exec
}
if (arg.quiet) {
QUIET = true
}
if (arg.pattern) {
pattern = arg.pattern.split(':')
pattern = pattern.map(el =>{
return parseInt(el);
})
} else {
for (let i = 0; i <input.length; i++) {
pattern.push(1);
}
}
if (arg.realtime) realtime = true;
TMPPATH = path.join(TMPDIR, 'frameloom');
try {
await clear()
} catch (err) {
log('Error clearing temp directory', err)
return process.exit(3)
}
log(`Processing video files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`)
for (let i = 0; i < input.length; i++) {
try {
await frames(input[i], i, avconv)
} catch (err) {
log('Error exporting video fie to image sequence', err)
return process.exit(4)
}
}
log('Weaving frames')
try {
await weave(pattern, realtime, random)
} catch (err) {
log('Error weaving', err)
return process.exit(5)
}
if (e) {
try {
await subExec(e)
} catch (err) {
log('Error performing subcommand', err)
return process.exit(7)
}
}
try {
await render(output, avconv)
} catch (err) {
log('Error rendering', err)
return process.exit(6)
}
try {
await clear()
} catch (err) {
log('Error clearing files', err)
return process.exit(7)
}
console.timeEnd('frameloom')
}
program
.version(pkg.version)
.option('-i, --input [files]', 'Specify input videos with paths seperated by colon')
.option('-o, --output [file]', 'Specify output path of video')
.option('-p, --pattern [pattern]', 'Specify a pattern for the flicker 1:1 is standard')
.option('-r, --realtime', 'Specify if videos should preserve realtime speed')
.option('-t, --tmp [dir]', 'Specify tmp directory for exporting frames')
.option('-a, --avconv', 'Specify avconv if preferred to ffmpeg')
.option('-R, --random', 'Randomize frames. Ignores pattern if included')
.option('-e, --exec', 'Command to execute on every frame. Specify {{i}} and {{o}} if the command requires it, otherwise frame path will be appended to command')
.option('-q, --quiet', 'Suppresses all log messages')
.parse(process.argv)
main(program)