typed-ffmpeg-compatible 2.7.3__py3-none-any.whl → 3.0.0__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/__init__.py +2 -1
- typed_ffmpeg/common/cache/.gitignore +3 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/acrossover.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/afir.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aiir.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ainterleave.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amerge.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amix.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amovie.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/anequalizer.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aphasemeter.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asegment.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aselect.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asplit.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/astreamselect.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/bm3d.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/channelsplit.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/concat.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/decimate.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ebur128.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/extractplanes.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/fieldmatch.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/guided.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/headphone.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/hstack.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/interleave.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/join.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/libplacebo.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/limitdiff.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mergeplanes.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mix.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/movie.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/premultiply.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/segment.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/select.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/signature.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/split.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/streamselect.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/unpremultiply.json +6 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/vstack.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xmedian.json +9 -0
- typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xstack.json +9 -0
- typed_ffmpeg/common/cache/list/filters.json +90747 -0
- typed_ffmpeg/common/cache.py +66 -0
- typed_ffmpeg/common/schema.py +9 -5
- typed_ffmpeg/common/serialize.py +12 -8
- typed_ffmpeg/compile/__init__.py +0 -0
- typed_ffmpeg/compile/compile_cli.py +351 -0
- typed_ffmpeg/compile/compile_python.py +319 -0
- typed_ffmpeg/{dag → compile}/context.py +26 -45
- typed_ffmpeg/{dag → compile}/validate.py +2 -2
- typed_ffmpeg/dag/global_runnable/runnable.py +1 -1
- typed_ffmpeg/dag/nodes.py +1 -227
- typed_ffmpeg/dag/schema.py +2 -18
- typed_ffmpeg/utils/view.py +1 -1
- {typed_ffmpeg_compatible-2.7.3.dist-info → typed_ffmpeg_compatible-3.0.0.dist-info}/METADATA +1 -1
- typed_ffmpeg_compatible-3.0.0.dist-info/RECORD +93 -0
- typed_ffmpeg/dag/compile.py +0 -86
- typed_ffmpeg_compatible-2.7.3.dist-info/RECORD +0 -48
- {typed_ffmpeg_compatible-2.7.3.dist-info → typed_ffmpeg_compatible-3.0.0.dist-info}/LICENSE +0 -0
- {typed_ffmpeg_compatible-2.7.3.dist-info → typed_ffmpeg_compatible-3.0.0.dist-info}/WHEEL +0 -0
- {typed_ffmpeg_compatible-2.7.3.dist-info → typed_ffmpeg_compatible-3.0.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,66 @@
|
|
1
|
+
from pathlib import Path
|
2
|
+
from typing import TypeVar
|
3
|
+
|
4
|
+
from .serialize import dumps, loads
|
5
|
+
|
6
|
+
T = TypeVar("T")
|
7
|
+
|
8
|
+
cache_path = Path(__file__).parent / "cache"
|
9
|
+
cache_path.mkdir(exist_ok=True)
|
10
|
+
|
11
|
+
|
12
|
+
def load(cls: type[T], id: str) -> T:
|
13
|
+
"""
|
14
|
+
Load an object from the cache
|
15
|
+
|
16
|
+
Args:
|
17
|
+
cls: The class of the object
|
18
|
+
id: The id of the object
|
19
|
+
|
20
|
+
Returns:
|
21
|
+
The loaded object
|
22
|
+
"""
|
23
|
+
path = cache_path / f"{cls.__name__}/{id}.json"
|
24
|
+
|
25
|
+
with path.open() as ifile:
|
26
|
+
obj = loads(ifile.read())
|
27
|
+
return obj
|
28
|
+
|
29
|
+
|
30
|
+
def save(obj: T, id: str) -> None:
|
31
|
+
"""
|
32
|
+
Save an object to the cache
|
33
|
+
|
34
|
+
Args:
|
35
|
+
obj: The object to save
|
36
|
+
id: The id of the object
|
37
|
+
"""
|
38
|
+
schema_path = cache_path / f"{obj.__class__.__name__}"
|
39
|
+
schema_path.mkdir(exist_ok=True)
|
40
|
+
|
41
|
+
with (schema_path / f"{id}.json").open("w") as ofile:
|
42
|
+
ofile.write(dumps(obj))
|
43
|
+
|
44
|
+
|
45
|
+
def list_all(cls: type[T]) -> list[T]:
|
46
|
+
"""
|
47
|
+
List all objects of a class in the cache
|
48
|
+
|
49
|
+
Args:
|
50
|
+
cls: The class of the objects
|
51
|
+
|
52
|
+
Returns:
|
53
|
+
A list of all objects of the class in the cache
|
54
|
+
"""
|
55
|
+
path = cache_path / f"{cls.__name__}"
|
56
|
+
|
57
|
+
return [loads(i.read_text()) for i in path.glob("*.json")]
|
58
|
+
|
59
|
+
|
60
|
+
def clean(cls: type[T]) -> None:
|
61
|
+
"""
|
62
|
+
Clean the cache for a class
|
63
|
+
"""
|
64
|
+
path = cache_path / f"{cls.__name__}"
|
65
|
+
for i in path.glob("*.json"):
|
66
|
+
i.unlink()
|
typed_ffmpeg/common/schema.py
CHANGED
@@ -33,6 +33,7 @@ class StreamType(str, Enum):
|
|
33
33
|
"""Represents a video stream containing visual frame data"""
|
34
34
|
|
35
35
|
|
36
|
+
@serializable
|
36
37
|
class FFMpegFilterOptionType(str, Enum):
|
37
38
|
"""
|
38
39
|
Enumeration of possible data types for FFmpeg filter options.
|
@@ -60,6 +61,7 @@ class FFMpegFilterOptionType(str, Enum):
|
|
60
61
|
binary = "binary"
|
61
62
|
|
62
63
|
|
64
|
+
@serializable
|
63
65
|
class FFMpegFilterType(str, Enum):
|
64
66
|
"""
|
65
67
|
Enumeration of FFmpeg filter types based on input/output stream types.
|
@@ -98,7 +100,7 @@ class FFMpegFilterType(str, Enum):
|
|
98
100
|
|
99
101
|
|
100
102
|
@dataclass(frozen=True, kw_only=True)
|
101
|
-
class FFMpegFilterOptionChoice:
|
103
|
+
class FFMpegFilterOptionChoice(Serializable):
|
102
104
|
"""
|
103
105
|
Represents a single choice for an FFmpeg filter option with enumerated values.
|
104
106
|
|
@@ -120,7 +122,7 @@ class FFMpegFilterOptionChoice:
|
|
120
122
|
|
121
123
|
|
122
124
|
@dataclass(frozen=True, kw_only=True)
|
123
|
-
class FFMpegFilterOption:
|
125
|
+
class FFMpegFilterOption(Serializable):
|
124
126
|
"""
|
125
127
|
Represents a configurable option for an FFmpeg filter.
|
126
128
|
|
@@ -160,7 +162,7 @@ class FFMpegFilterOption:
|
|
160
162
|
|
161
163
|
|
162
164
|
@dataclass(frozen=True, kw_only=True)
|
163
|
-
class FFMpegIOType:
|
165
|
+
class FFMpegIOType(Serializable):
|
164
166
|
"""
|
165
167
|
Defines the type information for a filter's input or output stream.
|
166
168
|
|
@@ -176,7 +178,7 @@ class FFMpegIOType:
|
|
176
178
|
|
177
179
|
|
178
180
|
@dataclass(frozen=True, kw_only=True)
|
179
|
-
class FFMpegFilterDef:
|
181
|
+
class FFMpegFilterDef(Serializable):
|
180
182
|
"""
|
181
183
|
Defines the basic structure of an FFmpeg filter.
|
182
184
|
|
@@ -419,6 +421,7 @@ class FFMpegFilter(Serializable):
|
|
419
421
|
raise ValueError(f"Unknown filter type for {self.name}")
|
420
422
|
|
421
423
|
|
424
|
+
@serializable
|
422
425
|
class FFMpegOptionFlag(int, Enum):
|
423
426
|
OPT_FUNC_ARG = 1 << 0
|
424
427
|
"""
|
@@ -494,6 +497,7 @@ class FFMpegOptionFlag(int, Enum):
|
|
494
497
|
"""
|
495
498
|
|
496
499
|
|
500
|
+
@serializable
|
497
501
|
class FFMpegOptionType(str, Enum):
|
498
502
|
"""
|
499
503
|
Enumeration of FFmpeg option data types.
|
@@ -529,7 +533,7 @@ class FFMpegOptionType(str, Enum):
|
|
529
533
|
|
530
534
|
|
531
535
|
@dataclass(frozen=True, kw_only=True)
|
532
|
-
class FFMpegOption:
|
536
|
+
class FFMpegOption(Serializable):
|
533
537
|
"""
|
534
538
|
Represents a command-line option for FFmpeg.
|
535
539
|
|
typed_ffmpeg/common/serialize.py
CHANGED
@@ -12,7 +12,6 @@ from __future__ import annotations
|
|
12
12
|
import json
|
13
13
|
from dataclasses import fields, is_dataclass
|
14
14
|
from enum import Enum
|
15
|
-
from functools import partial
|
16
15
|
from pathlib import Path
|
17
16
|
from typing import Any
|
18
17
|
|
@@ -29,6 +28,15 @@ def serializable(
|
|
29
28
|
) -> type[Serializable] | type[Enum]:
|
30
29
|
"""
|
31
30
|
Register a class with the serialization system.
|
31
|
+
|
32
|
+
This function is used by the `serializable` decorator to register classes
|
33
|
+
with the serialization system, enabling them to be serialized and deserialized.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
cls: The class to register
|
37
|
+
|
38
|
+
Returns:
|
39
|
+
The class itself
|
32
40
|
"""
|
33
41
|
assert cls.__name__ not in CLASS_REGISTRY, (
|
34
42
|
f"Class {cls.__name__} already registered"
|
@@ -109,7 +117,7 @@ def frozen(v: Any) -> Any:
|
|
109
117
|
return v
|
110
118
|
|
111
119
|
|
112
|
-
def object_hook(obj: Any
|
120
|
+
def object_hook(obj: Any) -> Any:
|
113
121
|
"""
|
114
122
|
Custom JSON object hook for deserializing FFmpeg objects.
|
115
123
|
|
@@ -119,7 +127,6 @@ def object_hook(obj: Any, strict: bool = True) -> Any:
|
|
119
127
|
|
120
128
|
Args:
|
121
129
|
obj: A dictionary from the JSON parser
|
122
|
-
strict: If True, only allow loading classes from the ffmpeg package
|
123
130
|
|
124
131
|
Returns:
|
125
132
|
Either the original dictionary or an instance of the specified class
|
@@ -148,7 +155,7 @@ def object_hook(obj: Any, strict: bool = True) -> Any:
|
|
148
155
|
return obj
|
149
156
|
|
150
157
|
|
151
|
-
def loads(raw: str
|
158
|
+
def loads(raw: str) -> Any:
|
152
159
|
"""
|
153
160
|
Deserialize a JSON string into Python objects with proper class types.
|
154
161
|
|
@@ -158,7 +165,6 @@ def loads(raw: str, strict: bool = True) -> Any:
|
|
158
165
|
|
159
166
|
Args:
|
160
167
|
raw: The JSON string to deserialize
|
161
|
-
strict: If True, only allow loading classes from the ffmpeg package
|
162
168
|
|
163
169
|
Returns:
|
164
170
|
The deserialized Python object with proper types
|
@@ -171,9 +177,7 @@ def loads(raw: str, strict: bool = True) -> Any:
|
|
171
177
|
# filter_node is now a FilterNode instance
|
172
178
|
```
|
173
179
|
"""
|
174
|
-
|
175
|
-
|
176
|
-
return json.loads(raw, object_hook=object_hook_strict)
|
180
|
+
return json.loads(raw, object_hook=object_hook)
|
177
181
|
|
178
182
|
|
179
183
|
def to_dict_with_class_info(instance: Any) -> Any:
|
File without changes
|
@@ -0,0 +1,351 @@
|
|
1
|
+
"""
|
2
|
+
Compiles FFmpeg filter graphs into command-line arguments.
|
3
|
+
|
4
|
+
This module provides functionality to convert the internal DAG (Directed Acyclic Graph)
|
5
|
+
representation of an FFmpeg filter chain into the actual command-line arguments
|
6
|
+
that would be passed to FFmpeg. It traverses the graph in the correct order,
|
7
|
+
handling global options, inputs, complex filtergraphs, and outputs.
|
8
|
+
"""
|
9
|
+
|
10
|
+
from __future__ import annotations
|
11
|
+
|
12
|
+
from ..dag.nodes import FilterableStream, FilterNode, GlobalNode, InputNode, OutputNode
|
13
|
+
from ..dag.schema import Node, Stream
|
14
|
+
from ..exceptions import FFMpegValueError
|
15
|
+
from ..schema import Default
|
16
|
+
from ..utils.escaping import escape
|
17
|
+
from ..utils.lazy_eval.schema import LazyValue
|
18
|
+
from .context import DAGContext
|
19
|
+
from .validate import validate
|
20
|
+
|
21
|
+
|
22
|
+
def compile(stream: Stream, auto_fix: bool = True) -> list[str]:
|
23
|
+
"""
|
24
|
+
Compile a stream into a list of FFmpeg command-line arguments.
|
25
|
+
|
26
|
+
This function takes a Stream object representing an FFmpeg filter graph
|
27
|
+
and converts it into a list of command-line arguments that can be passed
|
28
|
+
to FFmpeg. It processes the graph in the correct order:
|
29
|
+
1. Global nodes (general FFmpeg options)
|
30
|
+
2. Input nodes (input files and their options)
|
31
|
+
3. Filter nodes (combined into a -filter_complex argument)
|
32
|
+
4. Output nodes (output files and their options)
|
33
|
+
|
34
|
+
The function validates the graph before compilation to ensure it's properly
|
35
|
+
formed. If auto_fix is enabled, it will attempt to fix common issues.
|
36
|
+
|
37
|
+
Args:
|
38
|
+
stream: The Stream object to compile into arguments
|
39
|
+
auto_fix: Whether to automatically fix issues in the stream
|
40
|
+
(e.g., reconnecting disconnected nodes)
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
A list of strings representing FFmpeg command-line arguments
|
44
|
+
|
45
|
+
Example:
|
46
|
+
```python
|
47
|
+
# Create a simple video scaling filter graph
|
48
|
+
input_stream = ffmpeg.input("input.mp4")
|
49
|
+
scaled = input_stream.filter("scale", 1280, 720)
|
50
|
+
output_stream = scaled.output("output.mp4")
|
51
|
+
|
52
|
+
# Compile to FFmpeg arguments
|
53
|
+
args = ffmpeg.dag.compile(output_stream)
|
54
|
+
print(
|
55
|
+
args
|
56
|
+
) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
|
57
|
+
```
|
58
|
+
"""
|
59
|
+
|
60
|
+
stream = validate(stream, auto_fix=auto_fix)
|
61
|
+
node = stream.node
|
62
|
+
context = DAGContext.build(node)
|
63
|
+
|
64
|
+
# compile the global nodes
|
65
|
+
commands = []
|
66
|
+
global_nodes = [node for node in context.all_nodes if isinstance(node, GlobalNode)]
|
67
|
+
for node in global_nodes:
|
68
|
+
commands += get_args(node, context)
|
69
|
+
|
70
|
+
# compile the input nodes
|
71
|
+
input_nodes = [node for node in context.all_nodes if isinstance(node, InputNode)]
|
72
|
+
for node in input_nodes:
|
73
|
+
commands += get_args(node, context)
|
74
|
+
|
75
|
+
# compile the filter nodes
|
76
|
+
vf_commands = []
|
77
|
+
filter_nodes = [node for node in context.all_nodes if isinstance(node, FilterNode)]
|
78
|
+
|
79
|
+
for node in sorted(filter_nodes, key=lambda node: len(node.upstream_nodes)):
|
80
|
+
vf_commands += ["".join(get_args(node, context))]
|
81
|
+
|
82
|
+
if vf_commands:
|
83
|
+
commands += ["-filter_complex", ";".join(vf_commands)]
|
84
|
+
|
85
|
+
# compile the output nodes
|
86
|
+
output_nodes = [node for node in context.all_nodes if isinstance(node, OutputNode)]
|
87
|
+
for node in output_nodes:
|
88
|
+
commands += get_args(node, context)
|
89
|
+
|
90
|
+
return commands
|
91
|
+
|
92
|
+
|
93
|
+
def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
|
94
|
+
"""
|
95
|
+
Generate the FFmpeg label for this stream in filter graphs.
|
96
|
+
|
97
|
+
This method creates the label string used to identify this stream in
|
98
|
+
FFmpeg filter graphs. The format of the label depends on the stream's
|
99
|
+
source (input file or filter) and type (video or audio).
|
100
|
+
|
101
|
+
For input streams, labels follow FFmpeg's stream specifier syntax:
|
102
|
+
- Video streams: "0:v" (first input, video stream)
|
103
|
+
- Audio streams: "0:a" (first input, audio stream)
|
104
|
+
- AV streams: "0" (first input, all streams)
|
105
|
+
|
106
|
+
For filter outputs, labels use the filter's label:
|
107
|
+
- Single output filters: "filterlabel"
|
108
|
+
- Multi-output filters: "filterlabel#index"
|
109
|
+
|
110
|
+
Args:
|
111
|
+
context: Optional DAG context for resolving node labels.
|
112
|
+
If not provided, a new context will be built.
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
A string label for this stream in FFmpeg filter syntax
|
116
|
+
|
117
|
+
Raises:
|
118
|
+
FFMpegValueError: If the stream has an unknown type or node type
|
119
|
+
"""
|
120
|
+
from ..streams.audio import AudioStream
|
121
|
+
from ..streams.av import AVStream
|
122
|
+
from ..streams.video import VideoStream
|
123
|
+
|
124
|
+
if not context:
|
125
|
+
context = DAGContext.build(stream.node)
|
126
|
+
|
127
|
+
match stream.node:
|
128
|
+
case InputNode():
|
129
|
+
match stream:
|
130
|
+
case AVStream():
|
131
|
+
return f"{get_node_label(stream.node, context)}"
|
132
|
+
case VideoStream():
|
133
|
+
return f"{get_node_label(stream.node, context)}:v"
|
134
|
+
case AudioStream():
|
135
|
+
return f"{get_node_label(stream.node, context)}:a"
|
136
|
+
case _:
|
137
|
+
raise FFMpegValueError(
|
138
|
+
f"Unknown stream type: {stream.__class__.__name__}"
|
139
|
+
) # pragma: no cover
|
140
|
+
case FilterNode():
|
141
|
+
if len(stream.node.output_typings) > 1:
|
142
|
+
return f"{get_node_label(stream.node, context)}#{stream.index}"
|
143
|
+
return f"{get_node_label(stream.node, context)}"
|
144
|
+
case _:
|
145
|
+
raise FFMpegValueError(
|
146
|
+
f"Unknown node type: {stream.node.__class__.__name__}"
|
147
|
+
) # pragma: no cover
|
148
|
+
|
149
|
+
|
150
|
+
def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
|
151
|
+
"""
|
152
|
+
Generate the FFmpeg filter string for this filter node.
|
153
|
+
|
154
|
+
This method creates the filter string that will be used in the
|
155
|
+
filter_complex argument of the FFmpeg command. The format follows
|
156
|
+
FFmpeg's syntax where input labels are followed by the filter name
|
157
|
+
and parameters, and then output labels.
|
158
|
+
|
159
|
+
Args:
|
160
|
+
context: Optional DAG context for resolving stream labels.
|
161
|
+
If not provided, a new context will be built.
|
162
|
+
|
163
|
+
Returns:
|
164
|
+
A list of strings that, when joined, form the filter string
|
165
|
+
for this node in the filter_complex argument
|
166
|
+
|
167
|
+
Example:
|
168
|
+
For a scale filter with width=1280 and height=720, this might return:
|
169
|
+
['[0:v]', 'scale=', 'width=1280:height=720', '[s0]']
|
170
|
+
"""
|
171
|
+
|
172
|
+
incoming_labels = "".join(f"[{get_stream_label(k, context)}]" for k in node.inputs)
|
173
|
+
outputs = context.get_outgoing_streams(node)
|
174
|
+
|
175
|
+
outgoing_labels = ""
|
176
|
+
for output in sorted(outputs, key=lambda stream: stream.index or 0):
|
177
|
+
# NOTE: all outgoing streams must be filterable
|
178
|
+
assert isinstance(output, FilterableStream)
|
179
|
+
outgoing_labels += f"[{get_stream_label(output, context)}]"
|
180
|
+
|
181
|
+
commands = []
|
182
|
+
for key, value in node.kwargs.items():
|
183
|
+
assert not isinstance(value, LazyValue), (
|
184
|
+
f"LazyValue should have been evaluated: {key}={value}"
|
185
|
+
)
|
186
|
+
|
187
|
+
# Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
|
188
|
+
if isinstance(value, bool):
|
189
|
+
value = str(int(value))
|
190
|
+
|
191
|
+
if not isinstance(value, Default):
|
192
|
+
commands += [f"{key}={escape(value)}"]
|
193
|
+
|
194
|
+
if commands:
|
195
|
+
return (
|
196
|
+
[incoming_labels]
|
197
|
+
+ [f"{node.name}="]
|
198
|
+
+ [escape(":".join(commands), "\\'[],;")]
|
199
|
+
+ [outgoing_labels]
|
200
|
+
)
|
201
|
+
return [incoming_labels] + [f"{node.name}"] + [outgoing_labels]
|
202
|
+
|
203
|
+
|
204
|
+
def get_args_input_node(node: InputNode, context: DAGContext) -> list[str]:
|
205
|
+
"""
|
206
|
+
Generate the FFmpeg command-line arguments for this input file.
|
207
|
+
|
208
|
+
This method creates the command-line arguments needed to specify
|
209
|
+
this input file to FFmpeg, including any input-specific options.
|
210
|
+
|
211
|
+
Args:
|
212
|
+
context: Optional DAG context (not used for input nodes)
|
213
|
+
|
214
|
+
Returns:
|
215
|
+
A list of strings representing FFmpeg command-line arguments
|
216
|
+
|
217
|
+
Example:
|
218
|
+
For an input file "input.mp4" with options like seeking to 10 seconds:
|
219
|
+
['-ss', '10', '-i', 'input.mp4']
|
220
|
+
"""
|
221
|
+
commands = []
|
222
|
+
for key, value in node.kwargs.items():
|
223
|
+
if isinstance(value, bool):
|
224
|
+
if value is True:
|
225
|
+
commands += [f"-{key}"]
|
226
|
+
elif value is False:
|
227
|
+
commands += [f"-no{key}"]
|
228
|
+
else:
|
229
|
+
commands += [f"-{key}", str(value)]
|
230
|
+
commands += ["-i", node.filename]
|
231
|
+
return commands
|
232
|
+
|
233
|
+
|
234
|
+
def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
|
235
|
+
"""
|
236
|
+
Generate the FFmpeg command-line arguments for this output file.
|
237
|
+
|
238
|
+
This method creates the command-line arguments needed to specify
|
239
|
+
this output file to FFmpeg, including stream mapping and output-specific
|
240
|
+
options like codecs and formats.
|
241
|
+
|
242
|
+
Args:
|
243
|
+
context: Optional DAG context for resolving stream labels.
|
244
|
+
If not provided, a new context will be built.
|
245
|
+
|
246
|
+
Returns:
|
247
|
+
A list of strings representing FFmpeg command-line arguments
|
248
|
+
|
249
|
+
Example:
|
250
|
+
For an output file "output.mp4" with H.264 video codec:
|
251
|
+
['-map', '[v0]', '-c:v', 'libx264', 'output.mp4']
|
252
|
+
"""
|
253
|
+
# !handle mapping
|
254
|
+
commands = []
|
255
|
+
|
256
|
+
if context:
|
257
|
+
for input in node.inputs:
|
258
|
+
if isinstance(input.node, InputNode):
|
259
|
+
commands += ["-map", get_stream_label(input, context)]
|
260
|
+
else:
|
261
|
+
commands += ["-map", f"[{get_stream_label(input, context)}]"]
|
262
|
+
|
263
|
+
for key, value in node.kwargs.items():
|
264
|
+
if isinstance(value, bool):
|
265
|
+
if value is True:
|
266
|
+
commands += [f"-{key}"]
|
267
|
+
elif value is False:
|
268
|
+
commands += [f"-no{key}"]
|
269
|
+
else:
|
270
|
+
commands += [f"-{key}", str(value)]
|
271
|
+
commands += [node.filename]
|
272
|
+
return commands
|
273
|
+
|
274
|
+
|
275
|
+
def get_args_global_node(node: GlobalNode, context: DAGContext) -> list[str]:
|
276
|
+
"""
|
277
|
+
Generate the FFmpeg command-line arguments for these global options.
|
278
|
+
|
279
|
+
This method creates the command-line arguments needed to specify
|
280
|
+
global options to FFmpeg, such as -y for overwrite or -loglevel for
|
281
|
+
controlling log output.
|
282
|
+
|
283
|
+
Args:
|
284
|
+
context: Optional DAG context (not used for global options)
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
A list of strings representing FFmpeg command-line arguments
|
288
|
+
|
289
|
+
Example:
|
290
|
+
For global options like overwrite and quiet logging:
|
291
|
+
['-y', '-loglevel', 'quiet']
|
292
|
+
"""
|
293
|
+
commands = []
|
294
|
+
for key, value in node.kwargs.items():
|
295
|
+
if isinstance(value, bool):
|
296
|
+
if value is True:
|
297
|
+
commands += [f"-{key}"]
|
298
|
+
elif value is False:
|
299
|
+
commands += [f"-no{key}"]
|
300
|
+
else:
|
301
|
+
commands += [f"-{key}", str(value)]
|
302
|
+
return commands
|
303
|
+
|
304
|
+
|
305
|
+
def get_args(node: Node, context: DAGContext | None = None) -> list[str]:
|
306
|
+
"""
|
307
|
+
Get the arguments for a node.
|
308
|
+
"""
|
309
|
+
|
310
|
+
context = context or DAGContext.build(node)
|
311
|
+
|
312
|
+
match node:
|
313
|
+
case FilterNode():
|
314
|
+
return get_args_filter_node(node, context)
|
315
|
+
case InputNode():
|
316
|
+
return get_args_input_node(node, context)
|
317
|
+
case OutputNode():
|
318
|
+
return get_args_output_node(node, context)
|
319
|
+
case GlobalNode():
|
320
|
+
return get_args_global_node(node, context)
|
321
|
+
case _:
|
322
|
+
raise FFMpegValueError(f"Unknown node type: {node.__class__.__name__}")
|
323
|
+
|
324
|
+
|
325
|
+
def get_node_label(node: Node, context: DAGContext) -> str:
|
326
|
+
"""
|
327
|
+
Get the string label for a specific node in the filter graph.
|
328
|
+
|
329
|
+
This method returns the label assigned to the node, which is used in FFmpeg
|
330
|
+
filter graph notation. The label format depends on the node type:
|
331
|
+
- Input nodes: sequential numbers (0, 1, 2...)
|
332
|
+
- Filter nodes: 's' prefix followed by a number (s0, s1, s2...)
|
333
|
+
|
334
|
+
Args:
|
335
|
+
node: The node to get the label for (must be an InputNode or FilterNode)
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
The string label for the node
|
339
|
+
|
340
|
+
Raises:
|
341
|
+
AssertionError: If the node is not an InputNode or FilterNode
|
342
|
+
"""
|
343
|
+
|
344
|
+
node_id = context.node_ids[node]
|
345
|
+
match node:
|
346
|
+
case InputNode():
|
347
|
+
return str(node_id)
|
348
|
+
case FilterNode():
|
349
|
+
return f"s{node_id}"
|
350
|
+
case _:
|
351
|
+
return "out"
|