Port to typescript. Compile (or transpile?) step enforces semicolon and whitespace rules on ./frameloom file.
This commit is contained in:
parent
2cd09f1e05
commit
3f1016d915
|
@ -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)
|
||||||
|
return false;
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error(msg, err)
|
console.error(msg, err);
|
||||||
} else {
|
|
||||||
console.log(msg)
|
|
||||||
}
|
}
|
||||||
return true
|
else {
|
||||||
|
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) {
|
}
|
||||||
log('Error checking if file exists', err)
|
catch (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,63 +126,62 @@ 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}"`
|
|
||||||
|
|
||||||
log(`Exporting ${video} as single frames...`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exec(cmd)
|
await exec(cmd);
|
||||||
} catch (err) {
|
|
||||||
log('Error exporting video', err)
|
|
||||||
return process.exit(3)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
return path.join(TMPPATH, `export-%05d_${order}`)
|
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
|
* 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) {
|
|
||||||
log('Error reading tmp directory', err)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
frames = frames.filter (file =>{
|
log('Error reading tmp directory', err);
|
||||||
if (file.indexOf('.tif') !== -1) return true
|
}
|
||||||
})
|
frames = frames.filter(file => {
|
||||||
|
if (file.indexOf('.tif') !== -1)
|
||||||
|
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 {
|
}
|
||||||
frameCmd = `${cmd} ${framePath}`
|
else {
|
||||||
|
frameCmd = `${cmd} ${framePath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await exec(frameCmd)
|
await exec(frameCmd);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error executing sub command on frame', err)
|
catch (err) {
|
||||||
return process.exit(10)
|
log('Error executing sub command on frame', err);
|
||||||
|
return process.exit(10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -192,50 +191,52 @@ async function subExec (cmd) {
|
||||||
*
|
*
|
||||||
* @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
|
|
||||||
|
|
||||||
log('Weaving frames...')
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
frames = await fs.readdir(TMPPATH)
|
frames = await fs.readdir(TMPPATH);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error reading tmp directory', err)
|
catch (err) {
|
||||||
|
log('Error reading tmp directory', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
//console.dir(frames)
|
//console.dir(frames)
|
||||||
frames = frames.filter (file =>{
|
frames = frames.filter(file => {
|
||||||
if (file.indexOf('.tif') !== -1) return true
|
if (file.indexOf('.tif') !== -1)
|
||||||
})
|
return true;
|
||||||
|
});
|
||||||
for (let el of pattern) {
|
for (let el of pattern) {
|
||||||
if (el !== 1) alt = true
|
if (el !== 1)
|
||||||
|
alt = true;
|
||||||
}
|
}
|
||||||
|
if (random) {
|
||||||
if (random){
|
|
||||||
try {
|
try {
|
||||||
seq = await randomSort(frames, realtime)
|
seq = await randomSort(frames, pattern, realtime);
|
||||||
} catch (err) {
|
|
||||||
log('Error sorting frames', err)
|
|
||||||
}
|
}
|
||||||
} else if (!alt) {
|
catch (err) {
|
||||||
try {
|
log('Error sorting frames', err);
|
||||||
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', {})
|
else if (!alt) {
|
||||||
process.exit(10)
|
|
||||||
try {
|
try {
|
||||||
seq = await altSort(frames, pattern, realtime)
|
seq = await standardSort(frames, pattern, realtime);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error sorting frames', err)
|
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)
|
//console.dir(seq)
|
||||||
|
@ -247,25 +248,23 @@ 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]);
|
/*oldPath = path.join(TMPPATH, list[i]);
|
||||||
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||||
newPath = path.join(TMPPATH, newName);
|
newPath = path.join(TMPPATH, newName);
|
||||||
|
@ -278,12 +277,11 @@ async function altSort (list, pattern, realtime) {
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log(err);
|
log(err);
|
||||||
}*/
|
}*/
|
||||||
|
frameCount++;
|
||||||
frameCount++
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return newList
|
return newList;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Standard frame sorting method.
|
* Standard frame sorting method.
|
||||||
|
@ -292,61 +290,54 @@ 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++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
if (realtime) {
|
if (realtime) {
|
||||||
skipCount--;
|
skipCount--;
|
||||||
if (skipCount === 0) {
|
if (skipCount === 0) {
|
||||||
skip = !skip;
|
skip = !skip;
|
||||||
skipCount = pattern.length
|
skipCount = pattern.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
oldPath = path.join(TMPPATH, list[i]);
|
||||||
oldPath = path.join(TMPPATH, list[i])
|
|
||||||
|
|
||||||
if (skip) {
|
if (skip) {
|
||||||
log(`Skipping ${list[i]}`)
|
log(`Skipping ${list[i]}`);
|
||||||
try {
|
try {
|
||||||
await fs.unlink(oldPath)
|
await fs.unlink(oldPath);
|
||||||
} catch (err) {
|
|
||||||
log('Error deleting frame', err)
|
|
||||||
}
|
}
|
||||||
continue
|
catch (err) {
|
||||||
|
log('Error deleting frame', err);
|
||||||
}
|
}
|
||||||
|
continue;
|
||||||
newName = `./render_${zeroPad(frameCount)}${ext}`
|
}
|
||||||
newPath = path.join(TMPPATH, newName)
|
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||||
log(`Renaming ${list[i]} -> ${newName}`)
|
newPath = path.join(TMPPATH, newName);
|
||||||
|
log(`Renaming ${list[i]} -> ${newName}`);
|
||||||
try {
|
try {
|
||||||
await fs.move(oldPath, newPath)
|
await fs.move(oldPath, newPath);
|
||||||
newList.push(newName)
|
newList.push(newName);
|
||||||
frameCount++
|
frameCount++;
|
||||||
} catch (err) {
|
|
||||||
log('Error renaming frame', err)
|
|
||||||
return process.exit(10)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
log('Error renaming frame', err);
|
||||||
|
return process.exit(10);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return newList
|
return newList;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Ramdomly sort frames for re-stitching.
|
* Ramdomly sort frames for re-stitching.
|
||||||
|
@ -355,83 +346,78 @@ async function standardSort (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 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) {
|
if (realtime) {
|
||||||
removeLen = Math.floor(list.length / pattern.length)
|
removeLen = Math.floor(list.length / pattern.length);
|
||||||
remove = list.slice(removeLen, list.length)
|
remove = list.slice(removeLen, list.length);
|
||||||
list = list.slice(0, removeLen)
|
list = list.slice(0, removeLen);
|
||||||
|
log(`Skipping extra frames...`);
|
||||||
log(`Skipping extra frames...`)
|
|
||||||
for (let i = 0; i < remove.length; i++) {
|
for (let i = 0; i < remove.length; i++) {
|
||||||
oldPath = path.join(TMPPATH, remove[i])
|
oldPath = path.join(TMPPATH, remove[i]);
|
||||||
log(`Skipping ${list[i]}`)
|
log(`Skipping ${list[i]}`);
|
||||||
try {
|
try {
|
||||||
await fs.unlink(oldPath)
|
await fs.unlink(oldPath);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error deleting frame', err)
|
catch (err) {
|
||||||
|
log('Error deleting frame', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0; i < list.length; i++) {
|
for (let i = 0; i < list.length; i++) {
|
||||||
oldPath = path.join(TMPPATH, list[i])
|
oldPath = path.join(TMPPATH, list[i]);
|
||||||
|
newName = `./render_${zeroPad(frameCount)}${ext}`;
|
||||||
newName = `./render_${zeroPad(frameCount)}${ext}`
|
newPath = path.join(TMPPATH, newName);
|
||||||
newPath = path.join(TMPPATH, newName)
|
log(`Renaming ${list[i]} -> ${newName}`);
|
||||||
log(`Renaming ${list[i]} -> ${newName}`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.move(oldPath, newPath)
|
await fs.move(oldPath, newPath);
|
||||||
newList.push(newName)
|
newList.push(newName);
|
||||||
} catch (err) {
|
|
||||||
log('Error moving frame', err)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
frameCount++
|
log('Error moving frame', err);
|
||||||
}
|
}
|
||||||
|
frameCount++;
|
||||||
return newList
|
}
|
||||||
|
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) {
|
}
|
||||||
log('Error rendering video with ffmpeg', err)
|
catch (err) {
|
||||||
|
log('Error rendering video with ffmpeg', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
@ -440,113 +426,102 @@ async function render (output, avconv) {
|
||||||
*
|
*
|
||||||
* @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) {
|
if (!output) {
|
||||||
log('Must provide video output path', {})
|
log('Must provide video output path', {});
|
||||||
return process.exit(2)
|
return process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.random) {
|
if (arg.random) {
|
||||||
random = true
|
random = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.avconv) {
|
if (arg.avconv) {
|
||||||
avconv = true
|
avconv = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.tmp) {
|
if (arg.tmp) {
|
||||||
TMPDIR = arg.tmp
|
TMPDIR = arg.tmp;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.exec) {
|
if (arg.exec) {
|
||||||
e = arg.exec
|
e = arg.exec;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.quiet) {
|
if (arg.quiet) {
|
||||||
QUIET = true
|
QUIET = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (arg.pattern) {
|
if (arg.pattern) {
|
||||||
pattern = arg.pattern.split(':')
|
pattern = arg.pattern.split(':');
|
||||||
pattern = pattern.map(el =>{
|
pattern = pattern.map(el => {
|
||||||
return parseInt(el);
|
return parseInt(el);
|
||||||
})
|
});
|
||||||
} else {
|
}
|
||||||
for (let i = 0; i <input.length; i++) {
|
else {
|
||||||
|
for (let i = 0; i < input.length; i++) {
|
||||||
pattern.push(1);
|
pattern.push(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (arg.realtime)
|
||||||
if (arg.realtime) realtime = true;
|
realtime = true;
|
||||||
|
|
||||||
TMPPATH = path.join(TMPDIR, 'frameloom');
|
TMPPATH = path.join(TMPDIR, 'frameloom');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await clear()
|
await clear();
|
||||||
} catch (err) {
|
|
||||||
log('Error clearing temp directory', err)
|
|
||||||
return process.exit(3)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
log(`Processing video files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`)
|
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++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
try {
|
try {
|
||||||
await frames(input[i], i, avconv)
|
await frames(input[i], i, avconv);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error exporting video fie to image sequence', err)
|
catch (err) {
|
||||||
return process.exit(4)
|
log('Error exporting video fie to image sequence', err);
|
||||||
|
return process.exit(4);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log('Weaving frames');
|
||||||
log('Weaving frames')
|
|
||||||
try {
|
try {
|
||||||
await weave(pattern, realtime, random)
|
await weave(pattern, realtime, random);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error weaving', err)
|
catch (err) {
|
||||||
return process.exit(5)
|
log('Error weaving', err);
|
||||||
|
return process.exit(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e) {
|
if (e) {
|
||||||
try {
|
try {
|
||||||
await subExec(e)
|
await subExec(e);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error performing subcommand', err)
|
catch (err) {
|
||||||
return process.exit(7)
|
log('Error performing subcommand', err);
|
||||||
|
return process.exit(7);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await render(output, avconv)
|
await render(output, avconv);
|
||||||
} catch (err) {
|
}
|
||||||
log('Error rendering', err)
|
catch (err) {
|
||||||
return process.exit(6)
|
log('Error rendering', err);
|
||||||
|
return process.exit(6);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await clear()
|
await clear();
|
||||||
} catch (err) {
|
|
||||||
log('Error clearing files', err)
|
|
||||||
return process.exit(7)
|
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
console.timeEnd('frameloom')
|
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')
|
||||||
|
@ -558,6 +533,5 @@ program
|
||||||
.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)
|
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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)
|
Loading…
Reference in New Issue