|
|
from __future__ import unicode_literals |
|
|
|
|
|
from past.builtins import basestring |
|
|
from .dag import KwargReprNode |
|
|
from ._utils import escape_chars, get_hash_int |
|
|
from builtins import object |
|
|
import os |
|
|
|
|
|
|
|
|
def _is_of_types(obj, types): |
|
|
valid = False |
|
|
for stream_type in types: |
|
|
if isinstance(obj, stream_type): |
|
|
valid = True |
|
|
break |
|
|
return valid |
|
|
|
|
|
|
|
|
def _get_types_str(types): |
|
|
return ', '.join(['{}.{}'.format(x.__module__, x.__name__) for x in 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, 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)]) |
|
|
|
|
|
def __eq__(self, other): |
|
|
return hash(self) == hash(other) |
|
|
|
|
|
def __repr__(self): |
|
|
node_repr = self.node.long_repr(include_hash=False) |
|
|
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) |
|
|
|
|
|
@property |
|
|
def audio(self): |
|
|
"""Select the audio-portion of a stream. |
|
|
|
|
|
Some ffmpeg filters drop audio streams, and care must be taken |
|
|
to preserve the audio in the final output. The ``.audio`` and |
|
|
``.video`` operators can be used to reference the audio/video |
|
|
portions of a stream so that they can be processed separately |
|
|
and then re-combined later in the pipeline. This dilemma is |
|
|
intrinsic to ffmpeg, and ffmpeg-python tries to stay out of the |
|
|
way while users may refer to the official ffmpeg documentation |
|
|
as to why certain filters drop audio. |
|
|
|
|
|
``stream.audio`` is a shorthand for ``stream['a']``. |
|
|
|
|
|
Example: |
|
|
Process the audio and video portions of a stream independently:: |
|
|
|
|
|
input = ffmpeg.input('in.mp4') |
|
|
audio = input.audio.filter("aecho", 0.8, 0.9, 1000, 0.3) |
|
|
video = input.video.hflip() |
|
|
out = ffmpeg.output(audio, video, 'out.mp4') |
|
|
""" |
|
|
return self['a'] |
|
|
|
|
|
@property |
|
|
def video(self): |
|
|
"""Select the video-portion of a stream. |
|
|
|
|
|
Some ffmpeg filters drop audio streams, and care must be taken |
|
|
to preserve the audio in the final output. The ``.audio`` and |
|
|
``.video`` operators can be used to reference the audio/video |
|
|
portions of a stream so that they can be processed separately |
|
|
and then re-combined later in the pipeline. This dilemma is |
|
|
intrinsic to ffmpeg, and ffmpeg-python tries to stay out of the |
|
|
way while users may refer to the official ffmpeg documentation |
|
|
as to why certain filters drop audio. |
|
|
|
|
|
``stream.video`` is a shorthand for ``stream['v']``. |
|
|
|
|
|
Example: |
|
|
Process the audio and video portions of a stream independently:: |
|
|
|
|
|
input = ffmpeg.input('in.mp4') |
|
|
audio = input.audio.filter("aecho", 0.8, 0.9, 1000, 0.3) |
|
|
video = input.video.hflip() |
|
|
out = ffmpeg.output(audio, video, 'out.mp4') |
|
|
""" |
|
|
return self['v'] |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
def get_stream_map_nodes(stream_map): |
|
|
nodes = [] |
|
|
for stream in list(stream_map.values()): |
|
|
if not isinstance(stream, Stream): |
|
|
raise TypeError('Expected Stream; got {}'.format(type(stream))) |
|
|
nodes.append(stream.node) |
|
|
return nodes |
|
|
|
|
|
|
|
|
def get_stream_spec_nodes(stream_spec): |
|
|
stream_map = get_stream_map(stream_spec) |
|
|
return get_stream_map_nodes(stream_map) |
|
|
|
|
|
|
|
|
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: |
|
|
raise ValueError( |
|
|
'Expected at least {} input stream(s); got {}'.format( |
|
|
min_inputs, len(stream_map) |
|
|
) |
|
|
) |
|
|
elif max_inputs is not None and len(stream_map) > max_inputs: |
|
|
raise ValueError( |
|
|
'Expected at most {} input stream(s); got {}'.format( |
|
|
max_inputs, len(stream_map) |
|
|
) |
|
|
) |
|
|
|
|
|
@classmethod |
|
|
def __check_input_types(cls, stream_map, incoming_stream_types): |
|
|
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) |
|
|
) |
|
|
) |
|
|
|
|
|
@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, |
|
|
upstream.selector, |
|
|
) |
|
|
return incoming_edge_map |
|
|
|
|
|
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, 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, upstream_selector=selector) |
|
|
|
|
|
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') |
|
|
""" |
|
|
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, upstream_selector=None): |
|
|
super(FilterableStream, self).__init__( |
|
|
upstream_node, upstream_label, {InputNode, FilterNode}, upstream_selector |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
class InputNode(Node): |
|
|
"""InputNode type""" |
|
|
|
|
|
def __init__(self, name, args=[], kwargs={}): |
|
|
super(InputNode, self).__init__( |
|
|
stream_spec=None, |
|
|
name=name, |
|
|
incoming_stream_types={}, |
|
|
outgoing_stream_type=FilterableStream, |
|
|
min_inputs=0, |
|
|
max_inputs=0, |
|
|
args=args, |
|
|
kwargs=kwargs, |
|
|
) |
|
|
|
|
|
@property |
|
|
def short_repr(self): |
|
|
return os.path.basename(self.kwargs['filename']) |
|
|
|
|
|
|
|
|
|
|
|
class FilterNode(Node): |
|
|
def __init__(self, stream_spec, name, max_inputs=1, args=[], kwargs={}): |
|
|
super(FilterNode, self).__init__( |
|
|
stream_spec=stream_spec, |
|
|
name=name, |
|
|
incoming_stream_types={FilterableStream}, |
|
|
outgoing_stream_type=FilterableStream, |
|
|
min_inputs=1, |
|
|
max_inputs=max_inputs, |
|
|
args=args, |
|
|
kwargs=kwargs, |
|
|
) |
|
|
|
|
|
"""FilterNode""" |
|
|
|
|
|
def _get_filter(self, outgoing_edges): |
|
|
args = self.args |
|
|
kwargs = self.kwargs |
|
|
if self.name in ('split', 'asplit'): |
|
|
args = [len(outgoing_edges)] |
|
|
|
|
|
out_args = [escape_chars(x, '\\\'=:') for x in args] |
|
|
out_kwargs = {} |
|
|
for k, v in list(kwargs.items()): |
|
|
k = escape_chars(k, '\\\'=:') |
|
|
v = escape_chars(v, '\\\'=:') |
|
|
out_kwargs[k] = v |
|
|
|
|
|
arg_params = [escape_chars(v, '\\\'=:') for v in out_args] |
|
|
kwarg_params = ['{}={}'.format(k, out_kwargs[k]) for k in sorted(out_kwargs)] |
|
|
params = arg_params + kwarg_params |
|
|
|
|
|
params_text = escape_chars(self.name, '\\\'=:') |
|
|
|
|
|
if params: |
|
|
params_text += '={}'.format(':'.join(params)) |
|
|
return escape_chars(params_text, '\\\'[],;') |
|
|
|
|
|
|
|
|
|
|
|
class OutputNode(Node): |
|
|
def __init__(self, stream, name, args=[], kwargs={}): |
|
|
super(OutputNode, self).__init__( |
|
|
stream_spec=stream, |
|
|
name=name, |
|
|
incoming_stream_types={FilterableStream}, |
|
|
outgoing_stream_type=OutputStream, |
|
|
min_inputs=1, |
|
|
max_inputs=None, |
|
|
args=args, |
|
|
kwargs=kwargs, |
|
|
) |
|
|
|
|
|
@property |
|
|
def short_repr(self): |
|
|
return os.path.basename(self.kwargs['filename']) |
|
|
|
|
|
|
|
|
class OutputStream(Stream): |
|
|
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, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
class MergeOutputsNode(Node): |
|
|
def __init__(self, streams, name): |
|
|
super(MergeOutputsNode, self).__init__( |
|
|
stream_spec=streams, |
|
|
name=name, |
|
|
incoming_stream_types={OutputStream}, |
|
|
outgoing_stream_type=OutputStream, |
|
|
min_inputs=1, |
|
|
max_inputs=None, |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
class GlobalNode(Node): |
|
|
def __init__(self, stream, name, args=[], kwargs={}): |
|
|
super(GlobalNode, self).__init__( |
|
|
stream_spec=stream, |
|
|
name=name, |
|
|
incoming_stream_types={OutputStream}, |
|
|
outgoing_stream_type=OutputStream, |
|
|
min_inputs=1, |
|
|
max_inputs=1, |
|
|
args=args, |
|
|
kwargs=kwargs, |
|
|
) |
|
|
|
|
|
|
|
|
def stream_operator(stream_classes={Stream}, name=None): |
|
|
def decorator(func): |
|
|
func_name = name or func.__name__ |
|
|
[setattr(stream_class, func_name, func) for stream_class in stream_classes] |
|
|
return func |
|
|
|
|
|
return decorator |
|
|
|
|
|
|
|
|
def filter_operator(name=None): |
|
|
return stream_operator(stream_classes={FilterableStream}, name=name) |
|
|
|
|
|
|
|
|
def output_operator(name=None): |
|
|
return stream_operator(stream_classes={OutputStream}, name=name) |
|
|
|
|
|
|
|
|
__all__ = ['Stream'] |
|
|
|