Compare commits

..

33 Commits
v1.0.0 ... main

Author SHA1 Message Date
Matt McWilliams 5c27aa4521 Add image to frameloom project 2025-02-27 19:30:44 -05:00
Matt McWilliams 43169174a9 Add url of new canonical git repo 2021-10-22 16:47:42 -04:00
Matt McWilliams 4dbdc4e3da Fixed breaking change to commander where options are not directly on the "program" object and need to be retrieved via the .opts() method 2021-04-07 09:13:04 -04:00
Matt McWilliams 5da0bcec11 Add the license to the readme 2021-03-27 11:09:54 -04:00
Matt McWilliams 1ceeaad33a Remove TODO 2021-03-27 11:09:30 -04:00
Matt McWilliams 6c12d4961c Update the docs by running the doc build process. 2021-03-26 18:46:45 -04:00
Matt McWilliams f569a27e32 Update readme to reflect all options on the application now. 2021-03-26 18:45:07 -04:00
Matt McWilliams d2b27620ca Accidentally broke reference to package.json 2021-03-26 18:43:17 -04:00
Matt McWilliams 54057021a4 Add return types to functions that are not void 2021-03-26 18:22:42 -04:00
Matt McWilliams c0fcdd62da Only require functions that are actually used rather than the entire module. 2021-03-26 18:19:43 -04:00
Matt McWilliams f4e799d647 Remove frameloom.sh from the root. This will constitute an entire separate repo now. 2021-03-26 17:09:03 -04:00
Matt McWilliams 501bcf8603 Update modules for porting process. 2021-03-26 17:00:17 -04:00
mmcwilliams 22344e8343 Target node10, node11 isn't supported 2019-08-05 12:21:29 -04:00
mmcwilliams cbc7745f02 Add spin back to frameloom as -s, --spin flag. Skip frames that aren't rotated. 2019-08-05 12:19:11 -04:00
mmcwilliams 4e27edcfa9 Restore square_gif script with automatic resizing in the first script. Performs the crop and re-encode in 1920x1080 2019-05-31 16:35:09 -04:00
mmcwilliams a501023f27 Update pkg 2019-05-31 16:20:00 -04:00
mmcwilliams aa42c8dc67 Implemented altsort for patterns other than 1:1. Added a check for the ffmpeg or avconv binaries before running. 2019-05-31 15:12:29 -04:00
mmcwilliams 2b2b98eb01 Script for generating square gifs from videos 2019-05-31 14:58:30 -04:00
mmcwilliams 3a561ad6c3 Merge remote-tracking branch 'origin/master' 2019-04-17 13:58:41 -04:00
mmcwilliams 847e9644ce Fix square gif generator 2019-04-16 18:08:28 -04:00
mmcw-dev 1ce8d323f6 bad merge 2019-04-15 20:16:33 -04:00
mmcwilliams 3fa0d6a14b Add a script for generating square gifs from videos (assumes 1920x1080 source) 2019-04-04 00:53:55 -04:00
mmcwilliams e8c949daea Increment patch to 1.0.2 following rewrite. API remains stable and features need to be added to get to 1.1.x 2019-04-02 12:58:36 -04:00
mmcwilliams 3f1016d915 Port to typescript. Compile (or transpile?) step enforces semicolon and whitespace rules on ./frameloom file. 2019-04-02 12:57:39 -04:00
mmcwilliams 2cd09f1e05 Add comment to frameloom.sh, explaining how the basic example works. This can be expanded upon to eventually replace the node script. 2019-04-02 12:49:16 -04:00
mmcwilliams 9d8739cf9b Install typescript and add npm script commands for existing shell scripts, as well as the tsc compile script. 2019-04-02 12:45:35 -04:00
mmcwilliams 6894861ff9 Move all scripts into scripts directory. 2019-04-02 12:44:59 -04:00
mmcwilliams cf39232d70 Get version from package.json 2019-02-23 22:38:20 -05:00
mmcw-dev b1370447bf Add todos 2019-01-03 23:35:04 -05:00
mmcw-dev 4c1ac8f997 Add exit state in alt sort because it does not work yet. Yeesh. 2019-01-03 23:11:28 -05:00
mmcw-dev 778f740047 Add "docs" as an npm command that can be run with `npm run docs` 2019-01-03 22:47:50 -05:00
mmcw-dev 53c8b4c82d Ignore macOS-speicific files 2019-01-03 22:37:47 -05:00
mmcw-dev f9f50159c3 Increment patch number 2019-01-03 22:37:23 -05:00
15 changed files with 5349 additions and 2397 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
node_modules node_modules
examples examples
dist dist
*.AppleDouble
*.Parent
*.DS_Store

View File

@ -1,9 +1,13 @@
# frameloom # frameloom
![Image illustrating two 5 frame sequences stitched together with frameloom](./img/frameloom.jpg)
Node script to generate flicker videos by interweaving frames from multiple videos Node script to generate flicker videos by interweaving frames from multiple videos
-------- --------
## Git URL [git.sixteenmillimeter.com/16mm/frameloom](https://git.sixteenmillimeter.com/16mm/frameloom)
## Requirements ## Requirements
This script relies on `ffmpeg` to export and stitch video back together This script relies on `ffmpeg` to export and stitch video back together
@ -21,13 +25,15 @@ chmod +x frameloom
## Basic Usage ## Basic Usage
```./frameloom -i /path/to/video1:/path/to/video2 -o /path/to/output``` ```bash
./frameloom -i /path/to/video1:/path/to/video2 -o /path/to/output
```
## Options ## Options
Run `./frameloom -h` to display help screen. Run `./frameloom -h` to display help screen.
``` ```bash
Usage: frameloom [options] Usage: frameloom [options]
Options: Options:
@ -39,6 +45,19 @@ Options:
-t, --tmp [dir] Specify tmp directory for exporting frames -t, --tmp [dir] Specify tmp directory for exporting frames
-a, --avconv Specify avconv if preferred to ffmpeg -a, --avconv Specify avconv if preferred to ffmpeg
-R, --random Randomize frames. Ignores pattern if included -R, --random Randomize frames. Ignores pattern if included
-h, --help output usage information -s, --spin Randomly rotate frames before rendering
-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
-q, --quiet Suppresses all log messages
-h, --help display help for command
``` ```
## License
Copyright 2018-2021 M McWilliams
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -34,7 +34,7 @@ if (!fs.existsSync(`./dist/${platform}_${arch}`)) {
console.log(`Building frameloom and saving in dist/${platform}_${arch}...`) console.log(`Building frameloom and saving in dist/${platform}_${arch}...`)
console.time('frameloom') console.time('frameloom')
exec([ 'frameloom', '--target', 'host', '--output', `./dist/${platform}_${arch}/frameloom` ]).then(async (res) => { exec([ 'frameloom', '--target', 'node10', '--output', `./dist/${platform}_${arch}/frameloom` ]).then(async (res) => {
try { try {
await shell_out(`zip -r ./dist/frameloom_${platform}_${arch}_${packageJson.version}.zip ./dist/${platform}_${arch}/frameloom`) await shell_out(`zip -r ./dist/frameloom_${platform}_${arch}_${packageJson.version}.zip ./dist/${platform}_${arch}/frameloom`)
console.log(`Compressed binary to dist/frameloom_${platform}_${arch}_${packageJson.version}.zip`) console.log(`Compressed binary to dist/frameloom_${platform}_${arch}_${packageJson.version}.zip`)

View File

@ -8,6 +8,10 @@
<dt><a href="#delay">delay(ms)</a><code>Promise</code></dt> <dt><a href="#delay">delay(ms)</a><code>Promise</code></dt>
<dd><p>Delays process for specified amount of time in milliseconds.</p> <dd><p>Delays process for specified amount of time in milliseconds.</p>
</dd> </dd>
<dt><a href="#log">log()</a></dt>
<dd><p>Log function wrapper that can silences logs when
QUIET == true</p>
</dd>
<dt><a href="#zeroPad">zeroPad(i, max)</a><code>string</code></dt> <dt><a href="#zeroPad">zeroPad(i, max)</a><code>string</code></dt>
<dd><p>Pads a numerical value with preceding zeros to make strings same length.</p> <dd><p>Pads a numerical value with preceding zeros to make strings same length.</p>
</dd> </dd>
@ -18,12 +22,15 @@
<dd><p>Clears the temporary directory of all files. <dd><p>Clears the temporary directory of all files.
Establishes a directory if none exists.</p> Establishes a directory if none exists.</p>
</dd> </dd>
<dt><a href="#frames">frames(video, order)</a><code>string</code></dt> <dt><a href="#frames">frames(video, order, avconv)</a><code>string</code></dt>
<dd><p>Exports all frames from video. Appends number to the string <dd><p>Exports all frames from video. Appends number to the string
to keep frames in alternating order to be quickly stitched together to keep frames in alternating order to be quickly stitched together
or re-sorted.</p> or re-sorted.</p>
</dd> </dd>
<dt><a href="#weave">weave(pattern, realtime)</a></dt> <dt><a href="#subExec">subExec(cmd)</a></dt>
<dd><p>Shells out to run a sub command on every frame to perform effects</p>
</dd>
<dt><a href="#weave">weave(pattern, realtime, random)</a></dt>
<dd><p>Re-arranges the frames into the order specified in the pattern. <dd><p>Re-arranges the frames into the order specified in the pattern.
Calls <code>patternSort()</code> to perform the rename and unlink actions</p> Calls <code>patternSort()</code> to perform the rename and unlink actions</p>
</dd> </dd>
@ -36,7 +43,7 @@
<dt><a href="#randomSort">randomSort(list, pattern, realtime)</a></dt> <dt><a href="#randomSort">randomSort(list, pattern, realtime)</a></dt>
<dd><p>Ramdomly sort frames for re-stitching.</p> <dd><p>Ramdomly sort frames for re-stitching.</p>
</dd> </dd>
<dt><a href="#render">render(output)</a></dt> <dt><a href="#render">render(output, avconv)</a></dt>
<dd><p>Render the frames into a video using ffmpeg.</p> <dd><p>Render the frames into a video using ffmpeg.</p>
</dd> </dd>
<dt><a href="#main">main(arg)</a></dt> <dt><a href="#main">main(arg)</a></dt>
@ -70,6 +77,13 @@ Delays process for specified amount of time in milliseconds.
| --- | --- | --- | | --- | --- | --- |
| ms | <code>integer</code> | Milliseconds to delay for | | ms | <code>integer</code> | Milliseconds to delay for |
<a name="log"></a>
## log()
Log function wrapper that can silences logs when
QUIET == true
**Kind**: global function
<a name="zeroPad"></a> <a name="zeroPad"></a>
## zeroPad(i, max) ⇒ <code>string</code> ## zeroPad(i, max) ⇒ <code>string</code>
@ -103,7 +117,7 @@ Clears the temporary directory of all files.
**Kind**: global function **Kind**: global function
<a name="frames"></a> <a name="frames"></a>
## frames(video, order) ⇒ <code>string</code> ## frames(video, order, avconv) ⇒ <code>string</code>
Exports all frames from video. Appends number to the string Exports all frames from video. Appends number to the string
to keep frames in alternating order to be quickly stitched together to keep frames in alternating order to be quickly stitched together
or re-sorted. or re-sorted.
@ -115,10 +129,22 @@ Exports all frames from video. Appends number to the string
| --- | --- | --- | | --- | --- | --- |
| video | <code>string</code> | String representing path to video | | video | <code>string</code> | String representing path to video |
| order | <code>integer</code> | Integer to be appended to pathname of file | | order | <code>integer</code> | Integer to be appended to pathname of file |
| avconv | <code>boolean</code> | Whether or not to use avconv instead of ffmpeg |
<a name="subExec"></a>
## subExec(cmd)
Shells out to run a sub command on every frame to perform effects
**Kind**: global function
| Param | Type | Description |
| --- | --- | --- |
| cmd | <code>string</code> | Command to execute on every frame |
<a name="weave"></a> <a name="weave"></a>
## weave(pattern, realtime) ## weave(pattern, realtime, random)
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
@ -128,6 +154,7 @@ Re-arranges the frames into the order specified in the pattern.
| --- | --- | --- | | --- | --- | --- |
| pattern | <code>array</code> | Pattern of the frames per input | | pattern | <code>array</code> | Pattern of the frames per input |
| realtime | <code>boolean</code> | Flag to turn on or off realtime behavior (drop frames / number of vids) | | realtime | <code>boolean</code> | Flag to turn on or off realtime behavior (drop frames / number of vids) |
| random | <code>boolean</code> | Whether or not to randomize frames |
<a name="altSort"></a> <a name="altSort"></a>
@ -170,7 +197,7 @@ Ramdomly sort frames for re-stitching.
<a name="render"></a> <a name="render"></a>
## render(output) ## render(output, avconv)
Render the frames into a video using ffmpeg. Render the frames into a video using ffmpeg.
**Kind**: global function **Kind**: global function
@ -178,6 +205,7 @@ Render the frames into a video using ffmpeg.
| Param | Type | Description | | Param | Type | Description |
| --- | --- | --- | | --- | --- | --- |
| output | <code>string</code> | Path to export the video to | | output | <code>string</code> | Path to export the video to |
| avconv | <code>boolean</code> | Whether or not to use avconv in place of ffmpeg |
<a name="main"></a> <a name="main"></a>

719
frameloom Executable file → Normal file
View File

@ -1,16 +1,16 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict';
'use strict' const execRaw = require('child_process').exec;
const { tmpdir } = require('os');
const execRaw = require('child_process').exec const { join, extname } = require('path');
const os = require('os') const program = require('commander');
const path = require('path') const { move, exists, unlink, readdir, mkdir } = require('fs-extra');
const program = require('commander') const { version } = require('./package.json');
const fs = require('fs-extra') const OUTPUT_RE = new RegExp('{{o}}', 'g');
const INPUT_RE = new RegExp('{{i}}', 'g');
let TMPDIR = os.tmpdir() || '/tmp' let QUIET = false;
let TMPPATH let TMPDIR = 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.
@ -21,11 +21,12 @@ let TMPPATH
**/ **/
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, { maxBuffer: 500 * 1024 * 1024 }, (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.
@ -36,8 +37,23 @@ async function exec (cmd) {
**/ **/
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
* QUIET == true
*/
function log(msg, err = 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. * Pads a numerical value with preceding zeros to make strings same length.
@ -48,57 +64,65 @@ async function delay (ms) {
* @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;
} }
} }
function randomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/** /**
* 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 dirExists;
try { try {
exists = await fs.exists(TMPPATH) dirExists = await exists(TMPPATH);
} catch (err) {
console.error(err)
} }
catch (err) {
if (exists) { log('Error checking if file exists', err);
console.log(`Clearing temp directory "${TMPPATH}"`) }
if (dirExists) {
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 mkdir(TMPPATH);
} catch (err) { }
catch (err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
console.error(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
@ -107,29 +131,64 @@ 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) { 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 = 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}"`
console.log(`Exporting ${video} as single frames...`)
try { try {
await exec(cmd) await exec(cmd);
} catch (err) { }
console.error('Error exporting video', err) catch (err) {
return process.exit(3) log('Error exporting video', err);
return process.exit(3);
}
return 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) {
let frames;
let frameCmd;
let framePath;
try {
frames = await 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 = 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);
}
} }
return path.join(TMPPATH, `export-%05d_${order}`)
} }
/** /**
* Re-arranges the frames into the order specified in the pattern. * Re-arranges the frames into the order specified in the pattern.
@ -137,48 +196,55 @@ async function frames (video, order, avconv) {
* *
* @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
console.log('Weaving frames...')
try { try {
frames = await fs.readdir(TMPPATH) frames = await readdir(TMPPATH);
} catch (err) { }
console.error('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) {
log('Sorting frames randomly...');
try { try {
seq = await randomSort(frames, realtime) seq = await randomSort(frames, pattern, realtime);
} catch (err) {
console.error('Error sorting frames')
} }
} else if (!alt) { catch (err) {
try { log('Error sorting frames', err);
seq = await standardSort(frames, pattern, realtime)
} catch (err) {
console.error('Error sorting frames')
} }
} else if (alt) { }
else if (!alt) {
log('Sorting frames normally...');
try { try {
seq = await altSort(frames, pattern, realtime) seq = await standardSort(frames, pattern, realtime);
} catch (err) { }
console.error('Error sorting frames') 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)
log('Sorting frames with alternate pattern...');
try {
seq = await altSort(frames, pattern, realtime);
}
catch (err) {
log('Error sorting frames', err);
} }
} }
//console.dir(seq) //console.dir(seq)
@ -191,42 +257,74 @@ async function weave (pattern, realtime, random) {
* @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 loops = 0;
let oldPath let patternIndexes = [];
let newName let frameCount = 0;
let newPath let skipCount;
let ext = path.extname(list[0]) let skip;
let oldName;
for (let g of pattern) { let oldPath;
groups.push([]) let newName;
let newPath;
let ext = extname(list[0]);
let x;
let i;
for (x = 0; x < pattern.length; x++) {
groups.push([]);
for (let i = 0; i < pattern[x]; i++) {
patternIndexes.push(x);
} }
for (let i = 0; i < list.length; i++) {
groups[i % pattern.length].push(list[i])
} }
for (let x = 0; x < list.length; x++) { for (i = 0; i < list.length; i++) {
for (let g of pattern) { groups[i % pattern.length].push(list[i]);
for (let i = 0; i < g; i++) { }
loops = Math.ceil(list.length / patternIndexes.length);
/*oldPath = path.join(TMPPATH, list[i]); if (realtime) {
newName = `./render_${zeroPad(frameCount)}${ext}`; skip = false;
newPath = path.join(TMPPATH, newName); skipCount = patternIndexes.length + 1;
}
console.log(`Renaming ${list[i]} -> ${newName}`); for (x = 0; x < loops; x++) {
for (i = 0; i < patternIndexes.length; i++) {
if (realtime) {
skipCount--;
if (skipCount === 0) {
skip = !skip;
skipCount = pattern.length;
}
}
if (typeof groups[patternIndexes[i]][0] === 'undefined') {
continue;
}
oldName = String(groups[patternIndexes[i]][0]);
oldPath = join(TMPPATH, oldName);
groups[patternIndexes[i]].shift();
if (skip) {
log(`Skipping ${oldName}`);
try { try {
//await fs.move(oldPath, newPath, { overwrite: true }) await unlink(oldPath);
}
catch (err) {
log('Error deleting frame', err);
}
continue;
}
newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = join(TMPPATH, newName);
log(`Renaming ${oldName} -> ${newName}`);
try {
await move(oldPath, newPath);
newList.push(newName); newList.push(newName);
} catch (err) { frameCount++;
console.error(err); }
}*/ catch (err) {
log('Error renaming frame', err);
frameCount++ return process.exit(10);
} }
} }
} }
return newList return newList;
} }
/** /**
* Standard frame sorting method. * Standard frame sorting method.
@ -236,60 +334,53 @@ async function altSort (list, pattern, realtime) {
* @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 = 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 = join(TMPPATH, list[i]);
oldPath = path.join(TMPPATH, list[i])
if (skip) { if (skip) {
console.log(`Skipping ${list[i]}`) log(`Skipping ${list[i]}`);
try { try {
await fs.unlink(oldPath) await unlink(oldPath);
} catch (err) {
console.error(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}`;
console.log(`Renaming ${list[i]} -> ${newName}`) newPath = join(TMPPATH, newName);
log(`Renaming ${list[i]} -> ${newName}`);
try { try {
await fs.move(oldPath, newPath) await move(oldPath, newPath);
newList.push(newName) newList.push(newName);
frameCount++ frameCount++;
} catch (err) {
console.error(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.
@ -299,82 +390,120 @@ async function standardSort (list, pattern, realtime) {
* @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 = 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...`);
console.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 = join(TMPPATH, remove[i]);
console.log(`Skipping ${list[i]}`) log(`Skipping ${list[i]}`);
try { try {
await fs.unlink(oldPath) await unlink(oldPath);
} catch (err) { }
console.error(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 = join(TMPPATH, list[i]);
newName = `./render_${zeroPad(frameCount)}${ext}`;
newName = `./render_${zeroPad(frameCount)}${ext}` newPath = join(TMPPATH, newName);
newPath = path.join(TMPPATH, newName) log(`Renaming ${list[i]} -> ${newName}`);
console.log(`Renaming ${list[i]} -> ${newName}`)
try { try {
await fs.move(oldPath, newPath) await move(oldPath, newPath);
newList.push(newName) newList.push(newName);
} catch (err) { }
console.error(err) catch (err) {
log('Error moving frame', err);
}
frameCount++;
}
return newList;
}
async function spinFrames() {
let frames;
let framePath;
let cmd;
let flip;
let flop;
let rotate;
console.log('Spinning frames...');
try {
frames = await readdir(TMPPATH);
}
catch (err) {
console.error('Error reading tmp directory', err);
}
//console.dir(frames)
frames = frames.filter(file => {
if (file.indexOf('.tif') !== -1)
return true;
});
for (let frame of frames) {
framePath = join(TMPPATH, frame);
rotate = '';
flip = '';
flop = '';
if (randomInt(0, 1) === 1) {
rotate = '-rotate 180 ';
}
if (randomInt(0, 1) === 1) {
flip = '-flip ';
}
if (randomInt(0, 1) === 1) {
flop = '-flop ';
}
if (flip === '' && flop === '' && rotate === '') {
//skip unrotated, unflipped and unflopped frames
continue;
}
cmd = `convert ${framePath} ${rotate}${flip}${flop} ${framePath}`;
console.log(cmd);
try {
await exec(cmd);
}
catch (err) {
console.error(err);
process.exit(10);
} }
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 = 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`;
console.log(`Exporting video ${output}`) const cmd = `${exe} -r ${framerate} -f image2 -s ${resolution} -i ${frames} ${format} -y ${output}`;
console.log(cmd) log(`Exporting video ${output}`);
log(cmd);
/*try {
await exec(`ls "${TMPPATH}"`)
} catch (err) {
console.log(err)
}*/
try { try {
await exec(cmd) await exec(cmd);
} catch (err) { }
console.error(err) catch (err) {
log('Error rendering video with ffmpeg', err);
} }
} }
/** /**
@ -383,96 +512,130 @@ 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(program) {
let input = arg.input.split(':') const arg = program.opts();
let output = arg.output let input = arg.input.split(':');
let pattern = [] let output = arg.output;
let realtime = false let pattern = [];
let avconv = false let realtime = false;
let random = false let avconv = false;
console.time('frameloom') let random = false;
let e = false;
let exe = arg.avconv ? 'avconv' : 'ffmpeg';
let fileExists;
console.time('frameloom');
if (input.length < 2) { if (input.length < 2) {
console.error('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) {
console.error('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) {
e = arg.exec;
}
if (arg.quiet) {
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 { }
else {
for (let i = 0; i < input.length; i++) { for (let i = 0; i < input.length; i++) {
pattern.push(1); pattern.push(1);
} }
} }
if (arg.realtime) realtime = true;
TMPPATH = path.join(TMPDIR, 'frameloom');
try { try {
await clear() fileExists = await exec(`which ${exe}`);
} catch (err) {
console.error(err)
return process.exit(3)
} }
catch (err) {
console.log(`Processing video files ${input.join(', ')} into ${output} with pattern ${pattern.join(':')}`) log(`Error checking for ${exe}`);
process.exit(11);
}
if (!fileExists || fileExists === '' || fileExists.indexOf(exe) === -1) {
log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`);
process.exit(12);
}
if (pattern.length !== input.length) {
log(`Number of inputs (${input.length}) doesn't match the pattern length (${pattern.length})`);
process.exit(10);
}
if (arg.realtime)
realtime = true;
TMPPATH = 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++) { for (let i = 0; i < input.length; i++) {
try { try {
await frames(input[i], i, avconv) await frames(input[i], i, avconv);
} catch (err) { }
console.error(err) catch (err) {
return process.exit(4) log('Error exporting video fie to image sequence', err);
return process.exit(4);
} }
} }
try { try {
await weave(pattern, realtime, random) await weave(pattern, realtime, random);
} catch (err) {
console.error(err)
return process.exit(5)
} }
catch (err) {
log('Error weaving', err);
return process.exit(5);
}
if (arg.spin) {
try { try {
await render(output, avconv) await spinFrames();
} catch (err) {
console.error(err)
return process.exit(6)
} }
catch (err) {
log('Error spinning', err);
return process.exit(13);
}
}
if (e) {
try { try {
await clear() await subExec(e);
} catch (err) {
console.error(err)
return process.exit(7)
} }
catch (err) {
console.timeEnd('frameloom') 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 program
.version('1.0.0') .version(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')
@ -480,6 +643,8 @@ program
.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')
.parse(process.argv) .option('-s, --spin', 'Randomly rotate frames before rendering')
.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')
main(program) .option('-q, --quiet', 'Suppresses all log messages')
.parse(process.argv);
main(program);

BIN
img/frameloom.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 KiB

5951
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,35 @@
{ {
"name": "frameloom", "name": "frameloom",
"version": "1.0.0", "version": "1.0.3",
"description": "Node script to generate flicker videos by interweaving frames from multiple videos", "description": "Node script to generate flicker videos by interweaving frames from multiple videos",
"main": "frameloom", "main": "frameloom",
"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",
"compile": "./node_modules/.bin/tsc src/frameloom.ts --outFile ./frameloom --noImplicitAny --lib ES2017 -t ES2017 --moduleResolution Node",
"build": "node build.js",
"docs": "sh ./scripts/docs.sh",
"examples": "sh ./scripts/examples.sh",
"examples:youtube": "sh ./scripts/examples_youtube.sh",
"examples:assemble": "sh ./scripts/examples_assemble.sh"
}, },
"author": "sixteenmillimeter", "author": "sixteenmillimeter",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"commander": "^2.19.0", "commander": "^7.2.0",
"fs-extra": "^7.0.1" "fs-extra": "^9.1.0"
}, },
"devDependencies": { "devDependencies": {
"jsdoc-to-markdown": "^4.0.1", "@types/node": "^14.14.36",
"pkg": "^4.3.5", "jsdoc-to-markdown": "^7.0.1",
"qunit": "^2.8.0" "pkg": "^4.5.1",
"qunit": "^2.14.1",
"typescript": "^4.2.3"
},
"pkg": {
"scripts": [
"./frameloom",
"./lib/**/*"
]
} }
} }

13
scripts/square_gif.sh Normal file
View File

@ -0,0 +1,13 @@
#!/bin/bash
TMP_CROP=$(mktemp)
TMP_GIF=$(mktemp)
TMP_PALETTE=$(mktemp)
echo "Generating square gif of ${1} as ${2}x${2}"
ffmpeg -y -i "$1" -c:v prores_ks -profile:v 3 -filter:v "scale=1920:1080:force_original_aspect_ratio=decrease,crop=1080:1080:420:0" "$TMP_CROP.mov"
ffmpeg -y -i "$TMP_CROP.mov" -c:v prores_ks -profile:v 3 -vf scale=$2:$2 "$TMP_GIF.mov"
ffmpeg -y -i "$TMP_GIF.mov" -vf palettegen "$TMP_PALETTE.png"
ffmpeg -y -i "$TMP_GIF.mov" -i "$TMP_PALETTE.png" -filter_complex paletteuse -f gif "square_${2}.gif"
echo "Generated square_${2}.gif"

700
src/frameloom.ts Executable file
View File

@ -0,0 +1,700 @@
#!/usr/bin/env node
'use strict'
const execRaw = require('child_process').exec
const { tmpdir } = require('os')
const { join, extname } = require('path')
const program = require('commander')
const { move, exists, unlink, readdir, mkdir } = require('fs-extra')
const { version } = 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 = 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) : Promise<string> {
return new Promise((resolve : any, reject : any) => {
return execRaw(cmd, { maxBuffer : 500 * 1024 * 1024}, (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) : Promise<any> {
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) : boolean {
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
}
}
function randomInt (min : number, max : number) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Clears the temporary directory of all files.
* Establishes a directory if none exists.
**/
async function clear () {
let cmd : string = `rm -r "${TMPPATH}"`
let dirExists : boolean
try {
dirExists = await exists(TMPPATH)
} catch (err) {
log('Error checking if file exists', err)
}
if (dirExists) {
log(`Clearing temp directory "${TMPPATH}"`)
try {
await exec(cmd)
} catch (err) {
//suppress error
console.dir(err)
}
}
try {
await 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) : Promise<string> {
let ext : string = 'tif'
let exe : string = avconv ? 'avconv' : 'ffmpeg'
let tmpoutput : string
let cmd : string
tmpoutput = 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 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 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 = 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 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){
log('Sorting frames randomly...')
try {
seq = await randomSort(frames, pattern, realtime)
} catch (err) {
log('Error sorting frames', err)
}
} else if (!alt) {
log('Sorting frames normally...')
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)
log('Sorting frames with alternate pattern...')
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 loops : number = 0
let patternIndexes : number[] = []
let frameCount : number = 0
let skipCount : number
let skip : boolean
let oldName : string
let oldPath : string
let newName : string
let newPath : string
let ext : string = extname(list[0])
let x : number
let i : number
for (x = 0; x < pattern.length; x++) {
groups.push([])
for (let i : number = 0; i < pattern[x]; i++) {
patternIndexes.push(x)
}
}
for (i = 0; i < list.length; i++) {
groups[i % pattern.length].push(list[i])
}
loops = Math.ceil(list.length / patternIndexes.length)
if (realtime) {
skip = false
skipCount = patternIndexes.length + 1
}
for (x = 0; x < loops; x++) {
for (i = 0; i < patternIndexes.length; i++) {
if (realtime) {
skipCount--;
if (skipCount === 0) {
skip = !skip;
skipCount = pattern.length
}
}
if (typeof groups[patternIndexes[i]][0] === 'undefined') {
continue
}
oldName = String(groups[patternIndexes[i]][0])
oldPath = join(TMPPATH, oldName)
groups[patternIndexes[i]].shift()
if (skip) {
log(`Skipping ${oldName}`)
try {
await unlink(oldPath)
} catch (err) {
log('Error deleting frame', err)
}
continue
}
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = join(TMPPATH, newName)
log(`Renaming ${oldName} -> ${newName}`)
try {
await move(oldPath, newPath)
newList.push(newName)
frameCount++
} catch (err) {
log('Error renaming frame', err)
return process.exit(10)
}
}
}
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 = 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 = join(TMPPATH, list[i])
if (skip) {
log(`Skipping ${list[i]}`)
try {
await unlink(oldPath)
} catch (err) {
log('Error deleting frame', err)
}
continue
}
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${newName}`)
try {
await 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 = 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 = join(TMPPATH, remove[i])
log(`Skipping ${list[i]}`)
try {
await unlink(oldPath)
} catch (err) {
log('Error deleting frame', err)
}
}
}
for (let i : number = 0; i < list.length; i++) {
oldPath = join(TMPPATH, list[i])
newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${newName}`)
try {
await move(oldPath, newPath)
newList.push(newName)
} catch (err) {
log('Error moving frame', err)
}
frameCount++
}
return newList
}
async function spinFrames () {
let frames : string[]
let framePath : string
let cmd : string
let flip : string
let flop : string
let rotate : string
console.log('Spinning frames...')
try {
frames = await readdir(TMPPATH)
} catch (err) {
console.error('Error reading tmp directory', err)
}
//console.dir(frames)
frames = frames.filter (file =>{
if (file.indexOf('.tif') !== -1) return true
})
for (let frame of frames) {
framePath = join(TMPPATH, frame)
rotate = ''
flip = ''
flop = ''
if (randomInt(0, 1) === 1) {
rotate = '-rotate 180 '
}
if (randomInt(0, 1) === 1) {
flip = '-flip '
}
if (randomInt(0, 1) === 1) {
flop = '-flop '
}
if (flip === '' && flop === '' && rotate === '') {
//skip unrotated, unflipped and unflopped frames
continue
}
cmd = `convert ${framePath} ${rotate}${flip}${flop} ${framePath}`
console.log(cmd)
try {
await exec(cmd)
} catch (err) {
console.error(err)
process.exit(10)
}
}
}
/**
* 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 = 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(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 (program : any) {
const arg = program.opts();
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
let exe : string = arg.avconv ? 'avconv' : 'ffmpeg'
let fileExists : any
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);
}
}
try {
fileExists = await exec(`which ${exe}`)
} catch (err) {
log(`Error checking for ${exe}`)
process.exit(11)
}
if (!fileExists || fileExists === '' || fileExists.indexOf(exe) === -1) {
log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`)
process.exit(12)
}
if (pattern.length !== input.length) {
log(`Number of inputs (${input.length}) doesn't match the pattern length (${pattern.length})`)
process.exit(10)
}
if (arg.realtime) realtime = true;
TMPPATH = 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)
}
}
try {
await weave(pattern, realtime, random)
} catch (err) {
log('Error weaving', err)
return process.exit(5)
}
if (arg.spin) {
try {
await spinFrames()
} catch (err) {
log('Error spinning', err)
return process.exit(13)
}
}
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(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('-s, --spin', 'Randomly rotate frames before rendering')
.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)