8000 Merge pull request #45 from Depau/stream_selectors · dreamCodeMan/ffmpeg-python@8420f3b · GitHub
[go: up one dir, main page]

Skip to content

Commit 8420f3b

Browse files
authored
Merge pull request kkroening#45 from Depau/stream_selectors
Stream selectors, `.map` operator (audio support)
2 parents a029d7a + c162eab commit 8420f3b

File tree

8 files changed

+224
-61
lines changed

8 files changed

+224
-61
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ pip install ffmpeg-python
8787
```
8888

8989
It's also possible to clone the source and put it on your python path (`$PYTHONPATH`, `sys.path`, etc.):
90+
9091
```bash
9192
$ git clone git@github.com:kkroening/ffmpeg-python.git
9293
$ export PYTHONPATH=${PYTHONPATH}:ffmpeg-python
@@ -99,6 +100,7 @@ $ python
99100
API documentation is automatically generated from python docstrings and hosted on github pages: https://kkroening.github.io/ffmpeg-python/
100101

101102
Alternatively, standard python help is available, such as at the python REPL prompt as follows:
103+
102104
```python
103105
>>> import ffmpeg
104106
>>> help(ffmpeg)

ffmpeg/_ffmpeg.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import unicode_literals
22

3+
from ._utils import basestring
4+
35
from .nodes import (
46
filter_operator,
57
GlobalNode,
@@ -41,19 +43,29 @@ def merge_outputs(*streams):
4143

4244

4345
@filter_operator()
44-
def output(stream, filename, **kwargs):
46+
def output(*streams_and_filename, **kwargs):
4547
"""Output file URL
4648
49+
Syntax:
50+
`ffmpeg.output(stream1[, stream2, stream3...], filename, **ffmpeg_args)`
51+
52+
If multiple streams are provided, they are mapped to the same output.
53+
4754
Official documentation: `Synopsis <https://ffmpeg.org/ffmpeg.html#Synopsis>`__
4855
"""
49-
kwargs['filename'] = filename
56+
streams_and_filename = list(streams_and_filename)
57+
if 'filename' not in kwargs:
58+
if not isinstance(streams_and_filename[-1], basestring):
59+
raise ValueError('A filename must be provided')
60+
kwargs['filename'] = streams_and_filename.pop(-1)
61+
streams = streams_and_filename
62+
5063
fmt = kwargs.pop('f', None)
5164
if fmt:
5265
if 'format' in kwargs:
5366
raise ValueError("Can't specify both `format` and `f` kwargs")
5467
kwargs['format'] = fmt
55-
return OutputNode(stream, output.__name__, kwargs=kwargs).stream()
56-
68+
return OutputNode(streams, output.__name__, kwargs=kwargs).stream()
5769

5870

5971
__all__ = [

ffmpeg/_run.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .dag import get_outgoing_edges, topo_sort
44
from functools import reduce
5-
from past.builtins import basestring
5+
from ._utils import basestring
66
import copy
77
import operator
88
import subprocess as _subprocess
@@ -22,10 +22,6 @@
2222
)
2323

2424

25-
def _get_stream_name(name):
26-
return '[{}]'.format(name)
27-
28-
2925
def _convert_kwargs_to_cmd_line_args(kwargs):
3026
args = []
3127
for k in sorted(kwargs.keys()):
@@ -54,11 +50,24 @@ def _get_input_args(input_node):
5450
return args
5551

5652

53+
def _format_input_stream_name(stream_name_map, edge):
54+
prefix = stream_name_map[edge.upstream_node, edge.upstream_label]
55+
if not edge.upstream_selector:
56+
suffix = ''
57+
else:
58+
suffix = ':{}'.format(edge.upstream_selector)
59+
return '[{}{}]'.format(prefix, suffix)
60+
61+
62+
def _format_output_stream_name(stream_name_map, edge):
63+
return '[{}]'.format(stream_name_map[edge.upstream_node, edge.upstream_label])
64+
65+
5766
def _get_filter_spec(node, outgoing_edge_map, stream_name_map):
5867
incoming_edges = node.incoming_edges
5968
outgoing_edges = get_outgoing_edges(node, outgoing_edge_map)
60-
inputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in incoming_edges]
61-
outputs = [stream_name_map[edge.upstream_node, edge.upstream_label] for edge in outgoing_edges]
69+
inputs = [_format_input_stream_name(stream_name_map, edge) for edge in incoming_edges]
70+
outputs = [_format_output_stream_name(stream_name_map, edge) for edge in outgoing_edges]
6271
filter_spec = '{}{}{}'.format(''.join(inputs), node._get_filter(outgoing_edges), ''.join(outputs))
6372
return filter_spec
6473

@@ -71,8 +80,8 @@ def _allocate_filter_stream_names(filter_nodes, outgoing_edge_maps, stream_name_
7180
if len(downstreams) > 1:
7281
# TODO: automatically insert `splits` ahead of time via graph transformation.
7382
raise ValueError('Encountered {} with multiple outgoing edges with same upstream label {!r}; a '
74-
'`split` filter is probably required'.format(upstream_node, upstream_label))
75-
stream_name_map[upstream_node, upstream_label] = _get_stream_name('s{}'.format(stream_count))
83+
'`split` filter is probably required'.format(upstream_node, upstream_label))
84+
stream_name_map[upstream_node, upstream_label] = 's{}'.format(stream_count)
7685
stream_count += 1
7786

7887

@@ -93,11 +102,16 @@ def _get_output_args(node, stream_name_map):
93102
if node.name != output.__name__:
94103
raise ValueError('Unsupported output node: {}'.format(node))
95104
args = []
96-
assert len(node.incoming_edges) == 1
97-
edge = node.incoming_edges[0]
98-
stream_name = stream_name_map[edge.upstream_node, edge.upstream_label]
99-
if stream_name != '[0]':
100-
args += ['-map', stream_name]
105+
106+
if len(node.incoming_edges) == 0:
107+
raise ValueError('Output node {} has no mapped streams'.format(node))
108+
109+
for edge in node.incoming_edges:
110+
# edge = node.incoming_edges[0]
111+
stream_name = _format_input_stream_name(stream_name_map, edge)
112+
if stream_name != '[0]' or len(node.incoming_edges) > 1:
113+
args += ['-map', stream_name]
114+
101115
kwargs = copy.copy(node.kwargs)
102116
filename = kwargs.pop('filename')
103117
fmt = kwargs.pop('format', None)
@@ -119,7 +133,7 @@ def get_args(stream_spec, overwrite_output=False):
119133
output_nodes = [node for node in sorted_nodes if isinstance(node, OutputNode)]
120134
global_nodes = [node for node in sorted_nodes if isinstance(node, GlobalNode)]
121135
filter_nodes = [node for node in sorted_nodes if isinstance(node, FilterNode)]
122-
stream_name_map = {(node, None): _get_stream_name(i) for i, node in enumerate(input_nodes)}
136+
stream_name_map = {(node, None): str(i) for i, node in enumerate(input_nodes)}
123137
filter_arg = _get_filter_arg(filter_nodes, outgoing_edge_maps, stream_name_map)
124138
args += reduce(operator.add, [_get_input_args(node) for node in input_nodes])
125139
if filter_arg:

ffmpeg/_utils.py

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,42 @@
11
from __future__ import unicode_literals
2-
3-
from builtins import str
4-
from past.builtins import basestring
52
import hashlib
3+
import sys
4+
5+
if sys.version_info.major == 2:
6+
# noinspection PyUnresolvedReferences,PyShadowingBuiltins
7+
str = unicode
8+
9+
10+
# `past.builtins.basestring` module can't be imported on Python3 in some environments (Ubuntu).
11+
# This code is copy-pasted from it to avoid crashes.
12+
class BaseBaseString(type):
13+
def __instancecheck__(cls, instance):
14+
return isinstance(instance, (bytes, str))
15+
16+
def __subclasshook__(cls, thing):
17+
# TODO: What should go here?
18+
raise NotImplemented
19+
20+
21+
def with_metaclass(meta, *bases):
22+
class metaclass(meta):
23+
__call__ = type.__call__
24+
__init__ = type.__init__
25+
26+
def __new__(cls, name, this_bases, d):
27+
if this_bases is None:
28+
return type.__new__(cls, name, (), d)
29+
return meta(name, bases, d)
30+
31+
return metaclass('temporary_class', None, {})
32+
33+
34+
if sys.version_info.major >= 3:
35+
class basestring(with_metaclass(BaseBaseString)):
36+
pass
37+
else:
38+
# noinspection PyUnresolvedReferences,PyCompatibility
39+
from builtins import basestring
640

741

842
def _recursive_repr(item):
@@ -27,6 +61,7 @@ def get_hash(item):
2761
repr_ = _recursive_repr(item).encode('utf-8')
2862
return hashlib.md5(repr_).hexdigest()
2963

64+
3065
def get_hash_int(item):
3166
return int(get_hash(item), base=16)
3267

ffmpeg/_view.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,13 @@
33
from builtins import str
44
from .dag import get_outgoing_edges
55
from ._run import topo_sort
6-
import os
76
import tempfile
87

98
from ffmpeg.nodes import (
109
FilterNode,
1110
get_stream_spec_nodes,
1211
InputNode,
1312
OutputNode,
14-
Stream,
1513
stream_operator,
1614
)
1715

@@ -62,9 +60,13 @@ def view(stream_spec, **kwargs):
6260
kwargs = {}
6361
up_label = edge.upstream_label
6462
down_label = edge.downstream_label
65-
if show_labels and (up_label is not None or down_label is not None):
63+
up_selector = edge.upstream_selector
64+
65+
if show_labels and (up_label is not None or down_label is not None or up_selector is not None):
6666
if up_label is None:
6767
up_label = ''
68+
if up_selector is not None:
69+
up_label += ":" + up_selector
6870
if down_label is None:
6971
down_label = ''
7072
if up_label != '' and down_label != '':

ffmpeg/dag.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ class DagNode(object):
4242
4343
Again, because nodes are immutable, the string representations should remain constant.
4444
"""
45+
4546
def __hash__(self):
4647
"""Return an integer hash of the node."""
4748
raise NotImplementedError()
@@ -69,32 +70,36 @@ def incoming_edge_map(self):
6970
raise NotImplementedError()
7071

7172

72-
DagEdge = namedtuple('DagEdge', ['downstream_node', 'downstream_label', 'upstream_node', 'upstream_label'])
73+
DagEdge = namedtuple('DagEdge', ['downstream_node', 'downstream_label', 'upstream_node', 'upstream_label', 'upstream_selector'])
7374

7475

7576
def get_incoming_edges(downstream_node, incoming_edge_map):
7677
edges = []
77-
for downstream_label, (upstream_node, upstream_label) in list(incoming_edge_map.items()):
78-
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)]
78+
for downstream_label, upstream_info in incoming_edge_map.items():
79+
upstream_node, upstream_label, upstream_selector = upstream_info
80+
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, upstream_selector)]
7981
return edges
8082

8183

8284
def get_outgoing_edges(upstream_node, outgoing_edge_map):
8385
edges = []
8486
for upstream_label, downstream_infos in list(outgoing_edge_map.items()):
85-
for (downstream_node, downstream_label) in downstream_infos:
86-
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label)]
87+
for downstream_info in downstream_infos:
88+
downstream_node, downstream_label, downstream_selector = downstream_info
89+
edges += [DagEdge(downstream_node, downstream_label, upstream_node, upstream_label, downstream_selector)]
8790
return edges
8891

8992

9093
class KwargReprNode(DagNode):
9194
"""A DagNode that can be represented as a set of args+kwargs.
9295
"""
96+
9397
@property
9498
def __upstream_hashes(self):
9599
hashes = []
96-
for downstream_label, (upstream_node, upstream_label) in list(self.incoming_edge_map.items()):
97-
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label]]
100+
for downstream_label, upstream_info in self.incoming_edge_map.items():
101+
upstream_node, upstream_label, upstream_selector = upstream_info
102+
hashes += [hash(x) for x in [downstream_label, upstream_node, upstream_label, upstream_selector]]
98103
return hashes
99104

100105
@property
@@ -152,21 +157,21 @@ def topo_sort(downstream_nodes):
152157
sorted_nodes = []
153158
outgoing_edge_maps = {}
154159

155-
def visit(upstream_node, upstream_label, downstream_node, downstream_label):
160+
def visit(upstream_node, upstream_label, downstream_node, downstream_label, downstream_selector=None):
156161
if upstream_node in marked_nodes:
157162
raise RuntimeError('Graph is not a DAG')
158163

159164
if downstream_node is not None:
160165
outgoing_edge_map = outgoing_edge_maps.get(upstream_node, {})
161166
outgoing_edge_infos = outgoing_edge_map.get(upstream_label, [])
162-
outgoing_edge_infos += [(downstream_node, downstream_label)]
167+
outgoing_edge_infos += [(downstream_node, downstream_label, downstream_selector)]
163168
outgoing_edge_map[upstream_label] = outgoing_edge_infos
164169
outgoing_edge_maps[upstream_node] = outgoing_edge_map
165170

166171
if upstream_node not in sorted_nodes:
167172
marked_nodes.append(upstream_node)
168173
for edge in upstream_node.incoming_edges:
169-
visit(edge.upstream_node, edge.upstream_label, edge.downstream_node, edge.downstream_label)
174+
visit(edge.upstream_node, edge.upstream_label, edge.downstream_node, edge.downstream_label, edge.upstream_selector)
170175
marked_nodes.remove(upstream_node)
171176
sorted_nodes.append(upstream_node)
172177

0 commit comments

Comments
 (0)
0