Add documentation. Add random sort method.

This commit is contained in:
mmcw-dev 2018-12-22 00:15:37 -05:00
parent 4619a1620c
commit 82a6235d65
1 changed files with 264 additions and 97 deletions

361
frameloom
View File

@ -10,7 +10,14 @@ const fs = require('fs-extra')
let TMPDIR = os.tmpdir() || '/tmp' let TMPDIR = os.tmpdir() || '/tmp'
let TMPPATH let TMPPATH
/**
* 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) { 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) => {
@ -19,9 +26,52 @@ async function exec (cmd) {
}) })
}) })
} }
/**
* 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) {
return new Promise((resolve, reject) =>{
return setTimeout(resolve, ms)
})
}
/**
* 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, max = 5) {
let len = (i + '').length
let str = i + ''
for (let x = 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 (a) {
for (let i = a.length; i; i--) {
let j = Math.floor(Math.random() * i);
[a[i - 1], a[j]] = [a[j], a[i - 1]]
}
}
/**
* Clears the temporary directory of all files.
* Establishes a directory if none exists.
**/
async function clear () { async function clear () {
let exists let exists
try { try {
exists = await fs.exists(TMPPATH) exists = await fs.exists(TMPPATH)
} catch (err) { } catch (err) {
@ -29,9 +79,9 @@ async function clear () {
} }
if (exists) { if (exists) {
console.log(`Clearing tmp directory ${TMPPATH}`) console.log(`Clearing temp directory "${TMPPATH}"`)
try { try {
await exec(`rm -r "${TMPPATH}"`) await fs.rmdir(TMPPATH)
} catch (err) { } catch (err) {
//suppress error //suppress error
} }
@ -39,19 +89,31 @@ async function clear () {
try { try {
await fs.mkdir(TMPPATH) await fs.mkdir(TMPPATH)
} catch (Err) { } catch (err) {
console.error(err); if (err.code !== 'EEXIST') {
console.error(err)
}
} }
} }
/**
async function frames (video, order) { * 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
*
* @returns {string} String with the export order, not sure why I did this
**/
async function frames (video, order, avconv) {
let ext = 'tif' let ext = 'tif'
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 = `ffmpeg -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"` cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`
console.log(`Exporting ${video} as single frames...`) console.log(`Exporting ${video} as single frames...`)
@ -61,46 +123,69 @@ async function frames (video, order) {
console.error('Error exporting video', err) console.error('Error exporting video', err)
return process.exit(3) return process.exit(3)
} }
return path.join(TMPPATH, `export_${order}`) return path.join(TMPPATH, `export_${order}`)
} }
/**
* 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)
**/
async function weave (pattern, realtime, random) {
let frames
let old
let seqFile
let seq
let alt = false
function zeroPad (i, max = 5) {
let len = (i + '').length
let str = i + ''
for (let x = 0; x < max - len; x++) {
str = '0' + str
}
return str
}
async function reorder (pattern, realtime) {
let frames;
let old;
let seqFile;
let seq;
console.log('Weaving frames...') console.log('Weaving frames...')
try { try {
frames = await fs.readdir(TMPPATH) frames = await fs.readdir(TMPPATH)
} catch (err) { } catch (err) {
console.error('Error reading tmp directory', err) console.error('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
}); })
//other patterns
for (let el of pattern) {
try { if (el !== 1) alt = true
seq = await patternSort(frames, pattern, realtime)
} catch (err) {
console.error('Error sorting frames')
} }
console.dir(seq)
//
}
function groupAlt (list, pattern, realtime) { if (random){
try {
seq = await randomSort(frames, realtime)
} catch (err) {
console.error('Error sorting frames')
}
} else if (!alt) {
try {
seq = await standardSort(frames, pattern, realtime)
} catch (err) {
console.error('Error sorting frames')
}
} else if (alt) {
try {
seq = await altSort(frames, pattern, realtime)
} catch (err) {
console.error('Error sorting frames')
}
}
//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, pattern, realtime) {
let groups = [] let groups = []
let newList = [] let newList = []
let frameCount = 0 let frameCount = 0
@ -126,7 +211,7 @@ function groupAlt (list, pattern, realtime) {
console.log(`Renaming ${list[i]} -> ${newName}`); console.log(`Renaming ${list[i]} -> ${newName}`);
try { try {
//fs.renameSync(oldPath, newPath) //await fs.move(oldPath, newPath, { overwrite: true })
newList.push(newName); newList.push(newName);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
@ -138,99 +223,169 @@ function groupAlt (list, pattern, realtime) {
} }
return newList return newList
} }
/**
async function patternSort (list, pattern, realtime = false) { * 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, 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 alt
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 = []
for (let el of pattern) {
if (el !== 1) alt = true
}
if (realtime) { if (realtime) {
skip = false skip = false
skipCount = pattern.length + 1 skipCount = pattern.length + 1
} }
if (!alt) { for (let i = 0; i < list.length; i++) {
for (let i = 0; i < list.length; i++) { if (realtime) {
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]) oldPath = path.join(TMPPATH, list[i])
if (skip) {
console.log(`Skipping ${list[i]}`)
try {
await fs.unlink(oldPath)
} catch (err) {
console.error(err)
}
continue
}
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName)
console.log(`Renaming ${list[i]} -> ${newName}`)
if (skip) {
console.log(`Skipping ${list[i]}`)
try { try {
await fs.rename(oldPath, newPath) await fs.unlink(oldPath)
newList.push(newName)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
continue
frameCount++
} }
} else {
newList = groupAlt(list, pattern, realtime) newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName)
console.log(`Renaming ${list[i]} -> ${newName}`)
try {
await fs.move(oldPath, newPath, { overwrite: true })
newList.push(newName)
frameCount++
} catch (err) {
console.error(err)
return process.exit(10)
}
} }
return newList 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, pattern, realtime) {
let frameCount = 0
let ext = path.extname(list[0])
let oldPath
let newName
let newPath
let newList = []
let removeLen = 0
let remove = []
async function render (output) { shuffle(list)
let exp = path.join(TMPPATH, `render_%05d.tif`)
if (realtime) {
removeLen = Math.floor(list.length / pattern.length)
remove = list.slice(removeLen, list.length)
list = list.slice(0, removeLen)
console.log(`Skipping extra frames...`)
for (let i = 0; i < remove.length; i++) {
oldPath = path.join(TMPPATH, remove[i])
console.log(`Skipping ${list[i]}`)
try {
await fs.unlink(oldPath)
} catch (err) {
console.error(err)
}
}
}
for (let i = 0; i < list.length; i++) {
oldPath = path.join(TMPPATH, list[i])
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName)
console.log(`Renaming ${list[i]} -> ${newName}`)
try {
await fs.move(oldPath, newPath, { overwrite : true })
newList.push(newName)
} catch (err) {
console.error(err)
}
frameCount++
}
return newList
}
/**
* Render the frames into a video using ffmpeg.
*
* @param {string} output Path to export the video to
**/
async function render (output, avconv) {
//process.exit()
let frames = path.join(TMPPATH, `render_%05d.tif`)
let exe = avconv ? 'avconv' : 'ffmpeg'
let resolution = '1920x1080' let resolution = '1920x1080'
let h264 = `-vcodec libx264 -g 1 -crf 25 -pix_fmt yuv420p` let h264 = `-vcodec libx264 -g 1 -crf 25 -pix_fmt yuv420p`
let prores = `-c:v prores -profile:v 3 -c:a pcm_s16le - g 1` let prores = `-c:v prores_ks -profile:v 3`
let format = (output.indexOf('.mov') !== -1) ? prores : h264 let format = (output.indexOf('.mov') !== -1) ? prores : h264
const cmd = `ffmpeg -r 24 -f image2 -s ${resolution} -i ${exp} ${format} -y ${output}` const cmd = `${exe} -r 24 -f image2 -s ${resolution} -i ${frames} ${format} -y ${output}`
console.log(`Exporting video ${output}`) console.log(`Exporting video ${output}`)
console.log(cmd) console.log(cmd)
/*try {
await exec(`ls "${TMPPATH}"`)
} catch (err) {
console.log(err)
}*/
try { try {
await exec(cmd) await exec(cmd)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} }
} }
/**
* Parses the arguments and runs the process of exporting, sorting and then
* "weaving" the frames back into a video
**/
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 random = false
console.time('frameloom') console.time('frameloom')
if (input.length < 2) { if (input.length < 2) {
console.error('Must provide more than 1 input'); console.error('Must provide more than 1 input')
return process.exit(1) return process.exit(1)
} }
@ -239,6 +394,14 @@ async function main (arg) {
return process.exit(2) return process.exit(2)
} }
if (arg.random) {
random = true
}
if (arg.avconv) {
avconv = true
}
if (arg.pattern) { if (arg.pattern) {
pattern = arg.pattern.split(':') pattern = arg.pattern.split(':')
pattern = pattern.map(el =>{ pattern = pattern.map(el =>{
@ -257,7 +420,7 @@ async function main (arg) {
try { try {
await clear() await clear()
} catch (err) { } catch (err) {
console.error(err); console.error(err)
return process.exit(3) return process.exit(3)
} }
@ -265,36 +428,39 @@ async function main (arg) {
for (let i = 0; i <input.length; i++) { for (let i = 0; i <input.length; i++) {
try { try {
await frames(input[i], i) await frames(input[i], i, avconv)
} catch (err) { } catch (err) {
console.error(err); console.error(err)
return process.exit(4)
} }
} }
try { try {
await reorder(pattern, realtime) await weave(pattern, realtime, random)
} catch (err) { } catch (err) {
console.error(err) console.error(err)
return process.exit(5)
}
await delay(2000)
try {
await render(output, avconv)
} catch (err) {
console.error(err)
return process.exit(6)
} }
try { try {
await render(output) //await clear()
} catch (err) { } catch (err) {
console.error(err) console.error(err)
} return process.exit(7)
try {
await clear()
} catch (err) {
console.error(err);
return process.exit(3)
} }
console.timeEnd('frameloom') console.timeEnd('frameloom')
} }
program program
.version('1.0.0') .version('1.0.0')
.option('-i, --input [files]', 'Specify input videos with paths seperated by colon') .option('-i, --input [files]', 'Specify input videos with paths seperated by colon')
@ -303,6 +469,7 @@ program
.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')
.parse(process.argv); .option('-R, --random', 'Randomize frames. Ignores pattern if included')
.parse(process.argv)
main(program) main(program)