Compare commits

...

11 Commits
v1.0.3 ... main

7 changed files with 4082 additions and 2014 deletions

View File

@ -4,6 +4,8 @@ Node script to generate flicker videos by interweaving frames from multiple vide
-------- --------
## 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 +23,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,11 +43,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
``` ```
## TODO ## License
* Generate example videos automatically Copyright 2018-2021 M McWilliams
* Publish example videos
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

@ -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>
@ -15,15 +19,18 @@
<dd><p>Shuffles an array into a random state.</p> <dd><p>Shuffles an array into a random state.</p>
</dd> </dd>
<dt><a href="#clear">clear()</a></dt> <dt><a href="#clear">clear()</a></dt>
<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>
@ -97,13 +111,13 @@ Shuffles an array into a random state.
<a name="clear"></a> <a name="clear"></a>
## clear() ## clear()
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.
**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>

View File

@ -1,15 +1,15 @@
#!/usr/bin/env node #!/usr/bin/env node
'use strict'; 'use strict';
const execRaw = require('child_process').exec; const execRaw = require('child_process').exec;
const os = require('os'); const { tmpdir } = require('os');
const path = require('path'); const { join, extname } = require('path');
const program = require('commander'); const program = require('commander');
const fs = require('fs-extra'); const { move, exists, unlink, readdir, mkdir } = require('fs-extra');
const pkg = require('./package.json'); const { version } = require('./package.json');
const OUTPUT_RE = new RegExp('{{o}}', 'g'); const OUTPUT_RE = new RegExp('{{o}}', 'g');
const INPUT_RE = new RegExp('{{i}}', 'g'); const INPUT_RE = new RegExp('{{i}}', 'g');
let QUIET = false; let QUIET = false;
let TMPDIR = os.tmpdir() || '/tmp'; let TMPDIR = tmpdir() || '/tmp';
let TMPPATH; let TMPPATH;
/** /**
* Shells out to execute a command with async/await. * Shells out to execute a command with async/await.
@ -97,14 +97,14 @@ function randomInt(min, max) {
**/ **/
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) { catch (err) {
log('Error checking if file exists', err); log('Error checking if file exists', err);
} }
if (exists) { if (dirExists) {
log(`Clearing temp directory "${TMPPATH}"`); log(`Clearing temp directory "${TMPPATH}"`);
try { try {
await exec(cmd); await exec(cmd);
@ -115,7 +115,7 @@ async function clear() {
} }
} }
try { try {
await fs.mkdir(TMPPATH); await mkdir(TMPPATH);
} }
catch (err) { catch (err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
@ -140,7 +140,7 @@ async function frames(video, order, avconv) {
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 = join(TMPPATH, `export-%05d_${order}.${ext}`);
cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`; cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`;
log(`Exporting ${video} as single frames...`); log(`Exporting ${video} as single frames...`);
try { try {
@ -150,7 +150,7 @@ async function frames(video, order, avconv) {
log('Error exporting video', err); log('Error exporting video', err);
return process.exit(3); return process.exit(3);
} }
return path.join(TMPPATH, `export-%05d_${order}`); return 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
@ -163,7 +163,7 @@ async function subExec(cmd) {
let frameCmd; let frameCmd;
let framePath; let framePath;
try { try {
frames = await fs.readdir(TMPPATH); frames = await readdir(TMPPATH);
} }
catch (err) { catch (err) {
log('Error reading tmp directory', err); log('Error reading tmp directory', err);
@ -173,7 +173,7 @@ async function subExec(cmd) {
return true; return true;
}); });
for (let frame of frames) { for (let frame of frames) {
framePath = path.join(TMPPATH, frame); framePath = 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);
@ -204,7 +204,7 @@ async function weave(pattern, realtime, random) {
let alt = false; let alt = false;
log('Weaving frames...'); log('Weaving frames...');
try { try {
frames = await fs.readdir(TMPPATH); frames = await readdir(TMPPATH);
} }
catch (err) { catch (err) {
log('Error reading tmp directory', err); log('Error reading tmp directory', err);
@ -268,7 +268,7 @@ async function altSort(list, pattern, realtime) {
let oldPath; let oldPath;
let newName; let newName;
let newPath; let newPath;
let ext = path.extname(list[0]); let ext = extname(list[0]);
let x; let x;
let i; let i;
for (x = 0; x < pattern.length; x++) { for (x = 0; x < pattern.length; x++) {
@ -298,12 +298,12 @@ async function altSort(list, pattern, realtime) {
continue; continue;
} }
oldName = String(groups[patternIndexes[i]][0]); oldName = String(groups[patternIndexes[i]][0]);
oldPath = path.join(TMPPATH, oldName); oldPath = join(TMPPATH, oldName);
groups[patternIndexes[i]].shift(); groups[patternIndexes[i]].shift();
if (skip) { if (skip) {
log(`Skipping ${oldName}`); log(`Skipping ${oldName}`);
try { try {
await fs.unlink(oldPath); await unlink(oldPath);
} }
catch (err) { catch (err) {
log('Error deleting frame', err); log('Error deleting frame', err);
@ -311,10 +311,10 @@ async function altSort(list, pattern, realtime) {
continue; continue;
} }
newName = `./render_${zeroPad(frameCount)}${ext}`; newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = path.join(TMPPATH, newName); newPath = join(TMPPATH, newName);
log(`Renaming ${oldName} -> ${newName}`); log(`Renaming ${oldName} -> ${newName}`);
try { try {
await fs.move(oldPath, newPath); await move(oldPath, newPath);
newList.push(newName); newList.push(newName);
frameCount++; frameCount++;
} }
@ -339,7 +339,7 @@ async function standardSort(list, pattern, realtime) {
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;
@ -356,11 +356,11 @@ async function standardSort(list, pattern, realtime) {
skipCount = pattern.length; skipCount = pattern.length;
} }
} }
oldPath = path.join(TMPPATH, list[i]); oldPath = join(TMPPATH, list[i]);
if (skip) { if (skip) {
log(`Skipping ${list[i]}`); log(`Skipping ${list[i]}`);
try { try {
await fs.unlink(oldPath); await unlink(oldPath);
} }
catch (err) { catch (err) {
log('Error deleting frame', err); log('Error deleting frame', err);
@ -368,10 +368,10 @@ async function standardSort(list, pattern, realtime) {
continue; continue;
} }
newName = `./render_${zeroPad(frameCount)}${ext}`; newName = `./render_${zeroPad(frameCount)}${ext}`;
newPath = path.join(TMPPATH, newName); newPath = join(TMPPATH, newName);
log(`Renaming ${list[i]} -> ${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++;
} }
@ -391,7 +391,7 @@ async function standardSort(list, pattern, realtime) {
**/ **/
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;
@ -405,10 +405,10 @@ async function randomSort(list, pattern, realtime) {
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 = join(TMPPATH, remove[i]);
log(`Skipping ${list[i]}`); log(`Skipping ${list[i]}`);
try { try {
await fs.unlink(oldPath); await unlink(oldPath);
} }
catch (err) { catch (err) {
log('Error deleting frame', err); log('Error deleting frame', err);
@ -416,12 +416,12 @@ async function randomSort(list, pattern, realtime) {
} }
} }
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 = path.join(TMPPATH, newName); newPath = join(TMPPATH, newName);
log(`Renaming ${list[i]} -> ${newName}`); log(`Renaming ${list[i]} -> ${newName}`);
try { try {
await fs.move(oldPath, newPath); await move(oldPath, newPath);
newList.push(newName); newList.push(newName);
} }
catch (err) { catch (err) {
@ -440,7 +440,7 @@ async function spinFrames() {
let rotate; let rotate;
console.log('Spinning frames...'); console.log('Spinning frames...');
try { try {
frames = await fs.readdir(TMPPATH); frames = await readdir(TMPPATH);
} }
catch (err) { catch (err) {
console.error('Error reading tmp directory', err); console.error('Error reading tmp directory', err);
@ -451,7 +451,7 @@ async function spinFrames() {
return true; return true;
}); });
for (let frame of frames) { for (let frame of frames) {
framePath = path.join(TMPPATH, frame); framePath = join(TMPPATH, frame);
rotate = ''; rotate = '';
flip = ''; flip = '';
flop = ''; flop = '';
@ -487,7 +487,7 @@ async function spinFrames() {
**/ **/
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'; //TODO: make variable/argument let resolution = '1920x1080'; //TODO: make variable/argument
//TODO: make object configurable with shorthand names //TODO: make object configurable with shorthand names
@ -512,7 +512,8 @@ 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) {
const arg = program.opts();
let input = arg.input.split(':'); let input = arg.input.split(':');
let output = arg.output; let output = arg.output;
let pattern = []; let pattern = [];
@ -521,7 +522,7 @@ async function main(arg) {
let random = false; let random = false;
let e = false; let e = false;
let exe = arg.avconv ? 'avconv' : 'ffmpeg'; let exe = arg.avconv ? 'avconv' : 'ffmpeg';
let exists; let fileExists;
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', {});
@ -558,13 +559,13 @@ async function main(arg) {
} }
} }
try { try {
exists = await exec(`which ${exe}`); fileExists = await exec(`which ${exe}`);
} }
catch (err) { catch (err) {
log(`Error checking for ${exe}`); log(`Error checking for ${exe}`);
process.exit(11); process.exit(11);
} }
if (!exists || exists === '' || exists.indexOf(exe) === -1) { if (!fileExists || fileExists === '' || fileExists.indexOf(exe) === -1) {
log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`); log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`);
process.exit(12); process.exit(12);
} }
@ -574,7 +575,7 @@ async function main(arg) {
} }
if (arg.realtime) if (arg.realtime)
realtime = true; realtime = true;
TMPPATH = path.join(TMPDIR, 'frameloom'); TMPPATH = join(TMPDIR, 'frameloom');
try { try {
await clear(); await clear();
} }
@ -634,7 +635,7 @@ async function main(arg) {
console.timeEnd('frameloom'); console.timeEnd('frameloom');
} }
program program
.version(pkg.version) .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')

View File

@ -1,35 +0,0 @@
#!/bin/sh
# Simple shell script version of frameloom
# Only creates a 1:1 pattern between 2 videos
# Usage : sh frameloom.sh examples/A.mp4 examples/B.mp4 examples/OUTPUT.mp4
TMPDIR=/tmp/frameloom/
i=0
if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ] ;
then
echo "Not enough arguments supplied"
fi
mkdir -p $TMPDIR
#relies on the alphanumeric sorting that occurs when getting
#the list of files in the for loop below
ffmpeg -i "$1" -compression_algo raw -pix_fmt rgb24 "${TMPDIR}export-%05d_a.tif"
ffmpeg -i "$2" -compression_algo raw -pix_fmt rgb24 "${TMPDIR}export-%05d_b.tif"
#rm -r $TMP
for filename in ${TMPDIR}*.tif; do
value=`printf %05d $i`
#echo $filename
#echo "${TMPDIR}render_${value}.tif"
mv "$filename" "${TMPDIR}render_${value}.tif"
i=`expr $i + 1`
done
ffmpeg -r 30 -f image2 -s 1920x1080 -i "${TMPDIR}render_%05d.tif" -c:v prores_ks -profile:v 3 -y "$3"
rm -r $TMPDIR

5811
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -16,15 +16,15 @@
"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": {
"@types/node": "^11.13.0", "@types/node": "^14.14.36",
"jsdoc-to-markdown": "^4.0.1", "jsdoc-to-markdown": "^7.0.1",
"pkg": "^4.4.0", "pkg": "^4.5.1",
"qunit": "^2.8.0", "qunit": "^2.14.1",
"typescript": "^3.4.1" "typescript": "^4.2.3"
}, },
"pkg": { "pkg": {
"scripts": [ "scripts": [

View File

@ -3,18 +3,18 @@
'use strict' 'use strict'
const execRaw = require('child_process').exec const execRaw = require('child_process').exec
const os = require('os') const { tmpdir } = require('os')
const path = require('path') const { join, extname } = require('path')
const program = require('commander') const program = require('commander')
const fs = require('fs-extra') const { move, exists, unlink, readdir, mkdir } = require('fs-extra')
const pkg : any = require('./package.json') const { version } = require('./package.json')
const OUTPUT_RE : RegExp = new RegExp('{{o}}', 'g') const OUTPUT_RE : RegExp = new RegExp('{{o}}', 'g')
const INPUT_RE : RegExp = new RegExp('{{i}}', 'g') const INPUT_RE : RegExp = new RegExp('{{i}}', 'g')
let QUIET : boolean = false let QUIET : boolean = false
let TMPDIR : string = os.tmpdir() || '/tmp' let TMPDIR : string = tmpdir() || '/tmp'
let TMPPATH : string let TMPPATH : string
/** /**
@ -25,7 +25,7 @@ let TMPPATH : string
* *
* @returns {Promise} Promise containing the complete stdio * @returns {Promise} Promise containing the complete stdio
**/ **/
async function exec (cmd : string) { async function exec (cmd : string) : Promise<string> {
return new Promise((resolve : any, reject : any) => { return new Promise((resolve : any, reject : any) => {
return execRaw(cmd, { maxBuffer : 500 * 1024 * 1024}, (err : any, stdio : string, stderr : string) => { return execRaw(cmd, { maxBuffer : 500 * 1024 * 1024}, (err : any, stdio : string, stderr : string) => {
if (err) return reject(err) if (err) return reject(err)
@ -40,7 +40,7 @@ async function exec (cmd : string) {
* *
* @returns {Promise} Promise that resolves after set time * @returns {Promise} Promise that resolves after set time
**/ **/
async function delay (ms : number) { async function delay (ms : number) : Promise<any> {
return new Promise((resolve : any, reject : any) =>{ return new Promise((resolve : any, reject : any) =>{
return setTimeout(resolve, ms) return setTimeout(resolve, ms)
}) })
@ -49,7 +49,7 @@ async function delay (ms : number) {
* Log function wrapper that can silences logs when * Log function wrapper that can silences logs when
* QUIET == true * QUIET == true
*/ */
function log (msg : string, err : any = false) { function log (msg : string, err : any = false) : boolean {
if (QUIET) return false if (QUIET) return false
if (err) { if (err) {
console.error(msg, err) console.error(msg, err)
@ -102,15 +102,15 @@ function randomInt (min : number, max : number) {
**/ **/
async function clear () { async function clear () {
let cmd : string = `rm -r "${TMPPATH}"` let cmd : string = `rm -r "${TMPPATH}"`
let exists : boolean let dirExists : boolean
try { try {
exists = await fs.exists(TMPPATH) dirExists = await exists(TMPPATH)
} catch (err) { } catch (err) {
log('Error checking if file exists', err) log('Error checking if file exists', err)
} }
if (exists) { if (dirExists) {
log(`Clearing temp directory "${TMPPATH}"`) log(`Clearing temp directory "${TMPPATH}"`)
try { try {
await exec(cmd) await exec(cmd)
@ -121,7 +121,7 @@ async function clear () {
} }
try { try {
await fs.mkdir(TMPPATH) await mkdir(TMPPATH)
} catch (err) { } catch (err) {
if (err.code !== 'EEXIST') { if (err.code !== 'EEXIST') {
log('Error making directory', err) log('Error making directory', err)
@ -141,13 +141,13 @@ async function clear () {
* *
* @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 : string, order : number, avconv : boolean) { async function frames (video : string, order : number, avconv : boolean) : Promise<string> {
let ext : string = 'tif' let ext : string = 'tif'
let exe : string = avconv ? 'avconv' : 'ffmpeg' let exe : string = avconv ? 'avconv' : 'ffmpeg'
let tmpoutput : string let tmpoutput : string
let cmd : string let cmd : string
tmpoutput = path.join(TMPPATH, `export-%05d_${order}.${ext}`) tmpoutput = join(TMPPATH, `export-%05d_${order}.${ext}`)
cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"` cmd = `${exe} -i "${video}" -compression_algo raw -pix_fmt rgb24 "${tmpoutput}"`
@ -160,7 +160,7 @@ async function frames (video : string, order : number, avconv : boolean) {
return process.exit(3) return process.exit(3)
} }
return path.join(TMPPATH, `export-%05d_${order}`) return 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
@ -174,7 +174,7 @@ async function subExec (cmd : string) {
let framePath : string let framePath : string
try { try {
frames = await fs.readdir(TMPPATH) frames = await readdir(TMPPATH)
} catch (err) { } catch (err) {
log('Error reading tmp directory', err) log('Error reading tmp directory', err)
} }
@ -184,7 +184,7 @@ async function subExec (cmd : string) {
}) })
for (let frame of frames) { for (let frame of frames) {
framePath = path.join(TMPPATH, frame) framePath = 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)
@ -216,7 +216,7 @@ async function weave (pattern : number[], realtime : boolean, random : boolean)
log('Weaving frames...') log('Weaving frames...')
try { try {
frames = await fs.readdir(TMPPATH) frames = await readdir(TMPPATH)
} catch (err) { } catch (err) {
log('Error reading tmp directory', err) log('Error reading tmp directory', err)
} }
@ -275,7 +275,7 @@ async function altSort (list : string[], pattern : number[], realtime : boolean)
let oldPath : string let oldPath : string
let newName : string let newName : string
let newPath : string let newPath : string
let ext : string = path.extname(list[0]) let ext : string = extname(list[0])
let x : number let x : number
let i : number let i : number
@ -313,14 +313,14 @@ async function altSort (list : string[], pattern : number[], realtime : boolean)
} }
oldName = String(groups[patternIndexes[i]][0]) oldName = String(groups[patternIndexes[i]][0])
oldPath = path.join(TMPPATH, oldName) oldPath = join(TMPPATH, oldName)
groups[patternIndexes[i]].shift() groups[patternIndexes[i]].shift()
if (skip) { if (skip) {
log(`Skipping ${oldName}`) log(`Skipping ${oldName}`)
try { try {
await fs.unlink(oldPath) await unlink(oldPath)
} catch (err) { } catch (err) {
log('Error deleting frame', err) log('Error deleting frame', err)
} }
@ -328,11 +328,11 @@ async function altSort (list : string[], pattern : number[], realtime : boolean)
} }
newName = `./render_${zeroPad(frameCount)}${ext}` newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName) newPath = join(TMPPATH, newName)
log(`Renaming ${oldName} -> ${newName}`) log(`Renaming ${oldName} -> ${newName}`)
try { try {
await fs.move(oldPath, newPath) await move(oldPath, newPath)
newList.push(newName) newList.push(newName)
frameCount++ frameCount++
} catch (err) { } catch (err) {
@ -357,7 +357,7 @@ async function standardSort (list : string[], pattern : number[], realtime : boo
let step : any let step : any
let skipCount : number let skipCount : number
let skip : boolean let skip : boolean
let ext : string = path.extname(list[0]) let ext : string = extname(list[0])
let oldPath : string let oldPath : string
let newName : string let newName : string
let newPath : string let newPath : string
@ -377,12 +377,12 @@ async function standardSort (list : string[], pattern : number[], realtime : boo
} }
} }
oldPath = path.join(TMPPATH, list[i]) oldPath = join(TMPPATH, list[i])
if (skip) { if (skip) {
log(`Skipping ${list[i]}`) log(`Skipping ${list[i]}`)
try { try {
await fs.unlink(oldPath) await unlink(oldPath)
} catch (err) { } catch (err) {
log('Error deleting frame', err) log('Error deleting frame', err)
} }
@ -390,11 +390,11 @@ async function standardSort (list : string[], pattern : number[], realtime : boo
} }
newName = `./render_${zeroPad(frameCount)}${ext}` newName = `./render_${zeroPad(frameCount)}${ext}`
newPath = path.join(TMPPATH, newName) newPath = join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${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) { } catch (err) {
@ -416,7 +416,7 @@ async function standardSort (list : string[], pattern : number[], realtime : boo
**/ **/
async function randomSort (list : string[], pattern : number[], realtime : boolean) { async function randomSort (list : string[], pattern : number[], realtime : boolean) {
let frameCount : number = 0 let frameCount : number = 0
let ext : string = path.extname(list[0]) let ext : string = extname(list[0])
let oldPath : string let oldPath : string
let newName : string let newName : string
let newPath : string let newPath : string
@ -433,10 +433,10 @@ async function randomSort (list : string[], pattern : number[], realtime : boole
log(`Skipping extra frames...`) log(`Skipping extra frames...`)
for (let i : number = 0; i < remove.length; i++) { for (let i : number = 0; i < remove.length; i++) {
oldPath = path.join(TMPPATH, remove[i]) oldPath = join(TMPPATH, remove[i])
log(`Skipping ${list[i]}`) log(`Skipping ${list[i]}`)
try { try {
await fs.unlink(oldPath) await unlink(oldPath)
} catch (err) { } catch (err) {
log('Error deleting frame', err) log('Error deleting frame', err)
} }
@ -444,14 +444,14 @@ async function randomSort (list : string[], pattern : number[], realtime : boole
} }
for (let i : number = 0; i < list.length; i++) { for (let i : number = 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 = path.join(TMPPATH, newName) newPath = join(TMPPATH, newName)
log(`Renaming ${list[i]} -> ${newName}`) log(`Renaming ${list[i]} -> ${newName}`)
try { try {
await fs.move(oldPath, newPath) await move(oldPath, newPath)
newList.push(newName) newList.push(newName)
} catch (err) { } catch (err) {
log('Error moving frame', err) log('Error moving frame', err)
@ -474,7 +474,7 @@ async function spinFrames () {
console.log('Spinning frames...') console.log('Spinning frames...')
try { try {
frames = await fs.readdir(TMPPATH) frames = await readdir(TMPPATH)
} catch (err) { } catch (err) {
console.error('Error reading tmp directory', err) console.error('Error reading tmp directory', err)
} }
@ -485,7 +485,7 @@ async function spinFrames () {
}) })
for (let frame of frames) { for (let frame of frames) {
framePath = path.join(TMPPATH, frame) framePath = join(TMPPATH, frame)
rotate = '' rotate = ''
flip = '' flip = ''
flop = '' flop = ''
@ -521,7 +521,7 @@ async function spinFrames () {
**/ **/
async function render (output : string, avconv : boolean) { async function render (output : string, avconv : boolean) {
//process.exit() //process.exit()
let frames : string = path.join(TMPPATH, `render_%05d.tif`) let frames : string = join(TMPPATH, `render_%05d.tif`)
let exe : string = avconv ? 'avconv' : 'ffmpeg' let exe : string = avconv ? 'avconv' : 'ffmpeg'
let resolution : string = '1920x1080' //TODO: make variable/argument let resolution : string = '1920x1080' //TODO: make variable/argument
//TODO: make object configurable with shorthand names //TODO: make object configurable with shorthand names
@ -547,7 +547,8 @@ async function render (output : string, avconv : boolean) {
* *
* @param {object} arg Object containing all arguments * @param {object} arg Object containing all arguments
**/ **/
async function main (arg : any) { async function main (program : any) {
const arg = program.opts();
let input : string[] = arg.input.split(':') let input : string[] = arg.input.split(':')
let output : string = arg.output let output : string = arg.output
let pattern : any[] = [] let pattern : any[] = []
@ -556,7 +557,7 @@ async function main (arg : any) {
let random : boolean = false let random : boolean = false
let e : any = false let e : any = false
let exe : string = arg.avconv ? 'avconv' : 'ffmpeg' let exe : string = arg.avconv ? 'avconv' : 'ffmpeg'
let exists : any let fileExists : any
console.time('frameloom') console.time('frameloom')
@ -602,13 +603,13 @@ async function main (arg : any) {
} }
try { try {
exists = await exec(`which ${exe}`) fileExists = await exec(`which ${exe}`)
} catch (err) { } catch (err) {
log(`Error checking for ${exe}`) log(`Error checking for ${exe}`)
process.exit(11) process.exit(11)
} }
if (!exists || exists === '' || exists.indexOf(exe) === -1) { if (!fileExists || fileExists === '' || fileExists.indexOf(exe) === -1) {
log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`) log(`${exe} is required and is not installed. Please install ${exe} to use frameloom.`)
process.exit(12) process.exit(12)
} }
@ -620,7 +621,7 @@ async function main (arg : any) {
if (arg.realtime) realtime = true; if (arg.realtime) realtime = true;
TMPPATH = path.join(TMPDIR, 'frameloom'); TMPPATH = join(TMPDIR, 'frameloom');
try { try {
await clear() await clear()
@ -683,7 +684,7 @@ async function main (arg : any) {
} }
program program
.version(pkg.version) .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')