typed-ffmpeg-compatible 2.1.0__py3-none-any.whl
Sign up to get free protection for your applications and to get access to all the features.
- typed_ffmpeg/__init__.py +25 -0
- typed_ffmpeg/base.py +114 -0
- typed_ffmpeg/common/__init__.py +0 -0
- typed_ffmpeg/common/schema.py +308 -0
- typed_ffmpeg/common/serialize.py +132 -0
- typed_ffmpeg/dag/__init__.py +13 -0
- typed_ffmpeg/dag/compile.py +51 -0
- typed_ffmpeg/dag/context.py +221 -0
- typed_ffmpeg/dag/factory.py +31 -0
- typed_ffmpeg/dag/global_runnable/__init__.py +0 -0
- typed_ffmpeg/dag/global_runnable/global_args.py +178 -0
- typed_ffmpeg/dag/global_runnable/runnable.py +174 -0
- typed_ffmpeg/dag/io/__init__.py +0 -0
- typed_ffmpeg/dag/io/_input.py +197 -0
- typed_ffmpeg/dag/io/_output.py +318 -0
- typed_ffmpeg/dag/io/output_args.py +327 -0
- typed_ffmpeg/dag/nodes.py +479 -0
- typed_ffmpeg/dag/schema.py +210 -0
- typed_ffmpeg/dag/utils.py +41 -0
- typed_ffmpeg/dag/validate.py +172 -0
- typed_ffmpeg/exceptions.py +42 -0
- typed_ffmpeg/filters.py +3510 -0
- typed_ffmpeg/probe.py +43 -0
- typed_ffmpeg/py.typed +0 -0
- typed_ffmpeg/schema.py +29 -0
- typed_ffmpeg/streams/__init__.py +5 -0
- typed_ffmpeg/streams/audio.py +6955 -0
- typed_ffmpeg/streams/av.py +22 -0
- typed_ffmpeg/streams/channel_layout.py +39 -0
- typed_ffmpeg/streams/video.py +12974 -0
- typed_ffmpeg/types.py +119 -0
- typed_ffmpeg/utils/__init__.py +0 -0
- typed_ffmpeg/utils/escaping.py +49 -0
- typed_ffmpeg/utils/lazy_eval/__init__.py +0 -0
- typed_ffmpeg/utils/lazy_eval/operator.py +134 -0
- typed_ffmpeg/utils/lazy_eval/schema.py +211 -0
- typed_ffmpeg/utils/run.py +27 -0
- typed_ffmpeg/utils/snapshot.py +26 -0
- typed_ffmpeg/utils/typing.py +17 -0
- typed_ffmpeg/utils/view.py +64 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/LICENSE +21 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/METADATA +183 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/RECORD +45 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/WHEEL +4 -0
- typed_ffmpeg_compatible-2.1.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,41 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from collections import deque
|
4
|
+
|
5
|
+
# Another approach to determine if a graph is a DAG is to try to perform a topological sort.
|
6
|
+
# If the topological sort is successful (i.e., all vertices are visited exactly once),
|
7
|
+
# the graph is a DAG. If the topological sort cannot include all vertices (i.e., the graph has a cycle),
|
8
|
+
# it is not a DAG. Here is a basic implementation using Kahn's Algorithm:
|
9
|
+
|
10
|
+
|
11
|
+
def is_dag(graph: dict[str, set[str]]) -> bool:
|
12
|
+
"""
|
13
|
+
Determine if a graph is a directed acyclic graph (DAG).
|
14
|
+
|
15
|
+
Args:
|
16
|
+
graph: The graph to check.
|
17
|
+
|
18
|
+
Returns:
|
19
|
+
Whether the graph is a DAG.
|
20
|
+
"""
|
21
|
+
|
22
|
+
in_degree = {u: 0 for u in graph} # Initialize in-degree of each node to 0
|
23
|
+
|
24
|
+
# Calculate in-degree of each node
|
25
|
+
for u in graph:
|
26
|
+
for v in graph[u]:
|
27
|
+
in_degree[v] += 1
|
28
|
+
|
29
|
+
queue = deque([u for u in graph if in_degree[u] == 0])
|
30
|
+
count = 0
|
31
|
+
|
32
|
+
while queue:
|
33
|
+
u = queue.popleft()
|
34
|
+
count += 1
|
35
|
+
|
36
|
+
for v in graph[u]:
|
37
|
+
in_degree[v] -= 1
|
38
|
+
if in_degree[v] == 0:
|
39
|
+
queue.append(v)
|
40
|
+
|
41
|
+
return count == len(graph)
|
@@ -0,0 +1,172 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import replace
|
4
|
+
|
5
|
+
from ..exceptions import FFMpegValueError
|
6
|
+
from ..streams.audio import AudioStream
|
7
|
+
from ..streams.video import VideoStream
|
8
|
+
from .context import DAGContext
|
9
|
+
from .nodes import FilterNode, InputNode
|
10
|
+
from .schema import Node, Stream
|
11
|
+
|
12
|
+
|
13
|
+
def remove_split(current_stream: Stream, mapping: dict[Stream, Stream] = None) -> tuple[Stream, dict[Stream, Stream]]:
|
14
|
+
"""
|
15
|
+
Rebuild the graph with the given mapping.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
current_stream: The stream to rebuild the graph with.
|
19
|
+
mapping: The mapping to rebuild the graph with.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
A tuple of the new node and the new mapping.
|
23
|
+
"""
|
24
|
+
|
25
|
+
# remove all split nodes
|
26
|
+
# add split nodes to the graph
|
27
|
+
if mapping is None:
|
28
|
+
mapping = {}
|
29
|
+
|
30
|
+
if current_stream in mapping:
|
31
|
+
return mapping[current_stream], mapping
|
32
|
+
|
33
|
+
if not current_stream.node.inputs:
|
34
|
+
mapping[current_stream] = current_stream
|
35
|
+
return current_stream, mapping
|
36
|
+
|
37
|
+
if isinstance(current_stream.node, FilterNode):
|
38
|
+
# if the current node is a split node, we need to remove it
|
39
|
+
if current_stream.node.name in ("split", "asplit"):
|
40
|
+
new_stream, _mapping = remove_split(current_stream=current_stream.node.inputs[0], mapping=mapping)
|
41
|
+
mapping[current_stream] = mapping[current_stream.node.inputs[0]]
|
42
|
+
return mapping[current_stream.node.inputs[0]], mapping
|
43
|
+
|
44
|
+
inputs = {}
|
45
|
+
for idx, input_stream in sorted(
|
46
|
+
enumerate(current_stream.node.inputs), key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes)
|
47
|
+
):
|
48
|
+
new_stream, _mapping = remove_split(current_stream=input_stream, mapping=mapping)
|
49
|
+
inputs[idx] = new_stream
|
50
|
+
mapping |= _mapping
|
51
|
+
|
52
|
+
new_node = replace(
|
53
|
+
current_stream.node, inputs=tuple(stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0]))
|
54
|
+
)
|
55
|
+
new_stream = replace(current_stream, node=new_node)
|
56
|
+
|
57
|
+
mapping[current_stream] = new_stream
|
58
|
+
return new_stream, mapping
|
59
|
+
|
60
|
+
|
61
|
+
def add_split(
|
62
|
+
current_stream: Stream,
|
63
|
+
down_node: Node = None,
|
64
|
+
down_index: int = None,
|
65
|
+
context: DAGContext = None,
|
66
|
+
mapping: dict[tuple[Stream, Node | None, int | None], Stream] = None,
|
67
|
+
) -> tuple[Stream, dict[tuple[Stream, Node | None, int | None], Stream]]:
|
68
|
+
"""
|
69
|
+
Add split nodes to the graph.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
current_stream: The stream to add split nodes to.
|
73
|
+
down_node: The node use current_stream as input.
|
74
|
+
down_index: The index of the input stream in down_node.
|
75
|
+
context: The DAG context.
|
76
|
+
mapping: The mapping to add split nodes to.
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
A tuple of the new node and the new mapping.
|
80
|
+
"""
|
81
|
+
|
82
|
+
if not context:
|
83
|
+
context = DAGContext.build(current_stream.node)
|
84
|
+
|
85
|
+
if mapping is None:
|
86
|
+
mapping = {}
|
87
|
+
|
88
|
+
if (current_stream, down_node, down_index) in mapping:
|
89
|
+
return mapping[(current_stream, down_node, down_index)], mapping
|
90
|
+
|
91
|
+
inputs = {}
|
92
|
+
|
93
|
+
for idx, input_stream in sorted(
|
94
|
+
enumerate(current_stream.node.inputs), key=lambda idx_stream: -len(idx_stream[1].node.upstream_nodes)
|
95
|
+
):
|
96
|
+
new_stream, _mapping = add_split(
|
97
|
+
current_stream=input_stream, down_node=current_stream.node, down_index=idx, mapping=mapping, context=context
|
98
|
+
)
|
99
|
+
inputs[idx] = new_stream
|
100
|
+
mapping |= _mapping
|
101
|
+
|
102
|
+
new_node = replace(
|
103
|
+
current_stream.node, inputs=tuple(stream for idx, stream in sorted(inputs.items(), key=lambda x: x[0]))
|
104
|
+
)
|
105
|
+
new_stream = replace(current_stream, node=new_node)
|
106
|
+
|
107
|
+
num = len(context.get_outgoing_nodes(current_stream))
|
108
|
+
if num < 2:
|
109
|
+
mapping[(current_stream, down_node, down_index)] = new_stream
|
110
|
+
return new_stream, mapping
|
111
|
+
|
112
|
+
if isinstance(current_stream.node, InputNode):
|
113
|
+
for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
|
114
|
+
# if the current node is InputNode, we don't need to split it
|
115
|
+
mapping[(current_stream, node, index)] = new_stream
|
116
|
+
return new_stream, mapping
|
117
|
+
|
118
|
+
if isinstance(new_stream, VideoStream):
|
119
|
+
split_node = new_stream.split(outputs=num)
|
120
|
+
for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
|
121
|
+
mapping[(current_stream, node, index)] = split_node.video(idx)
|
122
|
+
return mapping[(current_stream, down_node, down_index)], mapping
|
123
|
+
elif isinstance(new_stream, AudioStream):
|
124
|
+
split_node = new_stream.asplit(outputs=num)
|
125
|
+
for idx, (node, index) in enumerate(context.get_outgoing_nodes(current_stream)):
|
126
|
+
mapping[(current_stream, node, index)] = split_node.audio(idx)
|
127
|
+
return mapping[(current_stream, down_node, down_index)], mapping
|
128
|
+
else:
|
129
|
+
raise FFMpegValueError(f"unsupported stream type: {current_stream}")
|
130
|
+
|
131
|
+
|
132
|
+
def fix_graph(stream: Stream) -> Stream:
|
133
|
+
"""
|
134
|
+
Fix the graph by removing and adding split nodes.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
stream: The stream to fix.
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
The fixed stream.
|
141
|
+
|
142
|
+
Note:
|
143
|
+
Fix the graph by resetting split nodes.
|
144
|
+
This function is for internal use only.
|
145
|
+
"""
|
146
|
+
|
147
|
+
stream, _ = remove_split(stream)
|
148
|
+
stream, _ = add_split(stream)
|
149
|
+
return stream
|
150
|
+
|
151
|
+
|
152
|
+
def validate(stream: Stream, auto_fix: bool = True) -> Stream:
|
153
|
+
"""
|
154
|
+
Validate the given DAG. If auto_fix is True, the graph will be automatically fixed to follow ffmpeg's rule.
|
155
|
+
|
156
|
+
Args:
|
157
|
+
stream: The DAG to validate.
|
158
|
+
auto_fix: Whether to automatically fix the graph.
|
159
|
+
|
160
|
+
Returns:
|
161
|
+
The validated DAG context.
|
162
|
+
"""
|
163
|
+
if auto_fix:
|
164
|
+
stream = fix_graph(stream)
|
165
|
+
|
166
|
+
# NOTE: we don't want to modify the original node
|
167
|
+
# validators: list[] = []
|
168
|
+
|
169
|
+
# for validator in validators:
|
170
|
+
# context = validator(context)
|
171
|
+
|
172
|
+
return stream
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class FFMpegError(Exception):
|
2
|
+
"""
|
3
|
+
Base exception for all ffmpeg errors.
|
4
|
+
"""
|
5
|
+
|
6
|
+
...
|
7
|
+
|
8
|
+
|
9
|
+
class FFMpegTypeError(FFMpegError, TypeError):
|
10
|
+
"""
|
11
|
+
Base exception for all ffmpeg type errors.
|
12
|
+
"""
|
13
|
+
|
14
|
+
|
15
|
+
class FFMpegValueError(FFMpegError, ValueError):
|
16
|
+
"""
|
17
|
+
Base exception for all ffmpeg value errors.
|
18
|
+
"""
|
19
|
+
|
20
|
+
|
21
|
+
class FFMpegExecuteError(FFMpegError):
|
22
|
+
"""
|
23
|
+
FFmpeg error
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, retcode: int | None, cmd: str, stdout: bytes, stderr: bytes):
|
27
|
+
"""
|
28
|
+
Initialize the exception.
|
29
|
+
|
30
|
+
Args:
|
31
|
+
retcode: The return code of the command.
|
32
|
+
cmd: The command that was run.
|
33
|
+
stdout: The stdout of the command.
|
34
|
+
stderr: The stderr of the command.
|
35
|
+
"""
|
36
|
+
|
37
|
+
self.stdout = stdout
|
38
|
+
self.stderr = stderr
|
39
|
+
self.cmd = cmd
|
40
|
+
self.retcode = retcode
|
41
|
+
|
42
|
+
super(FFMpegExecuteError, self).__init__(f"{cmd} error (see stderr output for detail) {stderr!r}")
|