typed-ffmpeg-compatible 2.7.2__py3-none-any.whl → 2.7.4__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.
Files changed (66) hide show
  1. typed_ffmpeg/__init__.py +2 -1
  2. typed_ffmpeg/common/cache/.gitignore +3 -0
  3. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/acrossover.json +6 -0
  4. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/afir.json +9 -0
  5. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aiir.json +6 -0
  6. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ainterleave.json +9 -0
  7. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amerge.json +9 -0
  8. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amix.json +9 -0
  9. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/amovie.json +6 -0
  10. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/anequalizer.json +6 -0
  11. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aphasemeter.json +6 -0
  12. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asegment.json +6 -0
  13. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/aselect.json +6 -0
  14. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/asplit.json +6 -0
  15. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/astreamselect.json +9 -0
  16. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/bm3d.json +6 -0
  17. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/channelsplit.json +6 -0
  18. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/concat.json +9 -0
  19. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/decimate.json +6 -0
  20. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/ebur128.json +6 -0
  21. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/extractplanes.json +6 -0
  22. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/fieldmatch.json +6 -0
  23. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/guided.json +6 -0
  24. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/headphone.json +6 -0
  25. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/hstack.json +9 -0
  26. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/interleave.json +9 -0
  27. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/join.json +9 -0
  28. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/libplacebo.json +9 -0
  29. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/limitdiff.json +6 -0
  30. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mergeplanes.json +6 -0
  31. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/mix.json +9 -0
  32. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/movie.json +6 -0
  33. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/premultiply.json +6 -0
  34. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/segment.json +6 -0
  35. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/select.json +6 -0
  36. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/signature.json +9 -0
  37. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/split.json +6 -0
  38. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/streamselect.json +9 -0
  39. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/unpremultiply.json +6 -0
  40. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/vstack.json +9 -0
  41. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xmedian.json +9 -0
  42. typed_ffmpeg/common/cache/FFMpegFilterManuallyDefined/xstack.json +9 -0
  43. typed_ffmpeg/common/cache/list/filters.json +90747 -0
  44. typed_ffmpeg/common/cache/list/options.json +1694 -0
  45. typed_ffmpeg/common/cache.py +66 -0
  46. typed_ffmpeg/common/schema.py +9 -5
  47. typed_ffmpeg/common/serialize.py +12 -8
  48. typed_ffmpeg/compile/__init__.py +0 -0
  49. typed_ffmpeg/compile/compile_cli.py +415 -0
  50. typed_ffmpeg/compile/compile_json.py +38 -0
  51. typed_ffmpeg/compile/compile_python.py +325 -0
  52. typed_ffmpeg/{dag → compile}/context.py +26 -45
  53. typed_ffmpeg/{dag → compile}/validate.py +2 -2
  54. typed_ffmpeg/dag/factory.py +25 -4
  55. typed_ffmpeg/dag/global_runnable/runnable.py +6 -6
  56. typed_ffmpeg/dag/nodes.py +2 -230
  57. typed_ffmpeg/dag/schema.py +2 -18
  58. typed_ffmpeg/streams/av.py +36 -3
  59. typed_ffmpeg/utils/view.py +1 -1
  60. {typed_ffmpeg_compatible-2.7.2.dist-info → typed_ffmpeg_compatible-2.7.4.dist-info}/METADATA +1 -1
  61. typed_ffmpeg_compatible-2.7.4.dist-info/RECORD +95 -0
  62. typed_ffmpeg/dag/compile.py +0 -86
  63. typed_ffmpeg_compatible-2.7.2.dist-info/RECORD +0 -48
  64. {typed_ffmpeg_compatible-2.7.2.dist-info → typed_ffmpeg_compatible-2.7.4.dist-info}/LICENSE +0 -0
  65. {typed_ffmpeg_compatible-2.7.2.dist-info → typed_ffmpeg_compatible-2.7.4.dist-info}/WHEEL +0 -0
  66. {typed_ffmpeg_compatible-2.7.2.dist-info → typed_ffmpeg_compatible-2.7.4.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()
@@ -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
 
@@ -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, strict: bool = True) -> 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, strict: bool = True) -> Any:
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
- object_hook_strict = partial(object_hook, strict=strict)
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,415 @@
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 handles the following components:
7
+
8
+ 1. Global Options: General FFmpeg settings like log level, overwrite flags
9
+ 2. Input Files: Source media files with their specific options
10
+ 3. Filter Graphs: Complex filter chains with proper stream labeling
11
+ 4. Output Files: Destination files with codec and format settings
12
+
13
+ The module ensures proper ordering of arguments and handles stream mapping,
14
+ filter graph syntax, and escaping of special characters in FFmpeg commands.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from ..dag.nodes import FilterableStream, FilterNode, GlobalNode, InputNode, OutputNode
20
+ from ..dag.schema import Node, Stream
21
+ from ..exceptions import FFMpegValueError
22
+ from ..schema import Default
23
+ from ..utils.escaping import escape
24
+ from ..utils.lazy_eval.schema import LazyValue
25
+ from ..utils.run import command_line
26
+ from .context import DAGContext
27
+ from .validate import validate
28
+
29
+
30
+ def compile(stream: Stream, auto_fix: bool = True) -> str:
31
+ """
32
+ Compile a stream into a command-line string.
33
+
34
+ This function takes a Stream object representing an FFmpeg filter graph
35
+ and converts it into a command-line string that can be passed to FFmpeg.
36
+
37
+ Args:
38
+ stream: The Stream object to compile into a command-line string
39
+ auto_fix: Whether to automatically fix issues in the stream
40
+
41
+ Returns:
42
+ A command-line string that can be passed to FFmpeg
43
+ """
44
+ return command_line(compile_as_list(stream, auto_fix))
45
+
46
+
47
+ def compile_as_list(stream: Stream, auto_fix: bool = True) -> list[str]:
48
+ """
49
+ Compile a stream into a list of FFmpeg command-line arguments.
50
+
51
+ This function takes a Stream object representing an FFmpeg filter graph
52
+ and converts it into a list of command-line arguments that can be passed
53
+ to FFmpeg. It processes the graph in the correct order:
54
+ 1. Global nodes (general FFmpeg options)
55
+ 2. Input nodes (input files and their options)
56
+ 3. Filter nodes (combined into a -filter_complex argument)
57
+ 4. Output nodes (output files and their options)
58
+
59
+ The function validates the graph before compilation to ensure it's properly
60
+ formed. If auto_fix is enabled, it will attempt to fix common issues like
61
+ disconnected nodes or invalid stream mappings.
62
+
63
+ Args:
64
+ stream: The Stream object to compile into arguments
65
+ auto_fix: Whether to automatically fix issues in the stream
66
+ (e.g., reconnecting disconnected nodes)
67
+
68
+ Returns:
69
+ A list of strings representing FFmpeg command-line arguments
70
+
71
+ Raises:
72
+ FFMpegValueError: If the stream contains invalid configurations that cannot be fixed
73
+
74
+ Example:
75
+ ```python
76
+ # Create a simple video scaling filter graph
77
+ input_stream = ffmpeg.input("input.mp4")
78
+ scaled = input_stream.filter("scale", 1280, 720)
79
+ output_stream = scaled.output("output.mp4")
80
+
81
+ # Compile to FFmpeg arguments
82
+ args = ffmpeg.dag.compile(output_stream)
83
+ print(
84
+ args
85
+ ) # ['ffmpeg', '-i', 'input.mp4', '-filter_complex', '...', 'output.mp4']
86
+ ```
87
+ """
88
+
89
+ stream = validate(stream, auto_fix=auto_fix)
90
+ node = stream.node
91
+ context = DAGContext.build(node)
92
+
93
+ # compile the global nodes
94
+ commands = []
95
+ global_nodes = [node for node in context.all_nodes if isinstance(node, GlobalNode)]
96
+ for node in global_nodes:
97
+ commands += get_args(node, context)
98
+
99
+ # compile the input nodes
100
+ input_nodes = [node for node in context.all_nodes if isinstance(node, InputNode)]
101
+ for node in input_nodes:
102
+ commands += get_args(node, context)
103
+
104
+ # compile the filter nodes
105
+ vf_commands = []
106
+ filter_nodes = [node for node in context.all_nodes if isinstance(node, FilterNode)]
107
+
108
+ for node in sorted(filter_nodes, key=lambda node: len(node.upstream_nodes)):
109
+ vf_commands += ["".join(get_args(node, context))]
110
+
111
+ if vf_commands:
112
+ commands += ["-filter_complex", ";".join(vf_commands)]
113
+
114
+ # compile the output nodes
115
+ output_nodes = [node for node in context.all_nodes if isinstance(node, OutputNode)]
116
+ for node in output_nodes:
117
+ commands += get_args(node, context)
118
+
119
+ return commands
120
+
121
+
122
+ def get_stream_label(stream: Stream, context: DAGContext | None = None) -> str:
123
+ """
124
+ Generate the FFmpeg label for this stream in filter graphs.
125
+
126
+ This method creates the label string used to identify this stream in
127
+ FFmpeg filter graphs. The format of the label depends on the stream's
128
+ source (input file or filter) and type (video or audio).
129
+
130
+ For input streams, labels follow FFmpeg's stream specifier syntax:
131
+ - Video streams: "0:v" (first input, video stream)
132
+ - Audio streams: "0:a" (first input, audio stream)
133
+ - AV streams: "0" (first input, all streams)
134
+
135
+ For filter outputs, labels use the filter's label:
136
+ - Single output filters: "filterlabel"
137
+ - Multi-output filters: "filterlabel#index"
138
+
139
+ Args:
140
+ stream: The stream to generate a label for
141
+ context: Optional DAG context for resolving node labels.
142
+ If not provided, a new context will be built.
143
+
144
+ Returns:
145
+ A string label for this stream in FFmpeg filter syntax
146
+
147
+ Raises:
148
+ FFMpegValueError: If the stream has an unknown type or node type
149
+ """
150
+ from ..streams.audio import AudioStream
151
+ from ..streams.av import AVStream
152
+ from ..streams.video import VideoStream
153
+
154
+ if not context:
155
+ context = DAGContext.build(stream.node)
156
+
157
+ match stream.node:
158
+ case InputNode():
159
+ match stream:
160
+ case AVStream():
161
+ return f"{get_node_label(stream.node, context)}"
162
+ case VideoStream():
163
+ if stream.index is not None:
164
+ return (
165
+ f"{get_node_label(stream.node, context)}:v:{stream.index}"
166
+ )
167
+ return f"{get_node_label(stream.node, context)}:v"
168
+ case AudioStream():
169
+ if stream.index is not None:
170
+ return (
171
+ f"{get_node_label(stream.node, context)}:a:{stream.index}"
172
+ )
173
+ return f"{get_node_label(stream.node, context)}:a"
174
+ case _:
175
+ raise FFMpegValueError(
176
+ f"Unknown stream type: {stream.__class__.__name__}"
177
+ ) # pragma: no cover
178
+ case FilterNode():
179
+ if len(stream.node.output_typings) > 1:
180
+ return f"{get_node_label(stream.node, context)}#{stream.index}"
181
+ return f"{get_node_label(stream.node, context)}"
182
+ case _:
183
+ raise FFMpegValueError(
184
+ f"Unknown node type: {stream.node.__class__.__name__}"
185
+ ) # pragma: no cover
186
+
187
+
188
+ def get_args_filter_node(node: FilterNode, context: DAGContext) -> list[str]:
189
+ """
190
+ Generate the FFmpeg filter string for this filter node.
191
+
192
+ This method creates the filter string that will be used in the
193
+ filter_complex argument of the FFmpeg command. The format follows
194
+ FFmpeg's syntax where input labels are followed by the filter name
195
+ and parameters, and then output labels.
196
+
197
+ The filter string format is:
198
+ [input_label]filter_name=param1=value1:param2=value2[output_label]
199
+
200
+ Args:
201
+ node: The FilterNode to generate arguments for
202
+ context: DAG context for resolving stream labels
203
+
204
+ Returns:
205
+ A list of strings that, when joined, form the filter string
206
+ for this node in the filter_complex argument
207
+
208
+ Example:
209
+ For a scale filter with width=1280 and height=720, this might return:
210
+ ['[0:v]', 'scale=', 'width=1280:height=720', '[s0]']
211
+ """
212
+
213
+ incoming_labels = "".join(f"[{get_stream_label(k, context)}]" for k in node.inputs)
214
+ outputs = context.get_outgoing_streams(node)
215
+
216
+ outgoing_labels = ""
217
+ for output in sorted(outputs, key=lambda stream: stream.index or 0):
218
+ # NOTE: all outgoing streams must be filterable
219
+ assert isinstance(output, FilterableStream)
220
+ outgoing_labels += f"[{get_stream_label(output, context)}]"
221
+
222
+ commands = []
223
+ for key, value in node.kwargs.items():
224
+ assert not isinstance(value, LazyValue), (
225
+ f"LazyValue should have been evaluated: {key}={value}"
226
+ )
227
+
228
+ # Note: the -nooption syntax cannot be used for boolean AVOptions, use -option 0/-option 1.
229
+ if isinstance(value, bool):
230
+ value = str(int(value))
231
+
232
+ if not isinstance(value, Default):
233
+ commands += [f"{key}={escape(value)}"]
234
+
235
+ if commands:
236
+ return (
237
+ [incoming_labels]
238
+ + [f"{node.name}="]
239
+ + [escape(":".join(commands), "\\'[],;")]
240
+ + [outgoing_labels]
241
+ )
242
+ return [incoming_labels] + [f"{node.name}"] + [outgoing_labels]
243
+
244
+
245
+ def get_args_input_node(node: InputNode, context: DAGContext) -> list[str]:
246
+ """
247
+ Generate the FFmpeg command-line arguments for this input file.
248
+
249
+ This method creates the command-line arguments needed to specify
250
+ this input file to FFmpeg, including any input-specific options.
251
+ Options are converted to FFmpeg's command-line format, with boolean
252
+ options using -option or -nooption syntax.
253
+
254
+ Args:
255
+ node: The InputNode to generate arguments for
256
+ context: DAG context (not used for input nodes)
257
+
258
+ Returns:
259
+ A list of strings representing FFmpeg command-line arguments
260
+
261
+ Example:
262
+ For an input file "input.mp4" with options like seeking to 10 seconds:
263
+ ['-ss', '10', '-i', 'input.mp4']
264
+ """
265
+ commands = []
266
+ for key, value in node.kwargs.items():
267
+ if isinstance(value, bool):
268
+ if value is True:
269
+ commands += [f"-{key}"]
270
+ elif value is False:
271
+ commands += [f"-no{key}"]
272
+ else:
273
+ commands += [f"-{key}", str(value)]
274
+ commands += ["-i", node.filename]
275
+ return commands
276
+
277
+
278
+ def get_args_output_node(node: OutputNode, context: DAGContext) -> list[str]:
279
+ """
280
+ Generate the FFmpeg command-line arguments for this output file.
281
+
282
+ This method creates the command-line arguments needed to specify
283
+ this output file to FFmpeg, including stream mapping and output-specific
284
+ options like codecs and formats. It handles both direct input streams
285
+ and filter output streams appropriately.
286
+
287
+ Args:
288
+ node: The OutputNode to generate arguments for
289
+ context: DAG context for resolving stream labels
290
+
291
+ Returns:
292
+ A list of strings representing FFmpeg command-line arguments
293
+
294
+ Example:
295
+ For an output file "output.mp4" with H.264 video codec:
296
+ ['-map', '[v0]', '-c:v', 'libx264', 'output.mp4']
297
+ """
298
+ # !handle mapping
299
+ commands = []
300
+
301
+ if context:
302
+ for input in node.inputs:
303
+ if isinstance(input.node, InputNode):
304
+ commands += ["-map", get_stream_label(input, context)]
305
+ else:
306
+ commands += ["-map", f"[{get_stream_label(input, context)}]"]
307
+
308
+ for key, value in node.kwargs.items():
309
+ if isinstance(value, bool):
310
+ if value is True:
311
+ commands += [f"-{key}"]
312
+ elif value is False:
313
+ commands += [f"-no{key}"]
314
+ else:
315
+ commands += [f"-{key}", str(value)]
316
+ commands += [node.filename]
317
+ return commands
318
+
319
+
320
+ def get_args_global_node(node: GlobalNode, context: DAGContext) -> list[str]:
321
+ """
322
+ Generate the FFmpeg command-line arguments for these global options.
323
+
324
+ This method creates the command-line arguments needed to specify
325
+ global options to FFmpeg, such as -y for overwrite or -loglevel for
326
+ controlling log output. Boolean options are converted to -option or
327
+ -nooption syntax.
328
+
329
+ Args:
330
+ node: The GlobalNode to generate arguments for
331
+ context: DAG context (not used for global options)
332
+
333
+ Returns:
334
+ A list of strings representing FFmpeg command-line arguments
335
+
336
+ Example:
337
+ For global options like overwrite and quiet logging:
338
+ ['-y', '-loglevel', 'quiet']
339
+ """
340
+ commands = []
341
+ for key, value in node.kwargs.items():
342
+ if isinstance(value, bool):
343
+ if value is True:
344
+ commands += [f"-{key}"]
345
+ elif value is False:
346
+ commands += [f"-no{key}"]
347
+ else:
348
+ commands += [f"-{key}", str(value)]
349
+ return commands
350
+
351
+
352
+ def get_args(node: Node, context: DAGContext | None = None) -> list[str]:
353
+ """
354
+ Get the FFmpeg command-line arguments for a specific node.
355
+
356
+ This function dispatches to the appropriate argument generation function
357
+ based on the node type. It handles all node types in the FFmpeg DAG:
358
+ FilterNode, InputNode, OutputNode, and GlobalNode.
359
+
360
+ Args:
361
+ node: The node to generate arguments for
362
+ context: Optional DAG context for resolving stream labels.
363
+ If not provided, a new context will be built.
364
+
365
+ Returns:
366
+ A list of strings representing FFmpeg command-line arguments
367
+
368
+ Raises:
369
+ FFMpegValueError: If the node type is not recognized
370
+ """
371
+
372
+ context = context or DAGContext.build(node)
373
+
374
+ match node:
375
+ case FilterNode():
376
+ return get_args_filter_node(node, context)
377
+ case InputNode():
378
+ return get_args_input_node(node, context)
379
+ case OutputNode():
380
+ return get_args_output_node(node, context)
381
+ case GlobalNode():
382
+ return get_args_global_node(node, context)
383
+ case _:
384
+ raise FFMpegValueError(f"Unknown node type: {node.__class__.__name__}")
385
+
386
+
387
+ def get_node_label(node: Node, context: DAGContext) -> str:
388
+ """
389
+ Get the string label for a specific node in the filter graph.
390
+
391
+ This method returns the label assigned to the node, which is used in FFmpeg
392
+ filter graph notation. The label format depends on the node type:
393
+ - Input nodes: sequential numbers (0, 1, 2...)
394
+ - Filter nodes: 's' prefix followed by a number (s0, s1, s2...)
395
+ - Output nodes: 'out'
396
+
397
+ Args:
398
+ node: The node to get the label for
399
+ context: DAG context containing node ID mappings
400
+
401
+ Returns:
402
+ The string label for the node
403
+
404
+ Raises:
405
+ AssertionError: If the node is not an InputNode or FilterNode
406
+ """
407
+
408
+ node_id = context.node_ids[node]
409
+ match node:
410
+ case InputNode():
411
+ return str(node_id)
412
+ case FilterNode():
413
+ return f"s{node_id}"
414
+ case _:
415
+ return "out"
@@ -0,0 +1,38 @@
1
+ from ..common.serialize import dumps, loads
2
+ from ..dag.schema import Stream
3
+ from .validate import validate
4
+
5
+
6
+ def compile(stream: Stream, auto_fix: bool = True) -> str:
7
+ """
8
+ Compile a stream into a JSON string.
9
+
10
+ This function takes a Stream object representing an FFmpeg filter graph
11
+ and converts it into a JSON string that can be passed to FFmpeg.
12
+
13
+ Args:
14
+ stream: The Stream object to compile into a JSON string
15
+ auto_fix: Whether to automatically fix issues in the stream
16
+
17
+ Returns:
18
+ A JSON string that can be passed to FFmpeg
19
+ """
20
+ stream = validate(stream, auto_fix=auto_fix)
21
+
22
+ return dumps(stream)
23
+
24
+
25
+ def parse(json: str) -> Stream:
26
+ """
27
+ Parse a JSON string into a Stream object.
28
+
29
+ This function takes a JSON string that can be passed to FFmpeg
30
+ and converts it into a Stream object.
31
+
32
+ Args:
33
+ json: The JSON string to parse into a Stream object
34
+
35
+ Returns:
36
+ A Stream object
37
+ """
38
+ return loads(json)