Compare commits
32 Commits
Author | SHA1 | Date |
---|---|---|
Matt McWilliams | 43169174a9 | |
Matt McWilliams | 4dbdc4e3da | |
Matt McWilliams | 5da0bcec11 | |
Matt McWilliams | 1ceeaad33a | |
Matt McWilliams | 6c12d4961c | |
Matt McWilliams | f569a27e32 | |
Matt McWilliams | d2b27620ca | |
Matt McWilliams | 54057021a4 | |
Matt McWilliams | c0fcdd62da | |
Matt McWilliams | f4e799d647 | |
Matt McWilliams | 501bcf8603 | |
mmcwilliams | 22344e8343 | |
mmcwilliams | cbc7745f02 | |
mmcwilliams | 4e27edcfa9 | |
mmcwilliams | a501023f27 | |
mmcwilliams | aa42c8dc67 | |
mmcwilliams | 2b2b98eb01 | |
mmcwilliams | 3a561ad6c3 | |
mmcwilliams | 847e9644ce | |
mmcw-dev | 1ce8d323f6 | |
mmcwilliams | 3fa0d6a14b | |
mmcwilliams | e8c949daea | |
mmcwilliams | 3f1016d915 | |
mmcwilliams | 2cd09f1e05 | |
mmcwilliams | 9d8739cf9b | |
mmcwilliams | 6894861ff9 | |
mmcwilliams | cf39232d70 | |
mmcw-dev | b1370447bf | |
mmcw-dev | 4c1ac8f997 | |
mmcw-dev | 778f740047 | |
mmcw-dev | 53c8b4c82d | |
mmcw-dev | f9f50159c3 |
|
@ -1,3 +1,6 @@
|
|||
node_modules
|
||||
examples
|
||||
dist
|
||||
dist
|
||||
*.AppleDouble
|
||||
*.Parent
|
||||
*.DS_Store
|
25
README.md
25
README.md
|
@ -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.
|
2
build.js
2
build.js
|
@ -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`)
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
|
@ -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/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
|
@ -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)
|
Loading…
Reference in New Issue