diff --git a/frameloom b/frameloom index 1c0e7b6..28cf2c9 100755 --- a/frameloom +++ b/frameloom @@ -10,7 +10,14 @@ const fs = require('fs-extra') let TMPDIR = os.tmpdir() || '/tmp' 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) { return new Promise((resolve, reject) => { 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 () { let exists + try { exists = await fs.exists(TMPPATH) } catch (err) { @@ -29,9 +79,9 @@ async function clear () { } if (exists) { - console.log(`Clearing tmp directory ${TMPPATH}`) + console.log(`Clearing temp directory "${TMPPATH}"`) try { - await exec(`rm -r "${TMPPATH}"`) + await fs.rmdir(TMPPATH) } catch (err) { //suppress error } @@ -39,19 +89,31 @@ async function clear () { try { await fs.mkdir(TMPPATH) - } catch (Err) { - console.error(err); + } catch (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 exe = avconv ? 'avconv' : 'ffmpeg' let tmpoutput let cmd 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...`) @@ -61,46 +123,69 @@ async function frames (video, order) { console.error('Error exporting video', err) return process.exit(3) } + 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...') + try { frames = await fs.readdir(TMPPATH) } catch (err) { console.error('Error reading tmp directory', err) } - console.dir(frames) + //console.dir(frames) frames = frames.filter (file =>{ if (file.indexOf('.tif') !== -1) return true - }); - //other patterns - - try { - seq = await patternSort(frames, pattern, realtime) - } catch (err) { - console.error('Error sorting frames') + }) + + for (let el of pattern) { + if (el !== 1) alt = true } - 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 newList = [] let frameCount = 0 @@ -126,7 +211,7 @@ function groupAlt (list, pattern, realtime) { console.log(`Renaming ${list[i]} -> ${newName}`); try { - //fs.renameSync(oldPath, newPath) + //await fs.move(oldPath, newPath, { overwrite: true }) newList.push(newName); } catch (err) { console.error(err); @@ -138,99 +223,169 @@ function groupAlt (list, pattern, realtime) { } 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 stepCount let step let skipCount let skip - let alt let ext = path.extname(list[0]) let oldPath let newName let newPath let newList = [] - for (let el of pattern) { - if (el !== 1) alt = true - } - if (realtime) { skip = false skipCount = pattern.length + 1 } - if (!alt) { - for (let i = 0; i < list.length; i++) { - - if (realtime) { - skipCount--; - if (skipCount === 0) { - skip = !skip; - skipCount = pattern.length - } + for (let i = 0; i < list.length; i++) { + if (realtime) { + skipCount--; + if (skipCount === 0) { + skip = !skip; + skipCount = pattern.length } + } - 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}`) + oldPath = path.join(TMPPATH, list[i]) + if (skip) { + console.log(`Skipping ${list[i]}`) try { - await fs.rename(oldPath, newPath) - newList.push(newName) + await fs.unlink(oldPath) } catch (err) { console.error(err) } - - frameCount++ + continue } - } 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 } +/** + * 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) { - let exp = path.join(TMPPATH, `render_%05d.tif`) + shuffle(list) + + 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 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 - 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(cmd) + /*try { + await exec(`ls "${TMPPATH}"`) + } catch (err) { + console.log(err) + }*/ + try { await exec(cmd) } catch (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) { let input = arg.input.split(':') - let output = arg.output; - let pattern = []; - let realtime = false; + let output = arg.output + let pattern = [] + let realtime = false + let avconv = false + let random = false console.time('frameloom') if (input.length < 2) { - console.error('Must provide more than 1 input'); + console.error('Must provide more than 1 input') return process.exit(1) } @@ -239,6 +394,14 @@ async function main (arg) { return process.exit(2) } + if (arg.random) { + random = true + } + + if (arg.avconv) { + avconv = true + } + if (arg.pattern) { pattern = arg.pattern.split(':') pattern = pattern.map(el =>{ @@ -257,7 +420,7 @@ async function main (arg) { try { await clear() } catch (err) { - console.error(err); + console.error(err) return process.exit(3) } @@ -265,36 +428,39 @@ async function main (arg) { for (let i = 0; i