mirror of
https://github.com/sbrow/nu-ffmpeg.git
synced 2025-12-29 16:23:11 -05:00
feat: Filters can now be added as new chains, or appending the previous chain.
This commit is contained in:
105
ffmpeg.nu
Normal file
105
ffmpeg.nu
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
export def cmd [ inputs: list<string> outputs: list<string> ]: nothing -> record {
|
||||||
|
{
|
||||||
|
input: $inputs
|
||||||
|
filters: []
|
||||||
|
output: $outputs
|
||||||
|
args: []
|
||||||
|
options: {
|
||||||
|
chain_filters: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export def "cmd to-args" []: record -> list<string> {
|
||||||
|
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 -> 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<table<name: string params: table<param: string, value: string>>> {
|
||||||
|
split row ';' | each { parse filterchain }
|
||||||
|
}
|
||||||
|
|
||||||
|
export def "parse filterchain" [
|
||||||
|
]: string -> table<name: string params: table<param: string, value: string>> {
|
||||||
|
split row ',' | each { parse filter }
|
||||||
|
}
|
||||||
|
|
||||||
|
export def "parse filter" [
|
||||||
|
]: string -> table<name: string params: table<param: string, value: string>> {
|
||||||
|
parse --regex '^\s*(?:\[(?<input>[^\s]+)\]\s*)?(?<name>[^=\s\[]+)\s*(?:=(?<params>[^\[\s,;]*)\s*)?(?:\[(?<output>[^\s,;]+)\])?' | first | update params {
|
||||||
|
parse --regex `(?:(?<param>[^=]+)=)?(?<value>[^:]+):?`
|
||||||
|
} | 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<table> -> 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<input: list<string> name: string params: table<name: string value: string> output: list<string>> -> 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<string>
|
||||||
|
#output: list<string>
|
||||||
|
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<string> = []
|
||||||
|
--output (-o): list<string> = []
|
||||||
|
name: string
|
||||||
|
params: record = {}
|
||||||
|
]: nothing -> record<input: list<string> name: string params: table<param: string, value: string> output: list<string>> {
|
||||||
|
{
|
||||||
|
input: $input
|
||||||
|
name: $name
|
||||||
|
params: ($params | transpose param value | compact param value)
|
||||||
|
output: $output
|
||||||
|
}
|
||||||
|
}
|
||||||
170
ffmpeg_test.nu
Normal file
170
ffmpeg_test.nu
Normal file
@@ -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 }
|
||||||
|
};
|
||||||
|
}
|
||||||
2
ffprobe
2
ffprobe
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
# Multimedia stream analyzer.
|
# Multimedia stream analyzer.
|
||||||
export def main [
|
export def main [
|
||||||
...input_files: path
|
...input_files: string
|
||||||
]: nothing -> table {
|
]: nothing -> table {
|
||||||
$input_files | each {
|
$input_files | each {
|
||||||
^ffprobe -v quiet -print_format json -show_format -show_streams $in | from json
|
^ffprobe -v quiet -print_format json -show_format -show_streams $in | from json
|
||||||
|
|||||||
96
filters.nu
96
filters.nu
@@ -1,56 +1,74 @@
|
|||||||
#!/usr/bin/env -S nu --stdin
|
#!/usr/bin/env -S nu --stdin
|
||||||
|
|
||||||
export def "parse filter" [
|
use std [assert];
|
||||||
]: string -> table<name: string params: table<param: string, value: string>> {
|
|
||||||
$in | parse --regex '^(?<name>[^=]+)=(?<params>.*)' | first | update params {
|
|
||||||
parse --regex `(?<param>[^=]+)=(?<value>[^:]+):?`
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export def "filter to-string" [
|
use ./ffmpeg.nu ["complex-filter" "cmd filters append"]
|
||||||
]: table<name: string params: table<param: string, value: string>> -> 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: string params: table<param: string, value: string>> {
|
|
||||||
{
|
|
||||||
name: $name
|
|
||||||
params: ($params | transpose param value | compact param value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export def "complex-filters to-string" [
|
|
||||||
--pretty-print (-p)
|
|
||||||
]: table<input: list<string>, filters: table<name: string, params: table<param: string, value: string>>, 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
|
|
||||||
# =============
|
|
||||||
|
|
||||||
# loop video frames
|
# loop video frames
|
||||||
export def loop [
|
export def loop [
|
||||||
|
--input (-i): list<string>: = []
|
||||||
|
--output (-o): list<string>: = []
|
||||||
loop: int # Set the number of loops. Setting this value to -1 will result in infinite loops. Default is 0.
|
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.
|
size: int # Set maximal size in number of frames. Default is 0.
|
||||||
--start (-s): int # Set first frame of loop. 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.
|
--time (-t): float # Set the time of loop start in seconds. Only used if option named start is set to -1.
|
||||||
] {
|
] {
|
||||||
complex-filter loop {
|
cmd filters append [
|
||||||
|
(complex-filter loop {
|
||||||
loop: $loop
|
loop: $loop
|
||||||
size: $size
|
size: $size
|
||||||
start: (if ($time | is-empty) { $start } else { -1 })
|
start: (if ($time | is-empty) { $start } else { -1 })
|
||||||
time: $time
|
time: $time
|
||||||
|
} -i $input -o $output)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export def fps [
|
||||||
|
--input (-i): list<string>: = []
|
||||||
|
--output (-o): list<string>: = []
|
||||||
|
--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<string>: = []
|
||||||
|
--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<string> = []
|
||||||
|
--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)
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ use std [assert];
|
|||||||
|
|
||||||
use ./filters.nu [complex-filter loop "parse filter"];
|
use ./filters.nu [complex-filter loop "parse filter"];
|
||||||
|
|
||||||
#[test]
|
# #[test]
|
||||||
def loop_has_defaults [] {
|
def loop_has_defaults [] {
|
||||||
let got = (loop 10 1);
|
let got = (loop 10 1);
|
||||||
let want = (complex-filter 'loop' { loop: 10 size: 1 });
|
let want = (complex-filter 'loop' { loop: 10 size: 1 });
|
||||||
assert equal $got $want;
|
assert equal $got $want;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
# #[test]
|
||||||
def setting_time_sets_start_to-1 [] {
|
def setting_time_sets_start_to-1 [] {
|
||||||
let got = (loop 10 1 -t 0.5);
|
let got = (loop 10 1 -t 0.5);
|
||||||
let want = (complex-filter 'loop' {
|
let want = (complex-filter 'loop' {
|
||||||
|
|||||||
Reference in New Issue
Block a user