typed-ffmpeg-compatible 2.6.0__py3-none-any.whl → 2.6.2__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- typed_ffmpeg/base.py +24 -5
- typed_ffmpeg/common/schema.py +25 -9
- typed_ffmpeg/common/serialize.py +9 -4
- typed_ffmpeg/dag/__init__.py +8 -1
- typed_ffmpeg/dag/context.py +7 -2
- typed_ffmpeg/dag/factory.py +29 -9
- typed_ffmpeg/dag/global_runnable/global_args.py +1 -2
- typed_ffmpeg/dag/global_runnable/runnable.py +9 -3
- typed_ffmpeg/dag/io/_input.py +3 -1
- typed_ffmpeg/dag/io/_output.py +3 -1
- typed_ffmpeg/dag/io/output_args.py +7 -4
- typed_ffmpeg/dag/nodes.py +55 -26
- typed_ffmpeg/dag/schema.py +3 -3
- typed_ffmpeg/dag/validate.py +26 -8
- typed_ffmpeg/exceptions.py +1 -1
- typed_ffmpeg/filters.py +304 -72
- typed_ffmpeg/info.py +9 -3
- typed_ffmpeg/probe.py +9 -2
- typed_ffmpeg/schema.py +0 -1
- typed_ffmpeg/streams/audio.py +697 -214
- typed_ffmpeg/streams/video.py +1400 -401
- typed_ffmpeg/utils/escaping.py +6 -5
- typed_ffmpeg/utils/run.py +1 -1
- typed_ffmpeg/utils/snapshot.py +6 -1
- typed_ffmpeg/utils/view.py +7 -2
- {typed_ffmpeg_compatible-2.6.0.dist-info → typed_ffmpeg_compatible-2.6.2.dist-info}/METADATA +1 -1
- typed_ffmpeg_compatible-2.6.2.dist-info/RECORD +46 -0
- typed_ffmpeg_compatible-2.6.0.dist-info/RECORD +0 -46
- {typed_ffmpeg_compatible-2.6.0.dist-info → typed_ffmpeg_compatible-2.6.2.dist-info}/LICENSE +0 -0
- {typed_ffmpeg_compatible-2.6.0.dist-info → typed_ffmpeg_compatible-2.6.2.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-2.6.0.dist-info → typed_ffmpeg_compatible-2.6.2.dist-info}/entry_points.txt +0 -0
typed_ffmpeg/base.py
CHANGED
@@ -6,7 +6,13 @@ from typing import Any
|
|
6
6
|
|
7
7
|
from .dag.io._input import input
|
8
8
|
from .dag.io._output import output
|
9
|
-
from .dag.nodes import
|
9
|
+
from .dag.nodes import (
|
10
|
+
FilterableStream,
|
11
|
+
FilterNode,
|
12
|
+
GlobalNode,
|
13
|
+
GlobalStream,
|
14
|
+
OutputStream,
|
15
|
+
)
|
10
16
|
from .schema import StreamType
|
11
17
|
from .streams.audio import AudioStream
|
12
18
|
from .streams.video import VideoStream
|
@@ -26,7 +32,10 @@ def merge_outputs(*streams: OutputStream) -> GlobalStream:
|
|
26
32
|
|
27
33
|
|
28
34
|
def vfilter(
|
29
|
-
*streams: FilterableStream,
|
35
|
+
*streams: FilterableStream,
|
36
|
+
name: str,
|
37
|
+
input_typings: tuple[StreamType, ...] = (StreamType.video,),
|
38
|
+
**kwargs: Any,
|
30
39
|
) -> VideoStream:
|
31
40
|
"""
|
32
41
|
Apply a custom video filter which has only one output to this stream
|
@@ -53,7 +62,10 @@ def vfilter(
|
|
53
62
|
|
54
63
|
|
55
64
|
def afilter(
|
56
|
-
*streams: FilterableStream,
|
65
|
+
*streams: FilterableStream,
|
66
|
+
name: str,
|
67
|
+
input_typings: tuple[StreamType, ...] = (StreamType.audio,),
|
68
|
+
**kwargs: Any,
|
57
69
|
) -> AudioStream:
|
58
70
|
"""
|
59
71
|
Apply a custom audio filter which has only one output to this stream
|
@@ -84,7 +96,7 @@ def filter_multi_output(
|
|
84
96
|
name: str,
|
85
97
|
input_typings: tuple[StreamType, ...] = (),
|
86
98
|
output_tyings: tuple[StreamType, ...] = (),
|
87
|
-
**kwargs: Any
|
99
|
+
**kwargs: Any,
|
88
100
|
) -> FilterNode:
|
89
101
|
"""
|
90
102
|
Apply a custom filter which has multiple outputs to this stream
|
@@ -111,4 +123,11 @@ def filter_multi_output(
|
|
111
123
|
)
|
112
124
|
|
113
125
|
|
114
|
-
__all__ = [
|
126
|
+
__all__ = [
|
127
|
+
"input",
|
128
|
+
"output",
|
129
|
+
"merge_outputs",
|
130
|
+
"vfilter",
|
131
|
+
"afilter",
|
132
|
+
"filter_multi_output",
|
133
|
+
]
|
typed_ffmpeg/common/schema.py
CHANGED
@@ -118,8 +118,10 @@ class FFMpegFilter:
|
|
118
118
|
def to_def(self) -> FFMpegFilterDef:
|
119
119
|
return FFMpegFilterDef(
|
120
120
|
name=self.name,
|
121
|
-
typings_input=self.formula_typings_input
|
122
|
-
|
121
|
+
typings_input=self.formula_typings_input
|
122
|
+
or tuple(k.type.value for k in self.stream_typings_input),
|
123
|
+
typings_output=self.formula_typings_output
|
124
|
+
or tuple(k.type.value for k in self.stream_typings_output),
|
123
125
|
)
|
124
126
|
|
125
127
|
@property
|
@@ -131,13 +133,18 @@ class FFMpegFilter:
|
|
131
133
|
else:
|
132
134
|
assert self.formula_typings_input, f"{self.name} has no input"
|
133
135
|
if "video" not in self.formula_typings_input:
|
134
|
-
assert "audio" in self.formula_typings_input,
|
136
|
+
assert "audio" in self.formula_typings_input, (
|
137
|
+
f"{self.name} has no video input"
|
138
|
+
)
|
135
139
|
return {StreamType.audio}
|
136
140
|
elif "audio" not in self.formula_typings_input:
|
137
|
-
assert "video" in self.formula_typings_input,
|
141
|
+
assert "video" in self.formula_typings_input, (
|
142
|
+
f"{self.name} has no audio input"
|
143
|
+
)
|
138
144
|
return {StreamType.video}
|
139
145
|
assert (
|
140
|
-
"video" in self.formula_typings_input
|
146
|
+
"video" in self.formula_typings_input
|
147
|
+
and "audio" in self.formula_typings_input
|
141
148
|
), f"{self.name} has no video or audio input"
|
142
149
|
return {StreamType.video, StreamType.audio}
|
143
150
|
|
@@ -150,13 +157,18 @@ class FFMpegFilter:
|
|
150
157
|
else:
|
151
158
|
assert self.formula_typings_output, f"{self.name} has no output"
|
152
159
|
if "video" not in self.formula_typings_output:
|
153
|
-
assert "audio" in self.formula_typings_output,
|
160
|
+
assert "audio" in self.formula_typings_output, (
|
161
|
+
f"{self.name} has no video output"
|
162
|
+
)
|
154
163
|
return {StreamType.audio}
|
155
164
|
elif "audio" not in self.formula_typings_output:
|
156
|
-
assert "video" in self.formula_typings_output,
|
165
|
+
assert "video" in self.formula_typings_output, (
|
166
|
+
f"{self.name} has no audio output"
|
167
|
+
)
|
157
168
|
return {StreamType.video}
|
158
169
|
assert (
|
159
|
-
"video" in self.formula_typings_output
|
170
|
+
"video" in self.formula_typings_output
|
171
|
+
and "audio" in self.formula_typings_output
|
160
172
|
), f"{self.name} has no video or audio output"
|
161
173
|
return {StreamType.video, StreamType.audio}
|
162
174
|
|
@@ -301,7 +313,11 @@ class FFMpegOption:
|
|
301
313
|
|
302
314
|
@property
|
303
315
|
def is_global_option(self) -> bool:
|
304
|
-
return
|
316
|
+
return (
|
317
|
+
not self.is_input_option
|
318
|
+
and not self.is_output_option
|
319
|
+
and not (self.flags & FFMpegOptionFlag.OPT_EXIT)
|
320
|
+
)
|
305
321
|
|
306
322
|
@property
|
307
323
|
def is_support_stream_specifier(self) -> bool:
|
typed_ffmpeg/common/serialize.py
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
from __future__ import
|
1
|
+
from __future__ import annotations
|
2
2
|
|
3
3
|
import importlib
|
4
4
|
import json
|
@@ -21,7 +21,9 @@ def load_class(path: str, strict: bool = True) -> Any:
|
|
21
21
|
The class.
|
22
22
|
"""
|
23
23
|
if strict:
|
24
|
-
assert path.startswith("ffmpeg."),
|
24
|
+
assert path.startswith("ffmpeg."), (
|
25
|
+
f"Only support loading class from ffmpeg package: {path}"
|
26
|
+
)
|
25
27
|
|
26
28
|
module_path, class_name = path.rsplit(".", 1)
|
27
29
|
module = importlib.import_module(module_path)
|
@@ -65,7 +67,7 @@ def object_hook(obj: Any, strict: bool = True) -> Any:
|
|
65
67
|
# NOTE: in our application, the dataclass is always frozen
|
66
68
|
return cls(**{k: frozen(v) for k, v in obj.items()})
|
67
69
|
|
68
|
-
return cls(**
|
70
|
+
return cls(**dict(obj.items()))
|
69
71
|
|
70
72
|
return obj
|
71
73
|
|
@@ -107,7 +109,10 @@ def to_dict_with_class_info(instance: Any) -> Any:
|
|
107
109
|
elif is_dataclass(instance):
|
108
110
|
return {
|
109
111
|
"__class__": f"{instance.__class__.__module__}.{instance.__class__.__name__}",
|
110
|
-
**{
|
112
|
+
**{
|
113
|
+
k.name: to_dict_with_class_info(getattr(instance, k.name))
|
114
|
+
for k in fields(instance)
|
115
|
+
},
|
111
116
|
}
|
112
117
|
elif isinstance(instance, Enum):
|
113
118
|
return {
|
typed_ffmpeg/dag/__init__.py
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
from .nodes import
|
1
|
+
from .nodes import (
|
2
|
+
FilterableStream,
|
3
|
+
FilterNode,
|
4
|
+
GlobalNode,
|
5
|
+
InputNode,
|
6
|
+
OutputNode,
|
7
|
+
OutputStream,
|
8
|
+
)
|
2
9
|
from .schema import Node, Stream
|
3
10
|
|
4
11
|
__all__ = [
|
typed_ffmpeg/dag/context.py
CHANGED
@@ -105,7 +105,10 @@ class DAGContext:
|
|
105
105
|
"""
|
106
106
|
All streams in the graph sorted by the number of upstream nodes and the index of the stream.
|
107
107
|
"""
|
108
|
-
return sorted(
|
108
|
+
return sorted(
|
109
|
+
self.streams,
|
110
|
+
key=lambda stream: (len(stream.node.upstream_nodes), stream.index),
|
111
|
+
)
|
109
112
|
|
110
113
|
@cached_property
|
111
114
|
def outgoing_nodes(self) -> dict[Stream, list[tuple[Node, int]]]:
|
@@ -180,7 +183,9 @@ class DAGContext:
|
|
180
183
|
The label of the node.
|
181
184
|
"""
|
182
185
|
|
183
|
-
assert isinstance(node, (InputNode, FilterNode)),
|
186
|
+
assert isinstance(node, (InputNode, FilterNode)), (
|
187
|
+
"Only input and filter nodes have labels"
|
188
|
+
)
|
184
189
|
return self.node_labels[node]
|
185
190
|
|
186
191
|
@override
|
typed_ffmpeg/dag/factory.py
CHANGED
@@ -7,23 +7,43 @@ from ..utils.run import ignore_default
|
|
7
7
|
from .nodes import FilterableStream, FilterNode
|
8
8
|
|
9
9
|
|
10
|
-
def filter_node_factory(
|
10
|
+
def filter_node_factory(
|
11
|
+
ffmpeg_filter_def: FFMpegFilterDef, *inputs: FilterableStream, **kwargs: Any
|
12
|
+
) -> FilterNode:
|
11
13
|
for k, v in kwargs.items():
|
12
14
|
if isinstance(v, Auto):
|
13
|
-
kwargs[k] = eval(
|
15
|
+
kwargs[k] = eval(
|
16
|
+
v, {"StreamType": StreamType, "re": re, **kwargs, "streams": inputs}
|
17
|
+
)
|
14
18
|
|
15
|
-
if isinstance(
|
16
|
-
input_typings = tuple(
|
19
|
+
if isinstance(ffmpeg_filter_def.typings_input, str):
|
20
|
+
input_typings = tuple(
|
21
|
+
eval(
|
22
|
+
ffmpeg_filter_def.typings_input,
|
23
|
+
{"StreamType": StreamType, "re": re, **kwargs},
|
24
|
+
)
|
25
|
+
)
|
17
26
|
else:
|
18
|
-
input_typings = tuple(
|
27
|
+
input_typings = tuple(
|
28
|
+
StreamType.video if k == "video" else StreamType.audio
|
29
|
+
for k in ffmpeg_filter_def.typings_input
|
30
|
+
)
|
19
31
|
|
20
|
-
if isinstance(
|
21
|
-
output_typings = tuple(
|
32
|
+
if isinstance(ffmpeg_filter_def.typings_output, str):
|
33
|
+
output_typings = tuple(
|
34
|
+
eval(
|
35
|
+
ffmpeg_filter_def.typings_output,
|
36
|
+
{"StreamType": StreamType, "re": re, **kwargs},
|
37
|
+
)
|
38
|
+
)
|
22
39
|
else:
|
23
|
-
output_typings = tuple(
|
40
|
+
output_typings = tuple(
|
41
|
+
StreamType.video if k == "video" else StreamType.audio
|
42
|
+
for k in ffmpeg_filter_def.typings_output
|
43
|
+
)
|
24
44
|
|
25
45
|
return FilterNode(
|
26
|
-
name=
|
46
|
+
name=ffmpeg_filter_def.name,
|
27
47
|
input_typings=input_typings,
|
28
48
|
output_typings=output_typings,
|
29
49
|
inputs=inputs,
|
@@ -12,8 +12,7 @@ if TYPE_CHECKING:
|
|
12
12
|
|
13
13
|
class GlobalArgs(ABC):
|
14
14
|
@abstractmethod
|
15
|
-
def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode:
|
16
|
-
...
|
15
|
+
def _global_node(self, *streams: OutputStream, **kwargs: Any) -> GlobalNode: ...
|
17
16
|
|
18
17
|
def global_args(
|
19
18
|
self,
|
@@ -82,7 +82,9 @@ class GlobalRunable(GlobalArgs):
|
|
82
82
|
Returns:
|
83
83
|
the command-line
|
84
84
|
"""
|
85
|
-
return command_line(
|
85
|
+
return command_line(
|
86
|
+
self.compile(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix)
|
87
|
+
)
|
86
88
|
|
87
89
|
def run_async(
|
88
90
|
self,
|
@@ -115,7 +117,9 @@ class GlobalRunable(GlobalArgs):
|
|
115
117
|
stdout_stream = subprocess.PIPE if pipe_stdout or quiet else None
|
116
118
|
stderr_stream = subprocess.PIPE if pipe_stderr or quiet else None
|
117
119
|
|
118
|
-
logger.info(
|
120
|
+
logger.info(
|
121
|
+
f"Running command: {self.compile_line(cmd, overwrite_output=overwrite_output, auto_fix=auto_fix)}"
|
122
|
+
)
|
119
123
|
|
120
124
|
return subprocess.Popen(
|
121
125
|
args,
|
@@ -166,7 +170,9 @@ class GlobalRunable(GlobalArgs):
|
|
166
170
|
if retcode:
|
167
171
|
raise FFMpegExecuteError(
|
168
172
|
retcode=retcode,
|
169
|
-
cmd=self.compile_line(
|
173
|
+
cmd=self.compile_line(
|
174
|
+
cmd, overwrite_output=overwrite_output, auto_fix=auto_fix
|
175
|
+
),
|
170
176
|
stdout=stdout,
|
171
177
|
stderr=stderr,
|
172
178
|
)
|
typed_ffmpeg/dag/io/_input.py
CHANGED
@@ -194,4 +194,6 @@ def input(
|
|
194
194
|
if v is not None
|
195
195
|
}
|
196
196
|
|
197
|
-
return InputNode(
|
197
|
+
return InputNode(
|
198
|
+
filename=str(filename), kwargs=tuple((options | (extra_options or {})).items())
|
199
|
+
).stream()
|
typed_ffmpeg/dag/io/_output.py
CHANGED
@@ -316,5 +316,7 @@ def output(
|
|
316
316
|
}
|
317
317
|
|
318
318
|
return OutputNode(
|
319
|
-
inputs=streams,
|
319
|
+
inputs=streams,
|
320
|
+
filename=str(filename),
|
321
|
+
kwargs=tuple((options | (extra_options or {})).items()),
|
320
322
|
).stream()
|
@@ -13,12 +13,13 @@ if TYPE_CHECKING:
|
|
13
13
|
|
14
14
|
class OutputArgs(ABC):
|
15
15
|
@abstractmethod
|
16
|
-
def _output_node(
|
17
|
-
|
16
|
+
def _output_node(
|
17
|
+
self, *streams: FilterableStream, filename: str | Path, **kwargs: Any
|
18
|
+
) -> OutputNode: ...
|
18
19
|
|
19
20
|
def output(
|
20
21
|
self,
|
21
|
-
*streams:
|
22
|
+
*streams: FilterableStream,
|
22
23
|
filename: str | Path,
|
23
24
|
f: String = None,
|
24
25
|
c: String = None,
|
@@ -324,4 +325,6 @@ class OutputArgs(ABC):
|
|
324
325
|
if v is not None
|
325
326
|
}
|
326
327
|
|
327
|
-
return self._output_node(
|
328
|
+
return self._output_node(
|
329
|
+
*streams, filename=filename, **options, **(extra_options or {})
|
330
|
+
).stream()
|
typed_ffmpeg/dag/nodes.py
CHANGED
@@ -55,7 +55,7 @@ class FilterNode(Node):
|
|
55
55
|
def repr(self) -> str:
|
56
56
|
return self.name
|
57
57
|
|
58
|
-
def video(self, index: int) ->
|
58
|
+
def video(self, index: int) -> VideoStream:
|
59
59
|
"""
|
60
60
|
Return the video stream at the specified index
|
61
61
|
|
@@ -67,12 +67,16 @@ class FilterNode(Node):
|
|
67
67
|
"""
|
68
68
|
from ..streams.video import VideoStream
|
69
69
|
|
70
|
-
video_outputs = [
|
70
|
+
video_outputs = [
|
71
|
+
i for i, k in enumerate(self.output_typings) if k == StreamType.video
|
72
|
+
]
|
71
73
|
if not len(video_outputs) > index:
|
72
|
-
raise FFMpegValueError(
|
74
|
+
raise FFMpegValueError(
|
75
|
+
f"Specified index {index} is out of range for video outputs {len(video_outputs)}"
|
76
|
+
)
|
73
77
|
return VideoStream(node=self, index=video_outputs[index])
|
74
78
|
|
75
|
-
def audio(self, index: int) ->
|
79
|
+
def audio(self, index: int) -> AudioStream:
|
76
80
|
"""
|
77
81
|
Return the audio stream at the specified index
|
78
82
|
|
@@ -84,9 +88,13 @@ class FilterNode(Node):
|
|
84
88
|
"""
|
85
89
|
from ..streams.audio import AudioStream
|
86
90
|
|
87
|
-
audio_outputs = [
|
91
|
+
audio_outputs = [
|
92
|
+
i for i, k in enumerate(self.output_typings) if k == StreamType.audio
|
93
|
+
]
|
88
94
|
if not len(audio_outputs) > index:
|
89
|
-
raise FFMpegValueError(
|
95
|
+
raise FFMpegValueError(
|
96
|
+
f"Specified index {index} is out of range for audio outputs {len(audio_outputs)}"
|
97
|
+
)
|
90
98
|
|
91
99
|
return AudioStream(node=self, index=audio_outputs[index])
|
92
100
|
|
@@ -97,12 +105,16 @@ class FilterNode(Node):
|
|
97
105
|
super().__post_init__()
|
98
106
|
|
99
107
|
if len(self.inputs) != len(self.input_typings):
|
100
|
-
raise FFMpegValueError(
|
108
|
+
raise FFMpegValueError(
|
109
|
+
f"Expected {len(self.input_typings)} inputs, got {len(self.inputs)}"
|
110
|
+
)
|
101
111
|
|
102
112
|
stream: FilterableStream
|
103
113
|
expected_type: StreamType
|
104
114
|
|
105
|
-
for i, (stream, expected_type) in enumerate(
|
115
|
+
for i, (stream, expected_type) in enumerate(
|
116
|
+
zip(self.inputs, self.input_typings)
|
117
|
+
):
|
106
118
|
if expected_type == StreamType.video:
|
107
119
|
if not isinstance(stream, VideoStream):
|
108
120
|
raise FFMpegTypeError(
|
@@ -132,7 +144,9 @@ class FilterNode(Node):
|
|
132
144
|
|
133
145
|
commands = []
|
134
146
|
for key, value in self.kwargs:
|
135
|
-
assert not isinstance(value, LazyValue),
|
147
|
+
assert not isinstance(value, LazyValue), (
|
148
|
+
f"LazyValue should have been evaluated: {key}={value}"
|
149
|
+
)
|
136
150
|
|
137
151
|
# Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
|
138
152
|
if isinstance(value, bool):
|
@@ -142,7 +156,12 @@ class FilterNode(Node):
|
|
142
156
|
commands += [f"{key}={escape(value)}"]
|
143
157
|
|
144
158
|
if commands:
|
145
|
-
return
|
159
|
+
return (
|
160
|
+
[incoming_labels]
|
161
|
+
+ [f"{self.name}="]
|
162
|
+
+ [escape(":".join(commands), "\\'[],;")]
|
163
|
+
+ [outgoing_labels]
|
164
|
+
)
|
146
165
|
return [incoming_labels] + [f"{self.name}"] + [outgoing_labels]
|
147
166
|
|
148
167
|
|
@@ -152,10 +171,12 @@ class FilterableStream(Stream, OutputArgs):
|
|
152
171
|
A stream that can be used as input to a filter
|
153
172
|
"""
|
154
173
|
|
155
|
-
node:
|
174
|
+
node: FilterNode | InputNode
|
156
175
|
|
157
176
|
@override
|
158
|
-
def _output_node(
|
177
|
+
def _output_node(
|
178
|
+
self, *streams: FilterableStream, filename: str | Path, **kwargs: Any
|
179
|
+
) -> OutputNode:
|
159
180
|
"""
|
160
181
|
Output the streams to a file URL
|
161
182
|
|
@@ -167,15 +188,19 @@ class FilterableStream(Stream, OutputArgs):
|
|
167
188
|
Returns:
|
168
189
|
the output stream
|
169
190
|
"""
|
170
|
-
return OutputNode(
|
191
|
+
return OutputNode(
|
192
|
+
inputs=(self, *streams),
|
193
|
+
filename=str(filename),
|
194
|
+
kwargs=tuple(kwargs.items()),
|
195
|
+
)
|
171
196
|
|
172
197
|
def vfilter(
|
173
198
|
self,
|
174
|
-
*streams:
|
199
|
+
*streams: FilterableStream,
|
175
200
|
name: str,
|
176
201
|
input_typings: tuple[StreamType, ...] = (StreamType.video,),
|
177
202
|
**kwargs: Any,
|
178
|
-
) ->
|
203
|
+
) -> VideoStream:
|
179
204
|
"""
|
180
205
|
Apply a custom video filter which has only one output to this stream
|
181
206
|
|
@@ -198,11 +223,11 @@ class FilterableStream(Stream, OutputArgs):
|
|
198
223
|
|
199
224
|
def afilter(
|
200
225
|
self,
|
201
|
-
*streams:
|
226
|
+
*streams: FilterableStream,
|
202
227
|
name: str,
|
203
228
|
input_typings: tuple[StreamType, ...] = (StreamType.audio,),
|
204
229
|
**kwargs: Any,
|
205
|
-
) ->
|
230
|
+
) -> AudioStream:
|
206
231
|
"""
|
207
232
|
Apply a custom audio filter which has only one output to this stream
|
208
233
|
|
@@ -225,12 +250,12 @@ class FilterableStream(Stream, OutputArgs):
|
|
225
250
|
|
226
251
|
def filter_multi_output(
|
227
252
|
self,
|
228
|
-
*streams:
|
253
|
+
*streams: FilterableStream,
|
229
254
|
name: str,
|
230
255
|
input_typings: tuple[StreamType, ...] = (),
|
231
256
|
output_typings: tuple[StreamType, ...] = (),
|
232
257
|
**kwargs: Any,
|
233
|
-
) ->
|
258
|
+
) -> FilterNode:
|
234
259
|
"""
|
235
260
|
Apply a custom filter which has multiple outputs to this stream
|
236
261
|
|
@@ -277,13 +302,17 @@ class FilterableStream(Stream, OutputArgs):
|
|
277
302
|
return f"{context.get_node_label(self.node)}:v"
|
278
303
|
elif isinstance(self, AudioStream):
|
279
304
|
return f"{context.get_node_label(self.node)}:a"
|
280
|
-
raise FFMpegValueError(
|
305
|
+
raise FFMpegValueError(
|
306
|
+
f"Unknown stream type: {self.__class__.__name__}"
|
307
|
+
) # pragma: no cover
|
281
308
|
|
282
309
|
if isinstance(self.node, FilterNode):
|
283
310
|
if len(self.node.output_typings) > 1:
|
284
311
|
return f"{context.get_node_label(self.node)}#{self.index}"
|
285
312
|
return f"{context.get_node_label(self.node)}"
|
286
|
-
raise FFMpegValueError(
|
313
|
+
raise FFMpegValueError(
|
314
|
+
f"Unknown node type: {self.node.__class__.__name__}"
|
315
|
+
) # pragma: no cover
|
287
316
|
|
288
317
|
def __post_init__(self) -> None:
|
289
318
|
if isinstance(self.node, InputNode):
|
@@ -310,7 +339,7 @@ class InputNode(Node):
|
|
310
339
|
return os.path.basename(self.filename)
|
311
340
|
|
312
341
|
@property
|
313
|
-
def video(self) ->
|
342
|
+
def video(self) -> VideoStream:
|
314
343
|
"""
|
315
344
|
Return the video stream of this node
|
316
345
|
|
@@ -322,7 +351,7 @@ class InputNode(Node):
|
|
322
351
|
return VideoStream(node=self)
|
323
352
|
|
324
353
|
@property
|
325
|
-
def audio(self) ->
|
354
|
+
def audio(self) -> AudioStream:
|
326
355
|
"""
|
327
356
|
Return the audio stream of this node
|
328
357
|
|
@@ -333,7 +362,7 @@ class InputNode(Node):
|
|
333
362
|
|
334
363
|
return AudioStream(node=self)
|
335
364
|
|
336
|
-
def stream(self) ->
|
365
|
+
def stream(self) -> AVStream:
|
337
366
|
"""
|
338
367
|
Return the output stream of this node
|
339
368
|
|
@@ -371,7 +400,7 @@ class OutputNode(Node):
|
|
371
400
|
def repr(self) -> str:
|
372
401
|
return os.path.basename(self.filename)
|
373
402
|
|
374
|
-
def stream(self) ->
|
403
|
+
def stream(self) -> OutputStream:
|
375
404
|
"""
|
376
405
|
Return the output stream of this node
|
377
406
|
|
@@ -437,7 +466,7 @@ class GlobalNode(Node):
|
|
437
466
|
def repr(self) -> str:
|
438
467
|
return " ".join(self.get_args())
|
439
468
|
|
440
|
-
def stream(self) ->
|
469
|
+
def stream(self) -> GlobalStream:
|
441
470
|
"""
|
442
471
|
Return the output stream of this node
|
443
472
|
|
typed_ffmpeg/dag/schema.py
CHANGED
@@ -70,7 +70,7 @@ class Stream(HashableBaseModel):
|
|
70
70
|
return f.read()
|
71
71
|
|
72
72
|
def _repr_svg_(self) -> str: # pragma: no cover
|
73
|
-
with open(self.view(format="svg")
|
73
|
+
with open(self.view(format="svg")) as f:
|
74
74
|
return f.read()
|
75
75
|
|
76
76
|
|
@@ -109,7 +109,7 @@ class Node(HashableBaseModel, ABC):
|
|
109
109
|
|
110
110
|
nodes.extend(k.node for k in node.inputs)
|
111
111
|
|
112
|
-
output[node.hex] =
|
112
|
+
output[node.hex] = {k.node.hex for k in node.inputs}
|
113
113
|
|
114
114
|
if not is_dag(output):
|
115
115
|
raise ValueError(f"Graph is not a DAG: {output}") # pragma: no cover
|
@@ -206,5 +206,5 @@ class Node(HashableBaseModel, ABC):
|
|
206
206
|
return f.read()
|
207
207
|
|
208
208
|
def _repr_svg_(self) -> str: # pragma: no cover
|
209
|
-
with open(self.view(format="svg")
|
209
|
+
with open(self.view(format="svg")) as f:
|
210
210
|
return f.read()
|
typed_ffmpeg/dag/validate.py
CHANGED
@@ -10,7 +10,9 @@ from .nodes import FilterNode, InputNode
|
|
10
10
|
from .schema import Node, Stream
|
11
11
|
|
12
12
|
|
13
|
-
def remove_split(
|
13
|
+
def remove_split(
|
14
|
+
current_stream: Stream, mapping: dict[Stream, Stream] = None
|
15
|
+
) -> tuple[Stream, dict[Stream, Stream]]:
|
14
16
|
"""
|
15
17
|
Rebuild the graph with the given mapping.
|
16
18
|
|
@@ -37,20 +39,28 @@ def remove_split(current_stream: Stream, mapping: dict[Stream, Stream] = None) -
|
|
37
39
|
if isinstance(current_stream.node, FilterNode):
|
38
40
|
# if the current node is a split node, we need to remove it
|
39
41
|
if current_stream.node.name in ("split", "asplit"):
|
40
|
-
new_stream, _mapping = remove_split(
|
42
|
+
new_stream, _mapping = remove_split(
|
43
|
+
current_stream=current_stream.node.inputs[0], mapping=mapping
|
44
|
+
)
|
41
45
|
mapping[current_stream] = mapping[current_stream.node.inputs[0]]
|
42
46
|
return mapping[current_stream.node.inputs[0]], mapping
|
43
47
|
|
44
48
|
inputs = {}
|
45
49
|
for idx, input_stream in sorted(
|
46
|
-
enumerate(current_stream.node.inputs),
|
50
|
+
enumerate(current_stream.node.inputs),
|
51
|
+
key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes),
|
47
52
|
):
|
48
|
-
new_stream, _mapping = remove_split(
|
53
|
+
new_stream, _mapping = remove_split(
|
54
|
+
current_stream=input_stream, mapping=mapping
|
55
|
+
)
|
49
56
|
inputs[idx] = new_stream
|
50
57
|
mapping |= _mapping
|
51
58
|
|
52
59
|
new_node = replace(
|
53
|
-
current_stream.node,
|
60
|
+
current_stream.node,
|
61
|
+
inputs=tuple(
|
62
|
+
stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0])
|
63
|
+
),
|
54
64
|
)
|
55
65
|
new_stream = replace(current_stream, node=new_node)
|
56
66
|
|
@@ -91,16 +101,24 @@ def add_split(
|
|
91
101
|
inputs = {}
|
92
102
|
|
93
103
|
for idx, input_stream in sorted(
|
94
|
-
enumerate(current_stream.node.inputs),
|
104
|
+
enumerate(current_stream.node.inputs),
|
105
|
+
key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes),
|
95
106
|
):
|
96
107
|
new_stream, _mapping = add_split(
|
97
|
-
current_stream=input_stream,
|
108
|
+
current_stream=input_stream,
|
109
|
+
down_node=current_stream.node,
|
110
|
+
down_index=idx,
|
111
|
+
mapping=mapping,
|
112
|
+
context=context,
|
98
113
|
)
|
99
114
|
inputs[idx] = new_stream
|
100
115
|
mapping |= _mapping
|
101
116
|
|
102
117
|
new_node = replace(
|
103
|
-
current_stream.node,
|
118
|
+
current_stream.node,
|
119
|
+
inputs=tuple(
|
120
|
+
stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0])
|
121
|
+
),
|
104
122
|
)
|
105
123
|
new_stream = replace(current_stream, node=new_node)
|
106
124
|
|
typed_ffmpeg/exceptions.py
CHANGED