Change selector syntax from [:a] to [a]; remove map operator (for now)

This commit is contained in:
Karl Kroening 2018-03-11 21:27:26 -07:00
parent 6169b89321
commit 57abf6e86e
4 changed files with 57 additions and 187 deletions

View File

@ -9,7 +9,7 @@ from .nodes import (
MergeOutputsNode, MergeOutputsNode,
OutputNode, OutputNode,
output_operator, output_operator,
OutputStream) )
def input(filename, **kwargs): def input(filename, **kwargs):
@ -68,26 +68,9 @@ def output(*streams_and_filename, **kwargs):
return OutputNode(streams, output.__name__, kwargs=kwargs).stream() return OutputNode(streams, output.__name__, kwargs=kwargs).stream()
@output_operator()
def map(*streams):
"""Map multiple streams to the same output
"""
head = streams[0]
tail = streams[1:]
if not isinstance(head, OutputStream):
raise ValueError('First argument must be an output stream')
if not tail:
return head
return OutputNode(head.node, tail).stream()
__all__ = [ __all__ = [
'input', 'input',
'merge_outputs', 'merge_outputs',
'output', 'output',
'map',
'overwrite_output', 'overwrite_output',
] ]

View File

@ -76,10 +76,7 @@ DagEdge = namedtuple('DagEdge', ['downstream_node', 'downstream_label', 'upstrea
def get_incoming_edges(downstream_node, incoming_edge_map): def get_incoming_edges(downstream_node, incoming_edge_map):
edges = [] edges = []
for downstream_label, upstream_info in incoming_edge_map.items(): for downstream_label, upstream_info in incoming_edge_map.items():
# `upstream_info` may contain the upstream_selector. [:2] trims it away upstream_node, upstream_label, upstream_selector = upstream_info
upstream_node, upstream_label = upstream_info[:2]
# Take into account the stream selector if it's present (i.e. len(upstream_info) >= 3)
upstream_selector = None if len(upstream_info) < 3 else upstream_info[2]
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, upstream_selector)] edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, upstream_selector)]
return edges return edges
@ -88,10 +85,7 @@ def get_outgoing_edges(upstream_node, outgoing_edge_map):
edges = [] edges = []
for upstream_label, downstream_infos in list(outgoing_edge_map.items()): for upstream_label, downstream_infos in list(outgoing_edge_map.items()):
for downstream_info in downstream_infos: for downstream_info in downstream_infos:
# `downstream_info` may contain the downstream_selector. [:2] trims it away downstream_node, downstream_label, downstream_selector = downstream_info
downstream_node, downstream_label = downstream_info[:2]
# Take into account the stream selector if it's present
downstream_selector = None if len(downstream_info) < 3 else downstream_info[2]
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, downstream_selector)] edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, downstream_selector)]
return edges return edges
@ -104,10 +98,8 @@ class KwargReprNode(DagNode):
def __upstream_hashes(self): def __upstream_hashes(self):
hashes = [] hashes = []
for downstream_label, upstream_info in self.incoming_edge_map.items(): for downstream_label, upstream_info in self.incoming_edge_map.items():
# `upstream_info` may contain the upstream_selector. [:2] trims it away upstream_node, upstream_label, upstream_selector = upstream_info
upstream_node, upstream_label = upstream_info[:2] hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label, upstream_selector]]
# The stream selector is discarded when calculating the hash: the stream "as a whole" is still the same
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label]]
return hashes return hashes
@property @property

View File

@ -1,9 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from .dag import KwargReprNode from .dag import KwargReprNode
from ._utils import escape_chars, get_hash_int from ._utils import escape_chars, get_hash_int
from builtins import object from builtins import object
import os, sys import os
import inspect
def _is_of_types(obj, types): def _is_of_types(obj, types):
@ -19,13 +19,6 @@ def _get_types_str(types):
return ', '.join(['{}.{}'.format(x.__module__, x.__name__) for x in types]) return ', '.join(['{}.{}'.format(x.__module__, x.__name__) for x in types])
def _get_arg_count(callable):
if sys.version_info.major >= 3:
return len(inspect.getfullargspec(callable).args)
else:
return len(inspect.getargspec(callable).args)
class Stream(object): class Stream(object):
"""Represents the outgoing edge of an upstream node; may be used to create more downstream nodes.""" """Represents the outgoing edge of an upstream node; may be used to create more downstream nodes."""
@ -37,7 +30,6 @@ class Stream(object):
self.label = upstream_label self.label = upstream_label
self.selector = upstream_selector self.selector = upstream_selector
def __hash__(self): def __hash__(self):
return get_hash_int([hash(self.node), hash(self.label)]) return get_hash_int([hash(self.node), hash(self.label)])
@ -54,14 +46,22 @@ class Stream(object):
def __getitem__(self, index): def __getitem__(self, index):
""" """
Select a component of the stream. `stream[:X]` is analogous to `stream.node.stream(select=X)`. Select a component (audio, video) of the stream.
Please note that this can only be used to select a substream that already exist. If you want to split
the stream, use the `split` filter.
"""
if not isinstance(index, slice) or index.start is not None:
raise ValueError("Invalid syntax. Use `stream[:\'something\']`, not `stream[\'something\']`.")
return self.node.stream(select=index.stop) Example:
Process the audio and video portions of a stream independently::
in = ffmpeg.input('in.mp4')
audio = in['a'].filter_("aecho", 0.8, 0.9, 1000, 0.3)
video = in['v'].hflip()
ffmpeg.output(audio, video, 'out.mp4')
"""
if self.selector is not None:
raise ValueError('Stream already has a selector: {}'.format(self))
elif not isinstance(index, basestring):
raise TypeError("Expected string index (e.g. 'v'); got {!r}".format(index))
return self.node.stream(label=self.label, selector=index)
def get_stream_map(stream_spec): def get_stream_map(stream_spec):
@ -93,22 +93,6 @@ def get_stream_spec_nodes(stream_spec):
class Node(KwargReprNode): class Node(KwargReprNode):
"""Node base""" """Node base"""
@property
def min_inputs(self):
return self.__min_inputs
@property
def max_inputs(self):
return self.__max_inputs
@property
def incoming_stream_types(self):
return self.__incoming_stream_types
@property
def outgoing_stream_type(self):
return self.__outgoing_stream_type
@classmethod @classmethod
def __check_input_len(cls, stream_map, min_inputs, max_inputs): def __check_input_len(cls, stream_map, min_inputs, max_inputs):
if min_inputs is not None and len(stream_map) < min_inputs: if min_inputs is not None and len(stream_map) < min_inputs:
@ -130,9 +114,8 @@ class Node(KwargReprNode):
incoming_edge_map[downstream_label] = (upstream.node, upstream.label, upstream.selector) incoming_edge_map[downstream_label] = (upstream.node, upstream.label, upstream.selector)
return incoming_edge_map return incoming_edge_map
def __init_fromscratch__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, def __init__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs,
max_inputs, args=[], max_inputs, args=[], kwargs={}):
kwargs={}):
stream_map = 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)
@ -141,92 +124,21 @@ class Node(KwargReprNode):
super(Node, self).__init__(incoming_edge_map, name, args, kwargs) super(Node, self).__init__(incoming_edge_map, name, args, kwargs)
self.__outgoing_stream_type = outgoing_stream_type self.__outgoing_stream_type = outgoing_stream_type
self.__incoming_stream_types = incoming_stream_types self.__incoming_stream_types = incoming_stream_types
self.__min_inputs = min_inputs
self.__max_inputs = max_inputs
def __init_fromnode__(self, old_node, stream_spec): def stream(self, label=None, selector=None):
# Make sure old node and new node are of the same type
if type(self) != type(old_node):
raise TypeError('`old_node` should be of type {}'.format(self.__class__.__name__))
# Copy needed data from old node
name = old_node.name
incoming_stream_types = old_node.incoming_stream_types
outgoing_stream_type = old_node.outgoing_stream_type
min_inputs = old_node.min_inputs
max_inputs = old_node.max_inputs
prev_edges = old_node.incoming_edge_map.values()
args = old_node.args
kwargs = old_node.kwargs
# Check new stream spec - the old spec should have already been checked
new_stream_map = get_stream_map(stream_spec)
self.__check_input_types(new_stream_map, incoming_stream_types)
# Generate new edge map
new_inc_edge_map = self.__get_incoming_edge_map(new_stream_map)
new_edges = new_inc_edge_map.values()
# Rename all edges
new_edge_map = dict(enumerate(list(prev_edges) + list(new_edges)))
# Check new length
self.__check_input_len(new_edge_map, min_inputs, max_inputs)
super(Node, self).__init__(new_edge_map, name, args, kwargs)
self.__outgoing_stream_type = outgoing_stream_type
self.__incoming_stream_types = incoming_stream_types
self.__min_inputs = min_inputs
self.__max_inputs = max_inputs
# noinspection PyMissingConstructor
def __init__(self, *args, **kwargs):
"""
If called with the following arguments, the new Node is created from scratch:
- stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, max_inputs, args=[], kwargs={}
If called with the following arguments, the new node is a copy of `old_node` that includes the additional
`stream_spec`:
- old_node, stream_spec
"""
# Python doesn't support constructor overloading. This hacky code detects how we want to construct the object
# based on the number of arguments and the type of the first argument, then calls the appropriate constructor
# helper method
# "1+" is for `self`
argc = 1 + len(args) + len(kwargs)
first_arg = None
if 'old_node' in kwargs:
first_arg = kwargs['old_node']
elif len(args) > 0:
first_arg = args[0]
if argc == _get_arg_count(self.__init_fromnode__) and type(first_arg) == type(self):
self.__init_fromnode__(*args, **kwargs)
else:
if isinstance(first_arg, Node):
raise ValueError(
'{}.__init__() received an instance of {} as the first argument. If you want to create a '
'copy of an existing node, the types must match and you must provide an additional stream_spec.'
.format(self.__class__.__name__, first_arg.__class__.__name__)
)
self.__init_fromscratch__(*args, **kwargs)
def stream(self, label=None, select=None):
"""Create an outgoing stream originating from this node. """Create an outgoing stream originating from this node.
More nodes may be attached onto the outgoing stream. More nodes may be attached onto the outgoing stream.
""" """
return self.__outgoing_stream_type(self, label, upstream_selector=select) return self.__outgoing_stream_type(self, label, upstream_selector=selector)
def __getitem__(self, item): def __getitem__(self, item):
"""Create an outgoing stream originating from this node; syntactic sugar for ``self.stream(label)``. """Create an outgoing stream originating from this node; syntactic sugar for ``self.stream(label)``.
It can also be used to apply a selector: e.g. ``node[0:'audio']`` returns a stream with label 0 and It can also be used to apply a selector: e.g. ``node[0:'audio']`` returns a stream with label 0 and
selector ``'audio'``, which is the same as ``node.stream(label=0, select='audio')``. selector ``'audio'``, which is the same as ``node.stream(label=0, selector='audio')``.
""" """
if isinstance(item, slice): if isinstance(item, slice):
return self.stream(label=item.start, select=item.stop) return self.stream(label=item.start, selector=item.stop)
else: else:
return self.stream(label=item) return self.stream(label=item)
@ -241,8 +153,8 @@ class FilterableStream(Stream):
class InputNode(Node): class InputNode(Node):
"""InputNode type""" """InputNode type"""
def __init_fromscratch__(self, name, args=[], kwargs={}): def __init__(self, name, args=[], kwargs={}):
super(InputNode, self).__init_fromscratch__( super(InputNode, self).__init__(
stream_spec=None, stream_spec=None,
name=name, name=name,
incoming_stream_types={}, incoming_stream_types={},
@ -253,9 +165,6 @@ class InputNode(Node):
kwargs=kwargs kwargs=kwargs
) )
def __init_fromnode__(self, old_node, stream_spec):
raise TypeError("{} can't be constructed from an existing node".format(self.__class__.__name__))
@property @property
def short_repr(self): def short_repr(self):
return os.path.basename(self.kwargs['filename']) return os.path.basename(self.kwargs['filename'])
@ -263,8 +172,8 @@ class InputNode(Node):
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
class FilterNode(Node): class FilterNode(Node):
def __init_fromscratch__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}):
super(FilterNode, self).__init_fromscratch__( super(FilterNode, self).__init__(
stream_spec=stream_spec, stream_spec=stream_spec,
name=name, name=name,
incoming_stream_types={FilterableStream}, incoming_stream_types={FilterableStream},
@ -303,13 +212,13 @@ class FilterNode(Node):
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
class OutputNode(Node): class OutputNode(Node):
def __init_fromscratch__(self, stream, name, args=[], kwargs={}): def __init__(self, stream, name, args=[], kwargs={}):
super(OutputNode, self).__init_fromscratch__( super(OutputNode, self).__init__(
stream_spec=stream, stream_spec=stream,
name=name, name=name,
incoming_stream_types={FilterableStream}, incoming_stream_types={FilterableStream},
outgoing_stream_type=OutputStream, outgoing_stream_type=OutputStream,
min_inputs=0, # Allow streams to be mapped afterwards min_inputs=1,
max_inputs=None, max_inputs=None,
args=args, args=args,
kwargs=kwargs kwargs=kwargs
@ -328,8 +237,8 @@ class OutputStream(Stream):
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
class MergeOutputsNode(Node): class MergeOutputsNode(Node):
def __init_fromscratch__(self, streams, name): def __init__(self, streams, name):
super(MergeOutputsNode, self).__init_fromscratch__( super(MergeOutputsNode, self).__init__(
stream_spec=streams, stream_spec=streams,
name=name, name=name,
incoming_stream_types={OutputStream}, incoming_stream_types={OutputStream},
@ -341,8 +250,8 @@ class MergeOutputsNode(Node):
# noinspection PyMethodOverriding # noinspection PyMethodOverriding
class GlobalNode(Node): class GlobalNode(Node):
def __init_fromscratch__(self, stream, name, args=[], kwargs={}): def __init__(self, stream, name, args=[], kwargs={}):
super(GlobalNode, self).__init_fromscratch__( super(GlobalNode, self).__init__(
stream_spec=stream, stream_spec=stream,
name=name, name=name,
incoming_stream_types={OutputStream}, incoming_stream_types={OutputStream},
@ -353,9 +262,6 @@ class GlobalNode(Node):
kwargs=kwargs kwargs=kwargs
) )
def __init_fromnode__(self, old_node, stream_spec):
raise TypeError("{} can't be constructed from an existing node".format(self.__class__.__name__))
def stream_operator(stream_classes={Stream}, name=None): def stream_operator(stream_classes={Stream}, name=None):
def decorator(func): def decorator(func):

View File

@ -147,16 +147,25 @@ def test_get_args_complex_filter():
] ]
def _get_filter_with_select_example(): def test_combined_output():
i1 = ffmpeg.input(TEST_INPUT_FILE1)
i2 = ffmpeg.input(TEST_OVERLAY_FILE)
out = ffmpeg.output(i1, i2, TEST_OUTPUT_FILE1)
assert out.get_args() == [
'-i', TEST_INPUT_FILE1,
'-i', TEST_OVERLAY_FILE,
'-map', '[0]',
'-map', '[1]',
TEST_OUTPUT_FILE1
]
def test_filter_with_selector():
i = ffmpeg.input(TEST_INPUT_FILE1) i = ffmpeg.input(TEST_INPUT_FILE1)
v1 = i[:'v'].hflip() v1 = i['v'].hflip()
a1 = i[:'a'].filter_('aecho', 0.8, 0.9, 1000, 0.3) a1 = i['a'].filter_('aecho', 0.8, 0.9, 1000, 0.3)
out = ffmpeg.output(a1, v1, TEST_OUTPUT_FILE1)
return ffmpeg.output(a1, v1, TEST_OUTPUT_FILE1) assert out.get_args() == [
def test_filter_with_select():
assert _get_filter_with_select_example().get_args() == [
'-i', TEST_INPUT_FILE1, '-i', TEST_INPUT_FILE1,
'-filter_complex', '-filter_complex',
'[0:a]aecho=0.8:0.9:1000:0.3[s0];' \ '[0:a]aecho=0.8:0.9:1000:0.3[s0];' \
@ -166,26 +175,6 @@ def test_filter_with_select():
] ]
def test_map_same_effect_as_output():
i1 = ffmpeg.input(TEST_INPUT_FILE1)
i2 = ffmpeg.input(TEST_OVERLAY_FILE)
_o_map = i1.output(TEST_OUTPUT_FILE1)
o_map = _o_map.map(i2)
o_nomap = ffmpeg.output(i1, i2, TEST_OUTPUT_FILE1)
assert id(o_map) != id(_o_map) # Checks immutability
assert o_map.node.incoming_edge_map == o_nomap.node.incoming_edge_map
assert o_map.get_args() == o_nomap.get_args() == [
'-i', TEST_INPUT_FILE1,
'-i', TEST_OVERLAY_FILE,
'-map', '[0]',
'-map', '[1]',
TEST_OUTPUT_FILE1
]
def _get_complex_filter_asplit_example(): def _get_complex_filter_asplit_example():
split = (ffmpeg split = (ffmpeg
.input(TEST_INPUT_FILE1) .input(TEST_INPUT_FILE1)