mirror of
https://github.com/kkroening/ffmpeg-python.git
synced 2025-04-05 04:22:51 +08:00
Merge pull request #45 from Depau/stream_selectors
Stream selectors, `.map` operator (audio support)
This commit is contained in:
commit
8420f3b813
@ -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.):
|
||||
|
||||
```bash
|
||||
$ git clone git@github.com:kkroening/ffmpeg-python.git
|
||||
$ 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/
|
||||
|
||||
Alternatively, standard python help is available, such as at the python REPL prompt as follows:
|
||||
|
||||
```python
|
||||
>>> import ffmpeg
|
||||
>>> help(ffmpeg)
|
||||
|
@ -1,5 +1,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from ._utils import basestring
|
||||
|
||||
from .nodes import (
|
||||
filter_operator,
|
||||
GlobalNode,
|
||||
@ -41,19 +43,29 @@ def merge_outputs(*streams):
|
||||
|
||||
|
||||
@filter_operator()
|
||||
def output(stream, filename, **kwargs):
|
||||
def output(*streams_and_filename, **kwargs):
|
||||
"""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>`__
|
||||
"""
|
||||
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)
|
||||
if fmt:
|
||||
if 'format' in kwargs:
|
||||
raise ValueError("Can't specify both `format` and `f` kwargs")
|
||||
kwargs['format'] = fmt
|
||||
return OutputNode(stream, output.__name__, kwargs=kwargs).stream()
|
||||
|
||||
return OutputNode(streams, output.__name__, kwargs=kwargs).stream()
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
@ -2,7 +2,7 @@ from __future__ import unicode_literals
|
||||
|
||||
from .dag import get_outgoing_edges, topo_sort
|
||||
from functools import reduce
|
||||
from past.builtins import basestring
|
||||
from ._utils import basestring
|
||||
import copy
|
||||
import operator
|
||||
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):
|
||||
args = []
|
||||
for k in sorted(kwargs.keys()):
|
||||
@ -54,11 +50,24 @@ def _get_input_args(input_node):
|
||||
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):
|
||||
incoming_edges = node.incoming_edges
|
||||
outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
|
||||
inputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in incoming_edges]
|
||||
outputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in outgoing_edges]
|
||||
inputs = [_format_input_stream_name(stream_name_map, edge) for edge in incoming_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))
|
||||
return filter_spec
|
||||
|
||||
@ -71,8 +80,8 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_
|
||||
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))
|
||||
'`split` filter is probably required'.format(upstream_node, upstream_label))
|
||||
stream_name_map[upstream_node, upstream_label] = 's{}'.format(stream_count)
|
||||
stream_count += 1
|
||||
|
||||
|
||||
@ -93,11 +102,16 @@ def _get_output_args(node, stream_name_map):
|
||||
if node.name != output.__name__:
|
||||
raise ValueError('Unsupported output node: {}'.format(node))
|
||||
args = []
|
||||
assert len(node.incoming_edges) == 1
|
||||
edge = node.incoming_edges[0]
|
||||
stream_name = stream_name_map[edge.upstream_node, edge.upstream_label]
|
||||
if stream_name != '[0]':
|
||||
args += ['-map', stream_name]
|
||||
|
||||
if len(node.incoming_edges) == 0:
|
||||
raise ValueError('Output node {} has no mapped streams'.format(node))
|
||||
|
||||
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)
|
||||
filename = kwargs.pop('filename')
|
||||
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)]
|
||||
global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)]
|
||||
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)
|
||||
args += reduce(operator.add, [_get_input_args(node) for node in input_nodes])
|
||||
if filter_arg:
|
||||
|
@ -1,8 +1,42 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from builtins import str
|
||||
from past.builtins import basestring
|
||||
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):
|
||||
@ -27,6 +61,7 @@ def get_hash(item):
|
||||
repr_ = _recursive_repr(item).encode('utf-8')
|
||||
return hashlib.md5(repr_).hexdigest()
|
||||
|
||||
|
||||
def get_hash_int(item):
|
||||
return int(get_hash(item), base=16)
|
||||
|
||||
|
@ -3,7 +3,6 @@ from __future__ import unicode_literals
|
||||
from builtins import str
|
||||
from .dag import get_outgoing_edges
|
||||
from ._run import topo_sort
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from ffmpeg.nodes import (
|
||||
@ -11,7 +10,6 @@ from ffmpeg.nodes import (
|
||||
get_stream_spec_nodes,
|
||||
InputNode,
|
||||
OutputNode,
|
||||
Stream,
|
||||
stream_operator,
|
||||
)
|
||||
|
||||
@ -62,9 +60,13 @@ def view(stream_spec, **kwargs):
|
||||
kwargs = {}
|
||||
up_label = edge.upstream_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:
|
||||
up_label = ''
|
||||
if up_selector is not None:
|
||||
up_label += ":" + up_selector
|
||||
if down_label is None:
|
||||
down_label = ''
|
||||
if up_label != '' and down_label != '':
|
||||
|
@ -42,6 +42,7 @@ class DagNode(object):
|
||||
|
||||
Again, because nodes are immutable, the string representations should remain constant.
|
||||
"""
|
||||
|
||||
def __hash__(self):
|
||||
"""Return an integer hash of the node."""
|
||||
raise NotImplementedError()
|
||||
@ -69,32 +70,36 @@ class DagNode(object):
|
||||
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):
|
||||
edges = []
|
||||
for downstream_label, (upstream_node, upstream_label) in list(incoming_edge_map.items()):
|
||||
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)]
|
||||
for downstream_label, upstream_info in incoming_edge_map.items():
|
||||
upstream_node, upstream_label, upstream_selector = upstream_info
|
||||
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, upstream_selector)]
|
||||
return edges
|
||||
|
||||
|
||||
def get_outgoing_edges(upstream_node, outgoing_edge_map):
|
||||
edges = []
|
||||
for upstream_label, downstream_infos in list(outgoing_edge_map.items()):
|
||||
for (downstream_node, downstream_label) in downstream_infos:
|
||||
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)]
|
||||
for downstream_info in downstream_infos:
|
||||
downstream_node, downstream_label, downstream_selector = downstream_info
|
||||
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, downstream_selector)]
|
||||
return edges
|
||||
|
||||
|
||||
class KwargReprNode(DagNode):
|
||||
"""A DagNode that can be represented as a set of args+kwargs.
|
||||
"""
|
||||
|
||||
@property
|
||||
def __upstream_hashes(self):
|
||||
hashes = []
|
||||
for downstream_label, (upstream_node, upstream_label) in list(self.incoming_edge_map.items()):
|
||||
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label]]
|
||||
for downstream_label, upstream_info in self.incoming_edge_map.items():
|
||||
upstream_node, upstream_label, upstream_selector = upstream_info
|
||||
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label, upstream_selector]]
|
||||
return hashes
|
||||
|
||||
@property
|
||||
@ -152,21 +157,21 @@ def topo_sort(downstream_nodes):
|
||||
sorted_nodes = []
|
||||
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:
|
||||
raise RuntimeError('Graph is not a DAG')
|
||||
|
||||
if downstream_node is not None:
|
||||
outgoing_edge_map = outgoing_edge_maps.get(upstream_node, {})
|
||||
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_maps[upstream_node] = outgoing_edge_map
|
||||
|
||||
if upstream_node not in sorted_nodes:
|
||||
marked_nodes.append(upstream_node)
|
||||
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)
|
||||
sorted_nodes.append(upstream_node)
|
||||
|
||||
|
@ -21,12 +21,14 @@ def _get_types_str(types):
|
||||
|
||||
class Stream(object):
|
||||
"""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):
|
||||
raise TypeError('Expected upstream node to be of one of the following type(s): {}; got {}'.format(
|
||||
_get_types_str(node_types), type(upstream_node)))
|
||||
self.node = upstream_node
|
||||
self.label = upstream_label
|
||||
self.selector = upstream_selector
|
||||
|
||||
def __hash__(self):
|
||||
return get_hash_int([hash(self.node), hash(self.label)])
|
||||
@ -36,9 +38,30 @@ class Stream(object):
|
||||
|
||||
def __repr__(self):
|
||||
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
|
||||
|
||||
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):
|
||||
if stream_spec is None:
|
||||
@ -68,6 +91,7 @@ def get_stream_spec_nodes(stream_spec):
|
||||
|
||||
class Node(KwargReprNode):
|
||||
"""Node base"""
|
||||
|
||||
@classmethod
|
||||
def __check_input_len(cls, stream_map, min_inputs, max_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()):
|
||||
if not _is_of_types(stream, incoming_stream_types):
|
||||
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_incoming_edge_map(cls, stream_map):
|
||||
incoming_edge_map = {}
|
||||
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
|
||||
|
||||
def __init__(self, stream_spec, name, incoming_stream_types, outgoing_stream_type, min_inputs, max_inputs, args=[],
|
||||
kwargs={}):
|
||||
def __init__(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
|
||||
|
||||
def stream(self, label=None):
|
||||
def stream(self, label=None, selector=None):
|
||||
"""Create an outgoing stream originating from this node.
|
||||
|
||||
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)``.
|
||||
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):
|
||||
def __init__(self, upstream_node, upstream_label):
|
||||
super(FilterableStream, self).__init__(upstream_node, upstream_label, {InputNode, FilterNode})
|
||||
def __init__(self, upstream_node, upstream_label, upstream_selector=None):
|
||||
super(FilterableStream, self).__init__(upstream_node, upstream_label, {InputNode, FilterNode},
|
||||
upstream_selector)
|
||||
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
class InputNode(Node):
|
||||
"""InputNode type"""
|
||||
|
||||
def __init__(self, name, args=[], kwargs={}):
|
||||
super(InputNode, self).__init__(
|
||||
stream_spec=None,
|
||||
@ -135,6 +177,7 @@ class InputNode(Node):
|
||||
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__(
|
||||
@ -149,6 +192,7 @@ class FilterNode(Node):
|
||||
)
|
||||
|
||||
"""FilterNode"""
|
||||
|
||||
def _get_filter(self, outgoing_edges):
|
||||
args = self.args
|
||||
kwargs = self.kwargs
|
||||
@ -173,6 +217,7 @@ class FilterNode(Node):
|
||||
return escape_chars(params_text, '\\\'[],;')
|
||||
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
class OutputNode(Node):
|
||||
def __init__(self, stream, name, args=[], kwargs={}):
|
||||
super(OutputNode, self).__init__(
|
||||
@ -181,7 +226,7 @@ class OutputNode(Node):
|
||||
incoming_stream_types={FilterableStream},
|
||||
outgoing_stream_type=OutputStream,
|
||||
min_inputs=1,
|
||||
max_inputs=1,
|
||||
max_inputs=None,
|
||||
args=args,
|
||||
kwargs=kwargs
|
||||
)
|
||||
@ -192,10 +237,12 @@ class OutputNode(Node):
|
||||
|
||||
|
||||
class OutputStream(Stream):
|
||||
def __init__(self, upstream_node, upstream_label):
|
||||
super(OutputStream, self).__init__(upstream_node, upstream_label, {OutputNode, GlobalNode, MergeOutputsNode})
|
||||
def __init__(self, upstream_node, upstream_label, upstream_selector=None):
|
||||
super(OutputStream, self).__init__(upstream_node, upstream_label, {OutputNode, GlobalNode, MergeOutputsNode},
|
||||
upstream_selector=upstream_selector)
|
||||
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
class MergeOutputsNode(Node):
|
||||
def __init__(self, streams, name):
|
||||
super(MergeOutputsNode, self).__init__(
|
||||
@ -208,6 +255,7 @@ class MergeOutputsNode(Node):
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyMethodOverriding
|
||||
class GlobalNode(Node):
|
||||
def __init__(self, stream, name, args=[], kwargs={}):
|
||||
super(GlobalNode, self).__init__(
|
||||
@ -227,6 +275,7 @@ def stream_operator(stream_classes={Stream}, name=None):
|
||||
func_name = name or func.__name__
|
||||
[setattr(stream_class, func_name, func) for stream_class in stream_classes]
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
|
@ -82,21 +82,21 @@ def test_node_repr():
|
||||
trim3 = ffmpeg.trim(in_file, start_frame=50, end_frame=60)
|
||||
concatted = ffmpeg.concat(trim1, trim2, trim3)
|
||||
output = ffmpeg.output(concatted, 'dummy2.mp4')
|
||||
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(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(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(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(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(concatted.node) == 'concat(n=3) <{}>'.format(concatted.node.short_hash)
|
||||
assert repr(output.node) == 'output(filename={!r}) <{}>'.format('dummy2.mp4', output.node.short_hash)
|
||||
|
||||
|
||||
def test_stream_repr():
|
||||
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]
|
||||
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']
|
||||
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():
|
||||
@ -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():
|
||||
split = (ffmpeg
|
||||
.input(TEST_INPUT_FILE1)
|
||||
@ -158,8 +202,8 @@ def _get_complex_filter_asplit_example():
|
||||
|
||||
return (ffmpeg
|
||||
.concat(
|
||||
split0.filter_("atrim", start=10, end=20),
|
||||
split1.filter_("atrim", start=30, end=40),
|
||||
split0.filter_('atrim', start=10, end=20),
|
||||
split1.filter_('atrim', start=30, end=40),
|
||||
)
|
||||
.output(TEST_OUTPUT_FILE1)
|
||||
.overwrite_output()
|
||||
|
Loading…
x
Reference in New Issue
Block a user