2019-04-02 16:57:39 +00:00
# ! / u s r / b i n / e n v n o d e
'use strict'
const execRaw = require ( 'child_process' ) . exec
2021-03-26 22:19:43 +00:00
const { tmpdir } = require ( 'os' )
const { join , extname } = require ( 'path' )
2019-04-02 16:57:39 +00:00
const program = require ( 'commander' )
2021-03-26 22:19:43 +00:00
const { move , exists , unlink , readdir , mkdir } = require ( 'fs-extra' )
2019-04-02 16:57:39 +00:00
2021-03-26 22:43:17 +00:00
const { version } = require ( './package.json' )
2019-04-02 16:57:39 +00:00
const OUTPUT_RE : RegExp = new RegExp ( '{{o}}' , 'g' )
const INPUT_RE : RegExp = new RegExp ( '{{i}}' , 'g' )
let QUIET : boolean = false
2021-03-26 22:19:43 +00:00
let TMPDIR : string = tmpdir ( ) || '/tmp'
2019-04-02 16:57:39 +00:00
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
* * /
2021-03-26 22:22:42 +00:00
async function exec ( cmd : string ) : Promise < string > {
2019-04-02 16:57:39 +00:00
return new Promise ( ( resolve : any , reject : any ) = > {
2019-04-16 00:08:36 +00:00
return execRaw ( cmd , { maxBuffer : 500 * 1024 * 1024 } , ( err : any , stdio : string , stderr : string ) = > {
2019-04-02 16:57:39 +00:00
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
* * /
2021-03-26 22:22:42 +00:00
async function delay ( ms : number ) : Promise < any > {
2019-04-02 16:57:39 +00:00
return new Promise ( ( resolve : any , reject : any ) = > {
return setTimeout ( resolve , ms )
} )
}
/ * *
* Log function wrapper that can silences logs when
* QUIET == true
* /
2021-03-26 22:22:42 +00:00
function log ( msg : string , err : any = false ) : boolean {
2019-04-02 16:57:39 +00:00
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
}
}
2019-04-16 00:08:36 +00:00
function randomInt ( min : number , max : number ) {
min = Math . ceil ( min ) ;
max = Math . floor ( max ) ;
return Math . floor ( Math . random ( ) * ( max - min + 1 ) ) + min ;
}
2019-04-02 16:57:39 +00:00
/ * *
* Clears the temporary directory of all files .
* Establishes a directory if none exists .
* * /
async function clear ( ) {
let cmd : string = ` rm -r " ${ TMPPATH } " `
2021-03-26 22:19:43 +00:00
let dirExists : boolean
2019-04-02 16:57:39 +00:00
try {
2021-03-26 22:19:43 +00:00
dirExists = await exists ( TMPPATH )
2019-04-02 16:57:39 +00:00
} catch ( err ) {
log ( 'Error checking if file exists' , err )
}
2021-03-26 22:19:43 +00:00
if ( dirExists ) {
2019-04-02 16:57:39 +00:00
log ( ` Clearing temp directory " ${ TMPPATH } " ` )
try {
await exec ( cmd )
} catch ( err ) {
//suppress error
console . dir ( err )
}
}
try {
2021-03-26 22:19:43 +00:00
await mkdir ( TMPPATH )
2019-04-02 16:57:39 +00:00
} 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
* * /
2021-03-26 22:22:42 +00:00
async function frames ( video : string , order : number , avconv : boolean ) : Promise < string > {
2019-04-02 16:57:39 +00:00
let ext : string = 'tif'
let exe : string = avconv ? 'avconv' : 'ffmpeg'
let tmpoutput : string
let cmd : string
2021-03-26 22:19:43 +00:00
tmpoutput = join ( TMPPATH , ` export-%05d_ ${ order } . ${ ext } ` )
2019-04-02 16:57:39 +00:00
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 )
}
2021-03-26 22:19:43 +00:00
return join ( TMPPATH , ` export-%05d_ ${ order } ` )
2019-04-02 16:57:39 +00:00
}
/ * *
* 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 {
2021-03-26 22:19:43 +00:00
frames = await readdir ( TMPPATH )
2019-04-02 16:57:39 +00:00
} catch ( err ) {
log ( 'Error reading tmp directory' , err )
}
frames = frames . filter ( file = > {
if ( file . indexOf ( '.tif' ) !== - 1 ) return true
} )
for ( let frame of frames ) {
2021-03-26 22:19:43 +00:00
framePath = join ( TMPPATH , frame )
2019-04-02 16:57:39 +00:00
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 {
2021-03-26 22:19:43 +00:00
frames = await readdir ( TMPPATH )
2019-04-02 16:57:39 +00:00
} 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 ) {
2019-05-31 19:12:29 +00:00
log ( 'Sorting frames randomly...' )
2019-04-02 16:57:39 +00:00
try {
seq = await randomSort ( frames , pattern , realtime )
} catch ( err ) {
log ( 'Error sorting frames' , err )
}
} else if ( ! alt ) {
2019-05-31 19:12:29 +00:00
log ( 'Sorting frames normally...' )
2019-04-02 16:57:39 +00:00
try {
seq = await standardSort ( frames , pattern , realtime )
} catch ( err ) {
log ( 'Error sorting frames' , err )
}
} else if ( alt ) {
2019-05-31 19:12:29 +00:00
//log('This feature is not ready, please check https://github.com/sixteenmillimeter/frameloom.git', {})
//process.exit(10)
log ( 'Sorting frames with alternate pattern...' )
2019-04-02 16:57:39 +00:00
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 [ ] = [ ]
2019-05-31 19:12:29 +00:00
let loops : number = 0
let patternIndexes : number [ ] = [ ]
2019-04-02 16:57:39 +00:00
let frameCount : number = 0
2019-05-31 19:12:29 +00:00
let skipCount : number
let skip : boolean
let oldName : string
2019-04-02 16:57:39 +00:00
let oldPath : string
let newName : string
let newPath : string
2021-03-26 22:19:43 +00:00
let ext : string = extname ( list [ 0 ] )
2019-05-31 19:12:29 +00:00
let x : number
let i : number
2019-04-02 16:57:39 +00:00
2019-05-31 19:12:29 +00:00
for ( x = 0 ; x < pattern . length ; x ++ ) {
2019-04-02 16:57:39 +00:00
groups . push ( [ ] )
2019-05-31 19:12:29 +00:00
for ( let i : number = 0 ; i < pattern [ x ] ; i ++ ) {
patternIndexes . push ( x )
}
2019-04-02 16:57:39 +00:00
}
2019-05-31 19:12:29 +00:00
for ( i = 0 ; i < list . length ; i ++ ) {
2019-04-02 16:57:39 +00:00
groups [ i % pattern . length ] . push ( list [ i ] )
}
2019-05-31 19:12:29 +00:00
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 ] )
2021-03-26 22:19:43 +00:00
oldPath = join ( TMPPATH , oldName )
2019-04-02 16:57:39 +00:00
2019-05-31 19:12:29 +00:00
groups [ patternIndexes [ i ] ] . shift ( )
2019-04-02 16:57:39 +00:00
2019-05-31 19:12:29 +00:00
if ( skip ) {
log ( ` Skipping ${ oldName } ` )
2019-04-02 16:57:39 +00:00
try {
2021-03-26 22:19:43 +00:00
await unlink ( oldPath )
2019-04-02 16:57:39 +00:00
} catch ( err ) {
2019-05-31 19:12:29 +00:00
log ( 'Error deleting frame' , err )
}
continue
}
newName = ` ./render_ ${ zeroPad ( frameCount ) } ${ ext } `
2021-03-26 22:19:43 +00:00
newPath = join ( TMPPATH , newName )
2019-05-31 19:12:29 +00:00
log ( ` Renaming ${ oldName } -> ${ newName } ` )
2019-04-02 16:57:39 +00:00
2019-05-31 19:12:29 +00:00
try {
2021-03-26 22:19:43 +00:00
await move ( oldPath , newPath )
2019-05-31 19:12:29 +00:00
newList . push ( newName )
2019-04-02 16:57:39 +00:00
frameCount ++
2019-05-31 19:12:29 +00:00
} catch ( err ) {
log ( 'Error renaming frame' , err )
return process . exit ( 10 )
2019-04-02 16:57:39 +00:00
}
}
}
2019-05-31 19:12:29 +00:00
2019-04-02 16:57:39 +00:00
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
2021-03-26 22:19:43 +00:00
let ext : string = extname ( list [ 0 ] )
2019-04-02 16:57:39 +00:00
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
}
}
2021-03-26 22:19:43 +00:00
oldPath = join ( TMPPATH , list [ i ] )
2019-04-02 16:57:39 +00:00
if ( skip ) {
log ( ` Skipping ${ list [ i ] } ` )
try {
2021-03-26 22:19:43 +00:00
await unlink ( oldPath )
2019-04-02 16:57:39 +00:00
} catch ( err ) {
log ( 'Error deleting frame' , err )
}
continue
}
newName = ` ./render_ ${ zeroPad ( frameCount ) } ${ ext } `
2021-03-26 22:19:43 +00:00
newPath = join ( TMPPATH , newName )
2019-04-02 16:57:39 +00:00
log ( ` Renaming ${ list [ i ] } -> ${ newName } ` )
try {
2021-03-26 22:19:43 +00:00
await move ( oldPath , newPath )
2019-04-02 16:57:39 +00:00
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
2021-03-26 22:19:43 +00:00
let ext : string = extname ( list [ 0 ] )
2019-04-02 16:57:39 +00:00
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 ++ ) {
2021-03-26 22:19:43 +00:00
oldPath = join ( TMPPATH , remove [ i ] )
2019-04-02 16:57:39 +00:00
log ( ` Skipping ${ list [ i ] } ` )
try {
2021-03-26 22:19:43 +00:00
await unlink ( oldPath )
2019-04-02 16:57:39 +00:00
} catch ( err ) {
log ( 'Error deleting frame' , err )
}
}
}
for ( let i : number = 0 ; i < list . length ; i ++ ) {
2021-03-26 22:19:43 +00:00
oldPath = join ( TMPPATH , list [ i ] )
2019-04-02 16:57:39 +00:00
newName = ` ./render_ ${ zeroPad ( frameCount ) } ${ ext } `
2021-03-26 22:19:43 +00:00
newPath = join ( TMPPATH , newName )
2019-04-02 16:57:39 +00:00
log ( ` Renaming ${ list [ i ] } -> ${ newName } ` )
try {
2021-03-26 22:19:43 +00:00
await move ( oldPath , newPath )
2019-04-02 16:57:39 +00:00
newList . push ( newName )
} catch ( err ) {
log ( 'Error moving frame' , err )
}
frameCount ++
}
return newList
}
2019-04-16 00:08:36 +00:00
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 {
2021-03-26 22:19:43 +00:00
frames = await readdir ( TMPPATH )
2019-04-16 00:08:36 +00:00
} 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 ) {
2021-03-26 22:19:43 +00:00
framePath = join ( TMPPATH , frame )
2019-04-16 00:08:36 +00:00
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 '
}
2019-08-05 16:19:11 +00:00
if ( flip === '' && flop === '' && rotate === '' ) {
//skip unrotated, unflipped and unflopped frames
continue
}
2019-04-16 00:08:36 +00:00
cmd = ` convert ${ framePath } ${ rotate } ${ flip } ${ flop } ${ framePath } `
console . log ( cmd )
try {
await exec ( cmd )
} catch ( err ) {
console . error ( err )
process . exit ( 10 )
}
}
}
2019-04-02 16:57:39 +00:00
/ * *
* 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()
2021-03-26 22:19:43 +00:00
let frames : string = join ( TMPPATH , ` render_%05d.tif ` )
2019-04-02 16:57:39 +00:00
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
* * /
2021-04-07 13:13:04 +00:00
async function main ( program : any ) {
const arg = program . opts ( ) ;
2019-04-02 16:57:39 +00:00
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
2019-05-31 19:12:29 +00:00
let exe : string = arg . avconv ? 'avconv' : 'ffmpeg'
2021-03-26 22:19:43 +00:00
let fileExists : any
2019-04-02 16:57:39 +00:00
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 ) ;
}
}
2019-05-31 19:12:29 +00:00
try {
2021-03-26 22:19:43 +00:00
fileExists = await exec ( ` which ${ exe } ` )
2019-05-31 19:12:29 +00:00
} catch ( err ) {
log ( ` Error checking for ${ exe } ` )
process . exit ( 11 )
}
2021-03-26 22:19:43 +00:00
if ( ! fileExists || fileExists === '' || fileExists . indexOf ( exe ) === - 1 ) {
2019-05-31 19:12:29 +00:00
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 )
}
2019-04-02 16:57:39 +00:00
if ( arg . realtime ) realtime = true ;
2021-03-26 22:19:43 +00:00
TMPPATH = join ( TMPDIR , 'frameloom' ) ;
2019-04-02 16:57:39 +00:00
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 )
}
2019-08-05 16:19:11 +00:00
if ( arg . spin ) {
try {
await spinFrames ( )
} catch ( err ) {
log ( 'Error spinning' , err )
return process . exit ( 13 )
}
}
2019-04-02 16:57:39 +00:00
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
2021-03-26 22:19:43 +00:00
. version ( version )
2019-04-02 16:57:39 +00:00
. 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' )
2019-08-05 16:19:11 +00:00
. option ( '-s, --spin' , 'Randomly rotate frames before rendering' )
2019-04-02 16:57:39 +00:00
. 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 )