Compare commits

...

32 Commits
v1.0.0 ... main

Author SHA1 Message Date
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
14 changed files with 5347 additions and 2397 deletions

5
.gitignore vendored
View File

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

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
This script relies on `ffmpeg` to export and stitch video back together
@ -21,13 +23,15 @@ chmod +x frameloom
## 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
Run `./frameloom -h` to display help screen.
```
```bash
Usage: frameloom [options]
Options:
@ -39,6 +43,19 @@ Options:
-t, --tmp [dir] Specify tmp directory for exporting frames
-a, --avconv Specify avconv if preferred to ffmpeg
-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.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 {
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`)

View File

@ -8,6 +8,10 @@
<dt><a href="#delay">delay(ms)</a><code>Promise</code></dt>
<dd><p>Delays process for specified amount of time in milliseconds.</p>
</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>
<dd><p>Pads a numerical value with preceding zeros to make strings same length.</p>
</dd>
@ -15,15 +19,18 @@
<dd><p>Shuffles an array into a random state.</p>
</dd>
<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>
</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
to keep frames in alternating order to be quickly stitched together
or re-sorted.</p>
</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.
Calls <code>patternSort()</code> to perform the rename and unlink actions</p>
</dd>
@ -36,7 +43,7 @@
<dt><a href="#randomSort">randomSort(list, pattern, realtime)</a></dt>
<dd><p>Ramdomly sort frames for re-stitching.</p>
</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>
<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 |
<a name="log"></a>
## log()
Log function wrapper that can silences logs when
QUIET == true
**Kind**: global function
<a name="zeroPad"></a>
## zeroPad(i, max) ⇒ <code>string</code>
@ -97,13 +111,13 @@ Shuffles an array into a random state.
<a name="clear"></a>
## clear()
Clears the temporary directory of all files.
Clears the temporary directory of all files.
Establishes a directory if none exists.
**Kind**: global function
<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
to keep frames in alternating order to be quickly stitched together
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 |
| 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>
## weave(pattern, realtime)
## weave(pattern, realtime, random)
Re-arranges the frames into the order specified in the pattern.
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 |
| 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>
@ -170,7 +197,7 @@ Ramdomly sort frames for re-stitching.
<a name="render"></a>
## render(output)
## render(output, avconv)
Render the frames into a video using ffmpeg.
**Kind**: global function
@ -178,6 +205,7 @@ Render the frames into a video using ffmpeg.
| Param | Type | Description |
| --- | --- | --- |
| 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>

955
frameloom Executable file → Normal file

File diff suppressed because it is too large Load Diff

5971
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,20 +1,35 @@
{
"name": "frameloom",
"version": "1.0.0",
"version": "1.0.3",
"description": "Node script to generate flicker videos by interweaving frames from multiple videos",
"main": "frameloom",
"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",
"license": "MIT",
"dependencies": {
"commander": "^2.19.0",
"fs-extra": "^7.0.1"
"commander": "^7.2.0",
"fs-extra": "^9.1.0"
},
"devDependencies": {
"jsdoc-to-markdown": "^4.0.1",
"pkg": "^4.3.5",
"qunit": "^2.8.0"
"@types/node": "^14.14.36",
"jsdoc-to-markdown": "^7.0.1",
"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)