#17: add proper handling of split operator

This commit is contained in:
Karl Kroening 2017-07-06 03:35:03 -06:00
parent 662c56eb5b
commit 8337d34b82
3 changed files with 67 additions and 42 deletions

View File

@ -1,6 +1,6 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .dag import topo_sort from .dag import get_outgoing_edges, topo_sort
from functools import reduce from functools import reduce
from past.builtins import basestring from past.builtins import basestring
import copy import copy
@ -53,16 +53,31 @@ def _get_input_args(input_node):
return args return args
def _get_filter_spec(i, node, stream_name_map): def _get_filter_spec(node, outgoing_edge_map, stream_name_map):
stream_name = _get_stream_name('v{}'.format(i)) incoming_edges = node.incoming_edges
stream_name_map[node] = stream_name outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
inputs = [stream_name_map[edge.upstream_node] for edge in node.incoming_edges] inputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in incoming_edges]
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), stream_name) outputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in outgoing_edges]
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(), ''.join(outputs))
return filter_spec return filter_spec
def _get_filter_arg(filter_nodes, stream_name_map): def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_map):
filter_specs = [_get_filter_spec(i, node, stream_name_map) for i, node in enumerate(filter_nodes)] stream_count = 0
for upstream_node in filter_nodes:
outgoing_edge_map = outgoing_edge_maps[upstream_node]
for upstream_label, downstreams in outgoing_edge_map.items():
if len(downstreams) > 1:
# TODO: automatically insert `splits` ahead of time via graph transformation.
raise ValueError('Encountered {} with multiple outgoing edges with same upstream label {!r}; a '
'`split` filter is probably required'.format(upstream_node, upstream_label))
stream_name_map[upstream_node, upstream_label] = _get_stream_name('s{}'.format(stream_count))
stream_count += 1
def _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map):
_allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_map)
filter_specs = [_get_filter_spec(node, outgoing_edge_maps[node], stream_name_map) for node in filter_nodes]
return ';'.join(filter_specs) return ';'.join(filter_specs)
@ -78,7 +93,8 @@ def _get_output_args(node, stream_name_map):
raise ValueError('Unsupported output node: {}'.format(node)) raise ValueError('Unsupported output node: {}'.format(node))
args = [] args = []
assert len(node.incoming_edges) == 1 assert len(node.incoming_edges) == 1
stream_name = stream_name_map[node.incoming_edges[0].upstream_node] edge = node.incoming_edges[0]
stream_name = stream_name_map[edge.upstream_node, edge.upstream_label]
if stream_name != '[0]': if stream_name != '[0]':
args += ['-map', stream_name] args += ['-map', stream_name]
kwargs = copy.copy(node.kwargs) kwargs = copy.copy(node.kwargs)
@ -104,8 +120,8 @@ def get_args(stream):
isinstance(node, GlobalNode)] isinstance(node, GlobalNode)]
global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)] global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)]
filter_nodes = [node for node in sorted_nodes if node not in (input_nodes + output_nodes + global_nodes)] filter_nodes = [node for node in sorted_nodes if node not in (input_nodes + output_nodes + global_nodes)]
stream_name_map = {node: _get_stream_name(i) for i, node in enumerate(input_nodes)} stream_name_map = {(node, None): _get_stream_name(i) for i, node in enumerate(input_nodes)}
filter_arg = _get_filter_arg(filter_nodes, stream_name_map) filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map)
args += reduce(operator.add, [_get_input_args(node) for node in input_nodes]) args += reduce(operator.add, [_get_input_args(node) for node in input_nodes])
if filter_arg: if filter_arg:
args += ['-filter_complex', filter_arg] args += ['-filter_complex', filter_arg]

View File

@ -39,6 +39,18 @@ class Stream(object):
return out return out
def get_stream_map(stream_spec):
if stream_spec is None:
stream_map = {}
elif isinstance(stream_spec, Stream):
stream_map = {None: stream_spec}
elif isinstance(stream_spec, (list, tuple)):
stream_map = dict(enumerate(stream_spec))
elif isinstance(stream_spec, dict):
stream_map = stream_spec
return stream_map
class Node(KwargReprNode): class Node(KwargReprNode):
"""Node base""" """Node base"""
@classmethod @classmethod
@ -55,18 +67,6 @@ class Node(KwargReprNode):
raise TypeError('Expected incoming stream(s) to be of one of the following types: {}; got {}' raise TypeError('Expected incoming stream(s) to be of one of the following types: {}; got {}'
.format(_get_types_str(incoming_stream_types), type(stream))) .format(_get_types_str(incoming_stream_types), type(stream)))
@classmethod
def __get_stream_map(cls, stream_spec):
if stream_spec is None:
stream_map = {}
elif isinstance(stream_spec, Stream):
stream_map = {None: stream_spec}
elif isinstance(stream_spec, (list, tuple)):
stream_map = dict(enumerate(stream_spec))
elif isinstance(stream_spec, dict):
stream_map = stream_spec
return stream_map
@classmethod @classmethod
def __get_incoming_edge_map(cls, stream_map): def __get_incoming_edge_map(cls, stream_map):
incoming_edge_map = {} incoming_edge_map = {}
@ -76,7 +76,7 @@ class Node(KwargReprNode):
def __init__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, max_inputs, args, def __init__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, max_inputs, args,
kwargs): kwargs):
stream_map = self.__get_stream_map(stream_spec) stream_map = get_stream_map(stream_spec)
self.__check_input_len(stream_map, min_inputs, max_inputs) self.__check_input_len(stream_map, min_inputs, max_inputs)
self.__check_input_types(stream_map, incoming_stream_types) self.__check_input_types(stream_map, incoming_stream_types)
incoming_edge_map = self.__get_incoming_edge_map(stream_map) incoming_edge_map = self.__get_incoming_edge_map(stream_map)

View File

@ -93,12 +93,19 @@ def test_get_args_simple():
def _get_complex_filter_example(): def _get_complex_filter_example():
in_file = ffmpeg.input(TEST_INPUT_FILE) split = (ffmpeg
.input(TEST_INPUT_FILE)
.vflip()
.split()
)
split0 = split[0]
split1 = split[1]
overlay_file = ffmpeg.input(TEST_OVERLAY_FILE) overlay_file = ffmpeg.input(TEST_OVERLAY_FILE)
return (ffmpeg return (ffmpeg
.concat( .concat(
in_file.trim(start_frame=10, end_frame=20), split0.trim(start_frame=10, end_frame=20),
in_file.trim(start_frame=30, end_frame=40), split1.trim(start_frame=30, end_frame=40),
) )
.overlay(overlay_file.hflip()) .overlay(overlay_file.hflip())
.drawbox(50, 50, 120, 120, color='red', thickness=5) .drawbox(50, 50, 120, 120, color='red', thickness=5)
@ -110,21 +117,23 @@ def _get_complex_filter_example():
def test_get_args_complex_filter(): def test_get_args_complex_filter():
out = _get_complex_filter_example() out = _get_complex_filter_example()
args = ffmpeg.get_args(out) args = ffmpeg.get_args(out)
assert args == [ assert args == ['-i', TEST_INPUT_FILE,
'-i', TEST_INPUT_FILE,
'-i', TEST_OVERLAY_FILE, '-i', TEST_OVERLAY_FILE,
'-filter_complex', '-filter_complex',
'[0]trim=end_frame=20:start_frame=10[v0];' \ '[0]vflip[s0];' \
'[0]trim=end_frame=40:start_frame=30[v1];' \ '[s0]split[s1][s2];' \
'[v0][v1]concat=n=2[v2];' \ '[s1]trim=end_frame=20:start_frame=10[s3];' \
'[1]hflip[v3];' \ '[s2]trim=end_frame=40:start_frame=30[s4];' \
'[v2][v3]overlay=eof_action=repeat[v4];' \ '[s3][s4]concat=n=2[s5];' \
'[v4]drawbox=50:50:120:120:red:t=5[v5]', '[1]hflip[s6];' \
'-map', '[v5]', os.path.join(SAMPLE_DATA_DIR, 'dummy2.mp4'), '[s5][s6]overlay=eof_action=repeat[s7];' \
'[s7]drawbox=50:50:120:120:red:t=5[s8]',
'-map', '[s8]', os.path.join(SAMPLE_DATA_DIR, 'dummy2.mp4'),
'-y' '-y'
] ]
#def test_version(): #def test_version():
# subprocess.check_call(['ffmpeg', '-version']) # subprocess.check_call(['ffmpeg', '-version'])
@ -156,8 +165,8 @@ def test_custom_filter():
node = ffmpeg.output(node, 'dummy2.mp4') node = ffmpeg.output(node, 'dummy2.mp4')
assert node.get_args() == [ assert node.get_args() == [
'-i', 'dummy.mp4', '-i', 'dummy.mp4',
'-filter_complex', '[0]custom_filter=a:b:kwarg1=c[v0]', '-filter_complex', '[0]custom_filter=a:b:kwarg1=c[s0]',
'-map', '[v0]', '-map', '[s0]',
'dummy2.mp4' 'dummy2.mp4'
] ]
@ -170,8 +179,8 @@ def test_custom_filter_fluent():
) )
assert node.get_args() == [ assert node.get_args() == [
'-i', 'dummy.mp4', '-i', 'dummy.mp4',
'-filter_complex', '[0]custom_filter=a:b:kwarg1=c[v0]', '-filter_complex', '[0]custom_filter=a:b:kwarg1=c[s0]',
'-map', '[v0]', '-map', '[s0]',
'dummy2.mp4' 'dummy2.mp4'
] ]
@ -197,8 +206,8 @@ def test_pipe():
'-pixel_format', 'rgb24', '-pixel_format', 'rgb24',
'-i', 'pipe:0', '-i', 'pipe:0',
'-filter_complex', '-filter_complex',
'[0]trim=start_frame=2[v0]', '[0]trim=start_frame=2[s0]',
'-map', '[v0]', '-map', '[s0]',
'-f', 'rawvideo', '-f', 'rawvideo',
'pipe:1' 'pipe:1'
] ]