1
0
mirror of https://github.com/kkroening/ffmpeg-python.git synced 2025-04-06 04:15:44 +08:00

Reimplement .map logic making Node immutable

This commit is contained in:
Davide Depau 2018-01-09 14:45:27 +01:00
parent 861980db0b
commit 497105f929
No known key found for this signature in database
GPG Key ID: C7D999B6A55EFE86
3 changed files with 122 additions and 44 deletions

@ -83,9 +83,7 @@ def map(*streams):
if not tail:
return head
head.node._add_streams(tail)
return head
return OutputNode(head.node, tail).stream()
__all__ = [

@ -1,9 +1,12 @@
from __future__ import unicode_literals
import warnings
from .dag import KwargReprNode
from ._utils import escape_chars, get_hash_int
from builtins import object
import os
import os, sys
import inspect
def _is_of_types(obj, types):
@ -19,6 +22,13 @@ def _get_types_str(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):
"""Represents the outgoing edge of an upstream node; may be used to create more downstream nodes."""
@ -30,6 +40,7 @@ class Stream(object):
self.label = upstream_label
self.selector = upstream_selector
def __hash__(self):
return get_hash_int([hash(self.node), hash(self.label)])
@ -85,6 +96,22 @@ def get_stream_spec_nodes(stream_spec):
class Node(KwargReprNode):
"""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
def __check_input_len(cls, stream_map, min_inputs, max_inputs):
if min_inputs is not None and len(stream_map) < min_inputs:
@ -106,19 +133,85 @@ class Node(KwargReprNode):
incoming_edge_map[downstream_label] = (upstream.node, upstream.label, upstream.selector)
return incoming_edge_map
def __init__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, max_inputs, args=[],
kwargs={}):
def __init_fromscratch__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs,
max_inputs, args=[],
kwargs={}):
stream_map = get_stream_map(stream_spec)
self.__check_input_len(stream_map, min_inputs, max_inputs)
self.__check_input_types(stream_map, incoming_stream_types)
incoming_edge_map = self.__get_incoming_edge_map(stream_map)
super(Node, self).__init__(incoming_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
def __init_fromnode__(self, old_node, stream_spec):
# Make sure old node and new node are of the same type
if type(self) != type(old_node):
raise ValueError("'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 = "old_node" in kwargs and kwargs["old_node"] or 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.
@ -136,31 +229,6 @@ class Node(KwargReprNode):
else:
return self.stream(label=item)
def _add_streams(self, stream_spec):
"""Attach additional streams after the Node is initialized.
"""
# Back up previous edges
prev_edges = self.incoming_edge_map.values()
# Check new edges
new_stream_map = get_stream_map(stream_spec)
self.__check_input_types(new_stream_map, self.__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, self.__min_inputs, self.__max_inputs)
# Overwrite old map (exploiting the fact that dict is mutable; incoming_edge_map is a read-only property)
if None in self.incoming_edge_map:
self.incoming_edge_map.pop(None)
self.incoming_edge_map.update(new_edge_map)
class FilterableStream(Stream):
def __init__(self, upstream_node, upstream_label, upstream_selector=None):
@ -168,11 +236,12 @@ class FilterableStream(Stream):
upstream_selector)
# noinspection PyMethodOverriding
class InputNode(Node):
"""InputNode type"""
def __init__(self, name, args=[], kwargs={}):
super(InputNode, self).__init__(
def __init_fromscratch__(self, name, args=[], kwargs={}):
super(InputNode, self).__init_fromscratch__(
stream_spec=None,
name=name,
incoming_stream_types={},
@ -183,14 +252,18 @@ class InputNode(Node):
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
def short_repr(self):
return os.path.basename(self.kwargs['filename'])
# noinspection PyMethodOverriding
class FilterNode(Node):
def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}):
super(FilterNode, self).__init__(
def __init_fromscratch__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}):
super(FilterNode, self).__init_fromscratch__(
stream_spec=stream_spec,
name=name,
incoming_stream_types={FilterableStream},
@ -227,9 +300,10 @@ class FilterNode(Node):
return escape_chars(params_text, '\\\'[],;')
# noinspection PyMethodOverriding
class OutputNode(Node):
def __init__(self, stream, name, args=[], kwargs={}):
super(OutputNode, self).__init__(
def __init_fromscratch__(self, stream, name, args=[], kwargs={}):
super(OutputNode, self).__init_fromscratch__(
stream_spec=stream,
name=name,
incoming_stream_types={FilterableStream},
@ -251,9 +325,10 @@ class OutputStream(Stream):
upstream_selector=upstream_selector)
# noinspection PyMethodOverriding
class MergeOutputsNode(Node):
def __init__(self, streams, name):
super(MergeOutputsNode, self).__init__(
def __init_fromscratch__(self, streams, name):
super(MergeOutputsNode, self).__init_fromscratch__(
stream_spec=streams,
name=name,
incoming_stream_types={OutputStream},
@ -263,9 +338,10 @@ class MergeOutputsNode(Node):
)
# noinspection PyMethodOverriding
class GlobalNode(Node):
def __init__(self, stream, name, args=[], kwargs={}):
super(GlobalNode, self).__init__(
def __init_fromscratch__(self, stream, name, args=[], kwargs={}):
super(GlobalNode, self).__init_fromscratch__(
stream_spec=stream,
name=name,
incoming_stream_types={OutputStream},
@ -276,6 +352,9 @@ class GlobalNode(Node):
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 decorator(func):

@ -166,11 +166,12 @@ 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.map(i2)
_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,