diff --git a/ffmpeg.nu b/ffmpeg.nu new file mode 100644 index 0000000..ee01e52 --- /dev/null +++ b/ffmpeg.nu @@ -0,0 +1,105 @@ +export def cmd [ inputs: list outputs: list ]: nothing -> record { + { + input: $inputs + filters: [] + output: $outputs + args: [] + options: { + chain_filters: false + } + } +} + +export def "cmd to-args" []: record -> list { + let command = $in; + + [ + ...($command.input | reduce -f [] { |it, acc| $acc | append ['-i', $it ] }) + ...$command.args + ...['-filter_complex' ($command.filters | filtergraph to-string)] + ...($command.output) + ] +} + +export def "cmd filters append" [ + complex_filter: list +]: record -> record { + let cmd = $in; + + if ($cmd.options.chain_filters) { + $cmd | update filters { + let original = $in; + + ($original | range 0..-2) | append [ + (($original | default [] | last) | append $complex_filter) + ] + } + } else { + $cmd | update filters { append [$complex_filter] } + } +} + +export def "parse filtergraph" [ +]: string -> list>> { + split row ';' | each { parse filterchain } +} + +export def "parse filterchain" [ +]: string -> table> { + split row ',' | each { parse filter } +} + +export def "parse filter" [ +]: string -> table> { + parse --regex '^\s*(?:\[(?[^\s]+)\]\s*)?(?[^=\s\[]+)\s*(?:=(?[^\[\s,;]*)\s*)?(?:\[(?[^\s,;]+)\])?' | first | update params { + parse --regex `(?:(?[^=]+)=)?(?[^:]+):?` + } | update input { split row '][' | filter { not ($in | is-empty) } + } | update output { split row '][' | filter { not ($in | is-empty) } } +} + +# TODO: Remove export +export def "filtergraph to-string" [] { # : list -> string { + $in | each { filterchain to-string } | str join ';' +} + +def "filterchain to-string" []: table -> string { + $in | each { filter to-string }| str join ',' +} + +def "filter to-string" []: record name: string params: table output: list> -> string { + $in | update input { + str join '][' + } | update output { + str join '][' + } | update params { + each { format '{param}={value}' } | str join ':' | str replace -ar '(?<=^|:)=' '' + } | format '[{input}]{name}={params}[{output}]' | str replace -ar '\[\]|=(?=[\[,;])' '' +} + +# Set the input and outputs of a filter chain +export def filterchain [ + #input: list + #output: list + filter: closure +] { + let cmd = $in; + let original_option = $cmd.options.chain_filters; + + # TODO: Assign inputs and outputs + $cmd | update options.chain_filters { not $in } | do $filter | update options.chain_filters $original_option; +} + +# Build a record representaion of a complex filter +export def complex-filter [ + --input (-i): list = [] + --output (-o): list = [] + name: string + params: record = {} +]: nothing -> record name: string params: table output: list> { + { + input: $input + name: $name + params: ($params | transpose param value | compact param value) + output: $output + } +} diff --git a/ffmpeg_test.nu b/ffmpeg_test.nu new file mode 100644 index 0000000..7202404 --- /dev/null +++ b/ffmpeg_test.nu @@ -0,0 +1,170 @@ +use std [assert]; + +use ffmpeg.nu *; +use filters.nu *; + +#[test] +def can_parse_filters_with_inputs_and_outputs [] { + let got = '[foo]loop=loop=1[bar]' | parse filter; + + assert equal $got { input: ['foo'] name: 'loop' params: [{param: 'loop', value: '1'}] output: ['bar'] }; +} + +#[test] +def can_parse_filters_with_multiple_inputs [] { + let got = '[main][flip] overlay=0:H/2 ' | parse filter; + + assert equal $got { input: ['main' 'flip'] name: 'overlay' params: [{param: '', value: '0'} {param: '' value: 'H/2'}] output: [] }; +} + +#[test] +def can_parse_filters_with_multiple_outputs [] { + let got = 'overlay=0:H/2 [main][flip]' | parse filter; + + assert equal $got { input: [] name: 'overlay' params: [{param: '', value: '0'} {param: '' value: 'H/2'}] output: ['main' 'flip'] }; +} + +#[test] +def can_parse_filters_with_multiple_params [] { + let got = '[foo]loop=loop=2:size=1[bar]' | parse filter; + + assert equal $got { input: ['foo'] name: 'loop' params: [{param: 'loop', value: '2'} {param: 'size' value: '1'}] output: ['bar'] }; +} + +#[test] +def can_parse_filterchain [] { + let got = '[tmp] crop=iw:ih/2:0:0, vflip [flip]' | parse filterchain; + + assert equal $got [ + { + input: ['tmp'] + name: 'crop' + params: [ + {param: '' value: 'iw'} + { param: '' value: 'ih/2'} + { param: '', value: '0'} + { param: '', value: '0'} + ] + output: [] + } + { + input: [] + name: 'vflip' + params: [] + output: ['flip'] + } + ]; +} + +#[test] +def can_parse_filtergraph [] { + let got = 'split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2' | parse filtergraph; + + assert equal $got [ + [ + {input: [], name: 'split', params: [], output: ['main' 'tmp']} + ] + [ + {input: [tmp], name: 'crop', params: [ + { param: '' value: 'iw' } + { param: '' value: 'ih/2' } + { param: '' value: '0' } + { param: '' value: '0' } + ], output: []} + {input: [], name: 'vflip', params: [], output: ['flip']} + ] + [ + {input: ['main' 'flip'], name: 'overlay', params: [ + { param: '' value: '0' } + { param: '' value: 'H/2' } + ], output: []} + ] + ]; +} +#[test] +def can_convert_filtergraph_to_string [] { + let got = 'split [main][tmp]; [tmp] crop=iw:ih/2:0:0, vflip [flip]; [main][flip] overlay=0:H/2' | parse filtergraph | filtergraph to-string; + + let want = 'split[main][tmp];[tmp]crop=iw:ih/2:0:0,vflip[flip];[main][flip]overlay=0:H/2'; + + assert equal $got $want; +} + +#[test] +def without_filterchain_chains_are_concated [] { + let got = (cmd ['INPUT'] ['OUTPUT'] | fps 25 | loop 2 1); + + assert equal $got { + input: ['INPUT'] + filters: [ + [ + { + input: [] + name: 'fps' + params: [ + {param: 'fps' value: 25} + ] + output: [] + } + { + input: [] + name: 'settb' + params: [ + {param: 'expr' value: '1/25'} + ] + output: [] + } + ] + [{ + input: [] + name: 'loop' + params: [ + {param: 'loop' value: 2} + {param: 'size' value: 1} + ] + output: [] + }] + ] + output: ['OUTPUT'] + args: [] + options: { chain_filters: false } + }; +} +#[test] +def filterchain_concats_filters [] { + let got = (cmd ['INPUT'] ['OUTPUT'] | filterchain ['in'] ['out'] { fps 25 | loop 2 1 }); + + assert equal $got { + input: ['INPUT'] + filters: [[ + { + input: [] # ['in'] + name: 'fps' + params: [ + {param: 'fps' value: 25} + ] + output: [] + } + { + input: [] + name: 'settb' + params: [ + {param: 'expr' value: '1/25'} + ] + output: [] + } + { + input: [] + name: 'loop' + params: [ + {param: 'loop' value: 2} + {param: 'size' value: 1} + ] + output: [] # ['out'] + } + ]] + output: ['OUTPUT'] + args: [] + options: { chain_filters: false } + }; +} diff --git a/ffprobe b/ffprobe index 865f6b7..b2be615 100755 --- a/ffprobe +++ b/ffprobe @@ -3,7 +3,7 @@ # Multimedia stream analyzer. export def main [ - ...input_files: path + ...input_files: string ]: nothing -> table { $input_files | each { ^ffprobe -v quiet -print_format json -show_format -show_streams $in | from json diff --git a/filters.nu b/filters.nu index d312921..b5e4eaa 100755 --- a/filters.nu +++ b/filters.nu @@ -1,56 +1,74 @@ #!/usr/bin/env -S nu --stdin -export def "parse filter" [ -]: string -> table> { - $in | parse --regex '^(?[^=]+)=(?.*)' | first | update params { - parse --regex `(?[^=]+)=(?[^:]+):?` - } -} +use std [assert]; -export def "filter to-string" [ -]: table> -> string { - each { - $'($in.name)=($in.params | format '{param}={value}')' - } | str join ':' -} - -# Build a record representaion of a complex filter -export def complex-filter [ - name: string - params: record = {} -]: nothing -> record> { - { - name: $name - params: ($params | transpose param value | compact param value) - } -} - - -export def "complex-filters to-string" [ - --pretty-print (-p) -]: table, filters: table>, output: string> -> string { - $in | update filters { - flatten | filter to-string - } | update input { - str join '][' - } | format '[{input}]{filters}[{output}]' | str join (";" + (if $pretty_print { "\n" } else { "" })) | str replace --all '[]' '' -} - -# ============= -# Begin Filters -# ============= +use ./ffmpeg.nu ["complex-filter" "cmd filters append"] # loop video frames export def loop [ + --input (-i): list: = [] + --output (-o): list: = [] loop: int # Set the number of loops. Setting this value to -1 will result in infinite loops. Default is 0. size: int # Set maximal size in number of frames. Default is 0. --start (-s): int # Set first frame of loop. Default is 0. --time (-t): float # Set the time of loop start in seconds. Only used if option named start is set to -1. ] { - complex-filter loop { - loop: $loop - size: $size - start: (if ($time | is-empty) { $start } else { -1 }) - time: $time - } + cmd filters append [ + (complex-filter loop { + loop: $loop + size: $size + start: (if ($time | is-empty) { $start } else { -1 }) + time: $time + } -i $input -o $output) + ] +} + +export def fps [ + --input (-i): list: = [] + --output (-o): list: = [] + --round (-r): string + + fps: int + ] { + cmd filters append [ + (complex-filter fps {fps: $fps round: $round} -i $input) + (complex-filter settb {expr: $'1/($fps)' } -o $output) + ] +} + +export def split [ + --input (-i): list: = [] + --output (-o): string +] { + let cmd = $in; + + let n = ($output | length); + + $cmd | cmd filters append [(complex-filter 'split' (if $n != 2 { + {'n': $n} + } else { + {} + }) -i $input -o $output)] +} + +# TODO: Finish +export def crop [ + --input: list = [] + --width (-w): string = 'iw' + --height (-h): string + x = '0' + y = '0' +] { + cmd filters append [ + (complex-filter crop {w: $width h: $height x: $x, y: $y} -i $input) + ] +} + +# TODO: Finish +export def vflip [ + ...output: string +] { + cmd filters append [ + (complex-filter vflip -o $output) + ] } diff --git a/filters_test.nu b/filters_test.nu index 34346cd..c8d65d5 100644 --- a/filters_test.nu +++ b/filters_test.nu @@ -4,14 +4,14 @@ use std [assert]; use ./filters.nu [complex-filter loop "parse filter"]; -#[test] +# #[test] def loop_has_defaults [] { let got = (loop 10 1); let want = (complex-filter 'loop' { loop: 10 size: 1 }); assert equal $got $want; } -#[test] +# #[test] def setting_time_sets_start_to-1 [] { let got = (loop 10 1 -t 0.5); let want = (complex-filter 'loop' {