Merge pull request #45 from Depau/stream_selectors

Stream selectors, `.map` operator (audio support)
This commit is contained in:
Karl Kroening 2018-05-09 01:28:10 -05:00 committed by GitHub
commit 8420f3b813
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 224 additions and 61 deletions

View File

@ -87,6 +87,7 @@ pip install ffmpeg-python
``` ```
It's also possible to clone the source and put it on your python path (`$PYTHONPATH`, `sys.path`, etc.): It's also possible to clone the source and put it on your python path (`$PYTHONPATH`, `sys.path`, etc.):
```bash ```bash
$ git clone git@github.com:kkroening/ffmpeg-python.git $ git clone git@github.com:kkroening/ffmpeg-python.git
$ export PYTHONPATH=${PYTHONPATH}:ffmpeg-python $ export PYTHONPATH=${PYTHONPATH}:ffmpeg-python
@ -99,6 +100,7 @@ $ python
API documentation is automatically generated from python docstrings and hosted on github pages: https://kkroening.github.io/ffmpeg-python/ API documentation is automatically generated from python docstrings and hosted on github pages: https://kkroening.github.io/ffmpeg-python/
Alternatively, standard python help is available, such as at the python REPL prompt as follows: Alternatively, standard python help is available, such as at the python REPL prompt as follows:
```python ```python
>>> import ffmpeg >>> import ffmpeg
>>> help(ffmpeg) >>> help(ffmpeg)

View File

@ -1,5 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from ._utils import basestring
from .nodes import ( from .nodes import (
filter_operator, filter_operator,
GlobalNode, GlobalNode,
@ -41,19 +43,29 @@ def merge_outputs(*streams):
@filter_operator() @filter_operator()
def output(stream, filename, **kwargs): def output(*streams_and_filename, **kwargs):
"""Output file URL """Output file URL
Syntax:
`ffmpeg.output(stream1[, stream2, stream3...], filename, **ffmpeg_args)`
If multiple streams are provided, they are mapped to the same output.
Official documentation: `Synopsis <https://ffmpeg.org/ffmpeg.html#Synopsis>`__ Official documentation: `Synopsis <https://ffmpeg.org/ffmpeg.html#Synopsis>`__
""" """
kwargs['filename'] = filename streams_and_filename = list(streams_and_filename)
if 'filename' not in kwargs:
if not isinstance(streams_and_filename[-1], basestring):
raise ValueError('A filename must be provided')
kwargs['filename'] = streams_and_filename.pop(-1)
streams = streams_and_filename
fmt = kwargs.pop('f', None) fmt = kwargs.pop('f', None)
if fmt: if fmt:
if 'format' in kwargs: if 'format' in kwargs:
raise ValueError("Can't specify both `format` and `f` kwargs") raise ValueError("Can't specify both `format` and `f` kwargs")
kwargs['format'] = fmt kwargs['format'] = fmt
return OutputNode(stream, output.__name__, kwargs=kwargs).stream() return OutputNode(streams, output.__name__, kwargs=kwargs).stream()
__all__ = [ __all__ = [

View File

@ -2,7 +2,7 @@ from __future__ import unicode_literals
from .dag import get_outgoing_edges, topo_sort from .dag import get_outgoing_edges, topo_sort
from functools import reduce from functools import reduce
from past.builtins import basestring from ._utils import basestring
import copy import copy
import operator import operator
import subprocess as _subprocess import subprocess as _subprocess
@ -22,10 +22,6 @@ from .nodes import (
) )
def _get_stream_name(name):
return '[{}]'.format(name)
def _convert_kwargs_to_cmd_line_args(kwargs): def _convert_kwargs_to_cmd_line_args(kwargs):
args = [] args = []
for k in sorted(kwargs.keys()): for k in sorted(kwargs.keys()):
@ -54,11 +50,24 @@ def _get_input_args(input_node):
return args return args
def _format_input_stream_name(stream_name_map, edge):
prefix = stream_name_map[edge.upstream_node, edge.upstream_label]
if not edge.upstream_selector:
suffix = ''
else:
suffix = ':{}'.format(edge.upstream_selector)
return '[{}{}]'.format(prefix, suffix)
def _format_output_stream_name(stream_name_map, edge):
return '[{}]'.format(stream_name_map[edge.upstream_node, edge.upstream_label])
def _get_filter_spec(node, outgoing_edge_map, stream_name_map): def _get_filter_spec(node, outgoing_edge_map, stream_name_map):
incoming_edges = node.incoming_edges incoming_edges = node.incoming_edges
outgoing_edges = get_outgoing_edges(node, outgoing_edge_map) outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
inputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in incoming_edges] inputs = [_format_input_stream_name(stream_name_map, edge) for edge in incoming_edges]
outputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in outgoing_edges] outputs = [_format_output_stream_name(stream_name_map, edge) for edge in outgoing_edges]
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs)) filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs))
return filter_spec return filter_spec
@ -71,8 +80,8 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_
if len(downstreams) > 1: if len(downstreams) > 1:
# TODO: automatically insert `splits` ahead of time via graph transformation. # TODO: automatically insert `splits` ahead of time via graph transformation.
raise ValueError('Encountered {} with multiple outgoing edges with same upstream label {!r}; a ' raise ValueError('Encountered {} with multiple outgoing edges with same upstream label {!r}; a '
'`split` filter is probably required'.format(upstream_node, upstream_label)) '`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_name_map[upstream_node, upstream_label] = 's{}'.format(stream_count)
stream_count += 1 stream_count += 1
@ -93,11 +102,16 @@ def _get_output_args(node, stream_name_map):
if node.name != output.__name__: if node.name != output.__name__:
raise ValueError('Unsupported output node: {}'.format(node)) raise ValueError('Unsupported output node: {}'.format(node))
args = [] args = []
assert len(node.incoming_edges) == 1
edge = node.incoming_edges[0] if len(node.incoming_edges) == 0:
stream_name = stream_name_map[edge.upstream_node, edge.upstream_label] raise ValueError('Output node {} has no mapped streams'.format(node))
if stream_name != '[0]':
args += ['-map', stream_name] for edge in node.incoming_edges:
# edge = node.incoming_edges[0]
stream_name = _format_input_stream_name(stream_name_map, edge)
if stream_name != '[0]' or len(node.incoming_edges) > 1:
args += ['-map', stream_name]
kwargs = copy.copy(node.kwargs) kwargs = copy.copy(node.kwargs)
filename = kwargs.pop('filename') filename = kwargs.pop('filename')
fmt = kwargs.pop('format', None) fmt = kwargs.pop('format', None)
@ -119,7 +133,7 @@ def get_args(stream_spec, overwrite_output=False):
output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)] output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)]
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 isinstance(node, FilterNode)] filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)]
stream_name_map = {(node, None): _get_stream_name(i) for i, node in enumerate(input_nodes)} stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)}
filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, 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:

View File

@ -1,8 +1,42 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from builtins import str
from past.builtins import basestring
import hashlib import hashlib
import sys
if sys.version_info.major == 2:
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
str = unicode
# `past.builtins.basestring` module can't be imported on Python3 in some environments (Ubuntu).
# This code is copy-pasted from it to avoid crashes.
class BaseBaseString(type):
def __instancecheck__(cls, instance):
return isinstance(instance, (bytes, str))
def __subclasshook__(cls, thing):
# TODO: What should go here?
raise NotImplemented
def with_metaclass(meta, *bases):
class metaclass(meta):
__call__ = type.__call__
__init__ = type.__init__
def __new__(cls, name, this_bases, d):
if this_bases is None:
return type.__new__(cls, name, (), d)
return meta(name, bases, d)
return metaclass('temporary_class', None, {})
if sys.version_info.major >= 3:
class basestring(with_metaclass(BaseBaseString)):
pass
else:
# noinspection PyUnresolvedReferences,PyCompatibility
from builtins import basestring
def _recursive_repr(item): def _recursive_repr(item):
@ -27,6 +61,7 @@ def get_hash(item):
repr_ = _recursive_repr(item).encode('utf-8') repr_ = _recursive_repr(item).encode('utf-8')
return hashlib.md5(repr_).hexdigest() return hashlib.md5(repr_).hexdigest()
def get_hash_int(item): def get_hash_int(item):
return int(get_hash(item), base=16) return int(get_hash(item), base=16)

View File

@ -3,7 +3,6 @@ from __future__ import unicode_literals
from builtins import str from builtins import str
from .dag import get_outgoing_edges from .dag import get_outgoing_edges
from ._run import topo_sort from ._run import topo_sort
import os
import tempfile import tempfile
from ffmpeg.nodes import ( from ffmpeg.nodes import (
@ -11,7 +10,6 @@ from ffmpeg.nodes import (
get_stream_spec_nodes, get_stream_spec_nodes,
InputNode, InputNode,
OutputNode, OutputNode,
Stream,
stream_operator, stream_operator,
) )
@ -62,9 +60,13 @@ def view(stream_spec, **kwargs):
kwargs = {} kwargs = {}
up_label = edge.upstream_label up_label = edge.upstream_label
down_label = edge.downstream_label down_label = edge.downstream_label
if show_labels and (up_label is not None or down_label is not None): up_selector = edge.upstream_selector
if show_labels and (up_label is not None or down_label is not None or up_selector is not None):
if up_label is None: if up_label is None:
up_label = '' up_label = ''
if up_selector is not None:
up_label += ":" + up_selector
if down_label is None: if down_label is None:
down_label = '' down_label = ''
if up_label != '' and down_label != '': if up_label != '' and down_label != '':

View File

@ -42,6 +42,7 @@ class DagNode(object):
Again, because nodes are immutable, the string representations should remain constant. Again, because nodes are immutable, the string representations should remain constant.
""" """
def __hash__(self): def __hash__(self):
"""Return an integer hash of the node.""" """Return an integer hash of the node."""
raise NotImplementedError() raise NotImplementedError()
@ -69,32 +70,36 @@ class DagNode(object):
raise NotImplementedError() raise NotImplementedError()
DagEdge = namedtuple('DagEdge', ['downstream_node', 'downstream_label', 'upstream_node', 'upstream_label']) DagEdge = namedtuple('DagEdge', ['downstream_node', 'downstream_label', 'upstream_node', 'upstream_label', 'upstream_selector'])
def get_incoming_edges(downstream_node, incoming_edge_map): def get_incoming_edges(downstream_node, incoming_edge_map):
edges = [] edges = []
for downstream_label, (upstream_node, upstream_label) in list(incoming_edge_map.items()): for downstream_label, upstream_info in incoming_edge_map.items():
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)] upstream_node, upstream_label, upstream_selector = upstream_info
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, upstream_selector)]
return edges return edges
def get_outgoing_edges(upstream_node, outgoing_edge_map): 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_node, downstream_label) in downstream_infos: for downstream_info in downstream_infos:
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)] downstream_node, downstream_label, downstream_selector = downstream_info
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, downstream_selector)]
return edges return edges
class KwargReprNode(DagNode): class KwargReprNode(DagNode):
"""A DagNode that can be represented as a set of args+kwargs. """A DagNode that can be represented as a set of args+kwargs.
""" """
@property @property
def __upstream_hashes(self): def __upstream_hashes(self):
hashes = [] hashes = []
for downstream_label, (upstream_node, upstream_label) in list(self.incoming_edge_map.items()): for downstream_label, upstream_info in self.incoming_edge_map.items():
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label]] upstream_node, upstream_label, upstream_selector = upstream_info
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label, upstream_selector]]
return hashes return hashes
@property @property
@ -152,21 +157,21 @@ def topo_sort(downstream_nodes):
sorted_nodes = [] sorted_nodes = []
outgoing_edge_maps = {} outgoing_edge_maps = {}
def visit(upstream_node, upstream_label, downstream_node, downstream_label): def visit(upstream_node, upstream_label, downstream_node, downstream_label, downstream_selector=None):
if upstream_node in marked_nodes: if upstream_node in marked_nodes:
raise RuntimeError('Graph is not a DAG') raise RuntimeError('Graph is not a DAG')
if downstream_node is not None: if downstream_node is not None:
outgoing_edge_map = outgoing_edge_maps.get(upstream_node, {}) outgoing_edge_map = outgoing_edge_maps.get(upstream_node, {})
outgoing_edge_infos = outgoing_edge_map.get(upstream_label, []) outgoing_edge_infos = outgoing_edge_map.get(upstream_label, [])
outgoing_edge_infos += [(downstream_node, downstream_label)] outgoing_edge_infos += [(downstream_node, downstream_label, downstream_selector)]
outgoing_edge_map[upstream_label] = outgoing_edge_infos outgoing_edge_map[upstream_label] = outgoing_edge_infos
outgoing_edge_maps[upstream_node] = outgoing_edge_map outgoing_edge_maps[upstream_node] = outgoing_edge_map
if upstream_node not in sorted_nodes: if upstream_node not in sorted_nodes:
marked_nodes.append(upstream_node) marked_nodes.append(upstream_node)
for edge in upstream_node.incoming_edges: for edge in upstream_node.incoming_edges:
visit(edge.upstream_node, edge.upstream_label, edge.downstream_node, edge.downstream_label) visit(edge.upstream_node, edge.upstream_label, edge.downstream_node, edge.downstream_label, edge.upstream_selector)
marked_nodes.remove(upstream_node) marked_nodes.remove(upstream_node)
sorted_nodes.append(upstream_node) sorted_nodes.append(upstream_node)

View File

@ -21,12 +21,14 @@ def _get_types_str(types):
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."""
def __init__(self, upstream_node, upstream_label, node_types):
def __init__(self, upstream_node, upstream_label, node_types, upstream_selector=None):
if not _is_of_types(upstream_node, node_types): if not _is_of_types(upstream_node, node_types):
raise TypeError('Expected upstream node to be of one of the following type(s): {}; got {}'.format( raise TypeError('Expected upstream node to be of one of the following type(s): {}; got {}'.format(
_get_types_str(node_types), type(upstream_node))) _get_types_str(node_types), type(upstream_node)))
self.node = upstream_node self.node = upstream_node
self.label = upstream_label self.label = upstream_label
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)])
@ -36,9 +38,30 @@ class Stream(object):
def __repr__(self): def __repr__(self):
node_repr = self.node.long_repr(include_hash=False) node_repr = self.node.long_repr(include_hash=False)
out = '{}[{!r}] <{}>'.format(node_repr, self.label, self.node.short_hash) selector = ''
if self.selector:
selector = ':{}'.format(self.selector)
out = '{}[{!r}{}] <{}>'.format(node_repr, self.label, selector, self.node.short_hash)
return out return out
def __getitem__(self, index):
"""
Select a component (audio, video) of the stream.
Example:
Process the audio and video portions of a stream independently::
input = ffmpeg.input('in.mp4')
audio = input[:'a'].filter_("aecho", 0.8, 0.9, 1000, 0.3)
video = input[:'v'].hflip()
out = 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. 'a'); 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):
if stream_spec is None: if stream_spec is None:
@ -68,6 +91,7 @@ def get_stream_spec_nodes(stream_spec):
class Node(KwargReprNode): class Node(KwargReprNode):
"""Node base""" """Node base"""
@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:
@ -80,44 +104,62 @@ class Node(KwargReprNode):
for stream in list(stream_map.values()): for stream in list(stream_map.values()):
if not _is_of_types(stream, incoming_stream_types): if not _is_of_types(stream, incoming_stream_types):
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 @classmethod
def __get_incoming_edge_map(cls, stream_map): def __get_incoming_edge_map(cls, stream_map):
incoming_edge_map = {} incoming_edge_map = {}
for downstream_label, upstream in list(stream_map.items()): for downstream_label, upstream in list(stream_map.items()):
incoming_edge_map[downstream_label] = (upstream.node, upstream.label) incoming_edge_map[downstream_label] = (upstream.node, upstream.label, upstream.selector)
return incoming_edge_map return incoming_edge_map
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,
kwargs={}): max_inputs, args=[], 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)
incoming_edge_map = self.__get_incoming_edge_map(stream_map) incoming_edge_map = self.__get_incoming_edge_map(stream_map)
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
def stream(self, label=None): def stream(self, label=None, selector=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) return self.__outgoing_stream_type(self, label, upstream_selector=selector)
def __getitem__(self, label): 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:'a']`` returns a stream with label 0 and
selector ``'a'``, which is the same as ``node.stream(label=0, selector='a')``.
Example:
Process the audio and video portions of a stream independently::
input = ffmpeg.input('in.mp4')
audio = input[:'a'].filter_("aecho", 0.8, 0.9, 1000, 0.3)
video = input[:'v'].hflip()
out = ffmpeg.output(audio, video, 'out.mp4')
""" """
return self.stream(label) if isinstance(item, slice):
return self.stream(label=item.start, selector=item.stop)
else:
return self.stream(label=item)
class FilterableStream(Stream): class FilterableStream(Stream):
def __init__(self, upstream_node, upstream_label): def __init__(self, upstream_node, upstream_label, upstream_selector=None):
super(FilterableStream, self).__init__(upstream_node, upstream_label, {InputNode, FilterNode}) super(FilterableStream, self).__init__(upstream_node, upstream_label, {InputNode, FilterNode},
upstream_selector)
# noinspection PyMethodOverriding
class InputNode(Node): class InputNode(Node):
"""InputNode type""" """InputNode type"""
def __init__(self, name, args=[], kwargs={}): def __init__(self, name, args=[], kwargs={}):
super(InputNode, self).__init__( super(InputNode, self).__init__(
stream_spec=None, stream_spec=None,
@ -135,6 +177,7 @@ class InputNode(Node):
return os.path.basename(self.kwargs['filename']) return os.path.basename(self.kwargs['filename'])
# noinspection PyMethodOverriding
class FilterNode(Node): class FilterNode(Node):
def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}):
super(FilterNode, self).__init__( super(FilterNode, self).__init__(
@ -149,6 +192,7 @@ class FilterNode(Node):
) )
"""FilterNode""" """FilterNode"""
def _get_filter(self, outgoing_edges): def _get_filter(self, outgoing_edges):
args = self.args args = self.args
kwargs = self.kwargs kwargs = self.kwargs
@ -173,6 +217,7 @@ class FilterNode(Node):
return escape_chars(params_text, '\\\'[],;') return escape_chars(params_text, '\\\'[],;')
# noinspection PyMethodOverriding
class OutputNode(Node): class OutputNode(Node):
def __init__(self, stream, name, args=[], kwargs={}): def __init__(self, stream, name, args=[], kwargs={}):
super(OutputNode, self).__init__( super(OutputNode, self).__init__(
@ -181,7 +226,7 @@ class OutputNode(Node):
incoming_stream_types={FilterableStream}, incoming_stream_types={FilterableStream},
outgoing_stream_type=OutputStream, outgoing_stream_type=OutputStream,
min_inputs=1, min_inputs=1,
max_inputs=1, max_inputs=None,
args=args, args=args,
kwargs=kwargs kwargs=kwargs
) )
@ -192,10 +237,12 @@ class OutputNode(Node):
class OutputStream(Stream): class OutputStream(Stream):
def __init__(self, upstream_node, upstream_label): def __init__(self, upstream_node, upstream_label, upstream_selector=None):
super(OutputStream, self).__init__(upstream_node, upstream_label, {OutputNode, GlobalNode, MergeOutputsNode}) super(OutputStream, self).__init__(upstream_node, upstream_label, {OutputNode, GlobalNode, MergeOutputsNode},
upstream_selector=upstream_selector)
# noinspection PyMethodOverriding
class MergeOutputsNode(Node): class MergeOutputsNode(Node):
def __init__(self, streams, name): def __init__(self, streams, name):
super(MergeOutputsNode, self).__init__( super(MergeOutputsNode, self).__init__(
@ -208,6 +255,7 @@ class MergeOutputsNode(Node):
) )
# noinspection PyMethodOverriding
class GlobalNode(Node): class GlobalNode(Node):
def __init__(self, stream, name, args=[], kwargs={}): def __init__(self, stream, name, args=[], kwargs={}):
super(GlobalNode, self).__init__( super(GlobalNode, self).__init__(
@ -227,6 +275,7 @@ def stream_operator(stream_classes={Stream}, name=None):
func_name = name or func.__name__ func_name = name or func.__name__
[setattr(stream_class, func_name, func) for stream_class in stream_classes] [setattr(stream_class, func_name, func) for stream_class in stream_classes]
return func return func
return decorator return decorator

View File

@ -82,21 +82,21 @@ def test_node_repr():
trim3 = ffmpeg.trim(in_file, start_frame=50, end_frame=60) trim3 = ffmpeg.trim(in_file, start_frame=50, end_frame=60)
concatted = ffmpeg.concat(trim1, trim2, trim3) concatted = ffmpeg.concat(trim1, trim2, trim3)
output = ffmpeg.output(concatted, 'dummy2.mp4') output = ffmpeg.output(concatted, 'dummy2.mp4')
assert repr(in_file.node) == "input(filename={!r}) <{}>".format('dummy.mp4', in_file.node.short_hash) assert repr(in_file.node) == 'input(filename={!r}) <{}>'.format('dummy.mp4', in_file.node.short_hash)
assert repr(trim1.node) == "trim(end_frame=20, start_frame=10) <{}>".format(trim1.node.short_hash) assert repr(trim1.node) == 'trim(end_frame=20, start_frame=10) <{}>'.format(trim1.node.short_hash)
assert repr(trim2.node) == "trim(end_frame=40, start_frame=30) <{}>".format(trim2.node.short_hash) assert repr(trim2.node) == 'trim(end_frame=40, start_frame=30) <{}>'.format(trim2.node.short_hash)
assert repr(trim3.node) == "trim(end_frame=60, start_frame=50) <{}>".format(trim3.node.short_hash) assert repr(trim3.node) == 'trim(end_frame=60, start_frame=50) <{}>'.format(trim3.node.short_hash)
assert repr(concatted.node) == "concat(n=3) <{}>".format(concatted.node.short_hash) assert repr(concatted.node) == 'concat(n=3) <{}>'.format(concatted.node.short_hash)
assert repr(output.node) == "output(filename={!r}) <{}>".format('dummy2.mp4', output.node.short_hash) assert repr(output.node) == 'output(filename={!r}) <{}>'.format('dummy2.mp4', output.node.short_hash)
def test_stream_repr(): def test_stream_repr():
in_file = ffmpeg.input('dummy.mp4') in_file = ffmpeg.input('dummy.mp4')
assert repr(in_file) == "input(filename={!r})[None] <{}>".format('dummy.mp4', in_file.node.short_hash) assert repr(in_file) == 'input(filename={!r})[None] <{}>'.format('dummy.mp4', in_file.node.short_hash)
split0 = in_file.filter_multi_output('split')[0] split0 = in_file.filter_multi_output('split')[0]
assert repr(split0) == "split()[0] <{}>".format(split0.node.short_hash) assert repr(split0) == 'split()[0] <{}>'.format(split0.node.short_hash)
dummy_out = in_file.filter_multi_output('dummy')['out'] dummy_out = in_file.filter_multi_output('dummy')['out']
assert repr(dummy_out) == "dummy()[{!r}] <{}>".format(dummy_out.label, dummy_out.node.short_hash) assert repr(dummy_out) == 'dummy()[{!r}] <{}>'.format(dummy_out.label, dummy_out.node.short_hash)
def test_get_args_simple(): def test_get_args_simple():
@ -147,6 +147,50 @@ def test_get_args_complex_filter():
] ]
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)
v1 = i['v'].hflip()
a1 = i['a'].filter_('aecho', 0.8, 0.9, 1000, 0.3)
out = ffmpeg.output(a1, v1, TEST_OUTPUT_FILE1)
assert out.get_args() == [
'-i', TEST_INPUT_FILE1,
'-filter_complex',
'[0:a]aecho=0.8:0.9:1000:0.3[s0];' \
'[0:v]hflip[s1]',
'-map', '[s0]', '-map', '[s1]',
TEST_OUTPUT_FILE1
]
def test_get_item_with_bad_selectors():
input = ffmpeg.input(TEST_INPUT_FILE1)
with pytest.raises(ValueError) as excinfo:
input['a']['a']
assert str(excinfo.value).startswith('Stream already has a selector:')
with pytest.raises(TypeError) as excinfo:
input[:'a']
assert str(excinfo.value).startswith("Expected string index (e.g. 'a')")
with pytest.raises(TypeError) as excinfo:
input[5]
assert str(excinfo.value).startswith("Expected string index (e.g. 'a')")
def _get_complex_filter_asplit_example(): def _get_complex_filter_asplit_example():
split = (ffmpeg split = (ffmpeg
.input(TEST_INPUT_FILE1) .input(TEST_INPUT_FILE1)
@ -158,8 +202,8 @@ def _get_complex_filter_asplit_example():
return (ffmpeg return (ffmpeg
.concat( .concat(
split0.filter_("atrim", start=10, end=20), split0.filter_('atrim', start=10, end=20),
split1.filter_("atrim", start=30, end=40), split1.filter_('atrim', start=30, end=40),
) )
.output(TEST_OUTPUT_FILE1) .output(TEST_OUTPUT_FILE1)
.overwrite_output() .overwrite_output()