typed-ffmpeg-compatible 2.6.0__py3-none-any.whl → 2.6.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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